@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,955 @@
|
|
|
1
|
+
import { t as SocialError } from "./errors-Cm6LeKf7.mjs";
|
|
2
|
+
import { TwitterProvider } from "./providers/twitter.mjs";
|
|
3
|
+
import { FacebookProvider } from "./providers/facebook.mjs";
|
|
4
|
+
import { LinkedInProvider } from "./providers/linkedin.mjs";
|
|
5
|
+
import { RedditProvider } from "./providers/reddit.mjs";
|
|
6
|
+
import { InstagramProvider } from "./providers/instagram.mjs";
|
|
7
|
+
import { YouTubeProvider } from "./providers/youtube.mjs";
|
|
8
|
+
import { TikTokProvider } from "./providers/tiktok.mjs";
|
|
9
|
+
import { TelegramProvider } from "./providers/telegram.mjs";
|
|
10
|
+
import { WhatsAppProvider } from "./providers/whatsapp.mjs";
|
|
11
|
+
import { SocialClient } from "./client/index.mjs";
|
|
12
|
+
|
|
13
|
+
//#region src/client/twitter.ts
|
|
14
|
+
/**
|
|
15
|
+
* TwitterClient — credential-bound wrapper over `TwitterProvider`.
|
|
16
|
+
*/
|
|
17
|
+
var TwitterClient = class {
|
|
18
|
+
provider;
|
|
19
|
+
constructor(config, opts = {}) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.provider = new TwitterProvider({
|
|
22
|
+
port: opts.port,
|
|
23
|
+
redirectUri: config.redirectUri,
|
|
24
|
+
environment: opts.environment
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** OAuth: get the consent URL. Async variant uses S256 PKCE (preferred). */
|
|
28
|
+
async authUrl(state) {
|
|
29
|
+
return this.provider.getAuthUrlAsync(state, this.config);
|
|
30
|
+
}
|
|
31
|
+
/** OAuth: exchange the callback code for tokens. Stores them on `this.config.tokens`. */
|
|
32
|
+
async exchangeCode(code, state) {
|
|
33
|
+
const tokens = await this.provider.exchangeCode(code, this.config, state);
|
|
34
|
+
this.config.tokens = tokens;
|
|
35
|
+
return tokens;
|
|
36
|
+
}
|
|
37
|
+
/** Refresh the access token. Stores updated tokens. */
|
|
38
|
+
async refresh() {
|
|
39
|
+
const refreshToken = this.config.tokens?.refresh_token;
|
|
40
|
+
if (!refreshToken) throw new SocialError("twitter", "No refresh_token available");
|
|
41
|
+
const tokens = await this.provider.refreshToken(refreshToken, this.config);
|
|
42
|
+
this.config.tokens = {
|
|
43
|
+
...this.config.tokens,
|
|
44
|
+
...tokens
|
|
45
|
+
};
|
|
46
|
+
return this.config.tokens;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Post a tweet (text + optional media).
|
|
50
|
+
*
|
|
51
|
+
* Twitter requires media to be pre-uploaded via the v1.1 chunked endpoint —
|
|
52
|
+
* pass each pre-uploaded media's ID as `media[i].id`. Items with only a
|
|
53
|
+
* `url` are skipped (no automatic upload). For end-to-end upload see
|
|
54
|
+
* `provider.uploadPhoto()` / `provider.uploadVideo()` on the underlying
|
|
55
|
+
* `TwitterProvider`.
|
|
56
|
+
*/
|
|
57
|
+
async post(input) {
|
|
58
|
+
try {
|
|
59
|
+
const accessToken = this.assertAccessToken();
|
|
60
|
+
const mediaIds = input.media?.map((m) => m.id).filter((id) => !!id);
|
|
61
|
+
if ((input.media?.length ?? 0) - (mediaIds?.length ?? 0) > 0 && mediaIds?.length === 0) throw new SocialError("twitter", "Twitter requires pre-uploaded media IDs. Upload via TwitterProvider.uploadPhoto/uploadVideo first, then pass `media[i].id`.", { statusCode: 400 });
|
|
62
|
+
const result = await this.provider.createTweet(accessToken, {
|
|
63
|
+
text: input.text ?? "",
|
|
64
|
+
mediaIds: mediaIds?.length ? mediaIds : void 0,
|
|
65
|
+
replyTo: input.perPlatform?.twitter?.replyTo,
|
|
66
|
+
quoteTweetId: input.perPlatform?.twitter?.quoteTweet
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
platform: "twitter",
|
|
70
|
+
ok: true,
|
|
71
|
+
id: result.id,
|
|
72
|
+
url: result.id ? `https://twitter.com/i/status/${result.id}` : void 0,
|
|
73
|
+
raw: result
|
|
74
|
+
};
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return errorResult("twitter", err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Delete a tweet by ID. */
|
|
80
|
+
async delete(tweetId) {
|
|
81
|
+
const accessToken = this.assertAccessToken();
|
|
82
|
+
await this.provider.deleteTweet(accessToken, tweetId);
|
|
83
|
+
}
|
|
84
|
+
/** Get a tweet by ID. */
|
|
85
|
+
async get(tweetId) {
|
|
86
|
+
const accessToken = this.assertAccessToken();
|
|
87
|
+
return this.provider.getTweet(accessToken, tweetId);
|
|
88
|
+
}
|
|
89
|
+
/** Send a direct message. */
|
|
90
|
+
async sendDm(participantId, text) {
|
|
91
|
+
const accessToken = this.assertAccessToken();
|
|
92
|
+
return this.provider.sendDirectMessage(accessToken, {
|
|
93
|
+
participantId,
|
|
94
|
+
text
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Get the authenticated user's profile. */
|
|
98
|
+
async me() {
|
|
99
|
+
const accessToken = this.assertAccessToken();
|
|
100
|
+
return this.provider.getAccountInfo(accessToken);
|
|
101
|
+
}
|
|
102
|
+
assertAccessToken() {
|
|
103
|
+
const t = this.config.tokens?.access_token;
|
|
104
|
+
if (!t) throw new SocialError("twitter", "Not authenticated. Call exchangeCode() first or pass tokens in config.", { statusCode: 401 });
|
|
105
|
+
return t;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
function errorResult(platform, err) {
|
|
109
|
+
if (err instanceof SocialError) return {
|
|
110
|
+
platform,
|
|
111
|
+
ok: false,
|
|
112
|
+
error: {
|
|
113
|
+
message: err.message,
|
|
114
|
+
code: err.errorCode,
|
|
115
|
+
retryable: err.retryable
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
platform,
|
|
120
|
+
ok: false,
|
|
121
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/client/facebook.ts
|
|
127
|
+
/**
|
|
128
|
+
* FacebookClient — credential-bound wrapper over `FacebookProvider`.
|
|
129
|
+
*/
|
|
130
|
+
var FacebookClient = class {
|
|
131
|
+
provider;
|
|
132
|
+
constructor(config, opts = {}) {
|
|
133
|
+
this.config = config;
|
|
134
|
+
this.provider = new FacebookProvider({
|
|
135
|
+
port: opts.port,
|
|
136
|
+
redirectUri: config.redirectUri,
|
|
137
|
+
environment: opts.environment
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async authUrl(state) {
|
|
141
|
+
return this.provider.getAuthUrl(state, this.config);
|
|
142
|
+
}
|
|
143
|
+
async exchangeCode(code) {
|
|
144
|
+
const tokens = await this.provider.exchangeCode(code, this.config);
|
|
145
|
+
this.config.tokens = tokens;
|
|
146
|
+
return tokens;
|
|
147
|
+
}
|
|
148
|
+
async refresh() {
|
|
149
|
+
const refreshToken = this.config.tokens?.refresh_token ?? this.config.tokens?.access_token;
|
|
150
|
+
if (!refreshToken) throw new SocialError("facebook", "No refresh_token available");
|
|
151
|
+
const tokens = await this.provider.refreshToken(refreshToken, this.config);
|
|
152
|
+
this.config.tokens = {
|
|
153
|
+
...this.config.tokens,
|
|
154
|
+
...tokens
|
|
155
|
+
};
|
|
156
|
+
return this.config.tokens;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* List Pages the authenticated user manages. Each result contains a
|
|
160
|
+
* page-level access token. Set `pageId` + `pageAccessToken` on the config to
|
|
161
|
+
* select a target page for posting.
|
|
162
|
+
*/
|
|
163
|
+
async listPages() {
|
|
164
|
+
return this.provider.getPages(this.userToken());
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Convenience: pick a page by ID and bind its token to this client.
|
|
168
|
+
*/
|
|
169
|
+
async usePage(pageId) {
|
|
170
|
+
const match = (await this.listPages()).find((p) => p.id === pageId);
|
|
171
|
+
if (!match) throw new SocialError("facebook", `Page ${pageId} not found in /me/accounts`, { statusCode: 404 });
|
|
172
|
+
this.config.pageId = match.id;
|
|
173
|
+
this.config.pageAccessToken = match.access_token;
|
|
174
|
+
}
|
|
175
|
+
/** Create a Page post — text, link, or photo depending on input. */
|
|
176
|
+
async post(input) {
|
|
177
|
+
try {
|
|
178
|
+
const { pageId, token } = this.assertPage(input);
|
|
179
|
+
const scheduledAt = input.scheduledAt;
|
|
180
|
+
const photo = input.media?.find((m) => m.type === "image" && m.url);
|
|
181
|
+
if (photo?.url) {
|
|
182
|
+
const r = await this.provider.createPhotoPost(token, pageId, photo.url, {
|
|
183
|
+
caption: input.text,
|
|
184
|
+
scheduledAt
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
platform: "facebook",
|
|
188
|
+
ok: true,
|
|
189
|
+
id: r.postId,
|
|
190
|
+
url: `https://facebook.com/${r.postId}`,
|
|
191
|
+
raw: r
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (input.link) {
|
|
195
|
+
const r = await this.provider.createLinkPost(token, pageId, input.link, {
|
|
196
|
+
message: input.text,
|
|
197
|
+
scheduledAt
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
platform: "facebook",
|
|
201
|
+
ok: true,
|
|
202
|
+
id: r.postId,
|
|
203
|
+
url: `https://facebook.com/${r.postId}`,
|
|
204
|
+
raw: r
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (input.text) {
|
|
208
|
+
const r = await this.provider.createPost(token, pageId, input.text, { scheduledAt });
|
|
209
|
+
return {
|
|
210
|
+
platform: "facebook",
|
|
211
|
+
ok: true,
|
|
212
|
+
id: r.postId,
|
|
213
|
+
url: `https://facebook.com/${r.postId}`,
|
|
214
|
+
raw: r
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
throw new SocialError("facebook", "Provide either text, link, or photo media", { statusCode: 400 });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return errorResult("facebook", err);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async deletePost(postId) {
|
|
223
|
+
const { token } = this.assertPage();
|
|
224
|
+
await this.provider.deletePost(token, postId);
|
|
225
|
+
}
|
|
226
|
+
async getPost(postId) {
|
|
227
|
+
const { token } = this.assertPage();
|
|
228
|
+
return this.provider.getPost(token, postId);
|
|
229
|
+
}
|
|
230
|
+
async getFeed(limit) {
|
|
231
|
+
const { pageId, token } = this.assertPage();
|
|
232
|
+
return this.provider.getPageFeed(token, pageId, limit);
|
|
233
|
+
}
|
|
234
|
+
userToken() {
|
|
235
|
+
const t = this.config.tokens?.access_token;
|
|
236
|
+
if (!t) throw new SocialError("facebook", "Not authenticated. Call exchangeCode() first.", { statusCode: 401 });
|
|
237
|
+
return t;
|
|
238
|
+
}
|
|
239
|
+
assertPage(input) {
|
|
240
|
+
const pageId = input?.perPlatform?.facebook?.pageId ?? this.config.pageId;
|
|
241
|
+
const token = input?.perPlatform?.facebook?.pageAccessToken ?? this.config.pageAccessToken;
|
|
242
|
+
if (!pageId || !token) throw new SocialError("facebook", "No Page selected. Call usePage(pageId) or set pageId/pageAccessToken in config.", { statusCode: 400 });
|
|
243
|
+
return {
|
|
244
|
+
pageId,
|
|
245
|
+
token
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/client/linkedin.ts
|
|
252
|
+
/**
|
|
253
|
+
* LinkedInClient — credential-bound wrapper over `LinkedInProvider`.
|
|
254
|
+
*/
|
|
255
|
+
var LinkedInClient = class {
|
|
256
|
+
provider;
|
|
257
|
+
constructor(config, opts = {}) {
|
|
258
|
+
this.config = config;
|
|
259
|
+
this.provider = new LinkedInProvider({
|
|
260
|
+
port: opts.port,
|
|
261
|
+
redirectUri: config.redirectUri,
|
|
262
|
+
environment: opts.environment
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async authUrl(state) {
|
|
266
|
+
return this.provider.getAuthUrl(state, this.config);
|
|
267
|
+
}
|
|
268
|
+
async exchangeCode(code) {
|
|
269
|
+
const tokens = await this.provider.exchangeCode(code, this.config);
|
|
270
|
+
this.config.tokens = tokens;
|
|
271
|
+
return tokens;
|
|
272
|
+
}
|
|
273
|
+
async refresh() {
|
|
274
|
+
const refreshToken = this.config.tokens?.refresh_token;
|
|
275
|
+
if (!refreshToken) throw new SocialError("linkedin", "No refresh_token available");
|
|
276
|
+
const tokens = await this.provider.refreshToken(refreshToken, this.config);
|
|
277
|
+
this.config.tokens = {
|
|
278
|
+
...this.config.tokens,
|
|
279
|
+
...tokens
|
|
280
|
+
};
|
|
281
|
+
return this.config.tokens;
|
|
282
|
+
}
|
|
283
|
+
/** Resolve and cache the authenticated user's URN. */
|
|
284
|
+
async resolveAuthor() {
|
|
285
|
+
if (this.config.authorUrn) return this.config.authorUrn;
|
|
286
|
+
const accessToken = this.assertAccessToken();
|
|
287
|
+
const info = await this.provider.getAccountInfo(accessToken);
|
|
288
|
+
if (!info?.id) throw new SocialError("linkedin", "Could not resolve author URN", { statusCode: 502 });
|
|
289
|
+
const urn = `urn:li:person:${info.id}`;
|
|
290
|
+
this.config.authorUrn = urn;
|
|
291
|
+
return urn;
|
|
292
|
+
}
|
|
293
|
+
/** Create a post (text, article, or image). */
|
|
294
|
+
async post(input) {
|
|
295
|
+
try {
|
|
296
|
+
const accessToken = this.assertAccessToken();
|
|
297
|
+
const author = input.perPlatform?.linkedin?.authorUrn ?? await this.resolveAuthor();
|
|
298
|
+
if (input.link) {
|
|
299
|
+
const r = await this.provider.createArticlePost(accessToken, author, input.text ?? "", input.link);
|
|
300
|
+
return {
|
|
301
|
+
platform: "linkedin",
|
|
302
|
+
ok: true,
|
|
303
|
+
id: r.postId,
|
|
304
|
+
raw: r
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (!input.text) throw new SocialError("linkedin", "Provide text for the post", { statusCode: 400 });
|
|
308
|
+
const r = await this.provider.createTextPost(accessToken, author, input.text);
|
|
309
|
+
return {
|
|
310
|
+
platform: "linkedin",
|
|
311
|
+
ok: true,
|
|
312
|
+
id: r.postId,
|
|
313
|
+
raw: r
|
|
314
|
+
};
|
|
315
|
+
} catch (err) {
|
|
316
|
+
return errorResult("linkedin", err);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async deletePost(postUrn) {
|
|
320
|
+
const accessToken = this.assertAccessToken();
|
|
321
|
+
await this.provider.deletePost(accessToken, postUrn);
|
|
322
|
+
}
|
|
323
|
+
async getPost(postUrn) {
|
|
324
|
+
const accessToken = this.assertAccessToken();
|
|
325
|
+
return this.provider.getPost(accessToken, postUrn);
|
|
326
|
+
}
|
|
327
|
+
assertAccessToken() {
|
|
328
|
+
const t = this.config.tokens?.access_token;
|
|
329
|
+
if (!t) throw new SocialError("linkedin", "Not authenticated. Call exchangeCode() first.", { statusCode: 401 });
|
|
330
|
+
return t;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/client/reddit.ts
|
|
336
|
+
/**
|
|
337
|
+
* RedditClient — credential-bound wrapper over `RedditProvider`.
|
|
338
|
+
*
|
|
339
|
+
* Reddit posts always require a `subreddit` and `title`. Pass them via
|
|
340
|
+
* `input.perPlatform.reddit`.
|
|
341
|
+
*/
|
|
342
|
+
var RedditClient = class {
|
|
343
|
+
provider;
|
|
344
|
+
constructor(config, opts = {}) {
|
|
345
|
+
this.config = config;
|
|
346
|
+
this.provider = new RedditProvider({
|
|
347
|
+
port: opts.port,
|
|
348
|
+
redirectUri: config.redirectUri,
|
|
349
|
+
environment: opts.environment
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async authUrl(state) {
|
|
353
|
+
return this.provider.getAuthUrl(state, this.config);
|
|
354
|
+
}
|
|
355
|
+
async exchangeCode(code) {
|
|
356
|
+
const tokens = await this.provider.exchangeCode(code, this.config);
|
|
357
|
+
this.config.tokens = tokens;
|
|
358
|
+
return tokens;
|
|
359
|
+
}
|
|
360
|
+
async refresh() {
|
|
361
|
+
const refreshToken = this.config.tokens?.refresh_token;
|
|
362
|
+
if (!refreshToken) throw new SocialError("reddit", "No refresh_token available");
|
|
363
|
+
const tokens = await this.provider.refreshToken(refreshToken, this.config);
|
|
364
|
+
this.config.tokens = {
|
|
365
|
+
...this.config.tokens,
|
|
366
|
+
...tokens
|
|
367
|
+
};
|
|
368
|
+
return this.config.tokens;
|
|
369
|
+
}
|
|
370
|
+
/** Submit to a subreddit. Requires `subreddit` + `title` in `perPlatform.reddit`. */
|
|
371
|
+
async post(input) {
|
|
372
|
+
try {
|
|
373
|
+
const accessToken = this.assertAccessToken();
|
|
374
|
+
const r = input.perPlatform?.reddit;
|
|
375
|
+
if (!r?.subreddit || !r.title) throw new SocialError("reddit", "Reddit posts require `perPlatform.reddit.subreddit` and `perPlatform.reddit.title`", { statusCode: 400 });
|
|
376
|
+
const kind = r.kind ?? (input.link ? "link" : "self");
|
|
377
|
+
const result = await this.provider.createPost(accessToken, {
|
|
378
|
+
subreddit: r.subreddit,
|
|
379
|
+
title: r.title,
|
|
380
|
+
kind,
|
|
381
|
+
text: input.text,
|
|
382
|
+
url: input.link
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
platform: "reddit",
|
|
386
|
+
ok: true,
|
|
387
|
+
id: result.id,
|
|
388
|
+
url: result.url,
|
|
389
|
+
raw: result
|
|
390
|
+
};
|
|
391
|
+
} catch (err) {
|
|
392
|
+
return errorResult("reddit", err);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async deletePost(fullname) {
|
|
396
|
+
const accessToken = this.assertAccessToken();
|
|
397
|
+
await this.provider.deleteContent(accessToken, fullname);
|
|
398
|
+
}
|
|
399
|
+
async getPost(subreddit, postId) {
|
|
400
|
+
const accessToken = this.assertAccessToken();
|
|
401
|
+
return this.provider.getPost(accessToken, subreddit, postId);
|
|
402
|
+
}
|
|
403
|
+
assertAccessToken() {
|
|
404
|
+
const t = this.config.tokens?.access_token;
|
|
405
|
+
if (!t) throw new SocialError("reddit", "Not authenticated. Call exchangeCode() first.", { statusCode: 401 });
|
|
406
|
+
return t;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/client/instagram.ts
|
|
412
|
+
/**
|
|
413
|
+
* InstagramClient — credential-bound wrapper over `InstagramProvider`.
|
|
414
|
+
*/
|
|
415
|
+
var InstagramClient = class {
|
|
416
|
+
provider;
|
|
417
|
+
constructor(config, opts = {}) {
|
|
418
|
+
this.config = config;
|
|
419
|
+
this.provider = new InstagramProvider({
|
|
420
|
+
port: opts.port,
|
|
421
|
+
redirectUri: config.redirectUri,
|
|
422
|
+
environment: opts.environment
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
async authUrl(state) {
|
|
426
|
+
return this.provider.getAuthUrl(state, this.config);
|
|
427
|
+
}
|
|
428
|
+
async exchangeCode(code) {
|
|
429
|
+
const tokens = await this.provider.exchangeCode(code, this.config);
|
|
430
|
+
this.config.tokens = tokens;
|
|
431
|
+
return tokens;
|
|
432
|
+
}
|
|
433
|
+
async refresh() {
|
|
434
|
+
const refreshToken = this.config.tokens?.refresh_token ?? this.config.tokens?.access_token;
|
|
435
|
+
if (!refreshToken) throw new SocialError("instagram", "No refresh_token available");
|
|
436
|
+
const tokens = await this.provider.refreshToken(refreshToken);
|
|
437
|
+
this.config.tokens = {
|
|
438
|
+
...this.config.tokens,
|
|
439
|
+
...tokens
|
|
440
|
+
};
|
|
441
|
+
return this.config.tokens;
|
|
442
|
+
}
|
|
443
|
+
/** Resolve and cache the Instagram Business User ID. */
|
|
444
|
+
async resolveIgUser() {
|
|
445
|
+
if (this.config.igUserId) return this.config.igUserId;
|
|
446
|
+
const accessToken = this.assertAccessToken();
|
|
447
|
+
const info = await this.provider.getAccountInfo(accessToken);
|
|
448
|
+
if (!info?.id) throw new SocialError("instagram", "Could not resolve Instagram Business Account ID", { statusCode: 502 });
|
|
449
|
+
this.config.igUserId = info.id;
|
|
450
|
+
return info.id;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Post to Instagram. Behavior:
|
|
454
|
+
* - 1 image → `uploadPhoto`
|
|
455
|
+
* - 1 video → `uploadVideo` (Reel)
|
|
456
|
+
* - 2-10 mixed media → `uploadCarousel`
|
|
457
|
+
*
|
|
458
|
+
* Text-only posts are not supported by Instagram.
|
|
459
|
+
*/
|
|
460
|
+
async post(input) {
|
|
461
|
+
try {
|
|
462
|
+
const accessToken = this.assertAccessToken();
|
|
463
|
+
const igUserId = input.perPlatform?.instagram?.igUserId ?? await this.resolveIgUser();
|
|
464
|
+
const media = input.media ?? [];
|
|
465
|
+
if (media.length === 0) throw new SocialError("instagram", "Instagram requires at least one media item (image or video)", { statusCode: 400 });
|
|
466
|
+
if (media.length === 1) {
|
|
467
|
+
const m = media[0];
|
|
468
|
+
if (!m.url) throw new SocialError("instagram", "Instagram requires a public URL for each media item (no pre-uploaded media support)", { statusCode: 400 });
|
|
469
|
+
if (m.type === "image") {
|
|
470
|
+
const r = await this.provider.uploadPhoto({
|
|
471
|
+
imageUrl: m.url,
|
|
472
|
+
caption: input.text,
|
|
473
|
+
tokens: {
|
|
474
|
+
access_token: accessToken,
|
|
475
|
+
igUserId
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
return {
|
|
479
|
+
platform: "instagram",
|
|
480
|
+
ok: true,
|
|
481
|
+
id: r.mediaId,
|
|
482
|
+
raw: r
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const r = await this.provider.uploadVideo({
|
|
486
|
+
videoUrl: m.url,
|
|
487
|
+
caption: input.text,
|
|
488
|
+
tokens: {
|
|
489
|
+
access_token: accessToken,
|
|
490
|
+
igUserId
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
platform: "instagram",
|
|
495
|
+
ok: true,
|
|
496
|
+
id: r.platformVideoId ?? r.platformPostId,
|
|
497
|
+
raw: r
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const items = media.filter((m) => !!m.url);
|
|
501
|
+
if (items.length !== media.length) throw new SocialError("instagram", "Instagram carousels require URLs for every media item", { statusCode: 400 });
|
|
502
|
+
const r = await this.provider.uploadCarousel({
|
|
503
|
+
items: items.map((m) => ({
|
|
504
|
+
type: m.type === "image" ? "IMAGE" : "VIDEO",
|
|
505
|
+
url: m.url
|
|
506
|
+
})),
|
|
507
|
+
caption: input.text,
|
|
508
|
+
tokens: {
|
|
509
|
+
access_token: accessToken,
|
|
510
|
+
igUserId
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
platform: "instagram",
|
|
515
|
+
ok: true,
|
|
516
|
+
id: r.mediaId,
|
|
517
|
+
raw: r
|
|
518
|
+
};
|
|
519
|
+
} catch (err) {
|
|
520
|
+
return errorResult("instagram", err);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async getMedia(mediaId) {
|
|
524
|
+
const accessToken = this.assertAccessToken();
|
|
525
|
+
return this.provider.getMedia(accessToken, mediaId);
|
|
526
|
+
}
|
|
527
|
+
async listMedia(opts) {
|
|
528
|
+
const accessToken = this.assertAccessToken();
|
|
529
|
+
const igUserId = this.config.igUserId ?? await this.resolveIgUser();
|
|
530
|
+
return this.provider.listMedia(accessToken, igUserId, opts);
|
|
531
|
+
}
|
|
532
|
+
assertAccessToken() {
|
|
533
|
+
const t = this.config.tokens?.access_token;
|
|
534
|
+
if (!t) throw new SocialError("instagram", "Not authenticated. Call exchangeCode() first.", { statusCode: 401 });
|
|
535
|
+
return t;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/client/youtube.ts
|
|
541
|
+
/**
|
|
542
|
+
* YouTubeClient — credential-bound wrapper over `YouTubeProvider`.
|
|
543
|
+
*
|
|
544
|
+
* YouTube doesn't support text-only posts; use `upload()` to publish videos.
|
|
545
|
+
*/
|
|
546
|
+
var YouTubeClient = class {
|
|
547
|
+
provider;
|
|
548
|
+
constructor(config, opts = {}) {
|
|
549
|
+
this.config = config;
|
|
550
|
+
this.provider = new YouTubeProvider({
|
|
551
|
+
port: opts.port,
|
|
552
|
+
redirectUri: config.redirectUri,
|
|
553
|
+
environment: opts.environment
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
authUrl(state) {
|
|
557
|
+
return this.provider.getAuthUrl(state, this.config);
|
|
558
|
+
}
|
|
559
|
+
async exchangeCode(code) {
|
|
560
|
+
const tokens = await this.provider.exchangeCode(code, this.config);
|
|
561
|
+
this.config.tokens = tokens;
|
|
562
|
+
return tokens;
|
|
563
|
+
}
|
|
564
|
+
async refresh() {
|
|
565
|
+
const refreshToken = this.config.tokens?.refresh_token;
|
|
566
|
+
if (!refreshToken) throw new SocialError("youtube", "No refresh_token available");
|
|
567
|
+
const tokens = await this.provider.refreshToken(refreshToken, this.config);
|
|
568
|
+
this.config.tokens = {
|
|
569
|
+
...this.config.tokens,
|
|
570
|
+
...tokens
|
|
571
|
+
};
|
|
572
|
+
return this.config.tokens;
|
|
573
|
+
}
|
|
574
|
+
/** Upload a video. */
|
|
575
|
+
async upload(input) {
|
|
576
|
+
try {
|
|
577
|
+
this.assertAccessToken();
|
|
578
|
+
const r = await this.provider.uploadVideo({
|
|
579
|
+
filePath: input.filePath,
|
|
580
|
+
videoUrl: input.videoUrl,
|
|
581
|
+
title: input.title ?? "",
|
|
582
|
+
description: input.description,
|
|
583
|
+
tags: input.tags,
|
|
584
|
+
privacy: input.privacy,
|
|
585
|
+
scheduledAt: input.scheduledAt,
|
|
586
|
+
credentials: this.config,
|
|
587
|
+
tokens: this.config.tokens
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
platform: "youtube",
|
|
591
|
+
ok: true,
|
|
592
|
+
id: r.platformVideoId ?? r.platformPostId,
|
|
593
|
+
url: r.platformUrl ?? (r.platformVideoId ? `https://youtu.be/${r.platformVideoId}` : void 0),
|
|
594
|
+
raw: r
|
|
595
|
+
};
|
|
596
|
+
} catch (err) {
|
|
597
|
+
return errorResult("youtube", err);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async deleteVideo(videoId) {
|
|
601
|
+
const accessToken = this.assertAccessToken();
|
|
602
|
+
await this.provider.deleteVideo(accessToken, videoId, this.config);
|
|
603
|
+
}
|
|
604
|
+
async getVideo(videoId) {
|
|
605
|
+
const accessToken = this.assertAccessToken();
|
|
606
|
+
return this.provider.getVideo(accessToken, videoId, this.config);
|
|
607
|
+
}
|
|
608
|
+
async listVideos(opts) {
|
|
609
|
+
const accessToken = this.assertAccessToken();
|
|
610
|
+
return this.provider.listVideos(accessToken, this.config, opts);
|
|
611
|
+
}
|
|
612
|
+
assertAccessToken() {
|
|
613
|
+
const t = this.config.tokens?.access_token;
|
|
614
|
+
if (!t) throw new SocialError("youtube", "Not authenticated. Call exchangeCode() first.", { statusCode: 401 });
|
|
615
|
+
return t;
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/client/tiktok.ts
|
|
621
|
+
/**
|
|
622
|
+
* TikTokClient — credential-bound wrapper over `TikTokProvider`.
|
|
623
|
+
*/
|
|
624
|
+
var TikTokClient = class {
|
|
625
|
+
provider;
|
|
626
|
+
constructor(config, opts = {}) {
|
|
627
|
+
this.config = config;
|
|
628
|
+
this.provider = new TikTokProvider({
|
|
629
|
+
port: opts.port,
|
|
630
|
+
redirectUri: config.redirectUri,
|
|
631
|
+
environment: opts.environment
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
authUrl(state) {
|
|
635
|
+
return this.provider.getAuthUrl(state, this.config);
|
|
636
|
+
}
|
|
637
|
+
async exchangeCode(code, state) {
|
|
638
|
+
const tokens = await this.provider.exchangeCode(code, this.config, state);
|
|
639
|
+
this.config.tokens = tokens;
|
|
640
|
+
return tokens;
|
|
641
|
+
}
|
|
642
|
+
async refresh() {
|
|
643
|
+
const refreshToken = this.config.tokens?.refresh_token;
|
|
644
|
+
if (!refreshToken) throw new SocialError("tiktok", "No refresh_token available");
|
|
645
|
+
const tokens = await this.provider.refreshToken(refreshToken, this.config);
|
|
646
|
+
this.config.tokens = {
|
|
647
|
+
...this.config.tokens,
|
|
648
|
+
...tokens
|
|
649
|
+
};
|
|
650
|
+
return this.config.tokens;
|
|
651
|
+
}
|
|
652
|
+
/** Upload a video. */
|
|
653
|
+
async upload(input) {
|
|
654
|
+
try {
|
|
655
|
+
this.assertAccessToken();
|
|
656
|
+
const r = await this.provider.uploadVideo({
|
|
657
|
+
filePath: input.filePath,
|
|
658
|
+
videoUrl: input.videoUrl,
|
|
659
|
+
title: input.title ?? input.caption,
|
|
660
|
+
description: input.description ?? input.caption,
|
|
661
|
+
privacy: input.privacy,
|
|
662
|
+
scheduledAt: input.scheduledAt,
|
|
663
|
+
credentials: this.config,
|
|
664
|
+
tokens: this.config.tokens
|
|
665
|
+
});
|
|
666
|
+
return {
|
|
667
|
+
platform: "tiktok",
|
|
668
|
+
ok: true,
|
|
669
|
+
id: r.platformVideoId ?? r.platformPostId,
|
|
670
|
+
url: r.platformUrl ?? null,
|
|
671
|
+
raw: r
|
|
672
|
+
};
|
|
673
|
+
} catch (err) {
|
|
674
|
+
return errorResult("tiktok", err);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async listVideos(opts) {
|
|
678
|
+
const accessToken = this.assertAccessToken();
|
|
679
|
+
return this.provider.listVideos(accessToken, opts);
|
|
680
|
+
}
|
|
681
|
+
assertAccessToken() {
|
|
682
|
+
const t = this.config.tokens?.access_token;
|
|
683
|
+
if (!t) throw new SocialError("tiktok", "Not authenticated. Call exchangeCode() first.", { statusCode: 401 });
|
|
684
|
+
return t;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/client/telegram.ts
|
|
690
|
+
/**
|
|
691
|
+
* TelegramClient — credential-bound wrapper over `TelegramProvider`.
|
|
692
|
+
*
|
|
693
|
+
* Telegram uses Bot Tokens (no OAuth). The bot must be added as admin to the
|
|
694
|
+
* target channel/group.
|
|
695
|
+
*/
|
|
696
|
+
var TelegramClient = class {
|
|
697
|
+
provider;
|
|
698
|
+
constructor(config, opts = {}) {
|
|
699
|
+
this.config = config;
|
|
700
|
+
this.provider = new TelegramProvider({ port: opts.port });
|
|
701
|
+
}
|
|
702
|
+
/** Send a text message. Falls back to `config.chatId` if `to` is omitted. */
|
|
703
|
+
async send(text, to) {
|
|
704
|
+
try {
|
|
705
|
+
const chatId = this.assertChatId(to);
|
|
706
|
+
const result = await this.provider.sendMessage(this.config.botToken, chatId, text);
|
|
707
|
+
return {
|
|
708
|
+
platform: "telegram",
|
|
709
|
+
ok: true,
|
|
710
|
+
id: String(result.message_id ?? ""),
|
|
711
|
+
raw: result
|
|
712
|
+
};
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return errorResultMsg$1(err);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async sendPhoto(photoUrl, opts = {}) {
|
|
718
|
+
const chatId = this.assertChatId(opts.to);
|
|
719
|
+
return this.provider.sendPhoto(this.config.botToken, chatId, photoUrl, { caption: opts.caption });
|
|
720
|
+
}
|
|
721
|
+
async sendVideo(videoUrl, opts = {}) {
|
|
722
|
+
const chatId = this.assertChatId(opts.to);
|
|
723
|
+
return this.provider.sendVideo(this.config.botToken, chatId, videoUrl, { caption: opts.caption });
|
|
724
|
+
}
|
|
725
|
+
async sendDocument(documentUrl, opts = {}) {
|
|
726
|
+
const chatId = this.assertChatId(opts.to);
|
|
727
|
+
return this.provider.sendDocument(this.config.botToken, chatId, documentUrl, { caption: opts.caption });
|
|
728
|
+
}
|
|
729
|
+
async deleteMessage(messageId, to) {
|
|
730
|
+
const chatId = this.assertChatId(to);
|
|
731
|
+
return this.provider.deleteMessage(this.config.botToken, chatId, Number(messageId));
|
|
732
|
+
}
|
|
733
|
+
async getChat(chatId) {
|
|
734
|
+
return this.provider.getChat(this.config.botToken, this.assertChatId(chatId));
|
|
735
|
+
}
|
|
736
|
+
async discoverChats() {
|
|
737
|
+
return this.provider.discoverChats(this.config.botToken);
|
|
738
|
+
}
|
|
739
|
+
async me() {
|
|
740
|
+
return this.provider.getAccountInfo(this.config.botToken);
|
|
741
|
+
}
|
|
742
|
+
assertChatId(to) {
|
|
743
|
+
const id = to ?? this.config.chatId;
|
|
744
|
+
if (id === void 0 || id === null || id === "") throw new SocialError("telegram", "No chat ID provided. Pass `to` or set `chatId` in config.", { statusCode: 400 });
|
|
745
|
+
return id;
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
function errorResultMsg$1(err) {
|
|
749
|
+
if (err instanceof SocialError) return {
|
|
750
|
+
platform: "telegram",
|
|
751
|
+
ok: false,
|
|
752
|
+
error: {
|
|
753
|
+
message: err.message,
|
|
754
|
+
code: err.errorCode
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
return {
|
|
758
|
+
platform: "telegram",
|
|
759
|
+
ok: false,
|
|
760
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region src/client/whatsapp.ts
|
|
766
|
+
/**
|
|
767
|
+
* WhatsAppClient — credential-bound wrapper over `WhatsAppProvider`.
|
|
768
|
+
*
|
|
769
|
+
* WhatsApp uses permanent System User tokens (no OAuth).
|
|
770
|
+
*/
|
|
771
|
+
var WhatsAppClient = class {
|
|
772
|
+
provider;
|
|
773
|
+
constructor(config, opts = {}) {
|
|
774
|
+
this.config = config;
|
|
775
|
+
this.provider = new WhatsAppProvider({ port: opts.port });
|
|
776
|
+
}
|
|
777
|
+
/** Send a text message to an E.164 phone number. */
|
|
778
|
+
async send(text, to) {
|
|
779
|
+
try {
|
|
780
|
+
const phoneNumberId = this.assertPhoneNumberId();
|
|
781
|
+
const result = await this.provider.sendMessage(this.config.accessToken, phoneNumberId, {
|
|
782
|
+
to,
|
|
783
|
+
type: "text",
|
|
784
|
+
text: { body: text }
|
|
785
|
+
});
|
|
786
|
+
return {
|
|
787
|
+
platform: "whatsapp",
|
|
788
|
+
ok: true,
|
|
789
|
+
id: result.messages?.[0]?.id,
|
|
790
|
+
raw: result
|
|
791
|
+
};
|
|
792
|
+
} catch (err) {
|
|
793
|
+
return errorResultMsg(err);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async sendImage(to, imageUrl, caption) {
|
|
797
|
+
const phoneNumberId = this.assertPhoneNumberId();
|
|
798
|
+
return this.provider.sendImage(this.config.accessToken, phoneNumberId, {
|
|
799
|
+
to,
|
|
800
|
+
imageUrl,
|
|
801
|
+
caption
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
async sendVideo(to, videoUrl, caption) {
|
|
805
|
+
const phoneNumberId = this.assertPhoneNumberId();
|
|
806
|
+
return this.provider.sendVideo(this.config.accessToken, phoneNumberId, {
|
|
807
|
+
to,
|
|
808
|
+
videoUrl,
|
|
809
|
+
caption
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
async sendDocument(to, documentUrl, opts = {}) {
|
|
813
|
+
const phoneNumberId = this.assertPhoneNumberId();
|
|
814
|
+
return this.provider.sendDocument(this.config.accessToken, phoneNumberId, {
|
|
815
|
+
to,
|
|
816
|
+
documentUrl,
|
|
817
|
+
caption: opts.caption,
|
|
818
|
+
filename: opts.filename
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
async listPhoneNumbers() {
|
|
822
|
+
return this.provider.getPhoneNumbers(this.config.accessToken, this.config.businessAccountId);
|
|
823
|
+
}
|
|
824
|
+
async listTemplates() {
|
|
825
|
+
return this.provider.getTemplates(this.config.accessToken, this.config.businessAccountId);
|
|
826
|
+
}
|
|
827
|
+
assertPhoneNumberId() {
|
|
828
|
+
if (!this.config.phoneNumberId) throw new SocialError("whatsapp", "phoneNumberId is required. Set it in config or call listPhoneNumbers() first.", { statusCode: 400 });
|
|
829
|
+
return this.config.phoneNumberId;
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
function errorResultMsg(err) {
|
|
833
|
+
if (err instanceof SocialError) return {
|
|
834
|
+
platform: "whatsapp",
|
|
835
|
+
ok: false,
|
|
836
|
+
error: {
|
|
837
|
+
message: err.message,
|
|
838
|
+
code: err.errorCode
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
return {
|
|
842
|
+
platform: "whatsapp",
|
|
843
|
+
ok: false,
|
|
844
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/client/env.ts
|
|
850
|
+
/**
|
|
851
|
+
* Auto-configure a `SocialClient` from environment variables.
|
|
852
|
+
*
|
|
853
|
+
* Recognized variables (every platform is optional):
|
|
854
|
+
*
|
|
855
|
+
* YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET, YOUTUBE_REDIRECT_URI,
|
|
856
|
+
* YOUTUBE_ACCESS_TOKEN, YOUTUBE_REFRESH_TOKEN
|
|
857
|
+
*
|
|
858
|
+
* TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET, TWITTER_REDIRECT_URI,
|
|
859
|
+
* TWITTER_ACCESS_TOKEN, TWITTER_REFRESH_TOKEN
|
|
860
|
+
*
|
|
861
|
+
* FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, FACEBOOK_REDIRECT_URI,
|
|
862
|
+
* FACEBOOK_ACCESS_TOKEN, FACEBOOK_PAGE_ID, FACEBOOK_PAGE_ACCESS_TOKEN
|
|
863
|
+
*
|
|
864
|
+
* INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET, INSTAGRAM_REDIRECT_URI,
|
|
865
|
+
* INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_USER_ID
|
|
866
|
+
*
|
|
867
|
+
* LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, LINKEDIN_REDIRECT_URI,
|
|
868
|
+
* LINKEDIN_ACCESS_TOKEN, LINKEDIN_AUTHOR_URN
|
|
869
|
+
*
|
|
870
|
+
* REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USER_AGENT,
|
|
871
|
+
* REDDIT_REDIRECT_URI, REDDIT_ACCESS_TOKEN, REDDIT_REFRESH_TOKEN
|
|
872
|
+
*
|
|
873
|
+
* TIKTOK_CLIENT_KEY, TIKTOK_CLIENT_SECRET, TIKTOK_REDIRECT_URI,
|
|
874
|
+
* TIKTOK_ACCESS_TOKEN, TIKTOK_REFRESH_TOKEN
|
|
875
|
+
*
|
|
876
|
+
* TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
|
|
877
|
+
*
|
|
878
|
+
* WHATSAPP_ACCESS_TOKEN, WHATSAPP_BUSINESS_ACCOUNT_ID, WHATSAPP_PHONE_NUMBER_ID
|
|
879
|
+
*/
|
|
880
|
+
function tokensFrom(env, prefix) {
|
|
881
|
+
const access = env[`${prefix}_ACCESS_TOKEN`];
|
|
882
|
+
if (!access) return void 0;
|
|
883
|
+
return {
|
|
884
|
+
access_token: access,
|
|
885
|
+
refresh_token: env[`${prefix}_REFRESH_TOKEN`]
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Build a `SocialClient` from `process.env` (or any compatible record).
|
|
890
|
+
* Only platforms whose required vars are present are enabled.
|
|
891
|
+
*/
|
|
892
|
+
function fromEnv(env = typeof process !== "undefined" ? process.env : {}, opts) {
|
|
893
|
+
const config = {};
|
|
894
|
+
if (env.YOUTUBE_CLIENT_ID && env.YOUTUBE_CLIENT_SECRET) config.youtube = {
|
|
895
|
+
clientId: env.YOUTUBE_CLIENT_ID,
|
|
896
|
+
clientSecret: env.YOUTUBE_CLIENT_SECRET,
|
|
897
|
+
redirectUri: env.YOUTUBE_REDIRECT_URI,
|
|
898
|
+
tokens: tokensFrom(env, "YOUTUBE")
|
|
899
|
+
};
|
|
900
|
+
if (env.TWITTER_CLIENT_ID) config.twitter = {
|
|
901
|
+
clientId: env.TWITTER_CLIENT_ID,
|
|
902
|
+
clientSecret: env.TWITTER_CLIENT_SECRET,
|
|
903
|
+
redirectUri: env.TWITTER_REDIRECT_URI,
|
|
904
|
+
tokens: tokensFrom(env, "TWITTER")
|
|
905
|
+
};
|
|
906
|
+
if (env.FACEBOOK_APP_ID && env.FACEBOOK_APP_SECRET) config.facebook = {
|
|
907
|
+
appId: env.FACEBOOK_APP_ID,
|
|
908
|
+
appSecret: env.FACEBOOK_APP_SECRET,
|
|
909
|
+
redirectUri: env.FACEBOOK_REDIRECT_URI,
|
|
910
|
+
tokens: tokensFrom(env, "FACEBOOK"),
|
|
911
|
+
pageId: env.FACEBOOK_PAGE_ID,
|
|
912
|
+
pageAccessToken: env.FACEBOOK_PAGE_ACCESS_TOKEN
|
|
913
|
+
};
|
|
914
|
+
if (env.INSTAGRAM_APP_ID && env.INSTAGRAM_APP_SECRET) config.instagram = {
|
|
915
|
+
appId: env.INSTAGRAM_APP_ID,
|
|
916
|
+
appSecret: env.INSTAGRAM_APP_SECRET,
|
|
917
|
+
redirectUri: env.INSTAGRAM_REDIRECT_URI,
|
|
918
|
+
tokens: tokensFrom(env, "INSTAGRAM"),
|
|
919
|
+
igUserId: env.INSTAGRAM_USER_ID
|
|
920
|
+
};
|
|
921
|
+
if (env.LINKEDIN_CLIENT_ID && env.LINKEDIN_CLIENT_SECRET) config.linkedin = {
|
|
922
|
+
clientId: env.LINKEDIN_CLIENT_ID,
|
|
923
|
+
clientSecret: env.LINKEDIN_CLIENT_SECRET,
|
|
924
|
+
redirectUri: env.LINKEDIN_REDIRECT_URI,
|
|
925
|
+
tokens: tokensFrom(env, "LINKEDIN"),
|
|
926
|
+
authorUrn: env.LINKEDIN_AUTHOR_URN
|
|
927
|
+
};
|
|
928
|
+
if (env.REDDIT_CLIENT_ID && env.REDDIT_CLIENT_SECRET && env.REDDIT_USER_AGENT) config.reddit = {
|
|
929
|
+
clientId: env.REDDIT_CLIENT_ID,
|
|
930
|
+
clientSecret: env.REDDIT_CLIENT_SECRET,
|
|
931
|
+
userAgent: env.REDDIT_USER_AGENT,
|
|
932
|
+
redirectUri: env.REDDIT_REDIRECT_URI,
|
|
933
|
+
tokens: tokensFrom(env, "REDDIT")
|
|
934
|
+
};
|
|
935
|
+
if (env.TIKTOK_CLIENT_KEY && env.TIKTOK_CLIENT_SECRET) config.tiktok = {
|
|
936
|
+
clientKey: env.TIKTOK_CLIENT_KEY,
|
|
937
|
+
clientSecret: env.TIKTOK_CLIENT_SECRET,
|
|
938
|
+
redirectUri: env.TIKTOK_REDIRECT_URI,
|
|
939
|
+
tokens: tokensFrom(env, "TIKTOK")
|
|
940
|
+
};
|
|
941
|
+
if (env.TELEGRAM_BOT_TOKEN) config.telegram = {
|
|
942
|
+
botToken: env.TELEGRAM_BOT_TOKEN,
|
|
943
|
+
chatId: env.TELEGRAM_CHAT_ID
|
|
944
|
+
};
|
|
945
|
+
if (env.WHATSAPP_ACCESS_TOKEN && env.WHATSAPP_BUSINESS_ACCOUNT_ID) config.whatsapp = {
|
|
946
|
+
accessToken: env.WHATSAPP_ACCESS_TOKEN,
|
|
947
|
+
businessAccountId: env.WHATSAPP_BUSINESS_ACCOUNT_ID,
|
|
948
|
+
phoneNumberId: env.WHATSAPP_PHONE_NUMBER_ID
|
|
949
|
+
};
|
|
950
|
+
return new SocialClient(config, opts);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
//#endregion
|
|
954
|
+
export { YouTubeClient as a, LinkedInClient as c, TikTokClient as i, FacebookClient as l, WhatsAppClient as n, InstagramClient as o, TelegramClient as r, RedditClient as s, fromEnv as t, TwitterClient as u };
|
|
955
|
+
//# sourceMappingURL=env-Bl0cwwjC.mjs.map
|