Skip to content

Commit d80e4a5

Browse files
committed
Import from RSS feeds
1 parent fca2dfa commit d80e4a5

File tree

10 files changed

+245
-6
lines changed

10 files changed

+245
-6
lines changed

.editorconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# EditorConfig is awesome: http://EditorConfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
6+
[*]
7+
charset = utf-8
8+
end_of_line = lf
9+
indent_size = 4
10+
indent_style = tab
11+
insert_final_newline = true
12+
trim_trailing_whitespace = true
13+
14+
[*.{json,yaml}]
15+
indent_size = 2
16+
indent_style = space

.github/workflows/gcr-deploy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121

2222
steps:
2323
- name: Checkout
24-
uses: actions/checkout@v1
24+
uses: actions/checkout@v4
2525

2626
- name: gcloud auth
2727
id: 'auth'

TODO.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# To Do
22

3+
- [ ] Redos check
4+
- [ ] Diagram
5+
- [ ] Glossary
6+
- [ ] Tools page w/above
37
- [ ] copy to clipboard for variations
48
- [ ] patterns into database
59
- [ ] search.html
@@ -16,6 +20,21 @@
1620
- [ ] post to places besides RXP
1721
- [ ] 404 page
1822

23+
## Patterns
24+
25+
- [ ] URL: https://mathiasbynens.be/demo/url-regex
26+
- [ ] Semver: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
27+
- [ ] RFC 3339 dates: https://datatracker.ietf.org/doc/html/rfc3339
28+
- [ ] RFC 822 dates: https://datatracker.ietf.org/doc/html/rfc822#section-5
29+
- [ ] IPv4
30+
- [ ] IPv6
31+
- [ ] SWIFT
32+
- [ ] NANP phone number
33+
- [ ] USA social security numbers
34+
- [ ] Other national numbers
35+
- [ ] Other postal codes
36+
- [ ]
37+
1938

2039
## Data Model
2140

