viruagent-cli 0.6.2 → 0.7.1

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.
@@ -0,0 +1,454 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { readRedditCredentials } = require('./utils');
5
+ const { saveRedditSession } = require('./session');
6
+
7
+ const REDDIT_STATE_DIR = path.join(os.homedir(), '.viruagent-cli', 'reddit-browser-state');
8
+
9
+ const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
10
+ const USER_AGENT_TEMPLATE = 'viruagent-cli/0.8.0 (by /u/USERNAME)';
11
+
12
+ const buildUserAgent = (username) =>
13
+ USER_AGENT_TEMPLATE.replace('USERNAME', username || 'unknown');
14
+
15
+ // ── OAuth2 Password Grant Login ──
16
+
17
+ const loginOAuth = async ({ sessionPath, clientId, clientSecret, username, password }) => {
18
+ const userAgent = buildUserAgent(username);
19
+ const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
20
+
21
+ const res = await fetch('https://www.reddit.com/api/v1/access_token', {
22
+ method: 'POST',
23
+ headers: {
24
+ 'User-Agent': userAgent,
25
+ Authorization: `Basic ${basicAuth}`,
26
+ 'Content-Type': 'application/x-www-form-urlencoded',
27
+ },
28
+ body: new URLSearchParams({
29
+ grant_type: 'password',
30
+ username,
31
+ password,
32
+ }).toString(),
33
+ });
34
+
35
+ if (!res.ok) {
36
+ throw new Error(`Reddit OAuth failed (${res.status}). Please check your credentials.`);
37
+ }
38
+
39
+ const data = await res.json();
40
+ if (data.error) {
41
+ throw new Error(`Reddit OAuth error: ${data.error} - ${data.message || data.error_description || ''}`);
42
+ }
43
+
44
+ const accessToken = data.access_token;
45
+ const expiresAt = Date.now() + (data.expires_in || 3600) * 1000;
46
+
47
+ const meRes = await fetch('https://oauth.reddit.com/api/v1/me', {
48
+ headers: { 'User-Agent': userAgent, Authorization: `Bearer ${accessToken}` },
49
+ });
50
+ if (!meRes.ok) throw new Error(`Token verification failed (${meRes.status}).`);
51
+ const meData = await meRes.json();
52
+ if (!meData.name) throw new Error('Failed to verify Reddit session.');
53
+
54
+ saveRedditSession(sessionPath, {
55
+ authMode: 'oauth',
56
+ accessToken,
57
+ expiresAt,
58
+ username: meData.name,
59
+ });
60
+
61
+ return { provider: 'reddit', loggedIn: true, username: meData.name, authMode: 'oauth', sessionPath };
62
+ };
63
+
64
+ // ── Cookie-based Login (old.reddit.com legacy API) ──
65
+
66
+ const parseCookies = (setCookieHeaders) => {
67
+ const cookies = {};
68
+ for (const header of setCookieHeaders) {
69
+ const [pair] = header.split(';');
70
+ const [name, ...valueParts] = pair.split('=');
71
+ cookies[name.trim()] = valueParts.join('=').trim();
72
+ }
73
+ return cookies;
74
+ };
75
+
76
+ const loginCookie = async ({ sessionPath, username, password }) => {
77
+ // Step 1: Login via old.reddit.com/api/login
78
+ const res = await fetch('https://old.reddit.com/api/login', {
79
+ method: 'POST',
80
+ headers: {
81
+ 'User-Agent': USER_AGENT,
82
+ 'Content-Type': 'application/x-www-form-urlencoded',
83
+ },
84
+ body: new URLSearchParams({
85
+ user: username,
86
+ passwd: password,
87
+ rem: 'true',
88
+ api_type: 'json',
89
+ }).toString(),
90
+ redirect: 'manual',
91
+ });
92
+
93
+ const setCookies = res.headers.getSetCookie ? res.headers.getSetCookie() : [];
94
+ const cookies = parseCookies(setCookies);
95
+
96
+ let data;
97
+ try { data = await res.json(); } catch { data = {}; }
98
+
99
+ const jsonData = data.json?.data || data.json || data;
100
+ const modhash = jsonData.modhash || '';
101
+ const redditSession = cookies.reddit_session || '';
102
+
103
+ if (!modhash && !redditSession) {
104
+ const errors = data.json?.errors;
105
+ if (errors?.length) {
106
+ throw new Error(`Reddit login failed: ${errors.map(e => e.join(' ')).join(', ')}`);
107
+ }
108
+ throw new Error('Reddit cookie login failed. No session cookie received. Check credentials or try OAuth.');
109
+ }
110
+
111
+ // Step 2: Verify session by getting /api/me.json
112
+ const cookieHeader = redditSession ? `reddit_session=${redditSession}` : '';
113
+ const meRes = await fetch('https://old.reddit.com/api/me.json', {
114
+ headers: {
115
+ 'User-Agent': USER_AGENT,
116
+ Cookie: cookieHeader,
117
+ },
118
+ });
119
+
120
+ let meData;
121
+ try { meData = await meRes.json(); } catch { meData = {}; }
122
+ const verifiedUsername = meData.data?.name || username;
123
+ const verifiedModhash = meData.data?.modhash || modhash;
124
+
125
+ saveRedditSession(sessionPath, {
126
+ authMode: 'cookie',
127
+ redditSession,
128
+ modhash: verifiedModhash,
129
+ username: verifiedUsername,
130
+ });
131
+
132
+ return { provider: 'reddit', loggedIn: true, username: verifiedUsername, authMode: 'cookie', sessionPath };
133
+ };
134
+
135
+ // ── Playwright Browser Login ──
136
+
137
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
138
+
139
+ const waitForRedditLoginFinish = async (page, timeoutMs = 60000) => {
140
+ const deadline = Date.now() + timeoutMs;
141
+ while (Date.now() < deadline) {
142
+ const url = page.url();
143
+ // Login page redirects to / or /home or user page on success
144
+ if (
145
+ url === 'https://www.reddit.com/' ||
146
+ url.startsWith('https://www.reddit.com/home') ||
147
+ url.startsWith('https://www.reddit.com/?') ||
148
+ (url.includes('reddit.com') && !url.includes('/login') && !url.includes('/register') && !url.includes('/account/login'))
149
+ ) {
150
+ return true;
151
+ }
152
+ await sleep(1000);
153
+ }
154
+ return false;
155
+ };
156
+
157
+ const loginBrowser = async ({ sessionPath, username, password, headless = false, manual = false }) => {
158
+ fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
159
+
160
+ // Reddit blocks CDP-based browsers (chromeManager).
161
+ // Use Playwright native launch mode instead — no remote-debugging-port.
162
+ const { chromium } = require('playwright');
163
+
164
+ // Persistent context preserves cookies/localStorage across sessions
165
+ fs.mkdirSync(REDDIT_STATE_DIR, { recursive: true });
166
+ const context = await chromium.launchPersistentContext(REDDIT_STATE_DIR, {
167
+ headless,
168
+ args: [
169
+ '--disable-blink-features=AutomationControlled',
170
+ '--no-first-run',
171
+ '--no-default-browser-check',
172
+ ],
173
+ userAgent: USER_AGENT,
174
+ locale: 'en-US',
175
+ viewport: { width: 1280, height: 900 },
176
+ });
177
+
178
+ const page = context.pages()[0] || (await context.newPage());
179
+
180
+ // Anti-detection script
181
+ await page.addInitScript(() => {
182
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
183
+ delete navigator.__proto__.webdriver;
184
+ window.chrome = { runtime: {} };
185
+ });
186
+
187
+ try {
188
+ // Check if already logged in (persistent context may have session)
189
+ await page.goto('https://www.reddit.com/', { waitUntil: 'domcontentloaded' });
190
+ await sleep(3000);
191
+
192
+ const alreadyLoggedIn = await page.evaluate(() => {
193
+ const hasUserMenu = document.querySelector('[id*="USER_DROPDOWN"]') ||
194
+ document.querySelector('button[aria-label*="profile"]') ||
195
+ document.querySelector('[data-testid="user-menu"]') ||
196
+ document.querySelector('#expand-user-drawer-button');
197
+ return Boolean(hasUserMenu);
198
+ });
199
+
200
+ if (alreadyLoggedIn) {
201
+ const cookies = await extractContextCookies(context);
202
+
203
+ saveRedditSession(sessionPath, {
204
+ authMode: 'browser',
205
+ cookies,
206
+ username,
207
+ });
208
+
209
+ return { provider: 'reddit', loggedIn: true, username, authMode: 'browser', sessionPath };
210
+ }
211
+
212
+ // Navigate to login page
213
+ await page.goto('https://www.reddit.com/login', { waitUntil: 'domcontentloaded' });
214
+ await sleep(2000);
215
+
216
+ let loginSuccess = false;
217
+
218
+ if (manual) {
219
+ console.log('');
220
+ console.log('==============================');
221
+ console.log('Switching to manual login mode.');
222
+ console.log('Please complete the Reddit login in the browser.');
223
+ console.log('Login must be completed within 5 minutes.');
224
+ console.log('==============================');
225
+ loginSuccess = await waitForRedditLoginFinish(page, 300000);
226
+ } else {
227
+ if (!username || !password) {
228
+ throw new Error('Reddit login requires username and password. Set REDDIT_USERNAME / REDDIT_PASSWORD environment variables.');
229
+ }
230
+
231
+ // Fill username — try multiple selectors (Reddit changes UI frequently)
232
+ const usernameSelectors = [
233
+ 'input[name="username"]',
234
+ '#loginUsername',
235
+ 'input[id="login-username"]',
236
+ 'input[type="text"][autocomplete="username"]',
237
+ ];
238
+
239
+ let usernameFilled = false;
240
+ for (const sel of usernameSelectors) {
241
+ try {
242
+ const el = await page.$(sel);
243
+ if (el) {
244
+ await el.click();
245
+ await sleep(200);
246
+ await el.fill(username);
247
+ usernameFilled = true;
248
+ break;
249
+ }
250
+ } catch { /* try next */ }
251
+ }
252
+
253
+ if (!usernameFilled) {
254
+ await page.evaluate((u) => {
255
+ const inputs = document.querySelectorAll('input[type="text"], input[name="username"]');
256
+ for (const inp of inputs) {
257
+ if (inp.name === 'username' || inp.id?.includes('username') || inp.id?.includes('login')) {
258
+ inp.value = u;
259
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
260
+ return;
261
+ }
262
+ }
263
+ if (inputs[0]) {
264
+ inputs[0].value = u;
265
+ inputs[0].dispatchEvent(new Event('input', { bubbles: true }));
266
+ }
267
+ }, username);
268
+ }
269
+
270
+ await sleep(500);
271
+
272
+ // Fill password
273
+ const passwordSelectors = [
274
+ 'input[name="password"]',
275
+ '#loginPassword',
276
+ 'input[id="login-password"]',
277
+ 'input[type="password"]',
278
+ ];
279
+
280
+ let passwordFilled = false;
281
+ for (const sel of passwordSelectors) {
282
+ try {
283
+ const el = await page.$(sel);
284
+ if (el) {
285
+ await el.click();
286
+ await sleep(200);
287
+ await el.fill(password);
288
+ passwordFilled = true;
289
+ break;
290
+ }
291
+ } catch { /* try next */ }
292
+ }
293
+
294
+ if (!passwordFilled) {
295
+ await page.evaluate((p) => {
296
+ const inp = document.querySelector('input[type="password"]');
297
+ if (inp) {
298
+ inp.value = p;
299
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
300
+ }
301
+ }, password);
302
+ }
303
+
304
+ await sleep(500);
305
+
306
+ // Click login button
307
+ const submitSelectors = [
308
+ 'button[type="submit"]',
309
+ 'button:has-text("Log In")',
310
+ 'button:has-text("Log in")',
311
+ 'button:has-text("Sign in")',
312
+ ];
313
+
314
+ let submitted = false;
315
+ for (const sel of submitSelectors) {
316
+ try {
317
+ const btn = await page.$(sel);
318
+ if (btn) {
319
+ await btn.click();
320
+ submitted = true;
321
+ break;
322
+ }
323
+ } catch { /* try next */ }
324
+ }
325
+
326
+ if (!submitted) {
327
+ await page.keyboard.press('Enter');
328
+ }
329
+
330
+ await sleep(3000);
331
+
332
+ // Check for CAPTCHA or 2FA — give user time to solve manually
333
+ // Note: avoid matching "verification" alone — Reddit login page contains this word normally
334
+ const hasCaptchaOr2FA = await page.evaluate(() => {
335
+ const url = window.location.href;
336
+ // If already redirected away from login, no captcha
337
+ if (!url.includes('/login') && !url.includes('/account/login')) return false;
338
+ const content = document.body.innerText?.toLowerCase() || '';
339
+ return content.includes('captcha') ||
340
+ content.includes('two-factor') ||
341
+ content.includes('2fa') ||
342
+ content.includes('verify your identity') ||
343
+ content.includes('check your email') ||
344
+ Boolean(document.querySelector('iframe[src*="captcha"]')) ||
345
+ Boolean(document.querySelector('iframe[src*="recaptcha"]'));
346
+ });
347
+
348
+ if (hasCaptchaOr2FA) {
349
+ console.log('');
350
+ console.log('==============================');
351
+ console.log('CAPTCHA or 2FA detected.');
352
+ console.log('Please complete verification in the browser within 2 minutes.');
353
+ console.log('==============================');
354
+ loginSuccess = await waitForRedditLoginFinish(page, 120000);
355
+ } else {
356
+ loginSuccess = await waitForRedditLoginFinish(page, 30000);
357
+ }
358
+ }
359
+
360
+ if (!loginSuccess) {
361
+ throw new Error('Reddit login failed. Please verify credentials or use --manual mode.');
362
+ }
363
+
364
+ // Extract all cookies
365
+ await sleep(1000);
366
+ const cookies = await extractContextCookies(context);
367
+
368
+ if (cookies.length === 0) {
369
+ throw new Error('No Reddit cookies found after login. Login may have failed silently.');
370
+ }
371
+
372
+ saveRedditSession(sessionPath, {
373
+ authMode: 'browser',
374
+ cookies,
375
+ username,
376
+ });
377
+
378
+ return { provider: 'reddit', loggedIn: true, username, authMode: 'browser', sessionPath };
379
+ } finally {
380
+ await context.close().catch(() => {});
381
+ }
382
+ };
383
+
384
+ /**
385
+ * Extract cookies from Playwright context and format for session storage.
386
+ * Filters to reddit.com domains only.
387
+ */
388
+ const extractContextCookies = async (context) => {
389
+ const allCookies = await context.cookies(['https://www.reddit.com', 'https://reddit.com']);
390
+ return allCookies
391
+ .filter((c) => c.domain.includes('reddit.com'))
392
+ .map((c) => ({
393
+ name: c.name,
394
+ value: c.value,
395
+ domain: c.domain,
396
+ path: c.path || '/',
397
+ expires: c.expires || -1,
398
+ httpOnly: c.httpOnly || false,
399
+ secure: c.secure || false,
400
+ sameSite: c.sameSite || 'Lax',
401
+ }));
402
+ };
403
+
404
+ // ── Main Login Router ──
405
+
406
+ const createLogin = ({ sessionPath }) => async ({
407
+ clientId,
408
+ clientSecret,
409
+ username,
410
+ password,
411
+ headless,
412
+ manual,
413
+ } = {}) => {
414
+ const creds = readRedditCredentials();
415
+ const resolvedClientId = clientId || creds.clientId;
416
+ const resolvedClientSecret = clientSecret || creds.clientSecret;
417
+ const resolvedUsername = username || creds.username;
418
+ const resolvedPassword = password || creds.password;
419
+
420
+ if (!resolvedUsername || !resolvedPassword) {
421
+ throw new Error(
422
+ 'Reddit login requires username and password. ' +
423
+ 'Set REDDIT_USERNAME / REDDIT_PASSWORD environment variables.',
424
+ );
425
+ }
426
+
427
+ // OAuth2 path: if client_id + client_secret are available
428
+ if (resolvedClientId && resolvedClientSecret) {
429
+ return loginOAuth({
430
+ sessionPath,
431
+ clientId: resolvedClientId,
432
+ clientSecret: resolvedClientSecret,
433
+ username: resolvedUsername,
434
+ password: resolvedPassword,
435
+ });
436
+ }
437
+
438
+ // Browser path: Playwright login (cookie API is blocked by Reddit)
439
+ return loginBrowser({
440
+ sessionPath,
441
+ username: resolvedUsername,
442
+ password: resolvedPassword,
443
+ headless: headless !== undefined ? headless : false,
444
+ manual: manual || false,
445
+ });
446
+ };
447
+
448
+ module.exports = {
449
+ createLogin,
450
+ buildUserAgent,
451
+ loginOAuth,
452
+ loginCookie,
453
+ loginBrowser,
454
+ };
@@ -0,0 +1,233 @@
1
+ const { saveProviderMeta, clearProviderMeta, getProviderMeta } = require('../../storage/sessionStore');
2
+ const createRedditApiClient = require('./apiClient');
3
+ const { readRedditCredentials } = require('./utils');
4
+ const { createRedditWithProviderSession } = require('./session');
5
+ const { createLogin } = require('./auth');
6
+
7
+ const createRedditProvider = ({ sessionPath, account }) => {
8
+ const redditApi = createRedditApiClient({ sessionPath });
9
+
10
+ const loginFn = createLogin({ sessionPath });
11
+
12
+ const withProviderSession = createRedditWithProviderSession(loginFn, account);
13
+
14
+ return {
15
+ id: 'reddit',
16
+ name: 'Reddit',
17
+
18
+ async authStatus() {
19
+ return withProviderSession(async () => {
20
+ try {
21
+ const me = await redditApi.getMe();
22
+ return {
23
+ provider: 'reddit',
24
+ loggedIn: true,
25
+ username: me.username,
26
+ karma: me.totalKarma,
27
+ sessionPath,
28
+ metadata: getProviderMeta('reddit', account) || {},
29
+ };
30
+ } catch (error) {
31
+ return {
32
+ provider: 'reddit',
33
+ loggedIn: false,
34
+ sessionPath,
35
+ error: error.message,
36
+ metadata: getProviderMeta('reddit', account) || {},
37
+ };
38
+ }
39
+ });
40
+ },
41
+
42
+ async login({ clientId, clientSecret, username, password, headless, manual } = {}) {
43
+ const creds = readRedditCredentials();
44
+ const resolved = {
45
+ clientId: clientId || creds.clientId,
46
+ clientSecret: clientSecret || creds.clientSecret,
47
+ username: username || creds.username,
48
+ password: password || creds.password,
49
+ headless,
50
+ manual,
51
+ };
52
+
53
+ if (!resolved.username || !resolved.password) {
54
+ throw new Error(
55
+ 'Reddit login requires username and password. ' +
56
+ 'Set REDDIT_USERNAME / REDDIT_PASSWORD environment variables. ' +
57
+ 'For OAuth, also set REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET.',
58
+ );
59
+ }
60
+
61
+ const result = await loginFn(resolved);
62
+ redditApi.resetState();
63
+
64
+ saveProviderMeta('reddit', {
65
+ loggedIn: result.loggedIn,
66
+ username: result.username,
67
+ sessionPath: result.sessionPath,
68
+ }, account);
69
+
70
+ return result;
71
+ },
72
+
73
+ async logout() {
74
+ clearProviderMeta('reddit', account);
75
+ return {
76
+ provider: 'reddit',
77
+ loggedOut: true,
78
+ sessionPath,
79
+ };
80
+ },
81
+
82
+ async publish({ subreddit, title, content, text, kind = 'self', url, flair } = {}) {
83
+ return withProviderSession(async () => {
84
+ if (!subreddit) throw new Error('subreddit is required.');
85
+ if (!title) throw new Error('title is required.');
86
+ const postText = content || text || '';
87
+ // For link posts, use content as URL if url is not explicitly provided
88
+ const linkUrl = url || (kind === 'link' ? postText.trim() : undefined);
89
+ const result = await redditApi.submitPost({ subreddit, title, text: kind === 'self' ? postText : undefined, kind, url: linkUrl, flair });
90
+ return {
91
+ provider: 'reddit',
92
+ mode: 'publish',
93
+ ...result,
94
+ };
95
+ });
96
+ },
97
+
98
+ async getProfile({ username } = {}) {
99
+ return withProviderSession(async () => {
100
+ if (!username) {
101
+ const me = await redditApi.getMe();
102
+ return { provider: 'reddit', mode: 'profile', ...me };
103
+ }
104
+ // For other users, fetch their posts as profile proxy
105
+ const posts = await redditApi.getUserPosts({ username, limit: 1 });
106
+ return {
107
+ provider: 'reddit',
108
+ mode: 'profile',
109
+ username,
110
+ recentPostCount: posts.length,
111
+ };
112
+ });
113
+ },
114
+
115
+ async getFeed({ subreddit, sort = 'hot', limit = 25 } = {}) {
116
+ return withProviderSession(async () => {
117
+ if (!subreddit) throw new Error('subreddit is required for Reddit feed.');
118
+ const items = await redditApi.getSubredditPosts({ subreddit, sort, limit });
119
+ return {
120
+ provider: 'reddit',
121
+ mode: 'feed',
122
+ subreddit,
123
+ sort,
124
+ count: items.length,
125
+ items,
126
+ };
127
+ });
128
+ },
129
+
130
+ async listPosts({ username, limit = 25 } = {}) {
131
+ return withProviderSession(async () => {
132
+ const resolvedUsername = username || (await redditApi.getMe()).username;
133
+ const posts = await redditApi.getUserPosts({ username: resolvedUsername, limit });
134
+ return {
135
+ provider: 'reddit',
136
+ mode: 'posts',
137
+ username: resolvedUsername,
138
+ totalCount: posts.length,
139
+ posts,
140
+ };
141
+ });
142
+ },
143
+
144
+ async getPost({ postId } = {}) {
145
+ return withProviderSession(async () => {
146
+ if (!postId) throw new Error('postId is required.');
147
+ const post = await redditApi.getPost({ postId });
148
+ return { provider: 'reddit', mode: 'post', ...post };
149
+ });
150
+ },
151
+
152
+ async search({ query, subreddit, limit = 25 } = {}) {
153
+ return withProviderSession(async () => {
154
+ if (!query) throw new Error('query is required.');
155
+ const results = await redditApi.search({ query, subreddit, limit });
156
+ return {
157
+ provider: 'reddit',
158
+ mode: 'search',
159
+ query,
160
+ subreddit: subreddit || null,
161
+ totalCount: results.length,
162
+ results,
163
+ };
164
+ });
165
+ },
166
+
167
+ async comment({ postId, text } = {}) {
168
+ return withProviderSession(async () => {
169
+ if (!postId || !text) throw new Error('postId and text are required.');
170
+ const cleanId = String(postId).replace(/^t3_/, '');
171
+ const parentFullname = `t3_${cleanId}`;
172
+ const result = await redditApi.comment({ parentFullname, text });
173
+ return { provider: 'reddit', mode: 'comment', postId, ...result };
174
+ });
175
+ },
176
+
177
+ async like({ postId } = {}) {
178
+ return withProviderSession(async () => {
179
+ if (!postId) throw new Error('postId is required.');
180
+ const cleanId = String(postId).replace(/^t3_/, '');
181
+ const fullname = `t3_${cleanId}`;
182
+ const result = await redditApi.vote({ fullname, direction: 1 });
183
+ return { provider: 'reddit', mode: 'like', postId, ...result };
184
+ });
185
+ },
186
+
187
+ async unlike({ postId } = {}) {
188
+ return withProviderSession(async () => {
189
+ if (!postId) throw new Error('postId is required.');
190
+ const cleanId = String(postId).replace(/^t3_/, '');
191
+ const fullname = `t3_${cleanId}`;
192
+ const result = await redditApi.vote({ fullname, direction: 0 });
193
+ return { provider: 'reddit', mode: 'unlike', postId, ...result };
194
+ });
195
+ },
196
+
197
+ async subscribe({ subreddit } = {}) {
198
+ return withProviderSession(async () => {
199
+ if (!subreddit) throw new Error('subreddit is required.');
200
+ const result = await redditApi.subscribe({ subreddit });
201
+ return { provider: 'reddit', mode: 'subscribe', ...result };
202
+ });
203
+ },
204
+
205
+ async unsubscribe({ subreddit } = {}) {
206
+ return withProviderSession(async () => {
207
+ if (!subreddit) throw new Error('subreddit is required.');
208
+ const result = await redditApi.unsubscribe({ subreddit });
209
+ return { provider: 'reddit', mode: 'unsubscribe', ...result };
210
+ });
211
+ },
212
+
213
+ async delete({ postId } = {}) {
214
+ return withProviderSession(async () => {
215
+ if (!postId) throw new Error('postId is required.');
216
+ const cleanId = String(postId).replace(/^t3_/, '');
217
+ const fullname = `t3_${cleanId}`;
218
+ const result = await redditApi.deletePost({ fullname });
219
+ return { provider: 'reddit', mode: 'delete', postId, ...result };
220
+ });
221
+ },
222
+
223
+ rateLimitStatus() {
224
+ return {
225
+ provider: 'reddit',
226
+ mode: 'rateLimitStatus',
227
+ ...redditApi.getRateLimitStatus(),
228
+ };
229
+ },
230
+ };
231
+ };
232
+
233
+ module.exports = createRedditProvider;