reddit-mcp-server 1.0.6 → 1.0.7
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/CHANGELOG.md +34 -0
- package/package.json +7 -1
- package/.claude/settings.local.json +0 -35
- package/.env.example +0 -7
- package/.prettierignore +0 -7
- package/.prettierrc +0 -8
- package/CLAUDE.md +0 -104
- package/Dockerfile +0 -17
- package/ROADMAP.md +0 -77
- package/eslint.config.js +0 -76
- package/pnpm-workspace.yaml +0 -2
- package/smithery.yaml +0 -37
- package/src/bin.ts +0 -7
- package/src/client/__tests__/reddit-client.test.ts +0 -478
- package/src/client/reddit-client.ts +0 -638
- package/src/index.ts +0 -518
- package/src/tools/__tests__/comment-tools.test.ts +0 -119
- package/src/tools/__tests__/search-tools.test.ts +0 -100
- package/src/tools/__tests__/user-tools.test.ts +0 -258
- package/src/tools/comment-tools.ts +0 -61
- package/src/tools/index.ts +0 -5
- package/src/tools/post-tools.ts +0 -178
- package/src/tools/search-tools.ts +0 -67
- package/src/tools/subreddit-tools.ts +0 -91
- package/src/tools/user-tools.ts +0 -147
- package/src/types.ts +0 -148
- package/src/utils/formatters.ts +0 -316
- package/tsconfig.json +0 -19
- package/tsup.config.ts +0 -12
- package/vitest.config.ts +0 -20
|
@@ -1,638 +0,0 @@
|
|
|
1
|
-
import { RedditClientConfig, RedditUser, RedditPost, RedditComment, RedditSubreddit } from "../types"
|
|
2
|
-
|
|
3
|
-
export class RedditClient {
|
|
4
|
-
private clientId: string
|
|
5
|
-
private clientSecret: string
|
|
6
|
-
private userAgent: string
|
|
7
|
-
private username?: string
|
|
8
|
-
private password?: string
|
|
9
|
-
private accessToken?: string
|
|
10
|
-
private tokenExpiry: number = 0
|
|
11
|
-
private baseUrl: string = "https://oauth.reddit.com"
|
|
12
|
-
private authenticated: boolean = false
|
|
13
|
-
|
|
14
|
-
constructor(config: RedditClientConfig) {
|
|
15
|
-
this.clientId = config.clientId
|
|
16
|
-
this.clientSecret = config.clientSecret
|
|
17
|
-
this.userAgent = config.userAgent
|
|
18
|
-
this.username = config.username
|
|
19
|
-
this.password = config.password
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private async makeRequest(path: string, options: RequestInit = {}): Promise<Response> {
|
|
23
|
-
// Check if we need to refresh token
|
|
24
|
-
if (Date.now() >= this.tokenExpiry || !this.authenticated) {
|
|
25
|
-
await this.authenticate()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const url = `${this.baseUrl}${path}`
|
|
29
|
-
const headers = {
|
|
30
|
-
"User-Agent": this.userAgent,
|
|
31
|
-
Authorization: `Bearer ${this.accessToken}`,
|
|
32
|
-
...options.headers,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const response = await fetch(url, {
|
|
36
|
-
...options,
|
|
37
|
-
headers,
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
// If unauthorized, try to refresh token and retry once
|
|
41
|
-
if (response.status === 401 && this.authenticated) {
|
|
42
|
-
await this.authenticate()
|
|
43
|
-
const retryHeaders = {
|
|
44
|
-
...headers,
|
|
45
|
-
Authorization: `Bearer ${this.accessToken}`,
|
|
46
|
-
}
|
|
47
|
-
return fetch(url, {
|
|
48
|
-
...options,
|
|
49
|
-
headers: retryHeaders,
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return response
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async authenticate(): Promise<void> {
|
|
57
|
-
try {
|
|
58
|
-
const now = Date.now()
|
|
59
|
-
if (this.accessToken && now < this.tokenExpiry) {
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const authUrl = "https://www.reddit.com/api/v1/access_token"
|
|
64
|
-
const authData = new URLSearchParams()
|
|
65
|
-
|
|
66
|
-
if (this.username && this.password) {
|
|
67
|
-
// Authenticating with user credentials
|
|
68
|
-
authData.append("grant_type", "password")
|
|
69
|
-
authData.append("username", this.username)
|
|
70
|
-
authData.append("password", this.password)
|
|
71
|
-
} else {
|
|
72
|
-
// Authenticating with client credentials (read-only)
|
|
73
|
-
authData.append("grant_type", "client_credentials")
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")
|
|
77
|
-
const response = await fetch(authUrl, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: {
|
|
80
|
-
"User-Agent": this.userAgent,
|
|
81
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
82
|
-
Authorization: `Basic ${credentials}`,
|
|
83
|
-
},
|
|
84
|
-
body: authData.toString(),
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
throw new Error(`Authentication failed: ${response.status}`)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const data = (await response.json()) as { access_token: string; expires_in: number }
|
|
92
|
-
this.accessToken = data.access_token
|
|
93
|
-
this.tokenExpiry = now + data.expires_in * 1000
|
|
94
|
-
this.authenticated = true
|
|
95
|
-
|
|
96
|
-
// Successfully authenticated with Reddit API
|
|
97
|
-
} catch {
|
|
98
|
-
// Authentication error occurred
|
|
99
|
-
throw new Error("Failed to authenticate with Reddit API")
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async checkAuthentication(): Promise<boolean> {
|
|
104
|
-
if (!this.authenticated) {
|
|
105
|
-
try {
|
|
106
|
-
await this.authenticate()
|
|
107
|
-
return true
|
|
108
|
-
} catch {
|
|
109
|
-
return false
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return true
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async getUser(username: string): Promise<RedditUser> {
|
|
116
|
-
await this.authenticate()
|
|
117
|
-
try {
|
|
118
|
-
const response = await this.makeRequest(`/user/${username}/about.json`)
|
|
119
|
-
if (!response.ok) {
|
|
120
|
-
throw new Error(`HTTP ${response.status}`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const json = (await response.json()) as { data: any }
|
|
124
|
-
const data = json.data
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
name: data.name,
|
|
128
|
-
id: data.id,
|
|
129
|
-
commentKarma: data.comment_karma,
|
|
130
|
-
linkKarma: data.link_karma,
|
|
131
|
-
totalKarma: data.total_karma || data.comment_karma + data.link_karma,
|
|
132
|
-
isMod: data.is_mod,
|
|
133
|
-
isGold: data.is_gold,
|
|
134
|
-
isEmployee: data.is_employee,
|
|
135
|
-
createdUtc: data.created_utc,
|
|
136
|
-
profileUrl: `https://reddit.com/user/${data.name}`,
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
// Failed to get user info
|
|
140
|
-
throw new Error(`Failed to get user info for ${username}`)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async getSubredditInfo(subredditName: string): Promise<RedditSubreddit> {
|
|
145
|
-
await this.authenticate()
|
|
146
|
-
try {
|
|
147
|
-
const response = await this.makeRequest(`/r/${subredditName}/about.json`)
|
|
148
|
-
if (!response.ok) {
|
|
149
|
-
throw new Error(`HTTP ${response.status}`)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const json = (await response.json()) as { data: any }
|
|
153
|
-
const data = json.data
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
displayName: data.display_name,
|
|
157
|
-
title: data.title,
|
|
158
|
-
description: data.description || "",
|
|
159
|
-
publicDescription: data.public_description || "",
|
|
160
|
-
subscribers: data.subscribers,
|
|
161
|
-
activeUserCount: data.active_user_count,
|
|
162
|
-
createdUtc: data.created_utc,
|
|
163
|
-
over18: data.over18,
|
|
164
|
-
subredditType: data.subreddit_type,
|
|
165
|
-
url: data.url,
|
|
166
|
-
}
|
|
167
|
-
} catch {
|
|
168
|
-
// Failed to get subreddit info
|
|
169
|
-
throw new Error(`Failed to get subreddit info for ${subredditName}`)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async getTopPosts(subreddit: string, timeFilter: string = "week", limit: number = 10): Promise<RedditPost[]> {
|
|
174
|
-
await this.authenticate()
|
|
175
|
-
try {
|
|
176
|
-
const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json"
|
|
177
|
-
const params = new URLSearchParams({
|
|
178
|
-
t: timeFilter,
|
|
179
|
-
limit: limit.toString(),
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
const response = await this.makeRequest(`${endpoint}?${params}`)
|
|
183
|
-
if (!response.ok) {
|
|
184
|
-
throw new Error(`HTTP ${response.status}`)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const json = (await response.json()) as { data: { children: any[] } }
|
|
188
|
-
|
|
189
|
-
return json.data.children.map((child: any) => {
|
|
190
|
-
const post = child.data
|
|
191
|
-
return {
|
|
192
|
-
id: post.id,
|
|
193
|
-
title: post.title,
|
|
194
|
-
author: post.author,
|
|
195
|
-
subreddit: post.subreddit,
|
|
196
|
-
selftext: post.selftext,
|
|
197
|
-
url: post.url,
|
|
198
|
-
score: post.score,
|
|
199
|
-
upvoteRatio: post.upvote_ratio,
|
|
200
|
-
numComments: post.num_comments,
|
|
201
|
-
createdUtc: post.created_utc,
|
|
202
|
-
over18: post.over_18,
|
|
203
|
-
spoiler: post.spoiler,
|
|
204
|
-
edited: !!post.edited,
|
|
205
|
-
isSelf: post.is_self,
|
|
206
|
-
linkFlairText: post.link_flair_text,
|
|
207
|
-
permalink: post.permalink,
|
|
208
|
-
}
|
|
209
|
-
})
|
|
210
|
-
} catch {
|
|
211
|
-
// Failed to get top posts
|
|
212
|
-
throw new Error(`Failed to get top posts for ${subreddit || "home"}`)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async getPost(postId: string, subreddit?: string): Promise<RedditPost> {
|
|
217
|
-
await this.authenticate()
|
|
218
|
-
try {
|
|
219
|
-
const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`
|
|
220
|
-
const response = await this.makeRequest(endpoint)
|
|
221
|
-
|
|
222
|
-
if (!response.ok) {
|
|
223
|
-
throw new Error(`HTTP ${response.status}`)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const json = (await response.json()) as any
|
|
227
|
-
|
|
228
|
-
let post
|
|
229
|
-
if (subreddit) {
|
|
230
|
-
// When using the comments endpoint
|
|
231
|
-
post = json[0].data.children[0].data
|
|
232
|
-
} else {
|
|
233
|
-
// When using the info endpoint
|
|
234
|
-
if (!json.data.children.length) {
|
|
235
|
-
throw new Error(`Post with ID ${postId} not found`)
|
|
236
|
-
}
|
|
237
|
-
post = json.data.children[0].data
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
id: post.id,
|
|
242
|
-
title: post.title,
|
|
243
|
-
author: post.author,
|
|
244
|
-
subreddit: post.subreddit,
|
|
245
|
-
selftext: post.selftext,
|
|
246
|
-
url: post.url,
|
|
247
|
-
score: post.score,
|
|
248
|
-
upvoteRatio: post.upvote_ratio,
|
|
249
|
-
numComments: post.num_comments,
|
|
250
|
-
createdUtc: post.created_utc,
|
|
251
|
-
over18: post.over_18,
|
|
252
|
-
spoiler: post.spoiler,
|
|
253
|
-
edited: !!post.edited,
|
|
254
|
-
isSelf: post.is_self,
|
|
255
|
-
linkFlairText: post.link_flair_text,
|
|
256
|
-
permalink: post.permalink,
|
|
257
|
-
}
|
|
258
|
-
} catch {
|
|
259
|
-
// Failed to get post
|
|
260
|
-
throw new Error(`Failed to get post with ID ${postId}`)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async getTrendingSubreddits(limit: number = 5): Promise<string[]> {
|
|
265
|
-
await this.authenticate()
|
|
266
|
-
try {
|
|
267
|
-
const params = new URLSearchParams({ limit: limit.toString() })
|
|
268
|
-
const response = await this.makeRequest(`/subreddits/popular.json?${params}`)
|
|
269
|
-
|
|
270
|
-
if (!response.ok) {
|
|
271
|
-
throw new Error(`HTTP ${response.status}`)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const json = (await response.json()) as { data: { children: any[] } }
|
|
275
|
-
return json.data.children.map((child: any) => child.data.display_name)
|
|
276
|
-
} catch {
|
|
277
|
-
// Failed to get trending subreddits
|
|
278
|
-
throw new Error("Failed to get trending subreddits")
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async createPost(subreddit: string, title: string, content: string, isSelf: boolean = true): Promise<RedditPost> {
|
|
283
|
-
await this.authenticate()
|
|
284
|
-
|
|
285
|
-
if (!this.username || !this.password) {
|
|
286
|
-
throw new Error("User authentication required for posting")
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const kind = isSelf ? "self" : "link"
|
|
291
|
-
const params = new URLSearchParams()
|
|
292
|
-
params.append("sr", subreddit)
|
|
293
|
-
params.append("kind", kind)
|
|
294
|
-
params.append("title", title)
|
|
295
|
-
params.append(isSelf ? "text" : "url", content)
|
|
296
|
-
|
|
297
|
-
const response = await this.makeRequest("/api/submit", {
|
|
298
|
-
method: "POST",
|
|
299
|
-
headers: {
|
|
300
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
301
|
-
},
|
|
302
|
-
body: params.toString(),
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
if (!response.ok) {
|
|
306
|
-
throw new Error(`HTTP ${response.status}`)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const json = (await response.json()) as { success: boolean; data: { id: string } }
|
|
310
|
-
if (json.success) {
|
|
311
|
-
// Get the newly created post
|
|
312
|
-
const postId = json.data.id
|
|
313
|
-
return await this.getPost(postId)
|
|
314
|
-
} else {
|
|
315
|
-
throw new Error("Failed to create post")
|
|
316
|
-
}
|
|
317
|
-
} catch {
|
|
318
|
-
// Failed to create post
|
|
319
|
-
throw new Error(`Failed to create post in ${subreddit}`)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
async checkPostExists(postId: string): Promise<boolean> {
|
|
324
|
-
await this.authenticate()
|
|
325
|
-
try {
|
|
326
|
-
const response = await this.makeRequest(`/api/info.json?id=t3_${postId}`)
|
|
327
|
-
if (!response.ok) {
|
|
328
|
-
return false
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const json = (await response.json()) as { data: { children: any[] } }
|
|
332
|
-
return json.data.children.length > 0
|
|
333
|
-
} catch {
|
|
334
|
-
return false
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async replyToPost(postId: string, content: string): Promise<RedditComment> {
|
|
339
|
-
await this.authenticate()
|
|
340
|
-
|
|
341
|
-
if (!this.username || !this.password) {
|
|
342
|
-
throw new Error("User authentication required for posting replies")
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
if (!(await this.checkPostExists(postId))) {
|
|
347
|
-
throw new Error(`Post with ID ${postId} does not exist or is not accessible`)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const params = new URLSearchParams()
|
|
351
|
-
params.append("thing_id", `t3_${postId}`)
|
|
352
|
-
params.append("text", content)
|
|
353
|
-
|
|
354
|
-
const response = await this.makeRequest("/api/comment", {
|
|
355
|
-
method: "POST",
|
|
356
|
-
headers: {
|
|
357
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
358
|
-
},
|
|
359
|
-
body: params.toString(),
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
if (!response.ok) {
|
|
363
|
-
throw new Error(`HTTP ${response.status}`)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Extract comment data from response
|
|
367
|
-
const commentData = (await response.json()) as any
|
|
368
|
-
return {
|
|
369
|
-
id: commentData.id,
|
|
370
|
-
author: this.username,
|
|
371
|
-
body: content,
|
|
372
|
-
score: 1,
|
|
373
|
-
controversiality: 0,
|
|
374
|
-
subreddit: commentData.subreddit,
|
|
375
|
-
submissionTitle: commentData.link_title,
|
|
376
|
-
createdUtc: Date.now() / 1000,
|
|
377
|
-
edited: false,
|
|
378
|
-
isSubmitter: false,
|
|
379
|
-
permalink: commentData.permalink,
|
|
380
|
-
}
|
|
381
|
-
} catch {
|
|
382
|
-
// Failed to reply to post
|
|
383
|
-
throw new Error(`Failed to reply to post ${postId}`)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async searchReddit(
|
|
388
|
-
query: string,
|
|
389
|
-
options: {
|
|
390
|
-
subreddit?: string
|
|
391
|
-
sort?: string
|
|
392
|
-
timeFilter?: string
|
|
393
|
-
limit?: number
|
|
394
|
-
type?: string
|
|
395
|
-
} = {},
|
|
396
|
-
): Promise<RedditPost[]> {
|
|
397
|
-
await this.authenticate()
|
|
398
|
-
try {
|
|
399
|
-
const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options
|
|
400
|
-
const endpoint = subreddit ? `/r/${subreddit}/search.json` : "/search.json"
|
|
401
|
-
|
|
402
|
-
const params = new URLSearchParams({
|
|
403
|
-
q: query,
|
|
404
|
-
sort,
|
|
405
|
-
t: timeFilter,
|
|
406
|
-
limit: limit.toString(),
|
|
407
|
-
type,
|
|
408
|
-
...(subreddit && { restrict_sr: "true" }),
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
const response = await this.makeRequest(`${endpoint}?${params}`)
|
|
412
|
-
if (!response.ok) {
|
|
413
|
-
throw new Error(`HTTP ${response.status}`)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const json = (await response.json()) as { data: { children: any[] } }
|
|
417
|
-
|
|
418
|
-
return json.data.children
|
|
419
|
-
.filter((child: any) => child.kind === "t3") // Only posts
|
|
420
|
-
.map((child: any) => {
|
|
421
|
-
const post = child.data
|
|
422
|
-
return {
|
|
423
|
-
id: post.id,
|
|
424
|
-
title: post.title,
|
|
425
|
-
author: post.author,
|
|
426
|
-
subreddit: post.subreddit,
|
|
427
|
-
selftext: post.selftext || "",
|
|
428
|
-
url: post.url,
|
|
429
|
-
score: post.score,
|
|
430
|
-
upvoteRatio: post.upvote_ratio,
|
|
431
|
-
numComments: post.num_comments,
|
|
432
|
-
createdUtc: post.created_utc,
|
|
433
|
-
over18: post.over_18,
|
|
434
|
-
spoiler: post.spoiler,
|
|
435
|
-
edited: !!post.edited,
|
|
436
|
-
isSelf: post.is_self,
|
|
437
|
-
linkFlairText: post.link_flair_text,
|
|
438
|
-
permalink: post.permalink,
|
|
439
|
-
}
|
|
440
|
-
})
|
|
441
|
-
} catch {
|
|
442
|
-
throw new Error(`Failed to search Reddit for: ${query}`)
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async getPostComments(
|
|
447
|
-
postId: string,
|
|
448
|
-
subreddit: string,
|
|
449
|
-
options: {
|
|
450
|
-
sort?: string
|
|
451
|
-
limit?: number
|
|
452
|
-
} = {},
|
|
453
|
-
): Promise<{ post: RedditPost; comments: RedditComment[] }> {
|
|
454
|
-
await this.authenticate()
|
|
455
|
-
try {
|
|
456
|
-
const { sort = "best", limit = 100 } = options
|
|
457
|
-
const params = new URLSearchParams({
|
|
458
|
-
sort,
|
|
459
|
-
limit: limit.toString(),
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
const response = await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`)
|
|
463
|
-
if (!response.ok) {
|
|
464
|
-
throw new Error(`HTTP ${response.status}`)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const json = (await response.json()) as any[]
|
|
468
|
-
|
|
469
|
-
// First element is the post, second is the comments
|
|
470
|
-
const postData = json[0].data.children[0].data
|
|
471
|
-
const post: RedditPost = {
|
|
472
|
-
id: postData.id,
|
|
473
|
-
title: postData.title,
|
|
474
|
-
author: postData.author,
|
|
475
|
-
subreddit: postData.subreddit,
|
|
476
|
-
selftext: postData.selftext || "",
|
|
477
|
-
url: postData.url,
|
|
478
|
-
score: postData.score,
|
|
479
|
-
upvoteRatio: postData.upvote_ratio,
|
|
480
|
-
numComments: postData.num_comments,
|
|
481
|
-
createdUtc: postData.created_utc,
|
|
482
|
-
over18: postData.over_18,
|
|
483
|
-
spoiler: postData.spoiler,
|
|
484
|
-
edited: !!postData.edited,
|
|
485
|
-
isSelf: postData.is_self,
|
|
486
|
-
linkFlairText: postData.link_flair_text,
|
|
487
|
-
permalink: postData.permalink,
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const comments: RedditComment[] = []
|
|
491
|
-
const parseComments = (commentData: any[], depth: number = 0) => {
|
|
492
|
-
for (const item of commentData) {
|
|
493
|
-
if (item.kind === "t1" && item.data.body) {
|
|
494
|
-
comments.push({
|
|
495
|
-
id: item.data.id,
|
|
496
|
-
author: item.data.author,
|
|
497
|
-
body: item.data.body,
|
|
498
|
-
score: item.data.score,
|
|
499
|
-
controversiality: item.data.controversiality,
|
|
500
|
-
subreddit: item.data.subreddit,
|
|
501
|
-
submissionTitle: post.title,
|
|
502
|
-
createdUtc: item.data.created_utc,
|
|
503
|
-
edited: !!item.data.edited,
|
|
504
|
-
isSubmitter: item.data.is_submitter,
|
|
505
|
-
permalink: item.data.permalink,
|
|
506
|
-
depth,
|
|
507
|
-
parentId: item.data.parent_id,
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
// Parse replies recursively
|
|
511
|
-
if (item.data.replies && item.data.replies.data && item.data.replies.data.children) {
|
|
512
|
-
parseComments(item.data.replies.data.children, depth + 1)
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (json[1] && json[1].data && json[1].data.children) {
|
|
519
|
-
parseComments(json[1].data.children)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return { post, comments }
|
|
523
|
-
} catch {
|
|
524
|
-
throw new Error(`Failed to get comments for post ${postId}`)
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async getUserPosts(
|
|
529
|
-
username: string,
|
|
530
|
-
options: {
|
|
531
|
-
sort?: string
|
|
532
|
-
timeFilter?: string
|
|
533
|
-
limit?: number
|
|
534
|
-
} = {},
|
|
535
|
-
): Promise<RedditPost[]> {
|
|
536
|
-
await this.authenticate()
|
|
537
|
-
try {
|
|
538
|
-
const { sort = "new", timeFilter = "all", limit = 25 } = options
|
|
539
|
-
const params = new URLSearchParams({
|
|
540
|
-
sort,
|
|
541
|
-
t: timeFilter,
|
|
542
|
-
limit: limit.toString(),
|
|
543
|
-
})
|
|
544
|
-
|
|
545
|
-
const response = await this.makeRequest(`/user/${username}/submitted.json?${params}`)
|
|
546
|
-
if (!response.ok) {
|
|
547
|
-
throw new Error(`HTTP ${response.status}`)
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const json = (await response.json()) as { data: { children: any[] } }
|
|
551
|
-
|
|
552
|
-
return json.data.children
|
|
553
|
-
.filter((child: any) => child.kind === "t3")
|
|
554
|
-
.map((child: any) => {
|
|
555
|
-
const post = child.data
|
|
556
|
-
return {
|
|
557
|
-
id: post.id,
|
|
558
|
-
title: post.title,
|
|
559
|
-
author: post.author,
|
|
560
|
-
subreddit: post.subreddit,
|
|
561
|
-
selftext: post.selftext || "",
|
|
562
|
-
url: post.url,
|
|
563
|
-
score: post.score,
|
|
564
|
-
upvoteRatio: post.upvote_ratio,
|
|
565
|
-
numComments: post.num_comments,
|
|
566
|
-
createdUtc: post.created_utc,
|
|
567
|
-
over18: post.over_18,
|
|
568
|
-
spoiler: post.spoiler,
|
|
569
|
-
edited: !!post.edited,
|
|
570
|
-
isSelf: post.is_self,
|
|
571
|
-
linkFlairText: post.link_flair_text,
|
|
572
|
-
permalink: post.permalink,
|
|
573
|
-
}
|
|
574
|
-
})
|
|
575
|
-
} catch {
|
|
576
|
-
throw new Error(`Failed to get posts for user ${username}`)
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
async getUserComments(
|
|
581
|
-
username: string,
|
|
582
|
-
options: {
|
|
583
|
-
sort?: string
|
|
584
|
-
timeFilter?: string
|
|
585
|
-
limit?: number
|
|
586
|
-
} = {},
|
|
587
|
-
): Promise<RedditComment[]> {
|
|
588
|
-
await this.authenticate()
|
|
589
|
-
try {
|
|
590
|
-
const { sort = "new", timeFilter = "all", limit = 25 } = options
|
|
591
|
-
const params = new URLSearchParams({
|
|
592
|
-
sort,
|
|
593
|
-
t: timeFilter,
|
|
594
|
-
limit: limit.toString(),
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
const response = await this.makeRequest(`/user/${username}/comments.json?${params}`)
|
|
598
|
-
if (!response.ok) {
|
|
599
|
-
throw new Error(`HTTP ${response.status}`)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const json = (await response.json()) as { data: { children: any[] } }
|
|
603
|
-
|
|
604
|
-
return json.data.children
|
|
605
|
-
.filter((child: any) => child.kind === "t1")
|
|
606
|
-
.map((child: any) => {
|
|
607
|
-
const comment = child.data
|
|
608
|
-
return {
|
|
609
|
-
id: comment.id,
|
|
610
|
-
author: comment.author,
|
|
611
|
-
body: comment.body,
|
|
612
|
-
score: comment.score,
|
|
613
|
-
controversiality: comment.controversiality,
|
|
614
|
-
subreddit: comment.subreddit,
|
|
615
|
-
submissionTitle: comment.link_title || "",
|
|
616
|
-
createdUtc: comment.created_utc,
|
|
617
|
-
edited: !!comment.edited,
|
|
618
|
-
isSubmitter: comment.is_submitter,
|
|
619
|
-
permalink: comment.permalink,
|
|
620
|
-
}
|
|
621
|
-
})
|
|
622
|
-
} catch {
|
|
623
|
-
throw new Error(`Failed to get comments for user ${username}`)
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Create and export singleton instance
|
|
629
|
-
let redditClient: RedditClient | null = null
|
|
630
|
-
|
|
631
|
-
export function initializeRedditClient(config: RedditClientConfig): RedditClient {
|
|
632
|
-
redditClient = new RedditClient(config)
|
|
633
|
-
return redditClient
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
export function getRedditClient(): RedditClient | null {
|
|
637
|
-
return redditClient
|
|
638
|
-
}
|