@classytic/social 0.1.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +368 -0
  4. package/dist/base-Bw7e52V8.mjs +246 -0
  5. package/dist/base-Bw7e52V8.mjs.map +1 -0
  6. package/dist/base-DBtKFiSX.d.mts +226 -0
  7. package/dist/base-DBtKFiSX.d.mts.map +1 -0
  8. package/dist/chunk-DQk6qfdC.mjs +18 -0
  9. package/dist/client/index.d.mts +44 -0
  10. package/dist/client/index.d.mts.map +1 -0
  11. package/dist/client/index.mjs +154 -0
  12. package/dist/client/index.mjs.map +1 -0
  13. package/dist/common/index.d.mts +3 -0
  14. package/dist/common/index.mjs +7 -0
  15. package/dist/contracts-Cdwa4zlg.d.mts +121 -0
  16. package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
  17. package/dist/contracts-lCa069IK.mjs +221 -0
  18. package/dist/contracts-lCa069IK.mjs.map +1 -0
  19. package/dist/env-Bl0cwwjC.mjs +955 -0
  20. package/dist/env-Bl0cwwjC.mjs.map +1 -0
  21. package/dist/env-DxOZHf0p.d.mts +394 -0
  22. package/dist/env-DxOZHf0p.d.mts.map +1 -0
  23. package/dist/errors-Cm6LeKf7.mjs +32 -0
  24. package/dist/errors-Cm6LeKf7.mjs.map +1 -0
  25. package/dist/facebook-l_4CghaA.mjs +95 -0
  26. package/dist/facebook-l_4CghaA.mjs.map +1 -0
  27. package/dist/http-DpcLSR1M.mjs +197 -0
  28. package/dist/http-DpcLSR1M.mjs.map +1 -0
  29. package/dist/index.d.mts +42 -0
  30. package/dist/index.d.mts.map +1 -0
  31. package/dist/index.mjs +71 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/instagram-BGaeUFU2.mjs +90 -0
  34. package/dist/instagram-BGaeUFU2.mjs.map +1 -0
  35. package/dist/linkedin-70whtVKa.mjs +101 -0
  36. package/dist/linkedin-70whtVKa.mjs.map +1 -0
  37. package/dist/meta-D3vcJU1c.mjs +126 -0
  38. package/dist/meta-D3vcJU1c.mjs.map +1 -0
  39. package/dist/pkce-jq5II68b.mjs +72 -0
  40. package/dist/pkce-jq5II68b.mjs.map +1 -0
  41. package/dist/polling-DZ1apXtA.mjs +25 -0
  42. package/dist/polling-DZ1apXtA.mjs.map +1 -0
  43. package/dist/providers/facebook.d.mts +135 -0
  44. package/dist/providers/facebook.d.mts.map +1 -0
  45. package/dist/providers/facebook.mjs +450 -0
  46. package/dist/providers/facebook.mjs.map +1 -0
  47. package/dist/providers/instagram.d.mts +122 -0
  48. package/dist/providers/instagram.d.mts.map +1 -0
  49. package/dist/providers/instagram.mjs +496 -0
  50. package/dist/providers/instagram.mjs.map +1 -0
  51. package/dist/providers/linkedin.d.mts +145 -0
  52. package/dist/providers/linkedin.d.mts.map +1 -0
  53. package/dist/providers/linkedin.mjs +574 -0
  54. package/dist/providers/linkedin.mjs.map +1 -0
  55. package/dist/providers/reddit.d.mts +102 -0
  56. package/dist/providers/reddit.d.mts.map +1 -0
  57. package/dist/providers/reddit.mjs +657 -0
  58. package/dist/providers/reddit.mjs.map +1 -0
  59. package/dist/providers/telegram.d.mts +139 -0
  60. package/dist/providers/telegram.d.mts.map +1 -0
  61. package/dist/providers/telegram.mjs +517 -0
  62. package/dist/providers/telegram.mjs.map +1 -0
  63. package/dist/providers/tiktok.d.mts +116 -0
  64. package/dist/providers/tiktok.d.mts.map +1 -0
  65. package/dist/providers/tiktok.mjs +676 -0
  66. package/dist/providers/tiktok.mjs.map +1 -0
  67. package/dist/providers/twitter.d.mts +150 -0
  68. package/dist/providers/twitter.d.mts.map +1 -0
  69. package/dist/providers/twitter.mjs +628 -0
  70. package/dist/providers/twitter.mjs.map +1 -0
  71. package/dist/providers/whatsapp.d.mts +79 -0
  72. package/dist/providers/whatsapp.d.mts.map +1 -0
  73. package/dist/providers/whatsapp.mjs +376 -0
  74. package/dist/providers/whatsapp.mjs.map +1 -0
  75. package/dist/providers/youtube.d.mts +153 -0
  76. package/dist/providers/youtube.d.mts.map +1 -0
  77. package/dist/providers/youtube.mjs +902 -0
  78. package/dist/providers/youtube.mjs.map +1 -0
  79. package/dist/reddit-B10kS4Se.mjs +126 -0
  80. package/dist/reddit-B10kS4Se.mjs.map +1 -0
  81. package/dist/schemas/index.d.mts +819 -0
  82. package/dist/schemas/index.d.mts.map +1 -0
  83. package/dist/schemas/index.mjs +31 -0
  84. package/dist/schemas/index.mjs.map +1 -0
  85. package/dist/security-BXhfebWm.d.mts +338 -0
  86. package/dist/security-BXhfebWm.d.mts.map +1 -0
  87. package/dist/shared-Fvc6xQku.mjs +100 -0
  88. package/dist/shared-Fvc6xQku.mjs.map +1 -0
  89. package/dist/telegram-FaUHpZgB.mjs +107 -0
  90. package/dist/telegram-FaUHpZgB.mjs.map +1 -0
  91. package/dist/tiktok-B_bMk4G-.mjs +94 -0
  92. package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
  93. package/dist/twitter-BC22zfuc.mjs +98 -0
  94. package/dist/twitter-BC22zfuc.mjs.map +1 -0
  95. package/dist/types-BFE4psYI.d.mts +102 -0
  96. package/dist/types-BFE4psYI.d.mts.map +1 -0
  97. package/dist/types-Bv27tcT0.d.mts +230 -0
  98. package/dist/types-Bv27tcT0.d.mts.map +1 -0
  99. package/dist/types-BwkKyqpi.d.mts +253 -0
  100. package/dist/types-BwkKyqpi.d.mts.map +1 -0
  101. package/dist/types-CJrHMDV9.mjs +27 -0
  102. package/dist/types-CJrHMDV9.mjs.map +1 -0
  103. package/dist/types-ClbVc2rc.d.mts +117 -0
  104. package/dist/types-ClbVc2rc.d.mts.map +1 -0
  105. package/dist/types-D91N16Ym.d.mts +242 -0
  106. package/dist/types-D91N16Ym.d.mts.map +1 -0
  107. package/dist/types-DfLp_ibQ.d.mts +178 -0
  108. package/dist/types-DfLp_ibQ.d.mts.map +1 -0
  109. package/dist/types-DfjDgEoJ.d.mts +88 -0
  110. package/dist/types-DfjDgEoJ.d.mts.map +1 -0
  111. package/dist/types-Dp5Z9VBr.mjs +23 -0
  112. package/dist/types-Dp5Z9VBr.mjs.map +1 -0
  113. package/dist/types-hriBJTsU.d.mts +129 -0
  114. package/dist/types-hriBJTsU.d.mts.map +1 -0
  115. package/dist/types-rn6UuLL8.d.mts +184 -0
  116. package/dist/types-rn6UuLL8.d.mts.map +1 -0
  117. package/dist/whatsapp-CFp7ryR4.mjs +101 -0
  118. package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
  119. package/dist/youtube-Bs0fdY7H.mjs +98 -0
  120. package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
  121. package/package.json +148 -0
