reddit-mcp-server 1.5.1 → 1.5.2
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/dist/bin.d.ts +1 -1
- package/dist/bin.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +132 -39
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/dist/bin.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/bin.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
process.env.TRANSPORT_TYPE ??= "stdio";
|
|
4
4
|
const args = process.argv.slice(2);
|
|
5
5
|
if (args.includes("--version") || args.includes("-v")) {
|
|
6
|
-
console.log("1.5.
|
|
6
|
+
console.log("1.5.2");
|
|
7
7
|
process.exit(0);
|
|
8
8
|
}
|
|
9
9
|
if (args.includes("--help") || args.includes("-h")) {
|
|
10
10
|
console.log(`
|
|
11
|
-
Reddit MCP Server v1.5.
|
|
11
|
+
Reddit MCP Server v1.5.2
|
|
12
12
|
|
|
13
13
|
Usage: reddit-mcp-server [options]
|
|
14
14
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,21 @@ var NotFoundError = class extends RedditErrorBase {
|
|
|
59
59
|
this.name = "NotFoundError";
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Reddit refused the request at the network level rather than for this specific resource.
|
|
64
|
+
*
|
|
65
|
+
* Reddit 403s the unauthenticated JSON API from many IP ranges (datacenters, VPNs, flagged
|
|
66
|
+
* addresses), answering with an HTML block page instead of JSON. A bare `HttpError(403)` reads
|
|
67
|
+
* as "this subreddit is private", which sends people looking in the wrong place — the fix is to
|
|
68
|
+
* supply OAuth credentials, which also raises the rate limit from ~10 to 60+ req/min.
|
|
69
|
+
*/
|
|
70
|
+
var NetworkBlockedError = class extends RedditErrorBase {
|
|
71
|
+
_tag = "NetworkBlockedError";
|
|
72
|
+
constructor(message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "NetworkBlockedError";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
62
77
|
/** Client-side input or safety-policy rejection (invalid sort, duplicate-content guard). */
|
|
63
78
|
var ValidationError = class extends RedditErrorBase {
|
|
64
79
|
_tag = "ValidationError";
|
|
@@ -90,6 +105,71 @@ function classifyRedditError(error, context) {
|
|
|
90
105
|
return new UnknownError(Option(context).fold(() => error.message, (ctx) => `${ctx}: ${error.message}`), error);
|
|
91
106
|
}
|
|
92
107
|
//#endregion
|
|
108
|
+
//#region src/utils/reddit-identifiers.ts
|
|
109
|
+
/**
|
|
110
|
+
* Validation and normalization for Reddit identifiers that get interpolated into API paths.
|
|
111
|
+
*
|
|
112
|
+
* Every identifier reaching the client is model- or user-supplied. Interpolating one raw is a
|
|
113
|
+
* path-injection hole: URL parsing resolves dot segments, so a `subreddit` of `../../api/v1/me`
|
|
114
|
+
* escapes `/r/{sub}/about.json` and steers the OAuth bearer token to a different endpoint, and an
|
|
115
|
+
* embedded `?` or `&` injects query parameters into the request we build.
|
|
116
|
+
*
|
|
117
|
+
* So each identifier is normalized (stripping the `r/`, `/u/`, `t3_` prefixes people naturally
|
|
118
|
+
* type) and then checked against Reddit's own charset. Every value returned from this module
|
|
119
|
+
* matches `[A-Za-z0-9_+-]+`, which is already URL-path-safe — no percent-encoding is applied,
|
|
120
|
+
* because encoding the `+` in a multireddit (`r/science+space`) would break it.
|
|
121
|
+
*/
|
|
122
|
+
const SUBREDDIT_PATTERN = /^(u_)?[A-Za-z0-9][A-Za-z0-9_]{1,20}$/;
|
|
123
|
+
const USERNAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{1,19}$/;
|
|
124
|
+
const THING_ID_PATTERN = /^[a-z0-9]{1,13}$/;
|
|
125
|
+
const stripLeading = (value, prefixes) => {
|
|
126
|
+
const lower = value.toLowerCase();
|
|
127
|
+
const matched = prefixes.find((prefix) => lower.startsWith(prefix));
|
|
128
|
+
return matched === void 0 ? value : value.slice(matched.length);
|
|
129
|
+
};
|
|
130
|
+
const bare = (value, prefixes) => stripLeading(stripLeading(value.trim(), ["/"]), prefixes).replace(/\/+$/, "");
|
|
131
|
+
/**
|
|
132
|
+
* Normalize a subreddit name for use in a path segment.
|
|
133
|
+
*
|
|
134
|
+
* Accepts `science`, `r/science`, `/r/science`, and `+`-joined multireddits (`science+space`).
|
|
135
|
+
* The empty string passes through unchanged — callers use it to mean "the home feed".
|
|
136
|
+
*/
|
|
137
|
+
function normalizeSubreddit(input) {
|
|
138
|
+
const trimmed = input.trim();
|
|
139
|
+
if (trimmed === "") return "";
|
|
140
|
+
const segments = bare(trimmed, ["r/"]).split("+").map((segment) => segment.trim());
|
|
141
|
+
if (segments.find((segment) => !SUBREDDIT_PATTERN.test(segment)) !== void 0) throw new ValidationError(`Invalid subreddit name "${input}". Expected 2-21 characters of letters, digits, or underscores (e.g. "science").`);
|
|
142
|
+
return segments.join("+");
|
|
143
|
+
}
|
|
144
|
+
/** Normalize a username for use in a path segment. Accepts `spez`, `u/spez`, `/user/spez`. */
|
|
145
|
+
function normalizeUsername(input) {
|
|
146
|
+
const name = bare(input, ["user/", "u/"]);
|
|
147
|
+
if (!USERNAME_PATTERN.test(name)) throw new ValidationError(`Invalid Reddit username "${input}". Expected 2-20 characters of letters, digits, underscores, or hyphens (e.g. "spez").`);
|
|
148
|
+
return name;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Normalize a post or comment ID to its bare base36 form, dropping any `t1_`/`t3_` fullname
|
|
152
|
+
* prefix. Use this wherever the ID lands in a path segment or query value.
|
|
153
|
+
*/
|
|
154
|
+
function normalizeThingId(input) {
|
|
155
|
+
const id = bare(input, [
|
|
156
|
+
"t1_",
|
|
157
|
+
"t3_",
|
|
158
|
+
"t4_",
|
|
159
|
+
"t5_"
|
|
160
|
+
]).toLowerCase();
|
|
161
|
+
if (!THING_ID_PATTERN.test(id)) throw new ValidationError(`Invalid Reddit ID "${input}". Expected a base36 id such as "1abc2de", optionally prefixed with t1_ or t3_.`);
|
|
162
|
+
return id;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Normalize an ID to a Reddit fullname (`t3_1abc2de`), preserving an explicit `t1_`/`t3_` prefix
|
|
166
|
+
* and falling back to `defaultKind` when the caller passed a bare ID.
|
|
167
|
+
*/
|
|
168
|
+
function normalizeFullname(input, defaultKind) {
|
|
169
|
+
const trimmed = input.trim().toLowerCase();
|
|
170
|
+
return `${trimmed.startsWith("t1_") ? "t1" : trimmed.startsWith("t3_") ? "t3" : defaultKind}_${normalizeThingId(input)}`;
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
93
173
|
//#region src/client/response-cache.ts
|
|
94
174
|
const SECOND = 1e3;
|
|
95
175
|
var ResponseCache = class {
|
|
@@ -160,6 +240,10 @@ function listingCursor(data) {
|
|
|
160
240
|
...before
|
|
161
241
|
};
|
|
162
242
|
}
|
|
243
|
+
function headerValue(response, name) {
|
|
244
|
+
const headers = response.headers;
|
|
245
|
+
return (headers === null || headers === void 0 ? void 0 : headers.get(name)) ?? "";
|
|
246
|
+
}
|
|
163
247
|
function parsePostData(post) {
|
|
164
248
|
return {
|
|
165
249
|
id: post.id,
|
|
@@ -254,6 +338,7 @@ var RedditClient = class {
|
|
|
254
338
|
...headers,
|
|
255
339
|
Authorization: await this.reauthorize()
|
|
256
340
|
}, path, 0) : first;
|
|
341
|
+
if (!requiresAuth && response.status === 403 && !headerValue(response, "content-type").includes("json")) throw new NetworkBlockedError("Reddit is blocking unauthenticated requests from this network (HTTP 403 with a block page). Set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET to authenticate with OAuth, which also raises the rate limit from ~10 to 60+ requests/min. See https://www.reddit.com/prefs/apps to create an app.");
|
|
257
342
|
if (cacheable && response.ok) {
|
|
258
343
|
const text = await response.text();
|
|
259
344
|
this.cache.set(url, text, response.status);
|
|
@@ -381,7 +466,7 @@ var RedditClient = class {
|
|
|
381
466
|
async getUser(username) {
|
|
382
467
|
const context = `Failed to get user info for ${username}`;
|
|
383
468
|
return (await Try.async(async () => {
|
|
384
|
-
const response = (await this.makeRequest(`/user/${username}/about.json`)).orThrow();
|
|
469
|
+
const response = (await this.makeRequest(`/user/${normalizeUsername(username)}/about.json`)).orThrow();
|
|
385
470
|
if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
|
|
386
471
|
const { data } = await response.json();
|
|
387
472
|
return {
|
|
@@ -431,14 +516,14 @@ var RedditClient = class {
|
|
|
431
516
|
const { limit = 25, after } = options;
|
|
432
517
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
433
518
|
if (after !== void 0) params.set("after", after);
|
|
434
|
-
return this.getUserContent(`/user/${this.username}/overview.json?${params}`, "Failed to get your overview");
|
|
519
|
+
return this.getUserContent(`/user/${encodeURIComponent(this.username)}/overview.json?${params}`, "Failed to get your overview");
|
|
435
520
|
}
|
|
436
521
|
async getMySaved(options = {}) {
|
|
437
522
|
if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching saved content requires REDDIT_USERNAME"));
|
|
438
523
|
const { limit = 25, after } = options;
|
|
439
524
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
440
525
|
if (after !== void 0) params.set("after", after);
|
|
441
|
-
return this.getUserContent(`/user/${this.username}/saved.json?${params}`, "Failed to get saved content");
|
|
526
|
+
return this.getUserContent(`/user/${encodeURIComponent(this.username)}/saved.json?${params}`, "Failed to get saved content");
|
|
442
527
|
}
|
|
443
528
|
async getMe() {
|
|
444
529
|
if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching your account requires REDDIT_USERNAME"));
|
|
@@ -464,7 +549,7 @@ var RedditClient = class {
|
|
|
464
549
|
async getSubredditInfo(subredditName) {
|
|
465
550
|
const context = `Failed to get subreddit info for ${subredditName}`;
|
|
466
551
|
return (await Try.async(async () => {
|
|
467
|
-
const response = (await this.makeRequest(`/r/${subredditName}/about.json`)).orThrow();
|
|
552
|
+
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subredditName)}/about.json`)).orThrow();
|
|
468
553
|
if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
|
|
469
554
|
const { data } = await response.json();
|
|
470
555
|
return {
|
|
@@ -484,7 +569,7 @@ var RedditClient = class {
|
|
|
484
569
|
async getSubredditRules(subreddit) {
|
|
485
570
|
const context = `Failed to get rules for r/${subreddit}`;
|
|
486
571
|
return (await Try.async(async () => {
|
|
487
|
-
const response = (await this.makeRequest(`/r/${subreddit}/about/rules.json`)).orThrow();
|
|
572
|
+
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/about/rules.json`)).orThrow();
|
|
488
573
|
if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
|
|
489
574
|
return (await response.json()).rules.map((rule) => ({
|
|
490
575
|
shortName: rule.short_name,
|
|
@@ -499,7 +584,7 @@ var RedditClient = class {
|
|
|
499
584
|
async getPostFlairs(subreddit) {
|
|
500
585
|
const context = `Failed to get post flairs for r/${subreddit}`;
|
|
501
586
|
return (await Try.async(async () => {
|
|
502
|
-
const response = (await this.makeRequest(`/r/${subreddit}/api/link_flair_v2.json`)).orThrow();
|
|
587
|
+
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/api/link_flair_v2.json`)).orThrow();
|
|
503
588
|
if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
|
|
504
589
|
return (await response.json()).map((flair) => ({
|
|
505
590
|
id: flair.id,
|
|
@@ -510,7 +595,6 @@ var RedditClient = class {
|
|
|
510
595
|
})).toEither((error) => classifyRedditError(error, context));
|
|
511
596
|
}
|
|
512
597
|
async getTopPosts(subreddit, timeFilter = "week", limit = 10, after) {
|
|
513
|
-
const endpoint = subreddit !== "" ? `/r/${subreddit}/top.json` : "/top.json";
|
|
514
598
|
const params = new URLSearchParams({
|
|
515
599
|
t: timeFilter,
|
|
516
600
|
limit: limit.toString()
|
|
@@ -518,6 +602,8 @@ var RedditClient = class {
|
|
|
518
602
|
if (after !== void 0) params.set("after", after);
|
|
519
603
|
const context = `Failed to get top posts for ${subreddit !== "" ? subreddit : "home"}`;
|
|
520
604
|
return (await Try.async(async () => {
|
|
605
|
+
const name = normalizeSubreddit(subreddit);
|
|
606
|
+
const endpoint = name !== "" ? `/r/${name}/top.json` : "/top.json";
|
|
521
607
|
const response = (await this.makeRequest(`${endpoint}?${params}`)).orThrow();
|
|
522
608
|
if (!response.ok) throw new HttpError(response.status, `Failed to get top posts: HTTP ${response.status}`);
|
|
523
609
|
const json = await response.json();
|
|
@@ -536,13 +622,14 @@ var RedditClient = class {
|
|
|
536
622
|
"controversial"
|
|
537
623
|
];
|
|
538
624
|
if (!validSorts.includes(sort)) return Left(new ValidationError(`Invalid sort "${sort}". Valid options are: ${validSorts.join(", ")}`));
|
|
539
|
-
const endpoint = subreddit !== "" ? `/r/${subreddit}/${sort}.json` : `/${sort}.json`;
|
|
540
625
|
const params = new URLSearchParams({ limit: limit.toString() });
|
|
541
626
|
if (sort === "top" || sort === "controversial") params.set("t", timeFilter);
|
|
542
627
|
if (after !== void 0) params.set("after", after);
|
|
543
628
|
const home = subreddit !== "" ? subreddit : "home";
|
|
544
629
|
const context = `Failed to browse r/${home} (${sort})`;
|
|
545
630
|
return (await Try.async(async () => {
|
|
631
|
+
const name = normalizeSubreddit(subreddit);
|
|
632
|
+
const endpoint = name !== "" ? `/r/${name}/${sort}.json` : `/${sort}.json`;
|
|
546
633
|
const response = (await this.makeRequest(`${endpoint}?${params}`)).orThrow();
|
|
547
634
|
if (!response.ok) throw new HttpError(response.status, `Failed to browse r/${home}: HTTP ${response.status}`);
|
|
548
635
|
const json = await response.json();
|
|
@@ -553,9 +640,10 @@ var RedditClient = class {
|
|
|
553
640
|
})).toEither((error) => classifyRedditError(error, context));
|
|
554
641
|
}
|
|
555
642
|
async getPost(postId, subreddit) {
|
|
556
|
-
const endpoint = Option(subreddit).fold(() => `/api/info.json?id=t3_${postId}`, (sr) => `/r/${sr}/comments/${postId}.json`);
|
|
557
643
|
const context = `Failed to get post with ID ${postId}`;
|
|
558
644
|
return (await Try.async(async () => {
|
|
645
|
+
const id = normalizeThingId(postId);
|
|
646
|
+
const endpoint = Option(subreddit).fold(() => `/api/info.json?id=t3_${id}`, (sr) => `/r/${normalizeSubreddit(sr)}/comments/${id}.json`);
|
|
559
647
|
const response = (await this.makeRequest(endpoint)).orThrow();
|
|
560
648
|
if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
|
|
561
649
|
if (subreddit !== void 0) return parsePostData((await response.json())[0].data.children[0].data);
|
|
@@ -579,10 +667,12 @@ var RedditClient = class {
|
|
|
579
667
|
this.validateWriteAccess();
|
|
580
668
|
await this.enforceWriteRateLimit();
|
|
581
669
|
this.checkDuplicateContent(title + content, subreddit);
|
|
670
|
+
const targetSubreddit = normalizeSubreddit(subreddit);
|
|
671
|
+
if (targetSubreddit === "") throw new ValidationError("A subreddit is required to create a post.");
|
|
582
672
|
const finalContent = isSelf ? this.appendBotDisclosure(content) : content;
|
|
583
673
|
const kind = isSelf ? "self" : "link";
|
|
584
674
|
const params = new URLSearchParams();
|
|
585
|
-
params.append("sr",
|
|
675
|
+
params.append("sr", targetSubreddit);
|
|
586
676
|
params.append("kind", kind);
|
|
587
677
|
params.append("title", title);
|
|
588
678
|
params.append(isSelf ? "text" : "url", finalContent);
|
|
@@ -599,12 +689,12 @@ var RedditClient = class {
|
|
|
599
689
|
if (json.json.errors !== void 0 && json.json.errors.length > 0) throw new ApiError(`Reddit API errors: ${json.json.errors.map((e) => e[1]).join(", ")}`);
|
|
600
690
|
const postId = ((_json$json$data = json.json.data) === null || _json$json$data === void 0 ? void 0 : _json$json$data.id) ?? ((_json$json$data2 = json.json.data) === null || _json$json$data2 === void 0 || (_json$json$data2 = _json$json$data2.name) === null || _json$json$data2 === void 0 ? void 0 : _json$json$data2.replace("t3_", ""));
|
|
601
691
|
if (postId === void 0) throw new ApiError("No post ID returned from Reddit");
|
|
602
|
-
return (await this.getPost(postId,
|
|
692
|
+
return (await this.getPost(postId, targetSubreddit)).orThrow();
|
|
603
693
|
})).toEither((error) => classifyRedditError(error));
|
|
604
694
|
}
|
|
605
695
|
async checkPostExists(postId) {
|
|
606
696
|
return (await Try.async(async () => {
|
|
607
|
-
const response = (await this.makeRequest(`/api/info.json?id=t3_${postId}`)).orThrow();
|
|
697
|
+
const response = (await this.makeRequest(`/api/info.json?id=t3_${normalizeThingId(postId)}`)).orThrow();
|
|
608
698
|
if (!response.ok) return false;
|
|
609
699
|
return (await response.json()).data.children.length > 0;
|
|
610
700
|
})).orElse(false);
|
|
@@ -616,9 +706,9 @@ var RedditClient = class {
|
|
|
616
706
|
await this.enforceWriteRateLimit();
|
|
617
707
|
this.checkDuplicateContent(content);
|
|
618
708
|
const finalContent = this.appendBotDisclosure(content);
|
|
619
|
-
const fullThingId = postId
|
|
620
|
-
if (!
|
|
621
|
-
if (!await this.checkPostExists(postId
|
|
709
|
+
const fullThingId = normalizeFullname(postId, "t3");
|
|
710
|
+
if (!fullThingId.startsWith("t1_")) {
|
|
711
|
+
if (!await this.checkPostExists(normalizeThingId(postId))) throw new NotFoundError(`Post with ID ${postId} does not exist or is not accessible`);
|
|
622
712
|
}
|
|
623
713
|
const params = new URLSearchParams();
|
|
624
714
|
params.append("thing_id", fullThingId);
|
|
@@ -651,10 +741,10 @@ var RedditClient = class {
|
|
|
651
741
|
else throw new ApiError("Failed to parse reply response");
|
|
652
742
|
})).toEither((error) => classifyRedditError(error));
|
|
653
743
|
}
|
|
654
|
-
async
|
|
744
|
+
async deleteThing(thingId, defaultKind) {
|
|
655
745
|
return (await Try.async(async () => {
|
|
656
746
|
this.validateWriteAccess();
|
|
657
|
-
const fullThingId = thingId
|
|
747
|
+
const fullThingId = normalizeFullname(thingId, defaultKind);
|
|
658
748
|
const params = new URLSearchParams();
|
|
659
749
|
params.append("id", fullThingId);
|
|
660
750
|
const response = (await this.makeRequest("/api/del", {
|
|
@@ -675,17 +765,19 @@ var RedditClient = class {
|
|
|
675
765
|
return classifyRedditError(error);
|
|
676
766
|
});
|
|
677
767
|
}
|
|
768
|
+
async deletePost(thingId) {
|
|
769
|
+
return this.deleteThing(thingId, "t3");
|
|
770
|
+
}
|
|
678
771
|
async deleteComment(thingId) {
|
|
679
|
-
|
|
680
|
-
return this.deletePost(fullThingId);
|
|
772
|
+
return this.deleteThing(thingId, "t1");
|
|
681
773
|
}
|
|
682
|
-
async
|
|
774
|
+
async editThing(thingId, newText, defaultKind) {
|
|
683
775
|
return (await Try.async(async () => {
|
|
684
776
|
this.validateWriteAccess();
|
|
685
777
|
await this.enforceWriteRateLimit();
|
|
686
778
|
this.checkDuplicateContent(newText);
|
|
687
779
|
const finalText = this.appendBotDisclosure(newText);
|
|
688
|
-
const fullThingId = thingId
|
|
780
|
+
const fullThingId = normalizeFullname(thingId, defaultKind);
|
|
689
781
|
const params = new URLSearchParams();
|
|
690
782
|
params.append("thing_id", fullThingId);
|
|
691
783
|
params.append("text", finalText);
|
|
@@ -701,13 +793,14 @@ var RedditClient = class {
|
|
|
701
793
|
return true;
|
|
702
794
|
})).toEither((error) => classifyRedditError(error));
|
|
703
795
|
}
|
|
796
|
+
async editPost(thingId, newText) {
|
|
797
|
+
return this.editThing(thingId, newText, "t3");
|
|
798
|
+
}
|
|
704
799
|
async editComment(thingId, newText) {
|
|
705
|
-
|
|
706
|
-
return this.editPost(fullThingId, newText);
|
|
800
|
+
return this.editThing(thingId, newText, "t1");
|
|
707
801
|
}
|
|
708
802
|
async searchReddit(query, options = {}) {
|
|
709
803
|
const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link", after, before } = options;
|
|
710
|
-
const endpoint = Option(subreddit).fold(() => "/search.json", (sr) => `/r/${sr}/search.json`);
|
|
711
804
|
const params = new URLSearchParams({
|
|
712
805
|
q: query,
|
|
713
806
|
sort,
|
|
@@ -720,6 +813,7 @@ var RedditClient = class {
|
|
|
720
813
|
});
|
|
721
814
|
const context = `Failed to search Reddit for: ${query}`;
|
|
722
815
|
return (await Try.async(async () => {
|
|
816
|
+
const endpoint = Option(subreddit).fold(() => "/search.json", (sr) => `/r/${normalizeSubreddit(sr)}/search.json`);
|
|
723
817
|
const response = (await this.makeRequest(`${endpoint}?${params}`)).orThrow();
|
|
724
818
|
if (!response.ok) throw new HttpError(response.status, `Failed to search Reddit: HTTP ${response.status}`);
|
|
725
819
|
const json = await response.json();
|
|
@@ -737,7 +831,7 @@ var RedditClient = class {
|
|
|
737
831
|
});
|
|
738
832
|
const context = `Failed to get comments for post ${postId}`;
|
|
739
833
|
return (await Try.async(async () => {
|
|
740
|
-
const response = (await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`)).orThrow();
|
|
834
|
+
const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/comments/${normalizeThingId(postId)}.json?${params}`)).orThrow();
|
|
741
835
|
if (!response.ok) throw new HttpError(response.status, `Failed to get comments: HTTP ${response.status}`);
|
|
742
836
|
const json = await response.json();
|
|
743
837
|
const postData = json[0].data.children[0].data;
|
|
@@ -769,15 +863,14 @@ var RedditClient = class {
|
|
|
769
863
|
})).toEither((error) => classifyRedditError(error, context));
|
|
770
864
|
}
|
|
771
865
|
async getMoreComments(linkId, commentIds) {
|
|
772
|
-
const
|
|
773
|
-
const context = `Failed to expand comments for ${fullLinkId}`;
|
|
774
|
-
const params = new URLSearchParams({
|
|
775
|
-
api_type: "json",
|
|
776
|
-
link_id: fullLinkId,
|
|
777
|
-
children: commentIds.join(",")
|
|
778
|
-
});
|
|
866
|
+
const context = `Failed to expand comments for ${linkId}`;
|
|
779
867
|
return (await Try.async(async () => {
|
|
780
868
|
var _json$json$data4;
|
|
869
|
+
const params = new URLSearchParams({
|
|
870
|
+
api_type: "json",
|
|
871
|
+
link_id: normalizeFullname(linkId, "t3"),
|
|
872
|
+
children: commentIds.map((id) => normalizeThingId(id)).join(",")
|
|
873
|
+
});
|
|
781
874
|
const response = (await this.makeRequest(`/api/morechildren?${params}`)).orThrow();
|
|
782
875
|
if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
|
|
783
876
|
return (((_json$json$data4 = (await response.json()).json.data) === null || _json$json$data4 === void 0 ? void 0 : _json$json$data4.things) ?? []).filter((thing) => thing.kind === "t1" && thing.data.body !== void 0).map((thing) => {
|
|
@@ -809,7 +902,7 @@ var RedditClient = class {
|
|
|
809
902
|
if (after !== void 0) params.set("after", after);
|
|
810
903
|
const context = `Failed to get posts for user ${username}`;
|
|
811
904
|
return (await Try.async(async () => {
|
|
812
|
-
const response = (await this.makeRequest(`/user/${username}/submitted.json?${params}`)).orThrow();
|
|
905
|
+
const response = (await this.makeRequest(`/user/${normalizeUsername(username)}/submitted.json?${params}`)).orThrow();
|
|
813
906
|
if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
|
|
814
907
|
const json = await response.json();
|
|
815
908
|
return {
|
|
@@ -828,7 +921,7 @@ var RedditClient = class {
|
|
|
828
921
|
if (after !== void 0) params.set("after", after);
|
|
829
922
|
const context = `Failed to get comments for user ${username}`;
|
|
830
923
|
return (await Try.async(async () => {
|
|
831
|
-
const response = (await this.makeRequest(`/user/${username}/comments.json?${params}`)).orThrow();
|
|
924
|
+
const response = (await this.makeRequest(`/user/${normalizeUsername(username)}/comments.json?${params}`)).orThrow();
|
|
832
925
|
if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
|
|
833
926
|
const json = await response.json();
|
|
834
927
|
return {
|
|
@@ -872,7 +965,7 @@ function formatTimestamp(timestamp) {
|
|
|
872
965
|
function analyzeUserActivity(karmaRatio, isMod, accountAgeDays) {
|
|
873
966
|
return [
|
|
874
967
|
...karmaRatio > 5 ? ["Primarily a commenter, highly engaged in discussions"] : karmaRatio < .2 ? ["Content creator, focuses on sharing posts"] : ["Balanced participation in both posting and commenting"],
|
|
875
|
-
...accountAgeDays < 30 ? ["New user, still exploring Reddit"] : accountAgeDays >
|
|
968
|
+
...accountAgeDays < 30 ? ["New user, still exploring Reddit"] : accountAgeDays > 1825 ? ["Long-time Redditor with extensive platform experience"] : [],
|
|
876
969
|
...isMod ? ["Community leader who helps maintain subreddit quality"] : []
|
|
877
970
|
].join("\n - ");
|
|
878
971
|
}
|
|
@@ -882,7 +975,7 @@ function analyzePostEngagement(score, ratio, numComments) {
|
|
|
882
975
|
function analyzeSubredditHealth(subscribers, activeUsers, ageDays) {
|
|
883
976
|
const sizeInsights = subscribers > 1e6 ? ["Major subreddit with massive following"] : subscribers > 1e5 ? ["Well-established community"] : subscribers < 1e3 ? ["Niche community, potential for growth"] : [];
|
|
884
977
|
const activityInsights = activeUsers.map((active) => active / subscribers).fold(() => [], (activityRatio) => activityRatio > .1 ? ["Highly active community with strong engagement"] : activityRatio < .01 ? ["Could benefit from more community engagement initiatives"] : []);
|
|
885
|
-
const ageInsights = ageDays >
|
|
978
|
+
const ageInsights = ageDays > 1825 ? ["Mature subreddit with established culture"] : ageDays < 90 ? ["New subreddit still forming its community"] : [];
|
|
886
979
|
return [
|
|
887
980
|
...sizeInsights,
|
|
888
981
|
...activityInsights,
|
|
@@ -910,7 +1003,7 @@ function getSubredditEngagementTips(subreddit) {
|
|
|
910
1003
|
return allTips.length > 0 ? allTips.join("\n - ") : "Regular engagement recommended to maintain community presence";
|
|
911
1004
|
}
|
|
912
1005
|
function formatUserInfo(user) {
|
|
913
|
-
const accountAgeDays = (Date.now() / 1e3 - user.createdUtc) /
|
|
1006
|
+
const accountAgeDays = (Date.now() / 1e3 - user.createdUtc) / 86400;
|
|
914
1007
|
const karmaRatio = user.commentKarma / (user.linkKarma === 0 ? 1 : user.linkKarma);
|
|
915
1008
|
const status = [
|
|
916
1009
|
...user.isMod ? ["Moderator"] : [],
|
|
@@ -965,7 +1058,7 @@ function formatPostInfo(post) {
|
|
|
965
1058
|
}
|
|
966
1059
|
function formatSubredditInfo(subreddit) {
|
|
967
1060
|
const flags = [...subreddit.over18 ? ["NSFW"] : [], ...Option(subreddit.subredditType).fold(() => [], (type) => [`Type: ${type}`])];
|
|
968
|
-
const ageDays = (Date.now() / 1e3 - subreddit.createdUtc) /
|
|
1061
|
+
const ageDays = (Date.now() / 1e3 - subreddit.createdUtc) / 86400;
|
|
969
1062
|
return {
|
|
970
1063
|
name: subreddit.displayName,
|
|
971
1064
|
title: subreddit.title,
|
|
@@ -992,7 +1085,7 @@ function formatSubredditInfo(subreddit) {
|
|
|
992
1085
|
//#endregion
|
|
993
1086
|
//#region src/index.ts
|
|
994
1087
|
dotenv.config({ quiet: true });
|
|
995
|
-
const VERSION = "1.5.
|
|
1088
|
+
const VERSION = "1.5.2";
|
|
996
1089
|
function validateUserAgent(userAgent, username) {
|
|
997
1090
|
if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
|
|
998
1091
|
console.error("[Warning] User-Agent does not follow Reddit's recommended format");
|