reddit-mcp-server 1.0.0
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/.claude/settings.local.json +18 -0
- package/.env.example +7 -0
- package/.prettierignore +7 -0
- package/.prettierrc +8 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +17 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1200 -0
- package/eslint.config.js +74 -0
- package/package.json +44 -0
- package/pnpm-workspace.yaml +2 -0
- package/smithery.yaml +37 -0
- package/src/client/reddit-client.ts +340 -0
- package/src/index.ts +303 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/post-tools.ts +178 -0
- package/src/tools/subreddit-tools.ts +91 -0
- package/src/tools/user-tools.ts +48 -0
- package/src/types.ts +146 -0
- package/src/utils/formatters.ts +299 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +12 -0
package/eslint.config.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const eslint = require('@eslint/js');
|
|
2
|
+
const tseslint = require('@typescript-eslint/eslint-plugin');
|
|
3
|
+
const tsParser = require('@typescript-eslint/parser');
|
|
4
|
+
const prettierConfig = require('eslint-config-prettier');
|
|
5
|
+
const prettierPlugin = require('eslint-plugin-prettier');
|
|
6
|
+
|
|
7
|
+
module.exports = [
|
|
8
|
+
{
|
|
9
|
+
...eslint.configs.recommended,
|
|
10
|
+
languageOptions: {
|
|
11
|
+
globals: {
|
|
12
|
+
console: 'readonly',
|
|
13
|
+
process: 'readonly',
|
|
14
|
+
Buffer: 'readonly',
|
|
15
|
+
__dirname: 'readonly',
|
|
16
|
+
__filename: 'readonly',
|
|
17
|
+
exports: 'writable',
|
|
18
|
+
module: 'writable',
|
|
19
|
+
require: 'readonly',
|
|
20
|
+
global: 'readonly',
|
|
21
|
+
URL: 'readonly',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
prettierConfig,
|
|
26
|
+
{
|
|
27
|
+
files: ['**/*.{ts,tsx}'],
|
|
28
|
+
languageOptions: {
|
|
29
|
+
parser: tsParser,
|
|
30
|
+
parserOptions: {
|
|
31
|
+
ecmaVersion: 2022,
|
|
32
|
+
sourceType: 'module',
|
|
33
|
+
project: './tsconfig.json',
|
|
34
|
+
},
|
|
35
|
+
globals: {
|
|
36
|
+
console: 'readonly',
|
|
37
|
+
process: 'readonly',
|
|
38
|
+
Buffer: 'readonly',
|
|
39
|
+
__dirname: 'readonly',
|
|
40
|
+
__filename: 'readonly',
|
|
41
|
+
exports: 'writable',
|
|
42
|
+
module: 'writable',
|
|
43
|
+
require: 'readonly',
|
|
44
|
+
global: 'readonly',
|
|
45
|
+
URL: 'readonly',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
plugins: {
|
|
49
|
+
'@typescript-eslint': tseslint,
|
|
50
|
+
prettier: prettierPlugin,
|
|
51
|
+
},
|
|
52
|
+
rules: {
|
|
53
|
+
...tseslint.configs.recommended.rules,
|
|
54
|
+
'prettier/prettier': 'error',
|
|
55
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
56
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
57
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
58
|
+
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
59
|
+
'@typescript-eslint/no-non-null-assertion': 'warn',
|
|
60
|
+
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
|
61
|
+
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
|
62
|
+
'@typescript-eslint/no-unsafe-argument': 'warn',
|
|
63
|
+
'@typescript-eslint/no-unsafe-return': 'warn',
|
|
64
|
+
'@typescript-eslint/no-unsafe-call': 'warn',
|
|
65
|
+
'@typescript-eslint/restrict-template-expressions': 'warn',
|
|
66
|
+
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
|
|
67
|
+
'no-console': 'off',
|
|
68
|
+
'no-regex-spaces': 'off',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
ignores: ['node_modules/**', 'dist/**', 'build/**', '*.config.js', '*.config.ts'],
|
|
73
|
+
},
|
|
74
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reddit-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Model Context Protocol (MCP) that provides tools for fetching and creating Reddit content. Fork of the alexandros-lekkas/reddit-mcp-server.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"reddit-mcp-server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"mcp",
|
|
11
|
+
"reddit",
|
|
12
|
+
"api",
|
|
13
|
+
"model-context-protocol"
|
|
14
|
+
],
|
|
15
|
+
"author": "Jordan Burke <jordan.burke@gmail.com>",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.13.1",
|
|
19
|
+
"axios": "^1.10.0",
|
|
20
|
+
"dotenv": "^16.5.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/js": "^9.29.0",
|
|
24
|
+
"@types/node": "^22.15.33",
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
|
26
|
+
"@typescript-eslint/parser": "^8.35.0",
|
|
27
|
+
"eslint": "^9.29.0",
|
|
28
|
+
"eslint-config-prettier": "^10.1.5",
|
|
29
|
+
"eslint-plugin-prettier": "^5.5.1",
|
|
30
|
+
"prettier": "^3.6.1",
|
|
31
|
+
"tsup": "^8.5.0",
|
|
32
|
+
"typescript": "^5.8.3"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"inspect": "pnpm build && npx @modelcontextprotocol/inspector dist/index.js",
|
|
37
|
+
"dev": "pnpm build && pnpm inspect",
|
|
38
|
+
"start": "pnpm build && npx reddit-mcp-server",
|
|
39
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
40
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
41
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
42
|
+
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Smithery configuration file: https://smithery.ai/docs/build/project-config
|
|
2
|
+
|
|
3
|
+
startCommand:
|
|
4
|
+
type: stdio
|
|
5
|
+
commandFunction:
|
|
6
|
+
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|
|
7
|
+
|-
|
|
8
|
+
(config) => ({ command: 'node', args: ['build/index.js'], env: { REDDIT_CLIENT_ID: config.redditClientId, REDDIT_CLIENT_SECRET: config.redditClientSecret, REDDIT_USER_AGENT: config.redditUserAgent ?? 'RedditMCPServer/0.1.0', ...(config.redditUsername ? { REDDIT_USERNAME: config.redditUsername } : {}), ...(config.redditPassword ? { REDDIT_PASSWORD: config.redditPassword } : {}) } })
|
|
9
|
+
configSchema:
|
|
10
|
+
# JSON Schema defining the configuration options for the MCP.
|
|
11
|
+
type: object
|
|
12
|
+
required:
|
|
13
|
+
- redditClientId
|
|
14
|
+
- redditClientSecret
|
|
15
|
+
properties:
|
|
16
|
+
redditClientId:
|
|
17
|
+
type: string
|
|
18
|
+
description: Reddit Application Client ID
|
|
19
|
+
redditClientSecret:
|
|
20
|
+
type: string
|
|
21
|
+
description: Reddit Application Client Secret
|
|
22
|
+
redditUserAgent:
|
|
23
|
+
type: string
|
|
24
|
+
default: RedditMCPServer/0.1.0
|
|
25
|
+
description: User-Agent header for Reddit API requests
|
|
26
|
+
redditUsername:
|
|
27
|
+
type: string
|
|
28
|
+
description: Optional Reddit username for authenticated actions
|
|
29
|
+
redditPassword:
|
|
30
|
+
type: string
|
|
31
|
+
description: Optional Reddit password for authenticated actions
|
|
32
|
+
exampleConfig:
|
|
33
|
+
redditClientId: abc123xyz
|
|
34
|
+
redditClientSecret: secretvalue
|
|
35
|
+
redditUserAgent: MyRedditAgent/1.0.0
|
|
36
|
+
redditUsername: my_reddit_user
|
|
37
|
+
redditPassword: supersecret
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from "axios"
|
|
2
|
+
import { RedditClientConfig, RedditUser, RedditPost, RedditComment, RedditSubreddit } from "../types"
|
|
3
|
+
|
|
4
|
+
export class RedditClient {
|
|
5
|
+
private clientId: string
|
|
6
|
+
private clientSecret: string
|
|
7
|
+
private userAgent: string
|
|
8
|
+
private username?: string
|
|
9
|
+
private password?: string
|
|
10
|
+
private accessToken?: string
|
|
11
|
+
private tokenExpiry: number = 0
|
|
12
|
+
private api: AxiosInstance
|
|
13
|
+
private authenticated: boolean = false
|
|
14
|
+
|
|
15
|
+
constructor(config: RedditClientConfig) {
|
|
16
|
+
this.clientId = config.clientId
|
|
17
|
+
this.clientSecret = config.clientSecret
|
|
18
|
+
this.userAgent = config.userAgent
|
|
19
|
+
this.username = config.username
|
|
20
|
+
this.password = config.password
|
|
21
|
+
|
|
22
|
+
this.api = axios.create({
|
|
23
|
+
baseURL: "https://oauth.reddit.com",
|
|
24
|
+
headers: {
|
|
25
|
+
"User-Agent": this.userAgent,
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Add response interceptor to handle token refresh
|
|
30
|
+
this.api.interceptors.response.use(
|
|
31
|
+
(response) => response,
|
|
32
|
+
async (error) => {
|
|
33
|
+
if (error.response?.status === 401 && this.authenticated) {
|
|
34
|
+
await this.authenticate()
|
|
35
|
+
const originalRequest = error.config
|
|
36
|
+
originalRequest.headers["Authorization"] = `Bearer ${this.accessToken}`
|
|
37
|
+
return this.api(originalRequest)
|
|
38
|
+
}
|
|
39
|
+
return Promise.reject(error)
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async authenticate(): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
const now = Date.now()
|
|
47
|
+
if (this.accessToken && now < this.tokenExpiry) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const authUrl = "https://www.reddit.com/api/v1/access_token"
|
|
52
|
+
const authData = new URLSearchParams()
|
|
53
|
+
|
|
54
|
+
if (this.username && this.password) {
|
|
55
|
+
console.log(`[Auth] Authenticating with user credentials for ${this.username}`)
|
|
56
|
+
authData.append("grant_type", "password")
|
|
57
|
+
authData.append("username", this.username)
|
|
58
|
+
authData.append("password", this.password)
|
|
59
|
+
} else {
|
|
60
|
+
console.log("[Auth] Authenticating with client credentials (read-only)")
|
|
61
|
+
authData.append("grant_type", "client_credentials")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const response = await axios.post(authUrl, authData, {
|
|
65
|
+
auth: {
|
|
66
|
+
username: this.clientId,
|
|
67
|
+
password: this.clientSecret,
|
|
68
|
+
},
|
|
69
|
+
headers: {
|
|
70
|
+
"User-Agent": this.userAgent,
|
|
71
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
this.accessToken = response.data.access_token
|
|
76
|
+
this.tokenExpiry = now + response.data.expires_in * 1000
|
|
77
|
+
this.authenticated = true
|
|
78
|
+
this.api.defaults.headers.common["Authorization"] = `Bearer ${this.accessToken}`
|
|
79
|
+
|
|
80
|
+
console.log("[Auth] Successfully authenticated with Reddit API")
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("[Auth] Authentication error:", error)
|
|
83
|
+
throw new Error("Failed to authenticate with Reddit API")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async checkAuthentication(): Promise<boolean> {
|
|
88
|
+
if (!this.authenticated) {
|
|
89
|
+
try {
|
|
90
|
+
await this.authenticate()
|
|
91
|
+
return true
|
|
92
|
+
} catch {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getUser(username: string): Promise<RedditUser> {
|
|
100
|
+
await this.authenticate()
|
|
101
|
+
try {
|
|
102
|
+
const response = await this.api.get(`/user/${username}/about.json`)
|
|
103
|
+
const data = response.data.data
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
name: data.name,
|
|
107
|
+
id: data.id,
|
|
108
|
+
commentKarma: data.comment_karma,
|
|
109
|
+
linkKarma: data.link_karma,
|
|
110
|
+
totalKarma: data.total_karma || data.comment_karma + data.link_karma,
|
|
111
|
+
isMod: data.is_mod,
|
|
112
|
+
isGold: data.is_gold,
|
|
113
|
+
isEmployee: data.is_employee,
|
|
114
|
+
createdUtc: data.created_utc,
|
|
115
|
+
profileUrl: `https://reddit.com/user/${data.name}`,
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(`[Error] Failed to get user info for ${username}:`, error)
|
|
119
|
+
throw new Error(`Failed to get user info for ${username}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getSubredditInfo(subredditName: string): Promise<RedditSubreddit> {
|
|
124
|
+
await this.authenticate()
|
|
125
|
+
try {
|
|
126
|
+
const response = await this.api.get(`/r/${subredditName}/about.json`)
|
|
127
|
+
const data = response.data.data
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
displayName: data.display_name,
|
|
131
|
+
title: data.title,
|
|
132
|
+
description: data.description || "",
|
|
133
|
+
publicDescription: data.public_description || "",
|
|
134
|
+
subscribers: data.subscribers,
|
|
135
|
+
activeUserCount: data.active_user_count,
|
|
136
|
+
createdUtc: data.created_utc,
|
|
137
|
+
over18: data.over18,
|
|
138
|
+
subredditType: data.subreddit_type,
|
|
139
|
+
url: data.url,
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`[Error] Failed to get subreddit info for ${subredditName}:`, error)
|
|
143
|
+
throw new Error(`Failed to get subreddit info for ${subredditName}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getTopPosts(subreddit: string, timeFilter: string = "week", limit: number = 10): Promise<RedditPost[]> {
|
|
148
|
+
await this.authenticate()
|
|
149
|
+
try {
|
|
150
|
+
const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json"
|
|
151
|
+
const response = await this.api.get(endpoint, {
|
|
152
|
+
params: {
|
|
153
|
+
t: timeFilter,
|
|
154
|
+
limit,
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return response.data.data.children.map((child: any) => {
|
|
159
|
+
const post = child.data
|
|
160
|
+
return {
|
|
161
|
+
id: post.id,
|
|
162
|
+
title: post.title,
|
|
163
|
+
author: post.author,
|
|
164
|
+
subreddit: post.subreddit,
|
|
165
|
+
selftext: post.selftext,
|
|
166
|
+
url: post.url,
|
|
167
|
+
score: post.score,
|
|
168
|
+
upvoteRatio: post.upvote_ratio,
|
|
169
|
+
numComments: post.num_comments,
|
|
170
|
+
createdUtc: post.created_utc,
|
|
171
|
+
over18: post.over_18,
|
|
172
|
+
spoiler: post.spoiler,
|
|
173
|
+
edited: !!post.edited,
|
|
174
|
+
isSelf: post.is_self,
|
|
175
|
+
linkFlairText: post.link_flair_text,
|
|
176
|
+
permalink: post.permalink,
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(`[Error] Failed to get top posts for ${subreddit || "home"}:`, error)
|
|
181
|
+
throw new Error(`Failed to get top posts for ${subreddit || "home"}`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getPost(postId: string, subreddit?: string): Promise<RedditPost> {
|
|
186
|
+
await this.authenticate()
|
|
187
|
+
try {
|
|
188
|
+
const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`
|
|
189
|
+
|
|
190
|
+
const response = await this.api.get(endpoint)
|
|
191
|
+
|
|
192
|
+
let post
|
|
193
|
+
if (subreddit) {
|
|
194
|
+
// When using the comments endpoint
|
|
195
|
+
post = response.data[0].data.children[0].data
|
|
196
|
+
} else {
|
|
197
|
+
// When using the info endpoint
|
|
198
|
+
if (!response.data.data.children.length) {
|
|
199
|
+
throw new Error(`Post with ID ${postId} not found`)
|
|
200
|
+
}
|
|
201
|
+
post = response.data.data.children[0].data
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
id: post.id,
|
|
206
|
+
title: post.title,
|
|
207
|
+
author: post.author,
|
|
208
|
+
subreddit: post.subreddit,
|
|
209
|
+
selftext: post.selftext,
|
|
210
|
+
url: post.url,
|
|
211
|
+
score: post.score,
|
|
212
|
+
upvoteRatio: post.upvote_ratio,
|
|
213
|
+
numComments: post.num_comments,
|
|
214
|
+
createdUtc: post.created_utc,
|
|
215
|
+
over18: post.over_18,
|
|
216
|
+
spoiler: post.spoiler,
|
|
217
|
+
edited: !!post.edited,
|
|
218
|
+
isSelf: post.is_self,
|
|
219
|
+
linkFlairText: post.link_flair_text,
|
|
220
|
+
permalink: post.permalink,
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(`[Error] Failed to get post with ID ${postId}:`, error)
|
|
224
|
+
throw new Error(`Failed to get post with ID ${postId}`)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async getTrendingSubreddits(limit: number = 5): Promise<string[]> {
|
|
229
|
+
await this.authenticate()
|
|
230
|
+
try {
|
|
231
|
+
const response = await this.api.get("/subreddits/popular.json", {
|
|
232
|
+
params: { limit },
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return response.data.data.children.map((child: any) => child.data.display_name)
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error("[Error] Failed to get trending subreddits:", error)
|
|
238
|
+
throw new Error("Failed to get trending subreddits")
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async createPost(subreddit: string, title: string, content: string, isSelf: boolean = true): Promise<RedditPost> {
|
|
243
|
+
await this.authenticate()
|
|
244
|
+
|
|
245
|
+
if (!this.username || !this.password) {
|
|
246
|
+
throw new Error("User authentication required for posting")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const kind = isSelf ? "self" : "link"
|
|
251
|
+
const params = new URLSearchParams()
|
|
252
|
+
params.append("sr", subreddit)
|
|
253
|
+
params.append("kind", kind)
|
|
254
|
+
params.append("title", title)
|
|
255
|
+
params.append(isSelf ? "text" : "url", content)
|
|
256
|
+
|
|
257
|
+
const response = await this.api.post("/api/submit", params, {
|
|
258
|
+
headers: {
|
|
259
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
if (response.data.success) {
|
|
264
|
+
// Get the newly created post
|
|
265
|
+
const postId = response.data.data.id
|
|
266
|
+
return await this.getPost(postId)
|
|
267
|
+
} else {
|
|
268
|
+
throw new Error("Failed to create post")
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error(`[Error] Failed to create post in ${subreddit}:`, error)
|
|
272
|
+
throw new Error(`Failed to create post in ${subreddit}`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async checkPostExists(postId: string): Promise<boolean> {
|
|
277
|
+
await this.authenticate()
|
|
278
|
+
try {
|
|
279
|
+
const response = await this.api.get(`/api/info.json?id=t3_${postId}`)
|
|
280
|
+
return response.data.data.children.length > 0
|
|
281
|
+
} catch {
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async replyToPost(postId: string, content: string): Promise<RedditComment> {
|
|
287
|
+
await this.authenticate()
|
|
288
|
+
|
|
289
|
+
if (!this.username || !this.password) {
|
|
290
|
+
throw new Error("User authentication required for posting replies")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
if (!(await this.checkPostExists(postId))) {
|
|
295
|
+
throw new Error(`Post with ID ${postId} does not exist or is not accessible`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const params = new URLSearchParams()
|
|
299
|
+
params.append("thing_id", `t3_${postId}`)
|
|
300
|
+
params.append("text", content)
|
|
301
|
+
|
|
302
|
+
const response = await this.api.post("/api/comment", params, {
|
|
303
|
+
headers: {
|
|
304
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Extract comment data from response
|
|
309
|
+
const commentData = response.data
|
|
310
|
+
return {
|
|
311
|
+
id: commentData.id,
|
|
312
|
+
author: this.username,
|
|
313
|
+
body: content,
|
|
314
|
+
score: 1,
|
|
315
|
+
controversiality: 0,
|
|
316
|
+
subreddit: commentData.subreddit,
|
|
317
|
+
submissionTitle: commentData.link_title,
|
|
318
|
+
createdUtc: Date.now() / 1000,
|
|
319
|
+
edited: false,
|
|
320
|
+
isSubmitter: false,
|
|
321
|
+
permalink: commentData.permalink,
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(`[Error] Failed to reply to post ${postId}:`, error)
|
|
325
|
+
throw new Error(`Failed to reply to post ${postId}`)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Create and export singleton instance
|
|
331
|
+
let redditClient: RedditClient | null = null
|
|
332
|
+
|
|
333
|
+
export function initializeRedditClient(config: RedditClientConfig): RedditClient {
|
|
334
|
+
redditClient = new RedditClient(config)
|
|
335
|
+
return redditClient
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function getRedditClient(): RedditClient | null {
|
|
339
|
+
return redditClient
|
|
340
|
+
}
|