app/routes/links._index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export default function Index() {
5757
{user && user.isAdmin ?
5858
<>
5959
<RemixLink to="/links/add.html" className="btn btn-primary mx-1"><AdminIcon /> Add</RemixLink>
60-
<RemixLink to="/links/import.html" className="btn btn-primary mx-1"><AdminIcon /> Import</RemixLink>
60+
<RemixLink to="/links/import-feed.html" className="btn btn-primary mx-1"><AdminIcon /> Import RSS/Adom Feed</RemixLink>
61+
<RemixLink to="/links/import-json.html" className="btn btn-primary mx-1"><AdminIcon /> Import JSON</RemixLink>
6162
<a href="/links/backup.json" className="btn btn-primary mx-1"><AdminIcon /> Backup</a>
6263
</>
6364
: null}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {
2+
ActionFunctionArgs,
3+
LoaderFunctionArgs,
4+
MetaFunction,
5+
redirect,
6+
} from "@remix-run/node";
7+
import { Form, useLoaderData } from "@remix-run/react";
8+
import { eq } from "drizzle-orm";
9+
import { parseFeed } from "@rowanmanning/feed-parser";
10+
11+
import { authenticator } from "~/services/auth.server";
12+
import { adminOnlyLoader } from "~/util/adminOnlyLoader";
13+
import { User } from "~/types/User";
14+
15+
export const meta: MetaFunction = () => {
16+
return [
17+
{ title: "Import RSS/Atom Links - Regex Zone" },
18+
{ name: "description", content: "Import JSON link dump from Pinboard.in" },
19+
];
20+
};
21+
22+
export const action = async ({
23+
request,
24+
}: ActionFunctionArgs) => {
25+
26+
const user: User | null = await authenticator.isAuthenticated(request);
27+
if (!user) {
28+
return redirect("/auth/");
29+
}
30+
if (!user.isAdmin) {
31+
throw new Response("Unauthorized", { status: 401 });
32+
}
33+
34+
const formData = await request.formData();
35+
36+
const formDataValue = formData.get("feedurl");
37+
if (!formDataValue) {
38+
throw new Response("No url specified", { status: 400 });
39+
}
40+
41+
const feedUrl = formDataValue as string;
42+
try {
43+
new URL(feedUrl);
44+
} catch (e) {
45+
throw new Response("Invalid URL", { status: 400 });
46+
}
47+
48+
const feedResponse = await fetch(feedUrl);
49+
if (!feedResponse.ok) {
50+
throw new Response("Error fetching feed", { status: 400 });
51+
}
52+
53+
const feedtext = await feedResponse.text();
54+
let feed:Feed;
55+
56+
try {
57+
feed = await parseFeed(feedtext);
58+
} catch (err: unknown) {
59+
throw new Response("Error parsing feed", { status: 400 });
60+
}
61+
62+
let count = 0;
63+
for (const entry of feed.items) {
64+
const url = entry.url;
65+
if (!url) {
66+
continue;
67+
}
68+
const existing = await dborm.select().from(regex_link).where(eq(regex_link.rxl_url, url));
69+
if (existing.length > 0) {
70+
console.log(`found ${url}, stopping...`);
71+
break;
72+
}
73+
const values = {
74+
rxl_url: entry.url,
75+
rxl_title: entry.title || "(untitled)",
76+
rxl_tags: entry.categories[0].term.split(' '),
77+
rxl_created_at: new Date(entry.published || "1970-01-01T00:00:00Z"),
78+
}
79+
console.log(JSON.stringify(values, null, 2));
80+
await dborm.insert(regex_link).values(values);
81+
count++;
82+
}
83+
84+
const session = await cookieStorage.getSession(request.headers.get("Cookie"));
85+
session.flash("message", { type: 'info', message: `You uploaded ${count} entries from ${feedUrl}!` });
86+
return redirect('/links/', { headers: { "Set-Cookie": await cookieStorage.commitSession(session) } });
87+
88+
/**
89+
* LATER: it would be nice if Remix supported action responses but `shouldRevalidate` doesn't seem to work
90+
*
91+
console.log("about to return");
92+
return new Response(`You uploaded ${data.length} bytes`, {
93+
status: 200,
94+
headers: {
95+
'Content-type': "text/plain; charset=utf-8",
96+
}
97+
});
98+
*/
99+
100+
};
101+
102+
export function loader(args: LoaderFunctionArgs) {
103+
const user = adminOnlyLoader(args);
104+
105+
return {
106+
user,
107+
url: process.env.BOOKMARK_FEED_URL,
108+
}
109+
}
110+
111+
import type { ShouldRevalidateFunction } from "@remix-run/react";
112+
import { cookieStorage } from "~/services/session.server";
113+
import { dborm } from "~/db/connection.server";
114+
import { regex_link } from "~/db/schema";
115+
import { Feed } from "@rowanmanning/feed-parser/lib/feed/base";
116+
117+
export const shouldRevalidate: ShouldRevalidateFunction = (params) => {
118+
console.log("shouldRevalidate", params); // never called
119+
return params.defaultShouldRevalidate;
120+
};
121+
122+
export default function Import() {
123+
124+
const data = useLoaderData<typeof loader>();
125+
126+
const url = data.url;
127+
128+
129+
return (
130+
<>
131+
<h1 className="py-2">Import RSS/Atom Feed Links</h1>
132+
<div className="d-flex justify-content-center">
133+
<Form method="post" className="border p-3" action="/links/import-feed.html" encType="multipart/form-data">
134+
<div className="mb-3">
135+
<label htmlFor="feedurl" className="form-label">RSS/Atom Feed</label>
136+
<input className="form-control" type="url" id="feedurl" name="feedurl" value={url}/>
137+
</div>
138+
<button type="submit" className="btn btn-primary">Import</button>
139+
</Form>
140+
</div>
141+
</>
142+
);
143+
}

app/routes/links.import[.]html.tsx renamed to app/routes/links.import-json[.]html.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type PinboardEntry = {
2222

2323
export const meta: MetaFunction = () => {
2424
return [
25-
{ title: "Import Links - Regex Zone" },
25+
{ title: "Import JSON Links - Regex Zone" },
2626
{ name: "description", content: "Import JSON link dump from Pinboard.in" },
2727
];
2828
};
@@ -112,7 +112,7 @@ export default function Import() {
112112
<>
113113
<h1 className="py-2">Import Links</h1>
114114
<div className="d-flex justify-content-center">
115-
<Form method="post" className="border p-3" action="/links/import.html" encType="multipart/form-data">
115+
<Form method="post" className="border p-3" action="/links/import-json.html" encType="multipart/form-data">
116116
<div className="mb-3">
117117
<label htmlFor="formFile" className="form-label">Pinboard.in Export JSON</label>
118118
<input className="form-control" type="file" id="file" name="file" />

package-lock.json

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@remix-run/express": "^2.11.2",
1515
"@remix-run/node": "^2.9.2",
1616
"@remix-run/react": "^2.9.2",
17+
"@rowanmanning/feed-parser": "^1.1.0",
1718
"@uidotdev/usehooks": "^2.4.1",
1819
"bootstrap": "^5.3.3",
1920
"compression": "^1.7.4",

patterns/ap-dates.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ featured: false
22
handle: ap-dates
33
title: Associated Press Date Format
44
tags:
5-
- date
5+
- dates
66
variations:
77
- title: "AP Date (standalone)"
88
regex: '^(January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$'

patterns/iso8601-date.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ featured: true
22
handle: iso8601-date
33
title: ISO 8601 Date Format
44
tags:
5-
- date
5+
- dates
66
todo: "Handle millis, missing separators, etc."
77
variations:
88
- title: "ISO 8601 Complete"

0 commit comments

Comments
 (0)