@@ -0,0 +1,657 @@
1
+ import { t as PlatformProvider } from "../base-Bw7e52V8.mjs";
2
+ import { t as SocialError } from "../errors-Cm6LeKf7.mjs";
3
+ import { t as httpRequest } from "../http-DpcLSR1M.mjs";
4
+ import { t as RedditCredentialsSchema } from "../reddit-B10kS4Se.mjs";
5
+ import { t as REDDIT_ERROR_HINTS } from "../types-CJrHMDV9.mjs";
6
+
7
+ //#region src/providers/reddit/index.ts
8
+ /**
9
+ * Reddit Provider
10
+ * ===============
11
+ * Full Reddit API integration with OAuth 2.0.
12
+ *
13
+ * Supports:
14
+ * - OAuth 2.0 (authorization code with permanent duration)
15
+ * - Post CRUD (create text/link/image, get, delete, search)
16
+ * - Comments (create, reply, delete, list)
17
+ * - Voting (upvote, downvote, unvote)
18
+ * - Subreddit info and rules
19
+ * - User profile and public user data
20
+ * - Subreddit browsing (hot, new, top, rising)
21
+ *
22
+ * @see https://www.reddit.com/dev/api/
23
+ * @see https://github.com/reddit-archive/reddit/wiki/OAuth2
24
+ */
25
+ const OAUTH_API = "https://oauth.reddit.com";
26
+ const PUBLIC_API = "https://www.reddit.com";
27
+ const AUTH_URL = "https://www.reddit.com/api/v1/authorize";
28
+ const TOKEN_URL = "https://www.reddit.com/api/v1/access_token";
29
+ const REVOKE_URL = "https://www.reddit.com/api/v1/revoke_token";
30
+ const USER_AGENT = "web:com.classytic.social:v0.1.0 (by /u/classytic)";
31
+ const DEFAULT_SCOPES = [
32
+ "identity",
33
+ "edit",
34
+ "history",
35
+ "mysubreddits",
36
+ "read",
37
+ "save",
38
+ "submit",
39
+ "vote",
40
+ "subscribe",
41
+ "privatemessages",
42
+ "flair"
43
+ ];
44
+ const SCOPE_DESCRIPTIONS = {
45
+ "identity": "Access your Reddit username and account information",
46
+ "edit": "Edit your posts and comments",
47
+ "history": "Access your voting and browsing history",
48
+ "mysubreddits": "View your subscribed subreddits",
49
+ "read": "Read posts, comments, and subreddit information",
50
+ "save": "Save and unsave posts and comments",
51
+ "submit": "Create new posts and comments",
52
+ "vote": "Upvote and downvote content",
53
+ "subscribe": "Subscribe and unsubscribe from subreddits",
54
+ "privatemessages": "Read and send private messages",
55
+ "flair": "Set and manage flair on posts"
56
+ };
57
+ var RedditProvider = class extends PlatformProvider {
58
+ constructor(config = {}) {
59
+ super(config);
60
+ this.name = "reddit";
61
+ this.displayName = "Reddit";
62
+ this.authType = "oauth2";
63
+ }
64
+ getAuthUrl(state, credData, options) {
65
+ const clientId = credData?.clientId;
66
+ if (!clientId) throw new SocialError("reddit", "Client ID is required", { statusCode: 400 });
67
+ const redirectUri = credData?.redirectUri || this.config.redirectUri || `http://localhost:${this.config.port || 8060}/api/oauth/reddit/callback`;
68
+ return `${AUTH_URL}?${new URLSearchParams({
69
+ client_id: clientId,
70
+ response_type: "code",
71
+ state,
72
+ redirect_uri: redirectUri,
73
+ duration: "permanent",
74
+ scope: DEFAULT_SCOPES.join(" ")
75
+ }).toString()}`;
76
+ }
77
+ async exchangeCode(code, credData) {
78
+ const clientId = credData?.clientId;
79
+ const clientSecret = credData?.clientSecret;
80
+ if (!clientId || !clientSecret) throw new SocialError("reddit", "Client ID and Client Secret are required for token exchange", { statusCode: 400 });
81
+ const redirectUri = credData?.redirectUri || this.config.redirectUri || `http://localhost:${this.config.port || 8060}/api/oauth/reddit/callback`;
82
+ const body = new URLSearchParams({
83
+ grant_type: "authorization_code",
84
+ code,
85
+ redirect_uri: redirectUri
86
+ });
87
+ const response = await fetch(TOKEN_URL, {
88
+ method: "POST",
89
+ headers: {
90
+ "Authorization": `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
91
+ "Content-Type": "application/x-www-form-urlencoded",
92
+ "User-Agent": USER_AGENT
93
+ },
94
+ body: body.toString()
95
+ });
96
+ const data = await response.json();
97
+ if (!response.ok || data.error) throw new SocialError("reddit", `Token exchange failed: ${data.error || response.statusText}`, {
98
+ statusCode: response.status,
99
+ errorCode: data.error,
100
+ hint: REDDIT_ERROR_HINTS["INVALID_GRANT"] || "Ensure your Client ID, Client Secret, and redirect URI are correct.",
101
+ originalError: new Error(JSON.stringify(data))
102
+ });
103
+ return {
104
+ access_token: data.access_token,
105
+ refresh_token: data.refresh_token,
106
+ expires_in: data.expires_in,
107
+ token_type: data.token_type,
108
+ scope: data.scope
109
+ };
110
+ }
111
+ async refreshToken(refreshToken, credData) {
112
+ const clientId = credData?.clientId;
113
+ const clientSecret = credData?.clientSecret;
114
+ if (!clientId || !clientSecret) throw new SocialError("reddit", "Client ID and Client Secret are required for token refresh", { statusCode: 400 });
115
+ const body = new URLSearchParams({
116
+ grant_type: "refresh_token",
117
+ refresh_token: refreshToken
118
+ });
119
+ const response = await fetch(TOKEN_URL, {
120
+ method: "POST",
121
+ headers: {
122
+ "Authorization": `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
123
+ "Content-Type": "application/x-www-form-urlencoded",
124
+ "User-Agent": USER_AGENT
125
+ },
126
+ body: body.toString()
127
+ });
128
+ const data = await response.json();
129
+ if (!response.ok || data.error) throw new SocialError("reddit", `Token refresh failed: ${data.error || response.statusText}`, {
130
+ statusCode: response.status,
131
+ errorCode: data.error,
132
+ hint: "The refresh token may have been revoked. Re-authenticate."
133
+ });
134
+ return {
135
+ access_token: data.access_token,
136
+ refresh_token: data.refresh_token,
137
+ expires_in: data.expires_in,
138
+ token_type: data.token_type,
139
+ scope: data.scope
140
+ };
141
+ }
142
+ async revokeToken(accessToken, credData) {
143
+ const clientId = credData?.clientId;
144
+ const clientSecret = credData?.clientSecret;
145
+ if (!clientId || !clientSecret) return;
146
+ const body = new URLSearchParams({
147
+ token: accessToken,
148
+ token_type_hint: "access_token"
149
+ });
150
+ await fetch(REVOKE_URL, {
151
+ method: "POST",
152
+ headers: {
153
+ "Authorization": `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
154
+ "Content-Type": "application/x-www-form-urlencoded",
155
+ "User-Agent": USER_AGENT
156
+ },
157
+ body: body.toString()
158
+ }).catch(() => {});
159
+ }
160
+ async getAccountInfo(accessToken) {
161
+ const data = await this._api("GET", "/api/v1/me", accessToken);
162
+ return {
163
+ id: data.id,
164
+ name: data.name,
165
+ username: data.name,
166
+ profileImage: data.icon_img?.split("?")[0] ?? null,
167
+ linkKarma: data.link_karma,
168
+ commentKarma: data.comment_karma,
169
+ totalKarma: data.total_karma,
170
+ createdUtc: data.created_utc,
171
+ hasVerifiedEmail: data.has_verified_email,
172
+ isGold: data.is_gold,
173
+ isMod: data.is_mod
174
+ };
175
+ }
176
+ async testCredential(credentialData) {
177
+ try {
178
+ const tokenData = typeof credentialData.oauthTokenData === "string" ? JSON.parse(credentialData.oauthTokenData) : credentialData.oauthTokenData;
179
+ if (!tokenData?.access_token) return {
180
+ status: "Error",
181
+ message: "No access token found. Complete OAuth authorization first."
182
+ };
183
+ const info = await this.getAccountInfo(tokenData.access_token);
184
+ return {
185
+ status: "OK",
186
+ message: `Connected as u/${info.name} (${info.totalKarma} karma)`,
187
+ data: {
188
+ id: info.id,
189
+ name: info.name,
190
+ username: info.name
191
+ }
192
+ };
193
+ } catch (err) {
194
+ return {
195
+ status: "Error",
196
+ message: err instanceof Error ? err.message : String(err)
197
+ };
198
+ }
199
+ }
200
+ /**
201
+ * Create a post (text, link, or image).
202
+ */
203
+ async createPost(accessToken, params) {
204
+ const body = {
205
+ api_type: "json",
206
+ sr: params.subreddit,
207
+ title: params.title,
208
+ kind: params.kind,
209
+ resubmit: String(params.resubmit ?? true),
210
+ send_replies: String(params.sendReplies ?? true)
211
+ };
212
+ if (params.kind === "self" && params.text) body.text = params.text;
213
+ if ((params.kind === "link" || params.kind === "image") && params.url) body.url = params.url;
214
+ if (params.nsfw) body.nsfw = "true";
215
+ if (params.spoiler) body.spoiler = "true";
216
+ if (params.flairId) body.flair_id = params.flairId;
217
+ if (params.flairText) body.flair_text = params.flairText;
218
+ const json = (await this._api("POST", "/api/submit", accessToken, body, true)).json;
219
+ if (json?.errors?.length) {
220
+ const [errorCode, errorMsg] = json.errors[0];
221
+ throw new SocialError("reddit", `Post creation failed: ${errorMsg}`, {
222
+ statusCode: 400,
223
+ errorCode,
224
+ hint: REDDIT_ERROR_HINTS[errorCode] || null
225
+ });
226
+ }
227
+ const postData = json?.data;
228
+ return {
229
+ id: postData?.id ?? "",
230
+ name: postData?.name ?? "",
231
+ url: postData?.url ?? `https://reddit.com${postData?.permalink ?? ""}`
232
+ };
233
+ }
234
+ /**
235
+ * Get a single post with its comments.
236
+ */
237
+ async getPost(accessToken, subreddit, postId) {
238
+ const data = await this._api("GET", `/r/${subreddit}/comments/${postId}.json`, accessToken);
239
+ const postRaw = data[0]?.data?.children?.[0]?.data;
240
+ if (!postRaw) throw new SocialError("reddit", `Post ${postId} not found in r/${subreddit}`, {
241
+ statusCode: 404,
242
+ hint: REDDIT_ERROR_HINTS["NOT_FOUND"]
243
+ });
244
+ const comments = (data[1]?.data?.children || []).filter((c) => c.kind === "t1").map((c) => this._parseComment(c.data));
245
+ return {
246
+ post: this._parsePost(postRaw),
247
+ comments
248
+ };
249
+ }
250
+ /**
251
+ * Delete a post or comment by fullname (t3_ or t1_ prefix).
252
+ */
253
+ async deleteContent(accessToken, fullname) {
254
+ await this._api("POST", "/api/del", accessToken, { id: fullname }, true);
255
+ return { success: true };
256
+ }
257
+ async deletePost(accessToken, fullname) {
258
+ return this.deleteContent(accessToken, fullname);
259
+ }
260
+ /**
261
+ * Get posts from a subreddit.
262
+ */
263
+ async getSubredditPosts(accessToken, subreddit, params = {}) {
264
+ const sort = params.sort || "hot";
265
+ const qs = new URLSearchParams();
266
+ if (params.limit) qs.set("limit", String(Math.min(params.limit, 100)));
267
+ if (params.after) qs.set("after", params.after);
268
+ if (params.before) qs.set("before", params.before);
269
+ if (params.time && (sort === "top" || sort === "controversial")) qs.set("t", params.time);
270
+ const queryStr = qs.toString();
271
+ const endpoint = `/r/${subreddit}/${sort}.json${queryStr ? `?${queryStr}` : ""}`;
272
+ const data = await this._api("GET", endpoint, accessToken);
273
+ return {
274
+ items: (data.data?.children || []).map((c) => this._parsePost(c.data)),
275
+ after: data.data?.after ?? null,
276
+ before: data.data?.before ?? null
277
+ };
278
+ }
279
+ /**
280
+ * Search posts across Reddit or within a specific subreddit.
281
+ */
282
+ async searchPosts(accessToken, params) {
283
+ const qs = new URLSearchParams({
284
+ q: params.query,
285
+ sort: params.sort || "relevance",
286
+ t: params.time || "all",
287
+ limit: String(params.limit || 25),
288
+ type: params.type || "link",
289
+ restrict_sr: params.subreddit ? "true" : "false"
290
+ });
291
+ if (params.after) qs.set("after", params.after);
292
+ const base = params.subreddit ? `/r/${params.subreddit}` : "";
293
+ const data = await this._api("GET", `${base}/search.json?${qs.toString()}`, accessToken);
294
+ return {
295
+ items: (data.data?.children || []).map((c) => this._parsePost(c.data)),
296
+ after: data.data?.after ?? null,
297
+ before: data.data?.before ?? null
298
+ };
299
+ }
300
+ /**
301
+ * Create a comment on a post or reply to a comment.
302
+ */
303
+ async createComment(accessToken, params) {
304
+ const json = (await this._api("POST", "/api/comment", accessToken, {
305
+ api_type: "json",
306
+ thing_id: params.parentFullname,
307
+ text: params.text
308
+ }, true)).json;
309
+ if (json?.errors?.length) {
310
+ const [errorCode, errorMsg] = json.errors[0];
311
+ throw new SocialError("reddit", `Comment creation failed: ${errorMsg}`, {
312
+ statusCode: 400,
313
+ errorCode,
314
+ hint: REDDIT_ERROR_HINTS[errorCode] || null
315
+ });
316
+ }
317
+ const commentData = json?.data?.things?.[0]?.data;
318
+ if (!commentData) throw new SocialError("reddit", "Unexpected response format from Reddit API", { statusCode: 502 });
319
+ return this._parseComment(commentData);
320
+ }
321
+ /**
322
+ * Get comments for a post.
323
+ */
324
+ async getPostComments(accessToken, subreddit, postId, params = {}) {
325
+ const qs = new URLSearchParams();
326
+ if (params.limit) qs.set("limit", String(Math.min(params.limit, 100)));
327
+ if (params.sort) qs.set("sort", params.sort);
328
+ const queryStr = qs.toString();
329
+ return ((await this._api("GET", `/r/${subreddit}/comments/${postId}.json${queryStr ? `?${queryStr}` : ""}`, accessToken))[1]?.data?.children || []).filter((c) => c.kind === "t1").map((c) => this._parseComment(c.data));
330
+ }
331
+ /**
332
+ * Vote on a post or comment.
333
+ * @param dir - 1 = upvote, 0 = unvote, -1 = downvote
334
+ */
335
+ async vote(accessToken, fullname, dir) {
336
+ await this._api("POST", "/api/vote", accessToken, {
337
+ id: fullname,
338
+ dir: String(dir)
339
+ }, true);
340
+ return { success: true };
341
+ }
342
+ /**
343
+ * Get subreddit information.
344
+ */
345
+ async getSubredditInfo(accessToken, subreddit) {
346
+ const data = await this._api("GET", `/r/${subreddit}/about.json`, accessToken);
347
+ return this._parseSubreddit(data.data);
348
+ }
349
+ /**
350
+ * Get subreddit rules.
351
+ */
352
+ async getSubredditRules(accessToken, subreddit) {
353
+ return ((await this._api("GET", `/r/${subreddit}/about/rules.json`, accessToken)).rules || []).map((r) => ({
354
+ kind: r.kind,
355
+ shortName: r.short_name,
356
+ description: r.description,
357
+ violationReason: r.violation_reason,
358
+ priority: r.priority,
359
+ createdUtc: r.created_utc
360
+ }));
361
+ }
362
+ /**
363
+ * Search for subreddits.
364
+ */
365
+ async searchSubreddits(accessToken, query, opts = {}) {
366
+ const qs = new URLSearchParams({
367
+ query,
368
+ limit: String(opts.limit || 25)
369
+ });
370
+ return ((await this._api("POST", `/api/search_subreddits.json?${qs.toString()}`, accessToken, {}, true)).subreddits || []).map((s) => ({
371
+ id: s.name,
372
+ name: s.name,
373
+ displayName: s.name,
374
+ displayNamePrefixed: `r/${s.name}`,
375
+ title: s.name,
376
+ publicDescription: "",
377
+ subscribers: s.subscriber_count ?? 0,
378
+ activeUserCount: s.active_user_count ?? 0,
379
+ createdUtc: 0,
380
+ over18: false,
381
+ subredditType: "public",
382
+ iconImg: s.icon_img,
383
+ url: `/r/${s.name}/`
384
+ }));
385
+ }
386
+ /**
387
+ * Get authenticated user's profile details.
388
+ */
389
+ async getProfile(accessToken, detail = "identity") {
390
+ if (detail === "identity") {
391
+ const data = await this._api("GET", "/api/v1/me", accessToken);
392
+ return {
393
+ id: data.id,
394
+ name: data.name,
395
+ iconImg: data.icon_img?.split("?")[0],
396
+ createdUtc: data.created_utc,
397
+ linkKarma: data.link_karma,
398
+ commentKarma: data.comment_karma,
399
+ totalKarma: data.total_karma,
400
+ isGold: data.is_gold,
401
+ isMod: data.is_mod,
402
+ hasVerifiedEmail: data.has_verified_email
403
+ };
404
+ }
405
+ if (detail === "karma") return {
406
+ id: "",
407
+ name: "",
408
+ createdUtc: 0,
409
+ linkKarma: 0,
410
+ commentKarma: 0,
411
+ totalKarma: 0,
412
+ isGold: false,
413
+ isMod: false,
414
+ hasVerifiedEmail: false,
415
+ subredditKarma: ((await this._api("GET", "/api/v1/me/karma", accessToken)).data || []).map((k) => ({
416
+ subreddit: k.sr,
417
+ linkKarma: k.link_karma,
418
+ commentKarma: k.comment_karma
419
+ }))
420
+ };
421
+ return {
422
+ id: "",
423
+ name: "",
424
+ createdUtc: 0,
425
+ linkKarma: 0,
426
+ commentKarma: 0,
427
+ totalKarma: 0,
428
+ isGold: false,
429
+ isMod: false,
430
+ hasVerifiedEmail: false,
431
+ ...await this._api("GET", {
432
+ trophies: "/api/v1/me/trophies",
433
+ friends: "/api/v1/me/friends",
434
+ blocked: "/api/v1/me/blocked",
435
+ prefs: "/api/v1/me/prefs",
436
+ saved: "/user/me/saved.json"
437
+ }[detail], accessToken)
438
+ };
439
+ }
440
+ /**
441
+ * Get a public user's information.
442
+ */
443
+ async getUser(accessToken, username) {
444
+ const user = (await this._api("GET", `/user/${username}/about.json`, accessToken)).data;
445
+ return {
446
+ id: user.id,
447
+ name: user.name,
448
+ iconImg: user.icon_img?.split("?")[0],
449
+ createdUtc: user.created_utc,
450
+ linkKarma: user.link_karma,
451
+ commentKarma: user.comment_karma,
452
+ totalKarma: (user.link_karma ?? 0) + (user.comment_karma ?? 0),
453
+ isGold: user.is_gold ?? false,
454
+ isMod: user.is_mod ?? false,
455
+ hasVerifiedEmail: user.has_verified_email ?? false
456
+ };
457
+ }
458
+ async saveContent(accessToken, fullname) {
459
+ await this._api("POST", "/api/save", accessToken, { id: fullname }, true);
460
+ return { success: true };
461
+ }
462
+ async unsaveContent(accessToken, fullname) {
463
+ await this._api("POST", "/api/unsave", accessToken, { id: fullname }, true);
464
+ return { success: true };
465
+ }
466
+ async uploadPhoto(params) {
467
+ const { tokens, title, caption, description } = params;
468
+ const imageUrl = params.videoUrl || params.imageUrl || params.url;
469
+ if (!imageUrl) throw new SocialError("reddit", "Image URL is required for Reddit image posts", { statusCode: 400 });
470
+ if (!params.subreddit) throw new SocialError("reddit", "Subreddit is required for Reddit posts", { statusCode: 400 });
471
+ const result = await this.createPost(tokens.access_token, {
472
+ subreddit: params.subreddit,
473
+ title: title || caption || description || "Image Post",
474
+ kind: "image",
475
+ url: imageUrl
476
+ });
477
+ return {
478
+ platformPostId: result.id,
479
+ platformUrl: result.url,
480
+ status: "published",
481
+ uploadedAt: /* @__PURE__ */ new Date()
482
+ };
483
+ }
484
+ async sendMessage(accessToken, subreddit, text) {
485
+ return this.createPost(accessToken, {
486
+ subreddit,
487
+ title: text.substring(0, 300),
488
+ kind: "self",
489
+ text
490
+ });
491
+ }
492
+ getCredentialZodSchema() {
493
+ return RedditCredentialsSchema;
494
+ }
495
+ getCredentialSchema() {
496
+ return [{
497
+ name: "clientId",
498
+ displayName: "Client ID",
499
+ type: "text",
500
+ required: true,
501
+ description: "Reddit app Client ID from reddit.com/prefs/apps",
502
+ placeholder: "abc123..."
503
+ }, {
504
+ name: "clientSecret",
505
+ displayName: "Client Secret",
506
+ type: "password",
507
+ required: true,
508
+ description: "Reddit app Client Secret",
509
+ placeholder: "secret..."
510
+ }];
511
+ }
512
+ getMetadata() {
513
+ return {
514
+ name: this.name,
515
+ displayName: this.displayName,
516
+ authType: this.authType,
517
+ icon: "reddit",
518
+ brandColor: "#FF4500",
519
+ description: "Post to Reddit communities, browse subreddits, and manage content",
520
+ scopes: DEFAULT_SCOPES,
521
+ scopeDescriptions: SCOPE_DESCRIPTIONS,
522
+ setupGuide: [
523
+ {
524
+ step: 1,
525
+ title: "Create a Reddit App",
526
+ description: "Go to reddit.com/prefs/apps → \"Create another app\". Select \"web app\" type."
527
+ },
528
+ {
529
+ step: 2,
530
+ title: "Set redirect URI",
531
+ description: "Set the redirect URI to your callback URL (e.g., http://localhost:8060/api/oauth/reddit/callback)."
532
+ },
533
+ {
534
+ step: 3,
535
+ title: "Copy credentials",
536
+ description: "The Client ID is shown under the app name. The Client Secret is labeled \"secret\"."
537
+ },
538
+ {
539
+ step: 4,
540
+ title: "User-Agent",
541
+ description: "Reddit requires a descriptive User-Agent header. This is handled automatically by the provider."
542
+ }
543
+ ],
544
+ supportsScheduling: false,
545
+ supportsEnvironment: false,
546
+ redirectUriPattern: "/api/oauth/reddit/callback",
547
+ credentialSchema: this.getCredentialSchema()
548
+ };
549
+ }
550
+ /**
551
+ * Core API request helper.
552
+ * Uses OAuth endpoint for authenticated requests, public endpoint otherwise.
553
+ */
554
+ async _api(method, endpoint, accessToken, body, isForm = false) {
555
+ const baseUrl = accessToken ? OAUTH_API : PUBLIC_API;
556
+ const url = endpoint.startsWith("http") ? endpoint : `${baseUrl}${endpoint}`;
557
+ const result = await httpRequest("reddit", {
558
+ method: method.toUpperCase(),
559
+ url,
560
+ bearer: accessToken || void 0,
561
+ headers: { "User-Agent": USER_AGENT },
562
+ json: body && !isForm ? body : void 0,
563
+ urlencoded: body && isForm ? body : void 0,
564
+ timeout: 3e4,
565
+ retry: { attempts: 2 },
566
+ parseError: (raw, status) => {
567
+ if (raw && typeof raw === "object") {
568
+ const r = raw;
569
+ const errorReason = r.reason;
570
+ return {
571
+ message: r.message || r.error || `Reddit API error (${status})`,
572
+ errorCode: errorReason || r.error || null,
573
+ hint: REDDIT_ERROR_HINTS[String(status)] || (errorReason ? REDDIT_ERROR_HINTS[errorReason] : null) || null
574
+ };
575
+ }
576
+ return null;
577
+ }
578
+ });
579
+ return result.status === 204 ? { success: true } : result.data;
580
+ }
581
+ _parsePost(raw) {
582
+ return {
583
+ id: raw.id,
584
+ name: raw.name,
585
+ title: raw.title,
586
+ author: raw.author,
587
+ subreddit: raw.subreddit,
588
+ subredditId: raw.subreddit_id,
589
+ selftext: raw.selftext || "",
590
+ selftextHtml: raw.selftext_html,
591
+ url: raw.url,
592
+ permalink: `https://reddit.com${raw.permalink}`,
593
+ domain: raw.domain,
594
+ score: raw.score ?? 0,
595
+ ups: raw.ups ?? 0,
596
+ downs: raw.downs ?? 0,
597
+ numComments: raw.num_comments ?? 0,
598
+ createdUtc: raw.created_utc,
599
+ isSelf: raw.is_self ?? false,
600
+ isVideo: raw.is_video ?? false,
601
+ over18: raw.over_18 ?? false,
602
+ spoiler: raw.spoiler ?? false,
603
+ stickied: raw.stickied ?? false,
604
+ locked: raw.locked ?? false,
605
+ archived: raw.archived ?? false,
606
+ thumbnail: raw.thumbnail !== "self" && raw.thumbnail !== "default" ? raw.thumbnail : void 0,
607
+ linkFlairText: raw.link_flair_text,
608
+ authorFlairText: raw.author_flair_text,
609
+ mediaUrl: raw.url_overridden_by_dest || raw.url
610
+ };
611
+ }
612
+ _parseComment(raw) {
613
+ return {
614
+ id: raw.id,
615
+ name: raw.name,
616
+ author: raw.author,
617
+ body: raw.body,
618
+ bodyHtml: raw.body_html,
619
+ score: raw.score ?? 0,
620
+ ups: raw.ups ?? 0,
621
+ downs: raw.downs ?? 0,
622
+ createdUtc: raw.created_utc,
623
+ parentId: raw.parent_id,
624
+ linkId: raw.link_id,
625
+ subreddit: raw.subreddit,
626
+ depth: raw.depth ?? 0,
627
+ isSubmitter: raw.is_submitter ?? false,
628
+ stickied: raw.stickied ?? false,
629
+ edited: raw.edited ?? false,
630
+ permalink: `https://reddit.com${raw.permalink || ""}`
631
+ };
632
+ }
633
+ _parseSubreddit(raw) {
634
+ return {
635
+ id: raw.id,
636
+ name: raw.name,
637
+ displayName: raw.display_name,
638
+ displayNamePrefixed: raw.display_name_prefixed,
639
+ title: raw.title,
640
+ publicDescription: raw.public_description,
641
+ description: raw.description,
642
+ subscribers: raw.subscribers ?? 0,
643
+ activeUserCount: raw.active_user_count,
644
+ createdUtc: raw.created_utc,
645
+ over18: raw.over18 ?? false,
646
+ subredditType: raw.subreddit_type || "public",
647
+ iconImg: raw.icon_img?.split("?")[0],
648
+ bannerImg: raw.banner_background_image?.split("?")[0],
649
+ communityIcon: raw.community_icon?.split("?")[0],
650
+ url: raw.url
651
+ };
652
+ }
653
+ };
654
+
655
+ //#endregion
656
+ export { RedditProvider };
657
+ //# sourceMappingURL=reddit.mjs.map