@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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/base-Bw7e52V8.mjs +246 -0
- package/dist/base-Bw7e52V8.mjs.map +1 -0
- package/dist/base-DBtKFiSX.d.mts +226 -0
- package/dist/base-DBtKFiSX.d.mts.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client/index.d.mts +44 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +154 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/common/index.d.mts +3 -0
- package/dist/common/index.mjs +7 -0
- package/dist/contracts-Cdwa4zlg.d.mts +121 -0
- package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
- package/dist/contracts-lCa069IK.mjs +221 -0
- package/dist/contracts-lCa069IK.mjs.map +1 -0
- package/dist/env-Bl0cwwjC.mjs +955 -0
- package/dist/env-Bl0cwwjC.mjs.map +1 -0
- package/dist/env-DxOZHf0p.d.mts +394 -0
- package/dist/env-DxOZHf0p.d.mts.map +1 -0
- package/dist/errors-Cm6LeKf7.mjs +32 -0
- package/dist/errors-Cm6LeKf7.mjs.map +1 -0
- package/dist/facebook-l_4CghaA.mjs +95 -0
- package/dist/facebook-l_4CghaA.mjs.map +1 -0
- package/dist/http-DpcLSR1M.mjs +197 -0
- package/dist/http-DpcLSR1M.mjs.map +1 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +71 -0
- package/dist/index.mjs.map +1 -0
- package/dist/instagram-BGaeUFU2.mjs +90 -0
- package/dist/instagram-BGaeUFU2.mjs.map +1 -0
- package/dist/linkedin-70whtVKa.mjs +101 -0
- package/dist/linkedin-70whtVKa.mjs.map +1 -0
- package/dist/meta-D3vcJU1c.mjs +126 -0
- package/dist/meta-D3vcJU1c.mjs.map +1 -0
- package/dist/pkce-jq5II68b.mjs +72 -0
- package/dist/pkce-jq5II68b.mjs.map +1 -0
- package/dist/polling-DZ1apXtA.mjs +25 -0
- package/dist/polling-DZ1apXtA.mjs.map +1 -0
- package/dist/providers/facebook.d.mts +135 -0
- package/dist/providers/facebook.d.mts.map +1 -0
- package/dist/providers/facebook.mjs +450 -0
- package/dist/providers/facebook.mjs.map +1 -0
- package/dist/providers/instagram.d.mts +122 -0
- package/dist/providers/instagram.d.mts.map +1 -0
- package/dist/providers/instagram.mjs +496 -0
- package/dist/providers/instagram.mjs.map +1 -0
- package/dist/providers/linkedin.d.mts +145 -0
- package/dist/providers/linkedin.d.mts.map +1 -0
- package/dist/providers/linkedin.mjs +574 -0
- package/dist/providers/linkedin.mjs.map +1 -0
- package/dist/providers/reddit.d.mts +102 -0
- package/dist/providers/reddit.d.mts.map +1 -0
- package/dist/providers/reddit.mjs +657 -0
- package/dist/providers/reddit.mjs.map +1 -0
- package/dist/providers/telegram.d.mts +139 -0
- package/dist/providers/telegram.d.mts.map +1 -0
- package/dist/providers/telegram.mjs +517 -0
- package/dist/providers/telegram.mjs.map +1 -0
- package/dist/providers/tiktok.d.mts +116 -0
- package/dist/providers/tiktok.d.mts.map +1 -0
- package/dist/providers/tiktok.mjs +676 -0
- package/dist/providers/tiktok.mjs.map +1 -0
- package/dist/providers/twitter.d.mts +150 -0
- package/dist/providers/twitter.d.mts.map +1 -0
- package/dist/providers/twitter.mjs +628 -0
- package/dist/providers/twitter.mjs.map +1 -0
- package/dist/providers/whatsapp.d.mts +79 -0
- package/dist/providers/whatsapp.d.mts.map +1 -0
- package/dist/providers/whatsapp.mjs +376 -0
- package/dist/providers/whatsapp.mjs.map +1 -0
- package/dist/providers/youtube.d.mts +153 -0
- package/dist/providers/youtube.d.mts.map +1 -0
- package/dist/providers/youtube.mjs +902 -0
- package/dist/providers/youtube.mjs.map +1 -0
- package/dist/reddit-B10kS4Se.mjs +126 -0
- package/dist/reddit-B10kS4Se.mjs.map +1 -0
- package/dist/schemas/index.d.mts +819 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +31 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/security-BXhfebWm.d.mts +338 -0
- package/dist/security-BXhfebWm.d.mts.map +1 -0
- package/dist/shared-Fvc6xQku.mjs +100 -0
- package/dist/shared-Fvc6xQku.mjs.map +1 -0
- package/dist/telegram-FaUHpZgB.mjs +107 -0
- package/dist/telegram-FaUHpZgB.mjs.map +1 -0
- package/dist/tiktok-B_bMk4G-.mjs +94 -0
- package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
- package/dist/twitter-BC22zfuc.mjs +98 -0
- package/dist/twitter-BC22zfuc.mjs.map +1 -0
- package/dist/types-BFE4psYI.d.mts +102 -0
- package/dist/types-BFE4psYI.d.mts.map +1 -0
- package/dist/types-Bv27tcT0.d.mts +230 -0
- package/dist/types-Bv27tcT0.d.mts.map +1 -0
- package/dist/types-BwkKyqpi.d.mts +253 -0
- package/dist/types-BwkKyqpi.d.mts.map +1 -0
- package/dist/types-CJrHMDV9.mjs +27 -0
- package/dist/types-CJrHMDV9.mjs.map +1 -0
- package/dist/types-ClbVc2rc.d.mts +117 -0
- package/dist/types-ClbVc2rc.d.mts.map +1 -0
- package/dist/types-D91N16Ym.d.mts +242 -0
- package/dist/types-D91N16Ym.d.mts.map +1 -0
- package/dist/types-DfLp_ibQ.d.mts +178 -0
- package/dist/types-DfLp_ibQ.d.mts.map +1 -0
- package/dist/types-DfjDgEoJ.d.mts +88 -0
- package/dist/types-DfjDgEoJ.d.mts.map +1 -0
- package/dist/types-Dp5Z9VBr.mjs +23 -0
- package/dist/types-Dp5Z9VBr.mjs.map +1 -0
- package/dist/types-hriBJTsU.d.mts +129 -0
- package/dist/types-hriBJTsU.d.mts.map +1 -0
- package/dist/types-rn6UuLL8.d.mts +184 -0
- package/dist/types-rn6UuLL8.d.mts.map +1 -0
- package/dist/whatsapp-CFp7ryR4.mjs +101 -0
- package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
- package/dist/youtube-Bs0fdY7H.mjs +98 -0
- package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
- 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
|