Skip to content

Commit 3c2d4d6

Browse files
authored
New Components - lobste.rs (#14999)
* new-components * pnpm-lock.yaml * pnpm-lock.yaml * pnpm-lock.yaml * typos
1 parent 9c2e2e9 commit 3c2d4d6

File tree

5 files changed

+219
-6
lines changed

5 files changed

+219
-6
lines changed
Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,111 @@
1+
import { axios } from "@pipedream/platform";
2+
import FeedParser from "feedparser";
3+
import hash from "object-hash";
4+
15
export default {
26
type: "app",
37
app: "lobste_rs",
48
propDefinitions: {},
59
methods: {
6-
// this.$auth contains connected account data
7-
authKeys() {
8-
console.log(Object.keys(this.$auth));
10+
makeRequest({
11+
$ = this, ...config
12+
}) {
13+
return axios($, config);
14+
},
15+
itemTs(item = {}) {
16+
const {
17+
pubdate, pubDate, date_published,
18+
} = item;
19+
const itemPubDate = pubdate ?? pubDate ?? date_published;
20+
if (itemPubDate) {
21+
return +new Date(itemPubDate);
22+
}
23+
return +new Date();
24+
},
25+
itemKey(item = {}) {
26+
const {
27+
id, guid, link, title,
28+
} = item;
29+
const itemId = id ?? guid ?? link ?? title;
30+
if (itemId) {
31+
// reduce itemId length for deduping
32+
return itemId.length > 64
33+
? itemId.slice(-64)
34+
: itemId;
35+
}
36+
return hash(item);
37+
},
38+
async fetchFeed(url) {
39+
const res = await axios(this, {
40+
url,
41+
method: "GET",
42+
headers: {
43+
"accept": "text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8, application/json, application/feed+json",
44+
},
45+
responseType: "stream", // stream is required for feedparser
46+
returnFullResponse: true,
47+
});
48+
return {
49+
data: res.data,
50+
contentType: res.headers["content-type"],
51+
};
52+
},
53+
async parseFeed(stream) {
54+
const feedparser = new FeedParser({
55+
addmeta: true,
56+
});
57+
const items = [];
58+
await new Promise((resolve, reject) => {
59+
feedparser.on("error", reject);
60+
feedparser.on("end", resolve);
61+
feedparser.on("readable", function () {
62+
let item = this.read();
63+
64+
while (item) {
65+
for (const k in item) {
66+
if (item[`rss:${k}`]) {
67+
delete item[`rss:${k}`];
68+
continue;
69+
}
70+
const o = item[k];
71+
if (o == null || (typeof o === "object" && !Object.keys(o).length) || Array.isArray(o) && !o.length) {
72+
delete item[k];
73+
continue;
74+
}
75+
}
76+
items.push(item);
77+
item = this.read();
78+
}
79+
});
80+
stream.pipe(feedparser);
81+
});
82+
return items;
83+
},
84+
isJSONFeed(response) {
85+
const acceptedJsonFeedMimes = [
86+
"application/feed+json",
87+
"application/json",
88+
];
89+
return acceptedJsonFeedMimes.includes(response?.contentType?.toLowerCase());
90+
},
91+
async parseJSONFeed(stream) {
92+
const buffer = await new Promise((resolve, reject) => {
93+
const _buf = [];
94+
stream.on("data", (chunk) => _buf.push(chunk));
95+
stream.on("end", () => resolve(Buffer.concat(_buf)));
96+
stream.on("error", (err) => reject(err));
97+
});
98+
const contentString = buffer.toString();
99+
const feed = JSON.parse(contentString);
100+
return feed?.items || [];
101+
},
102+
async fetchAndParseFeed(url) {
103+
const response = await this.fetchFeed(url);
104+
if (this.isJSONFeed(response)) {
105+
return await this.parseJSONFeed(response.data);
106+
} else {
107+
return await this.parseFeed(response.data);
108+
}
9109
},
10110
},
11111
};

components/lobste_rs/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pipedream/lobste_rs",
3-
"version": "0.0.1",
3+
"version": "0.1.0",
44
"description": "Pipedream lobste.rs Components",
55
"main": "lobste_rs.app.mjs",
66
"keywords": [
@@ -11,5 +11,10 @@
1111
"author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
1212
"publishConfig": {
1313
"access": "public"
14+
},
15+
"dependencies": {
16+
"@pipedream/platform": "^3.0.3",
17+
"feedparser": "^2.2.10",
18+
"object-hash": "^3.0.0"
1419
}
15-
}
20+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import lobsters from "../../lobste_rs.app.mjs";
2+
import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform";
3+
4+
export default {
5+
key: "lobste_rs-new-comment-in-thread",
6+
name: "New Comment in Thread",
7+
description: "Emit new event when a new comment is added to a thread.",
8+
version: "0.0.1",
9+
type: "source",
10+
dedupe: "unique",
11+
props: {
12+
lobsters,
13+
timer: {
14+
type: "$.interface.timer",
15+
default: {
16+
intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL,
17+
},
18+
},
19+
url: {
20+
type: "string",
21+
label: "URL",
22+
description: "The URL of the comment thread to retrieve. E.g. `https://lobste.rs/s/yqjtvy/cloud_container_iceberg`",
23+
},
24+
},
25+
methods: {
26+
generateMeta(comment) {
27+
return {
28+
id: comment.short_id,
29+
summary: comment.comment_plain.substring(0, 50),
30+
ts: Date.parse(comment.created_at),
31+
};
32+
},
33+
},
34+
async run() {
35+
const { comments } = await this.lobsters.makeRequest({
36+
url: `${this.url}.json`,
37+
});
38+
for (const comment of comments.reverse()) {
39+
const meta = this.generateMeta(comment);
40+
this.$emit(comment, meta);
41+
}
42+
},
43+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import lobsters from "../../lobste_rs.app.mjs";
2+
import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform";
3+
4+
export default {
5+
key: "lobste_rs-new-story-by-user",
6+
name: "New Story by User",
7+
description: "Emit new event when a new story is posted by the specified user.",
8+
version: "0.0.1",
9+
type: "source",
10+
dedupe: "unique",
11+
props: {
12+
lobsters,
13+
timer: {
14+
type: "$.interface.timer",
15+
default: {
16+
intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL,
17+
},
18+
},
19+
username: {
20+
type: "string",
21+
label: "Username",
22+
description: "The user to watch for stories from. E.g. `adamgordonbell`",
23+
},
24+
publishedAfter: {
25+
type: "string",
26+
label: "Published After",
27+
description: "Emit items published after the specified date in ISO 8601 format .e.g `2022-12-07T12:57:10+07:00`",
28+
default: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
29+
},
30+
},
31+
methods: {
32+
generateMeta(item) {
33+
return {
34+
id: this.lobsters.itemKey(item),
35+
summary: item.title,
36+
ts: Date.now(),
37+
};
38+
},
39+
},
40+
async run() {
41+
const url = `https://lobste.rs/~${this.username}/stories.rss`;
42+
43+
const items = await this.lobsters.fetchAndParseFeed(url);
44+
for (const item of items.reverse()) {
45+
const publishedAfter = +new Date(this.publishedAfter);
46+
const ts = this.lobsters.itemTs(item);
47+
if (Number.isNaN(publishedAfter) || publishedAfter > ts) {
48+
continue;
49+
}
50+
51+
const meta = this.generateMeta(item);
52+
this.$emit(item, meta);
53+
}
54+
},
55+
};

pnpm-lock.yaml

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)