@zhin.js/plugin-rss 1.1.19 → 1.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -4
- package/commands/rss-add/[url]/index.js +1 -1
- package/commands/rss-add/[url]/index.ts +1 -1
- package/commands/rss-preview/[url]/index.js +1 -1
- package/commands/rss-preview/[url]/index.ts +1 -1
- package/lib/admin-i18n.d.ts +6 -2
- package/lib/admin-i18n.d.ts.map +1 -1
- package/lib/admin-i18n.js +4 -0
- package/lib/admin-i18n.js.map +1 -1
- package/lib/admin-tasks.d.ts +5 -1
- package/lib/admin-tasks.d.ts.map +1 -1
- package/lib/admin-tasks.js +30 -12
- package/lib/admin-tasks.js.map +1 -1
- package/lib/console.d.ts.map +1 -1
- package/lib/console.js +2 -0
- package/lib/console.js.map +1 -1
- package/lib/feed.d.ts +9 -1
- package/lib/feed.d.ts.map +1 -1
- package/lib/feed.js +27 -7
- package/lib/feed.js.map +1 -1
- package/lib/poll.d.ts +7 -2
- package/lib/poll.d.ts.map +1 -1
- package/lib/poll.js +56 -23
- package/lib/poll.js.map +1 -1
- package/lib/public-fetch.d.ts +34 -0
- package/lib/public-fetch.d.ts.map +1 -0
- package/lib/public-fetch.js +144 -0
- package/lib/public-fetch.js.map +1 -0
- package/package.json +14 -19
- package/plugin.js +4 -1
- package/schema.json +46 -3
- package/skills/rss/tools/rss_preview_feed/index.js +1 -1
- package/skills/rss/tools/rss_preview_feed/index.ts +1 -1
- package/src/admin-i18n.ts +4 -0
- package/src/admin-tasks.ts +13 -3
- package/src/console.ts +2 -0
- package/src/feed.ts +30 -7
- package/src/poll.ts +58 -23
- package/src/public-fetch.ts +160 -0
package/lib/poll.d.ts
CHANGED
|
@@ -4,8 +4,12 @@ import { type Locale } from 'zhin.js/i18n';
|
|
|
4
4
|
*/
|
|
5
5
|
import { type FeedItem } from './feed.js';
|
|
6
6
|
import type { RssRuntime } from './runtime.js';
|
|
7
|
+
/** Seen rows are kept this long after the item was last present in its feed. */
|
|
8
|
+
export declare const SEEN_RETENTION_MS: number;
|
|
7
9
|
export declare function formatNewItems(feedTitle: string, items: FeedItem[], maxItems: number, locale?: Locale): string;
|
|
8
|
-
export declare function markItemsSeen(runtime: RssRuntime, feedUrl: string, items: FeedItem[]
|
|
10
|
+
export declare function markItemsSeen(runtime: RssRuntime, feedUrl: string, items: FeedItem[],
|
|
11
|
+
/** Guids already stored for this feed; loaded once when the caller does not have them. */
|
|
12
|
+
knownGuids?: ReadonlySet<string>): Promise<void>;
|
|
9
13
|
export interface CheckResult {
|
|
10
14
|
readonly text: string;
|
|
11
15
|
readonly totalNew: number;
|
|
@@ -20,7 +24,8 @@ export declare function checkSubscriptions(runtime: RssRuntime, options: {
|
|
|
20
24
|
locale?: Locale;
|
|
21
25
|
}): Promise<CheckResult>;
|
|
22
26
|
/**
|
|
23
|
-
* Periodically clean seen records older than 7 days.
|
|
27
|
+
* Periodically clean seen records older than 7 days. Rows of items still listed in their
|
|
28
|
+
* feed are re-stamped by checkSubscriptions and therefore survive.
|
|
24
29
|
*/
|
|
25
30
|
export declare function cleanOldSeen(runtime: RssRuntime): Promise<void>;
|
|
26
31
|
/**
|
package/lib/poll.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE1D;;GAEG;AACH,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE1D;;GAEG;AACH,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,QAA0B,CAAC;AAezD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,MAAgB,GAAG,MAAM,CAYvH;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,UAAU,EACnB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,QAAQ,EAAE;AACjB,0FAA0F;AAC1F,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AA2CD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE;IACrE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,WAAW,CAAC,CAqDvB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBrE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAQ5E"}
|
package/lib/poll.js
CHANGED
|
@@ -6,9 +6,20 @@ import { rssMessages as t } from './i18n.js';
|
|
|
6
6
|
*/
|
|
7
7
|
import { fetchFeed } from './feed.js';
|
|
8
8
|
import { getRssSeen, getRssSubs } from './db-store.js';
|
|
9
|
+
/** Seen rows are kept this long after the item was last present in its feed. */
|
|
10
|
+
export const SEEN_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
+
/** Seen rows of items still in the feed are re-stamped at most this often. */
|
|
12
|
+
const SEEN_REFRESH_MS = 24 * 60 * 60 * 1000;
|
|
9
13
|
function ts() {
|
|
10
14
|
return new Date().toISOString();
|
|
11
15
|
}
|
|
16
|
+
/** True for an item whose own publish date is past the retention window. Undated items are never stale. */
|
|
17
|
+
function isStale(item, cutoffMs) {
|
|
18
|
+
if (!item.date)
|
|
19
|
+
return false;
|
|
20
|
+
const published = Date.parse(item.date);
|
|
21
|
+
return Number.isFinite(published) && published < cutoffMs;
|
|
22
|
+
}
|
|
12
23
|
export function formatNewItems(feedTitle, items, maxItems, locale = 'zh-CN') {
|
|
13
24
|
const lines = [t(locale, 'newItems', { title: feedTitle, count: items.length })];
|
|
14
25
|
for (const item of items.slice(0, maxItems)) {
|
|
@@ -24,20 +35,26 @@ export function formatNewItems(feedTitle, items, maxItems, locale = 'zh-CN') {
|
|
|
24
35
|
}
|
|
25
36
|
return lines.join('\n');
|
|
26
37
|
}
|
|
27
|
-
export async function markItemsSeen(runtime, feedUrl, items
|
|
38
|
+
export async function markItemsSeen(runtime, feedUrl, items,
|
|
39
|
+
/** Guids already stored for this feed; loaded once when the caller does not have them. */
|
|
40
|
+
knownGuids) {
|
|
28
41
|
const Seen = getRssSeen(runtime.db);
|
|
29
|
-
if (!Seen)
|
|
42
|
+
if (!Seen || items.length === 0)
|
|
30
43
|
return;
|
|
44
|
+
const known = new Set(knownGuids
|
|
45
|
+
?? (await Seen.select().where({ feed_url: feedUrl })).map((row) => String(row.item_guid ?? '')));
|
|
46
|
+
// One timestamp per batch so cleanup can drop the whole batch with a single delete.
|
|
47
|
+
const seenAt = ts();
|
|
31
48
|
for (const item of items) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
49
|
+
if (known.has(item.guid))
|
|
50
|
+
continue;
|
|
51
|
+
known.add(item.guid);
|
|
52
|
+
await Seen.insert({
|
|
53
|
+
feed_url: feedUrl,
|
|
54
|
+
item_guid: item.guid,
|
|
55
|
+
item_title: item.title,
|
|
56
|
+
seen_at: seenAt,
|
|
57
|
+
});
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
60
|
async function pushToSubscribers(input) {
|
|
@@ -94,15 +111,28 @@ export async function checkSubscriptions(runtime, options) {
|
|
|
94
111
|
let pushed = 0;
|
|
95
112
|
for (const url of options.urls) {
|
|
96
113
|
try {
|
|
97
|
-
const { title, items } = await fetchFeed(url, runtime.config
|
|
114
|
+
const { title, items } = await fetchFeed(url, runtime.config);
|
|
98
115
|
const seenRows = await Seen.select().where({ feed_url: url });
|
|
99
|
-
const
|
|
100
|
-
const
|
|
116
|
+
const seenAt = new Map(seenRows.map((r) => [String(r.item_guid ?? ''), String(r.seen_at ?? '')]));
|
|
117
|
+
const seenGuids = new Set(seenAt.keys());
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
// Keep the seen row of an item that is still listed, so it is not re-announced after cleanup.
|
|
120
|
+
const refreshBefore = new Date(now - SEEN_REFRESH_MS).toISOString();
|
|
121
|
+
const refreshed = ts();
|
|
122
|
+
for (const item of items) {
|
|
123
|
+
const stamp = seenAt.get(item.guid);
|
|
124
|
+
if (item.guid && stamp !== undefined && stamp < refreshBefore) {
|
|
125
|
+
await Seen.update({ seen_at: refreshed }).where({ feed_url: url, item_guid: item.guid });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// An item older than the retention window can no longer have a seen row; never treat it as new.
|
|
129
|
+
const newItems = items.filter((item) => item.guid && !seenGuids.has(item.guid)
|
|
130
|
+
&& !isStale(item, now - SEEN_RETENTION_MS));
|
|
101
131
|
if (newItems.length === 0) {
|
|
102
132
|
parts.push(t(locale, 'noNew', { title: title || url }));
|
|
103
133
|
continue;
|
|
104
134
|
}
|
|
105
|
-
await markItemsSeen(runtime, url, newItems);
|
|
135
|
+
await markItemsSeen(runtime, url, newItems, seenGuids);
|
|
106
136
|
totalNew += newItems.length;
|
|
107
137
|
const body = formatNewItems(title || url, newItems, runtime.config.maxItems, locale);
|
|
108
138
|
parts.push(body);
|
|
@@ -123,22 +153,25 @@ export async function checkSubscriptions(runtime, options) {
|
|
|
123
153
|
return { text: `${header}\n\n${parts.join('\n\n')}`, totalNew, pushed };
|
|
124
154
|
}
|
|
125
155
|
/**
|
|
126
|
-
* Periodically clean seen records older than 7 days.
|
|
156
|
+
* Periodically clean seen records older than 7 days. Rows of items still listed in their
|
|
157
|
+
* feed are re-stamped by checkSubscriptions and therefore survive.
|
|
127
158
|
*/
|
|
128
159
|
export async function cleanOldSeen(runtime) {
|
|
129
160
|
const Seen = getRssSeen(runtime.db);
|
|
130
161
|
if (!Seen)
|
|
131
162
|
return;
|
|
132
163
|
try {
|
|
133
|
-
const
|
|
134
|
-
cutoff.setDate(cutoff.getDate() - 7);
|
|
135
|
-
const cutoffStr = cutoff.toISOString();
|
|
164
|
+
const cutoffStr = new Date(Date.now() - SEEN_RETENTION_MS).toISOString();
|
|
136
165
|
const all = await Seen.select();
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
// The table has no id column. Rows of one batch share (feed_url, seen_at), so one delete per batch.
|
|
167
|
+
const batches = new Map();
|
|
168
|
+
for (const row of all) {
|
|
169
|
+
if (typeof row.seen_at !== 'string' || row.seen_at >= cutoffStr)
|
|
170
|
+
continue;
|
|
171
|
+
batches.set(`${String(row.feed_url)}\0${row.seen_at}`, { feed_url: row.feed_url, seen_at: row.seen_at });
|
|
141
172
|
}
|
|
173
|
+
for (const batch of batches.values())
|
|
174
|
+
await Seen.delete().where(batch);
|
|
142
175
|
}
|
|
143
176
|
catch (e) {
|
|
144
177
|
// Cleanup failure must not break the main flow, but it must be visible.
|
package/lib/poll.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poll.js","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAe,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,WAAW,IAAI,CAAC,EAAE,MAAM,WAAW,CAAC;AAC7C;;GAEG;AACH,OAAO,EAAE,SAAS,EAAiB,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGvD,SAAS,EAAE;IACT,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,KAAiB,EAAE,QAAgB,EAAE,SAAiB,OAAO;IAC7G,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACjF,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;QAC5C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QAC5B,IAAI,IAAI,CAAC,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,IAAI,CAAC,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,
|
|
1
|
+
{"version":3,"file":"poll.js","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare class BlockedUrlError extends Error {
|
|
2
|
+
readonly code = "blocked_url";
|
|
3
|
+
constructor(reason: string);
|
|
4
|
+
}
|
|
5
|
+
/** True when a literal IP address is globally routable and not a known host-internal endpoint. */
|
|
6
|
+
export declare function isPublicAddress(address: string): boolean;
|
|
7
|
+
/** Resolves a host name to every address it maps to. */
|
|
8
|
+
export type HostLookup = (hostname: string) => Promise<readonly string[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Parse `input` and refuse anything that is not an http(s) URL on a public host.
|
|
11
|
+
* Fails closed: an unresolvable name, or a name with any non-public address, is refused.
|
|
12
|
+
*/
|
|
13
|
+
export declare function assertPublicUrl(input: string | URL, lookup?: HostLookup, allowPrivateNetwork?: boolean): Promise<URL>;
|
|
14
|
+
export interface PublicFetchOptions {
|
|
15
|
+
/** Bounds every hop and the body read of the returned response. */
|
|
16
|
+
readonly timeoutMs?: number;
|
|
17
|
+
readonly maxRedirects?: number;
|
|
18
|
+
readonly init?: Omit<RequestInit, 'redirect' | 'signal'>;
|
|
19
|
+
/** Extra per-hop rule on top of the address policy (for example a host allow-list). */
|
|
20
|
+
readonly allow?: (url: URL) => boolean;
|
|
21
|
+
readonly lookup?: HostLookup;
|
|
22
|
+
/** Skip the public-address requirement. Only for an explicit operator setting; default false. */
|
|
23
|
+
readonly allowPrivateNetwork?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface PublicResponse {
|
|
26
|
+
readonly response: Response;
|
|
27
|
+
/** URL of the hop that produced `response`. */
|
|
28
|
+
readonly url: string;
|
|
29
|
+
}
|
|
30
|
+
/** Fetch with redirects followed by hand so that every hop passes the address policy first. */
|
|
31
|
+
export declare function fetchPublic(input: string, options?: PublicFetchOptions): Promise<PublicResponse>;
|
|
32
|
+
/** Read a body with a byte cap; the stream is cancelled and `undefined` returned once it is exceeded. */
|
|
33
|
+
export declare function readCappedBody(response: Response, maxBytes: number): Promise<Buffer | undefined>;
|
|
34
|
+
//# sourceMappingURL=public-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-fetch.d.ts","sourceRoot":"","sources":["../src/public-fetch.ts"],"names":[],"mappings":"AAcA,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,iBAAiB;gBAClB,MAAM,EAAE,MAAM;CAI3B;AAqBD,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAOxD;AAOD,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;AAK1E;;;GAGG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,GAAG,GAAG,EACnB,MAAM,GAAE,UAAyB,EACjC,mBAAmB,UAAQ,GAC1B,OAAO,CAAC,GAAG,CAAC,CA8Bd;AAED,MAAM,WAAW,kBAAkB;IACjC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAC,CAAC;IACzD,uFAAuF;IACvF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,iGAAiG;IACjG,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,+FAA+F;AAC/F,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,cAAc,CAAC,CAc1G;AAED,yGAAyG;AACzG,wBAAsB,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAqBtG"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound address policy for URLs that come from chat users or third-party pages (SSRF guard).
|
|
3
|
+
*
|
|
4
|
+
* This process connects to the target itself, so a URL is requested only when its host is a
|
|
5
|
+
* public name and every address it resolves to is public. Redirects are followed by hand and
|
|
6
|
+
* each hop is checked again. The check and the connection use separate DNS lookups, so a
|
|
7
|
+
* rebinding resolver can still race them; that needs a connection-level lookup hook.
|
|
8
|
+
*
|
|
9
|
+
* Plugins may not depend on each other, so this small module is duplicated in the plugins that
|
|
10
|
+
* fetch user-supplied URLs. Keep the copies identical.
|
|
11
|
+
*/
|
|
12
|
+
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
13
|
+
import { BlockList, isIP } from 'node:net';
|
|
14
|
+
export class BlockedUrlError extends Error {
|
|
15
|
+
code = 'blocked_url';
|
|
16
|
+
constructor(reason) {
|
|
17
|
+
super(`refusing to fetch: ${reason}`);
|
|
18
|
+
this.name = 'BlockedUrlError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const blockedV4 = new BlockList();
|
|
22
|
+
for (const [base, bits] of [
|
|
23
|
+
['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], ['169.254.0.0', 16],
|
|
24
|
+
['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24], ['192.88.99.0', 24], ['192.168.0.0', 16],
|
|
25
|
+
['198.18.0.0', 15], ['198.51.100.0', 24], ['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4],
|
|
26
|
+
])
|
|
27
|
+
blockedV4.addSubnet(base, bits, 'ipv4');
|
|
28
|
+
// Host-internal endpoints that sit in otherwise public space (Alibaba metadata, Azure WireServer).
|
|
29
|
+
for (const address of ['100.100.100.200', '168.63.129.16'])
|
|
30
|
+
blockedV4.addAddress(address, 'ipv4');
|
|
31
|
+
// IPv6 is an allow-list: global unicast only, minus the special ranges inside it.
|
|
32
|
+
const globalV6 = new BlockList();
|
|
33
|
+
globalV6.addSubnet('2000::', 3, 'ipv6');
|
|
34
|
+
const blockedV6 = new BlockList();
|
|
35
|
+
for (const [base, bits] of [
|
|
36
|
+
['2001::', 32], ['2001:2::', 48], ['2001:10::', 28], ['2001:db8::', 32], ['2002::', 16], ['3fff::', 20],
|
|
37
|
+
])
|
|
38
|
+
blockedV6.addSubnet(base, bits, 'ipv6');
|
|
39
|
+
const mappedV4 = new BlockList();
|
|
40
|
+
mappedV4.addSubnet('::ffff:0:0', 96, 'ipv6');
|
|
41
|
+
/** True when a literal IP address is globally routable and not a known host-internal endpoint. */
|
|
42
|
+
export function isPublicAddress(address) {
|
|
43
|
+
const family = isIP(address);
|
|
44
|
+
if (family === 4)
|
|
45
|
+
return !blockedV4.check(address, 'ipv4');
|
|
46
|
+
if (family !== 6)
|
|
47
|
+
return false;
|
|
48
|
+
// BlockList matches IPv4 rules against IPv4-mapped IPv6 addresses.
|
|
49
|
+
if (mappedV4.check(address, 'ipv6'))
|
|
50
|
+
return !blockedV4.check(address, 'ipv6');
|
|
51
|
+
return globalV6.check(address, 'ipv6') && !blockedV6.check(address, 'ipv6');
|
|
52
|
+
}
|
|
53
|
+
const BLOCKED_SUFFIXES = [
|
|
54
|
+
'localhost', 'local', 'localdomain', 'internal', 'intranet', 'private', 'corp', 'home', 'lan',
|
|
55
|
+
'arpa', 'test', 'invalid', 'example', 'onion', 'metadata.goog',
|
|
56
|
+
];
|
|
57
|
+
const systemLookup = async (hostname) => (await dnsLookup(hostname, { all: true, verbatim: true })).map((entry) => entry.address);
|
|
58
|
+
/**
|
|
59
|
+
* Parse `input` and refuse anything that is not an http(s) URL on a public host.
|
|
60
|
+
* Fails closed: an unresolvable name, or a name with any non-public address, is refused.
|
|
61
|
+
*/
|
|
62
|
+
export async function assertPublicUrl(input, lookup = systemLookup, allowPrivateNetwork = false) {
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = new URL(input);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new BlockedUrlError('not a valid URL');
|
|
69
|
+
}
|
|
70
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
71
|
+
throw new BlockedUrlError('only http and https are allowed');
|
|
72
|
+
if (url.username || url.password)
|
|
73
|
+
throw new BlockedUrlError('credentials in the URL are not allowed');
|
|
74
|
+
// Operator opt-in (never chat users): scheme and credential rules still apply, the address policy does not.
|
|
75
|
+
if (allowPrivateNetwork)
|
|
76
|
+
return url;
|
|
77
|
+
// The WHATWG parser already canonicalised odd IPv4 spellings (0x7f.1, 2130706433) to dotted quads.
|
|
78
|
+
const host = url.hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
|
|
79
|
+
if (isIP(host)) {
|
|
80
|
+
if (!isPublicAddress(host))
|
|
81
|
+
throw new BlockedUrlError(`${host} is not a public address`);
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
84
|
+
if (!host.includes('.') || BLOCKED_SUFFIXES.some((suffix) => host === suffix || host.endsWith(`.${suffix}`))) {
|
|
85
|
+
throw new BlockedUrlError(`${host} is not a public host name`);
|
|
86
|
+
}
|
|
87
|
+
let addresses;
|
|
88
|
+
try {
|
|
89
|
+
addresses = await lookup(host);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new BlockedUrlError(`${host} could not be resolved`);
|
|
93
|
+
}
|
|
94
|
+
if (addresses.length === 0)
|
|
95
|
+
throw new BlockedUrlError(`${host} could not be resolved`);
|
|
96
|
+
const internal = addresses.find((address) => !isPublicAddress(address));
|
|
97
|
+
if (internal !== undefined)
|
|
98
|
+
throw new BlockedUrlError(`${host} resolves to a non-public address`);
|
|
99
|
+
return url;
|
|
100
|
+
}
|
|
101
|
+
/** Fetch with redirects followed by hand so that every hop passes the address policy first. */
|
|
102
|
+
export async function fetchPublic(input, options = {}) {
|
|
103
|
+
const signal = AbortSignal.timeout(options.timeoutMs ?? 10_000);
|
|
104
|
+
const maxRedirects = options.maxRedirects ?? 5;
|
|
105
|
+
let current = input;
|
|
106
|
+
for (let hop = 0; hop <= maxRedirects; hop += 1) {
|
|
107
|
+
const url = await assertPublicUrl(current, options.lookup, options.allowPrivateNetwork);
|
|
108
|
+
if (options.allow && !options.allow(url))
|
|
109
|
+
throw new BlockedUrlError(`${url.hostname} is outside the allow-list`);
|
|
110
|
+
const response = await fetch(url.href, { ...options.init, redirect: 'manual', signal });
|
|
111
|
+
const location = response.status >= 300 && response.status < 400 ? response.headers.get('location') : null;
|
|
112
|
+
if (!location)
|
|
113
|
+
return { response, url: url.href };
|
|
114
|
+
await response.body?.cancel();
|
|
115
|
+
current = new URL(location, url).href;
|
|
116
|
+
}
|
|
117
|
+
throw new BlockedUrlError('too many redirects');
|
|
118
|
+
}
|
|
119
|
+
/** Read a body with a byte cap; the stream is cancelled and `undefined` returned once it is exceeded. */
|
|
120
|
+
export async function readCappedBody(response, maxBytes) {
|
|
121
|
+
const declared = Number(response.headers.get('content-length'));
|
|
122
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
123
|
+
await response.body?.cancel();
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
if (!response.body)
|
|
127
|
+
return Buffer.alloc(0);
|
|
128
|
+
const chunks = [];
|
|
129
|
+
let total = 0;
|
|
130
|
+
const reader = response.body.getReader();
|
|
131
|
+
for (;;) {
|
|
132
|
+
const { done, value } = await reader.read();
|
|
133
|
+
if (done)
|
|
134
|
+
break;
|
|
135
|
+
total += value.byteLength;
|
|
136
|
+
if (total > maxBytes) {
|
|
137
|
+
await reader.cancel();
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
chunks.push(value);
|
|
141
|
+
}
|
|
142
|
+
return Buffer.concat(chunks);
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=public-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-fetch.js","sourceRoot":"","sources":["../src/public-fetch.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-rss",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.20",
|
|
4
4
|
"description": "RSS/Atom feed subscription plugin for Zhin.js (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"pages",
|
|
20
20
|
"src",
|
|
21
21
|
"lib",
|
|
22
|
-
"tools",
|
|
23
22
|
"skills",
|
|
24
23
|
"README.md"
|
|
25
24
|
],
|
|
@@ -42,32 +41,28 @@
|
|
|
42
41
|
"license": "MIT",
|
|
43
42
|
"dependencies": {
|
|
44
43
|
"rss-parser": "^3.13.0",
|
|
45
|
-
"
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"@zhin.js/
|
|
48
|
-
"@zhin.js/
|
|
49
|
-
"@zhin.js/
|
|
44
|
+
"zod": "^4.6.7",
|
|
45
|
+
"@zhin.js/console-contract": "1.1.6",
|
|
46
|
+
"@zhin.js/im-contract": "1.1.14",
|
|
47
|
+
"@zhin.js/page": "1.1.18",
|
|
48
|
+
"@zhin.js/tool": "1.1.19",
|
|
49
|
+
"@zhin.js/skill": "1.1.17"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"typescript": "^6.0.3",
|
|
53
53
|
"vitest": "^4.1.10",
|
|
54
|
-
"
|
|
55
|
-
"zhin.js": "1.1.25"
|
|
54
|
+
"zhin.js": "1.1.26"
|
|
56
55
|
},
|
|
57
56
|
"peerDependencies": {
|
|
58
|
-
"
|
|
59
|
-
"@zhin.js/
|
|
60
|
-
"@zhin.js/
|
|
61
|
-
"@zhin.js/
|
|
62
|
-
"
|
|
63
|
-
"zhin.js": "^1.1.25"
|
|
57
|
+
"@zhin.js/command": "^1.2.0",
|
|
58
|
+
"@zhin.js/skill": "^1.1.17",
|
|
59
|
+
"@zhin.js/page": "^1.1.18",
|
|
60
|
+
"@zhin.js/tool": "^1.1.19",
|
|
61
|
+
"zhin.js": "^1.1.26"
|
|
64
62
|
},
|
|
65
63
|
"peerDependenciesMeta": {
|
|
66
64
|
"@zhin.js/command": {
|
|
67
65
|
"optional": true
|
|
68
|
-
},
|
|
69
|
-
"zod": {
|
|
70
|
-
"optional": true
|
|
71
66
|
}
|
|
72
67
|
},
|
|
73
68
|
"repository": {
|
|
@@ -111,6 +106,6 @@
|
|
|
111
106
|
"scripts": {
|
|
112
107
|
"build": "tsc",
|
|
113
108
|
"clean": "rimraf lib",
|
|
114
|
-
"test": "
|
|
109
|
+
"test": "vitest run --root ../../.. plugins/utils/rss/tests"
|
|
115
110
|
}
|
|
116
111
|
}
|
package/plugin.js
CHANGED
|
@@ -78,6 +78,9 @@ export default definePlugin({
|
|
|
78
78
|
throw new DataMigrationError('unavailable', 'Historical RSS subscription awaits exact-target binding');
|
|
79
79
|
if (!outbound.sendTarget)
|
|
80
80
|
throw new Error('Canonical subscription delivery requires an exact-target Outbound Host');
|
|
81
|
+
// ADR 0050 / 0055: pushes are not messages, so the plugin checks its own switch per target conversation.
|
|
82
|
+
if (!context.isEnabledFor({ conversation: input.target }))
|
|
83
|
+
return;
|
|
81
84
|
const run = async () => { await outbound.sendTarget(input.target, input.content, { locale: input.locale }); };
|
|
82
85
|
if (migrationScopes)
|
|
83
86
|
await migrationScopes.run({ scope: subscriptionMigrationScope(input), run });
|
|
@@ -112,7 +115,7 @@ export default definePlugin({
|
|
|
112
115
|
}
|
|
113
116
|
catch { /* No persistent state port: no administrator provider. */ }
|
|
114
117
|
if (state) {
|
|
115
|
-
administrator = new AdministratorRssTasks(createAdministratorRssState(database), outbound, config, undefined, runtime.locale);
|
|
118
|
+
administrator = new AdministratorRssTasks(createAdministratorRssState(database), outbound, config, undefined, runtime.locale, target => context.isEnabledFor({ conversation: target }));
|
|
116
119
|
context.lifecycle.add(() => administrator?.stop());
|
|
117
120
|
}
|
|
118
121
|
}
|
package/schema.json
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"type": "object",
|
|
4
4
|
"additionalProperties": false,
|
|
5
|
+
"x-zhin-config": {
|
|
6
|
+
"levels": [
|
|
7
|
+
"global",
|
|
8
|
+
"adapter",
|
|
9
|
+
"endpoint",
|
|
10
|
+
"conversation"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
5
13
|
"properties": {
|
|
6
14
|
"pollCron": {
|
|
7
15
|
"type": "string",
|
|
@@ -11,25 +19,60 @@
|
|
|
11
19
|
"en": {
|
|
12
20
|
"description": "Polling schedule (6-field cron expression)."
|
|
13
21
|
}
|
|
22
|
+
},
|
|
23
|
+
"x-zhin-config": {
|
|
24
|
+
"levels": [
|
|
25
|
+
"global"
|
|
26
|
+
]
|
|
14
27
|
}
|
|
15
28
|
},
|
|
16
29
|
"maxPerGroup": {
|
|
17
30
|
"type": "number",
|
|
18
31
|
"default": 30,
|
|
19
32
|
"minimum": 1,
|
|
20
|
-
"maximum": 200
|
|
33
|
+
"maximum": 200,
|
|
34
|
+
"x-zhin-config": {
|
|
35
|
+
"levels": [
|
|
36
|
+
"global"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
21
39
|
},
|
|
22
40
|
"maxItems": {
|
|
23
41
|
"type": "number",
|
|
24
42
|
"default": 5,
|
|
25
43
|
"minimum": 1,
|
|
26
|
-
"maximum": 20
|
|
44
|
+
"maximum": 20,
|
|
45
|
+
"x-zhin-config": {
|
|
46
|
+
"levels": [
|
|
47
|
+
"global"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
27
50
|
},
|
|
28
51
|
"timeout": {
|
|
29
52
|
"type": "number",
|
|
30
53
|
"default": 15000,
|
|
31
54
|
"minimum": 1000,
|
|
32
|
-
"maximum": 60000
|
|
55
|
+
"maximum": 60000,
|
|
56
|
+
"x-zhin-config": {
|
|
57
|
+
"levels": [
|
|
58
|
+
"global"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"allowPrivateNetwork": {
|
|
63
|
+
"type": "boolean",
|
|
64
|
+
"default": false,
|
|
65
|
+
"description": "允许订阅回环/内网地址上的 feed(如自建 RSSHub)。默认拒绝,因为订阅地址来自聊天用户。",
|
|
66
|
+
"x-i18n": {
|
|
67
|
+
"en": {
|
|
68
|
+
"description": "Allow feeds on loopback/LAN addresses (for example a self-hosted RSSHub). Refused by default because feed URLs come from chat users."
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"x-zhin-config": {
|
|
72
|
+
"levels": [
|
|
73
|
+
"global"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
33
76
|
}
|
|
34
77
|
}
|
|
35
78
|
}
|
|
@@ -12,7 +12,7 @@ export default defineAgentTool({
|
|
|
12
12
|
return t(context.locale, 'urlRequired');
|
|
13
13
|
try {
|
|
14
14
|
const runtime = context.use(rssRuntimeToken);
|
|
15
|
-
const { title, items } = await fetchFeed(url, runtime.config
|
|
15
|
+
const { title, items } = await fetchFeed(url, runtime.config);
|
|
16
16
|
if (items.length === 0)
|
|
17
17
|
return t(context.locale, 'toolPreviewEmpty', { title });
|
|
18
18
|
return items
|
|
@@ -11,7 +11,7 @@ export default defineAgentTool<{ url: string }>({
|
|
|
11
11
|
if (!url) return t(context.locale, 'urlRequired');
|
|
12
12
|
try {
|
|
13
13
|
const runtime = context.use(rssRuntimeToken);
|
|
14
|
-
const { title, items } = await fetchFeed(url, runtime.config
|
|
14
|
+
const { title, items } = await fetchFeed(url, runtime.config);
|
|
15
15
|
if (items.length === 0) return t(context.locale, 'toolPreviewEmpty', { title });
|
|
16
16
|
return items
|
|
17
17
|
.slice(0, 5)
|
package/src/admin-i18n.ts
CHANGED
|
@@ -146,6 +146,10 @@ const catalog = {
|
|
|
146
146
|
"zh-CN": "平台未确认投递,请核实目标会话后取消并另建订阅;不会自动重试",
|
|
147
147
|
"en": "The platform did not confirm delivery. Check the target conversation, then cancel and create another subscription; no automatic retry."
|
|
148
148
|
},
|
|
149
|
+
"featureDisabled": {
|
|
150
|
+
"zh-CN": "RSS 已在目标会话停用(功能开关),本轮未投递;重新启用后从后续新文章开始推送",
|
|
151
|
+
"en": "RSS is switched off for the target conversation (feature switch); nothing was delivered this poll. Future articles are delivered once it is re-enabled."
|
|
152
|
+
},
|
|
149
153
|
"delivered": {
|
|
150
154
|
"zh-CN": "已确认投递;等待后续新文章",
|
|
151
155
|
"en": "Delivery confirmed; waiting for future articles."
|