universal-social-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +61 -0
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +546 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +664 -0
- package/dist/index.js +2656 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
- package/supported-methods.json +161 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2656 @@
|
|
|
1
|
+
// src/config/env.ts
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
dotenv.config();
|
|
4
|
+
function readNumberEnv(name, fallback) {
|
|
5
|
+
const value = process.env[name];
|
|
6
|
+
if (!value) {
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
11
|
+
}
|
|
12
|
+
var env = {
|
|
13
|
+
x: {
|
|
14
|
+
apiKey: process.env.X_API_KEY ?? "",
|
|
15
|
+
apiSecret: process.env.X_API_SECRET ?? "",
|
|
16
|
+
accessToken: process.env.X_ACCESS_TOKEN ?? "",
|
|
17
|
+
accessSecret: process.env.X_ACCESS_SECRET ?? "",
|
|
18
|
+
bearerToken: process.env.X_BEARER_TOKEN ?? "",
|
|
19
|
+
clientId: process.env.X_CLIENT_ID ?? "",
|
|
20
|
+
clientSecret: process.env.X_CLIENT_SECRET ?? ""
|
|
21
|
+
},
|
|
22
|
+
meta: {
|
|
23
|
+
appId: process.env.META_APP_ID ?? "",
|
|
24
|
+
appSecret: process.env.META_APP_SECRET ?? "",
|
|
25
|
+
graphVersion: process.env.META_GRAPH_VERSION ?? "v21.0",
|
|
26
|
+
fbPageToken: process.env.FB_PAGE_ACCESS_TOKEN ?? "",
|
|
27
|
+
fbPageId: process.env.FB_PAGE_ID ?? "",
|
|
28
|
+
igToken: process.env.IG_ACCESS_TOKEN ?? "",
|
|
29
|
+
igUserId: process.env.IG_USER_ID ?? ""
|
|
30
|
+
},
|
|
31
|
+
linkedin: {
|
|
32
|
+
accessToken: process.env.LINKEDIN_ACCESS_TOKEN ?? "",
|
|
33
|
+
refreshToken: process.env.LINKEDIN_REFRESH_TOKEN ?? "",
|
|
34
|
+
clientId: process.env.LINKEDIN_CLIENT_ID ?? "",
|
|
35
|
+
clientSecret: process.env.LINKEDIN_CLIENT_SECRET ?? "",
|
|
36
|
+
orgUrn: process.env.LINKEDIN_ORG_URN ?? "",
|
|
37
|
+
personUrn: process.env.LINKEDIN_PERSON_URN ?? "",
|
|
38
|
+
apiVersion: process.env.LINKEDIN_API_VERSION ?? "202510"
|
|
39
|
+
},
|
|
40
|
+
youtube: {
|
|
41
|
+
accessToken: process.env.YOUTUBE_ACCESS_TOKEN ?? "",
|
|
42
|
+
channelId: process.env.YOUTUBE_CHANNEL_ID ?? ""
|
|
43
|
+
},
|
|
44
|
+
tiktok: {
|
|
45
|
+
accessToken: process.env.TIKTOK_ACCESS_TOKEN ?? "",
|
|
46
|
+
openId: process.env.TIKTOK_OPEN_ID ?? "",
|
|
47
|
+
advertiserId: process.env.TIKTOK_ADVERTISER_ID ?? ""
|
|
48
|
+
},
|
|
49
|
+
pinterest: {
|
|
50
|
+
accessToken: process.env.PINTEREST_ACCESS_TOKEN ?? "",
|
|
51
|
+
boardId: process.env.PINTEREST_BOARD_ID ?? ""
|
|
52
|
+
},
|
|
53
|
+
bluesky: {
|
|
54
|
+
serviceUrl: process.env.BLUESKY_SERVICE_URL ?? "https://bsky.social",
|
|
55
|
+
identifier: process.env.BLUESKY_IDENTIFIER ?? "",
|
|
56
|
+
appPassword: process.env.BLUESKY_APP_PASSWORD ?? "",
|
|
57
|
+
accessJwt: process.env.BLUESKY_ACCESS_JWT ?? "",
|
|
58
|
+
refreshJwt: process.env.BLUESKY_REFRESH_JWT ?? ""
|
|
59
|
+
},
|
|
60
|
+
mastodon: {
|
|
61
|
+
baseUrl: process.env.MASTODON_BASE_URL ?? "",
|
|
62
|
+
accessToken: process.env.MASTODON_ACCESS_TOKEN ?? "",
|
|
63
|
+
accountId: process.env.MASTODON_ACCOUNT_ID ?? ""
|
|
64
|
+
},
|
|
65
|
+
threads: {
|
|
66
|
+
accessToken: process.env.THREADS_ACCESS_TOKEN ?? "",
|
|
67
|
+
userId: process.env.THREADS_USER_ID ?? ""
|
|
68
|
+
},
|
|
69
|
+
retry: {
|
|
70
|
+
maxRetries: readNumberEnv("SOCIAL_SDK_MAX_RETRIES", 3),
|
|
71
|
+
baseDelayMs: readNumberEnv("SOCIAL_SDK_RETRY_BASE_MS", 500)
|
|
72
|
+
},
|
|
73
|
+
ollama: {
|
|
74
|
+
host: process.env.OLLAMA_HOST ?? "http://127.0.0.1:11434",
|
|
75
|
+
model: process.env.OLLAMA_MODEL ?? "llama3.2:3b"
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/errors/SocialError.ts
|
|
80
|
+
var SocialError = class _SocialError extends Error {
|
|
81
|
+
platform;
|
|
82
|
+
endpoint;
|
|
83
|
+
statusCode;
|
|
84
|
+
details;
|
|
85
|
+
cause;
|
|
86
|
+
constructor(params) {
|
|
87
|
+
super(params.message);
|
|
88
|
+
this.name = "SocialError";
|
|
89
|
+
this.platform = params.platform;
|
|
90
|
+
this.endpoint = params.endpoint;
|
|
91
|
+
this.statusCode = params.statusCode;
|
|
92
|
+
this.details = params.details;
|
|
93
|
+
this.cause = params.cause;
|
|
94
|
+
}
|
|
95
|
+
toJSON() {
|
|
96
|
+
return {
|
|
97
|
+
name: this.name,
|
|
98
|
+
message: this.message,
|
|
99
|
+
platform: this.platform,
|
|
100
|
+
endpoint: this.endpoint,
|
|
101
|
+
statusCode: this.statusCode,
|
|
102
|
+
details: this.details
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
static normalize(params) {
|
|
106
|
+
const { platform, endpoint, error } = params;
|
|
107
|
+
if (error instanceof _SocialError) {
|
|
108
|
+
return error;
|
|
109
|
+
}
|
|
110
|
+
if (error && typeof error === "object") {
|
|
111
|
+
const maybeAny = error;
|
|
112
|
+
const response = maybeAny.response;
|
|
113
|
+
const message = maybeAny.message ?? "Unknown platform SDK error";
|
|
114
|
+
return new _SocialError({
|
|
115
|
+
platform,
|
|
116
|
+
endpoint,
|
|
117
|
+
message,
|
|
118
|
+
statusCode: response?.status,
|
|
119
|
+
details: response && typeof response === "object" ? {
|
|
120
|
+
data: response.data,
|
|
121
|
+
status: response.status,
|
|
122
|
+
headers: maybeAny.response?.headers
|
|
123
|
+
} : error,
|
|
124
|
+
cause: error
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return new _SocialError({
|
|
128
|
+
platform,
|
|
129
|
+
endpoint,
|
|
130
|
+
message: String(error ?? "Unknown error"),
|
|
131
|
+
cause: error
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/platforms/shared/metaClient.ts
|
|
137
|
+
import bizSdk from "facebook-nodejs-business-sdk";
|
|
138
|
+
|
|
139
|
+
// src/utils/retry.ts
|
|
140
|
+
function sleep(ms) {
|
|
141
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
142
|
+
}
|
|
143
|
+
function parseRetryAfterMs(details) {
|
|
144
|
+
if (!details || typeof details !== "object") {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const maybe = details;
|
|
148
|
+
const headers = maybe.headers;
|
|
149
|
+
if (!headers || typeof headers !== "object") {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const retryAfterRaw = headers["retry-after"];
|
|
153
|
+
if (typeof retryAfterRaw !== "string" && typeof retryAfterRaw !== "number") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const seconds = Number(retryAfterRaw);
|
|
157
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
158
|
+
return Math.round(seconds * 1e3);
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
function isRetryableStatus(code) {
|
|
163
|
+
if (!code) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return code === 429 || code >= 500 && code <= 599;
|
|
167
|
+
}
|
|
168
|
+
async function withRetries(params) {
|
|
169
|
+
const retries = params.retries ?? env.retry.maxRetries;
|
|
170
|
+
const baseDelayMs = params.baseDelayMs ?? env.retry.baseDelayMs;
|
|
171
|
+
let attempt = 0;
|
|
172
|
+
let lastError;
|
|
173
|
+
while (attempt <= retries) {
|
|
174
|
+
try {
|
|
175
|
+
return await params.execute();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
lastError = error;
|
|
178
|
+
const normalized = SocialError.normalize({
|
|
179
|
+
platform: params.platform,
|
|
180
|
+
endpoint: params.endpoint,
|
|
181
|
+
error
|
|
182
|
+
});
|
|
183
|
+
if (!isRetryableStatus(normalized.statusCode) || attempt === retries) {
|
|
184
|
+
throw normalized;
|
|
185
|
+
}
|
|
186
|
+
const backoffMs = baseDelayMs * 2 ** attempt;
|
|
187
|
+
const jitterMs = Math.floor(Math.random() * Math.max(50, baseDelayMs));
|
|
188
|
+
const retryAfterMs = parseRetryAfterMs(normalized.details) ?? backoffMs + jitterMs;
|
|
189
|
+
await sleep(retryAfterMs);
|
|
190
|
+
attempt += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
throw SocialError.normalize({
|
|
194
|
+
platform: params.platform,
|
|
195
|
+
endpoint: params.endpoint,
|
|
196
|
+
error: lastError
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/platforms/shared/metaClient.ts
|
|
201
|
+
var { FacebookAdsApi } = bizSdk;
|
|
202
|
+
var didInit = false;
|
|
203
|
+
function ensureInit() {
|
|
204
|
+
if (!didInit) {
|
|
205
|
+
const token = env.meta.fbPageToken || env.meta.igToken;
|
|
206
|
+
if (!token) {
|
|
207
|
+
throw new SocialError({
|
|
208
|
+
platform: "facebook",
|
|
209
|
+
endpoint: "meta:init",
|
|
210
|
+
message: "Missing FB_PAGE_ACCESS_TOKEN or IG_ACCESS_TOKEN for Meta SDK initialization."
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
FacebookAdsApi.init(token);
|
|
214
|
+
didInit = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function metaCall(params) {
|
|
218
|
+
ensureInit();
|
|
219
|
+
const api = FacebookAdsApi.getDefaultApi();
|
|
220
|
+
const endpointPath = `/${env.meta.graphVersion}${params.endpoint}`;
|
|
221
|
+
return withRetries({
|
|
222
|
+
platform: params.platform,
|
|
223
|
+
endpoint: endpointPath,
|
|
224
|
+
execute: async () => {
|
|
225
|
+
try {
|
|
226
|
+
return await api.call(
|
|
227
|
+
params.method,
|
|
228
|
+
endpointPath,
|
|
229
|
+
params.query ?? {},
|
|
230
|
+
params.body ?? {}
|
|
231
|
+
);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw SocialError.normalize({
|
|
234
|
+
platform: params.platform,
|
|
235
|
+
endpoint: endpointPath,
|
|
236
|
+
error
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/utils/scheduler.ts
|
|
244
|
+
var scheduledJobs = /* @__PURE__ */ new Map();
|
|
245
|
+
function toDate(value) {
|
|
246
|
+
if (value instanceof Date) {
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
return new Date(value);
|
|
250
|
+
}
|
|
251
|
+
function scheduleTask(params) {
|
|
252
|
+
const runAt = toDate(params.runAt).getTime();
|
|
253
|
+
const delay = Math.max(0, runAt - Date.now());
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const timeout = setTimeout(async () => {
|
|
256
|
+
scheduledJobs.delete(params.id);
|
|
257
|
+
try {
|
|
258
|
+
resolve(await params.task());
|
|
259
|
+
} catch (error) {
|
|
260
|
+
reject(error);
|
|
261
|
+
}
|
|
262
|
+
}, delay);
|
|
263
|
+
scheduledJobs.set(params.id, timeout);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/validation/platformSchemas.ts
|
|
268
|
+
import { z } from "zod";
|
|
269
|
+
var nonEmpty = z.string().min(1);
|
|
270
|
+
var optionalNonEmpty = nonEmpty.optional();
|
|
271
|
+
var dateLike = z.union([z.date(), z.string().min(1)]);
|
|
272
|
+
var schemaMap = {
|
|
273
|
+
x: {
|
|
274
|
+
postTweet: z.object({ text: nonEmpty, mediaIds: z.array(nonEmpty).optional() }),
|
|
275
|
+
postThread: z.object({ tweets: z.array(nonEmpty).min(1) }),
|
|
276
|
+
replyTweet: z.object({ text: nonEmpty, inReplyToTweetId: nonEmpty }),
|
|
277
|
+
quoteTweet: z.object({ text: nonEmpty, quoteTweetId: nonEmpty }),
|
|
278
|
+
deleteTweet: z.object({ tweetId: nonEmpty }),
|
|
279
|
+
retweet: z.object({ userId: nonEmpty, tweetId: nonEmpty }),
|
|
280
|
+
unretweet: z.object({ userId: nonEmpty, tweetId: nonEmpty }),
|
|
281
|
+
likeTweet: z.object({ userId: nonEmpty, tweetId: nonEmpty }),
|
|
282
|
+
unlikeTweet: z.object({ userId: nonEmpty, tweetId: nonEmpty }),
|
|
283
|
+
uploadMedia: z.object({ mediaPath: nonEmpty }),
|
|
284
|
+
postPhoto: z.object({ mediaPath: nonEmpty, text: nonEmpty }),
|
|
285
|
+
postVideo: z.object({ mediaPath: nonEmpty, text: nonEmpty }),
|
|
286
|
+
postPoll: z.object({
|
|
287
|
+
text: nonEmpty,
|
|
288
|
+
options: z.array(nonEmpty).min(2).max(4),
|
|
289
|
+
durationMinutes: z.number().int().min(5).max(10080)
|
|
290
|
+
}),
|
|
291
|
+
sendDirectMessage: z.object({ recipientId: nonEmpty, text: nonEmpty }),
|
|
292
|
+
getTweetAnalytics: z.object({ tweetId: nonEmpty }),
|
|
293
|
+
scheduleTweet: z.object({ text: nonEmpty, publishAt: dateLike })
|
|
294
|
+
},
|
|
295
|
+
facebook: {
|
|
296
|
+
publishToPage: z.object({
|
|
297
|
+
pageId: optionalNonEmpty,
|
|
298
|
+
message: nonEmpty,
|
|
299
|
+
link: optionalNonEmpty,
|
|
300
|
+
photoUrl: optionalNonEmpty
|
|
301
|
+
}),
|
|
302
|
+
publishPhoto: z.object({ pageId: optionalNonEmpty, url: nonEmpty, caption: optionalNonEmpty }),
|
|
303
|
+
publishVideo: z.object({
|
|
304
|
+
pageId: optionalNonEmpty,
|
|
305
|
+
fileUrl: nonEmpty,
|
|
306
|
+
description: optionalNonEmpty,
|
|
307
|
+
title: optionalNonEmpty
|
|
308
|
+
}),
|
|
309
|
+
publishCarousel: z.object({
|
|
310
|
+
pageId: optionalNonEmpty,
|
|
311
|
+
message: nonEmpty,
|
|
312
|
+
photoUrls: z.array(nonEmpty).min(2)
|
|
313
|
+
}),
|
|
314
|
+
publishStory: z.object({ pageId: optionalNonEmpty, photoUrl: nonEmpty }),
|
|
315
|
+
schedulePost: z.object({ pageId: optionalNonEmpty, message: nonEmpty, publishAt: dateLike }),
|
|
316
|
+
commentOnPost: z.object({ postId: nonEmpty, message: nonEmpty }),
|
|
317
|
+
replyToComment: z.object({ commentId: nonEmpty, message: nonEmpty }),
|
|
318
|
+
likeObject: z.object({ objectId: nonEmpty }),
|
|
319
|
+
deletePost: z.object({ objectId: nonEmpty }),
|
|
320
|
+
sendPageMessage: z.object({ recipientPsid: nonEmpty, message: nonEmpty, pageId: optionalNonEmpty }),
|
|
321
|
+
getPostInsights: z.object({ postId: nonEmpty, metrics: z.array(nonEmpty).optional() }),
|
|
322
|
+
getPageInsights: z.object({
|
|
323
|
+
pageId: optionalNonEmpty,
|
|
324
|
+
metrics: z.array(nonEmpty).optional(),
|
|
325
|
+
period: z.enum(["day", "week", "days_28"]).optional()
|
|
326
|
+
}),
|
|
327
|
+
uploadResumableVideo: z.object({
|
|
328
|
+
pageId: optionalNonEmpty,
|
|
329
|
+
fileSize: z.number().int().positive(),
|
|
330
|
+
startOffset: z.number().int().nonnegative().optional()
|
|
331
|
+
}),
|
|
332
|
+
listPublishedPosts: z.object({
|
|
333
|
+
pageId: optionalNonEmpty,
|
|
334
|
+
limit: z.number().int().positive().optional()
|
|
335
|
+
})
|
|
336
|
+
},
|
|
337
|
+
instagram: {
|
|
338
|
+
uploadPhoto: z.object({ igUserId: optionalNonEmpty, imageUrl: nonEmpty, caption: optionalNonEmpty }),
|
|
339
|
+
uploadVideo: z.object({ igUserId: optionalNonEmpty, videoUrl: nonEmpty, caption: optionalNonEmpty }),
|
|
340
|
+
uploadReel: z.object({
|
|
341
|
+
igUserId: optionalNonEmpty,
|
|
342
|
+
mediaPath: optionalNonEmpty,
|
|
343
|
+
videoUrl: nonEmpty,
|
|
344
|
+
caption: optionalNonEmpty
|
|
345
|
+
}),
|
|
346
|
+
uploadStoryPhoto: z.object({ igUserId: optionalNonEmpty, imageUrl: nonEmpty }),
|
|
347
|
+
uploadStoryVideo: z.object({ igUserId: optionalNonEmpty, videoUrl: nonEmpty }),
|
|
348
|
+
publishCarousel: z.object({
|
|
349
|
+
igUserId: optionalNonEmpty,
|
|
350
|
+
caption: optionalNonEmpty,
|
|
351
|
+
items: z.array(z.object({ imageUrl: optionalNonEmpty, videoUrl: optionalNonEmpty })).min(2)
|
|
352
|
+
}),
|
|
353
|
+
commentOnMedia: z.object({ mediaId: nonEmpty, message: nonEmpty }),
|
|
354
|
+
replyToComment: z.object({ commentId: nonEmpty, message: nonEmpty }),
|
|
355
|
+
hideComment: z.object({ commentId: nonEmpty, hide: z.boolean() }),
|
|
356
|
+
deleteComment: z.object({ commentId: nonEmpty }),
|
|
357
|
+
deleteMedia: z.object({ mediaId: nonEmpty }),
|
|
358
|
+
sendPrivateReply: z.object({ commentId: nonEmpty, message: nonEmpty }),
|
|
359
|
+
getMediaInsights: z.object({ mediaId: nonEmpty, metrics: z.array(nonEmpty).optional() }),
|
|
360
|
+
getAccountInsights: z.object({
|
|
361
|
+
igUserId: optionalNonEmpty,
|
|
362
|
+
metrics: z.array(nonEmpty).optional(),
|
|
363
|
+
period: z.enum(["day", "week", "days_28"]).optional()
|
|
364
|
+
}),
|
|
365
|
+
getPublishingLimit: z.object({ igUserId: optionalNonEmpty }),
|
|
366
|
+
scheduleReel: z.object({
|
|
367
|
+
igUserId: optionalNonEmpty,
|
|
368
|
+
videoUrl: nonEmpty,
|
|
369
|
+
caption: optionalNonEmpty,
|
|
370
|
+
publishAt: dateLike
|
|
371
|
+
})
|
|
372
|
+
},
|
|
373
|
+
linkedin: {
|
|
374
|
+
createTextPost: z.object({
|
|
375
|
+
author: optionalNonEmpty,
|
|
376
|
+
text: nonEmpty,
|
|
377
|
+
visibility: z.enum(["PUBLIC", "CONNECTIONS"]).optional()
|
|
378
|
+
}),
|
|
379
|
+
createImagePost: z.object({ author: optionalNonEmpty, text: nonEmpty, mediaUrn: nonEmpty }),
|
|
380
|
+
createVideoPost: z.object({ author: optionalNonEmpty, text: nonEmpty, mediaUrn: nonEmpty }),
|
|
381
|
+
createCarouselPost: z.object({
|
|
382
|
+
author: optionalNonEmpty,
|
|
383
|
+
text: nonEmpty,
|
|
384
|
+
mediaUrns: z.array(nonEmpty).min(2)
|
|
385
|
+
}),
|
|
386
|
+
schedulePost: z.object({ author: optionalNonEmpty, text: nonEmpty, publishAt: dateLike }),
|
|
387
|
+
commentOnPost: z.object({ actor: optionalNonEmpty, objectUrn: nonEmpty, message: nonEmpty }),
|
|
388
|
+
replyToComment: z.object({
|
|
389
|
+
actor: optionalNonEmpty,
|
|
390
|
+
parentCommentUrn: nonEmpty,
|
|
391
|
+
message: nonEmpty
|
|
392
|
+
}),
|
|
393
|
+
deleteComment: z.object({ encodedCommentUrn: nonEmpty }),
|
|
394
|
+
likePost: z.object({ actor: optionalNonEmpty, objectUrn: nonEmpty }),
|
|
395
|
+
unlikePost: z.object({ actorUrn: optionalNonEmpty, encodedObjectUrn: nonEmpty }),
|
|
396
|
+
sendDirectMessage: z.object({
|
|
397
|
+
actor: optionalNonEmpty,
|
|
398
|
+
recipientUrn: nonEmpty,
|
|
399
|
+
text: nonEmpty
|
|
400
|
+
}),
|
|
401
|
+
getPostAnalytics: z.object({ postUrn: nonEmpty }),
|
|
402
|
+
getOrganizationAnalytics: z.object({ orgUrn: optionalNonEmpty }),
|
|
403
|
+
registerUpload: z.object({
|
|
404
|
+
owner: optionalNonEmpty,
|
|
405
|
+
mediaType: z.enum(["image", "video"]),
|
|
406
|
+
fileSize: z.number().int().positive()
|
|
407
|
+
}),
|
|
408
|
+
uploadBinary: z.object({ uploadUrl: nonEmpty, mediaPath: nonEmpty }),
|
|
409
|
+
deletePost: z.object({ encodedPostUrn: nonEmpty })
|
|
410
|
+
},
|
|
411
|
+
youtube: {
|
|
412
|
+
createVideoUploadSession: z.object({
|
|
413
|
+
title: nonEmpty,
|
|
414
|
+
description: optionalNonEmpty,
|
|
415
|
+
privacyStatus: z.enum(["private", "public", "unlisted"]).optional()
|
|
416
|
+
}),
|
|
417
|
+
uploadBinary: z.object({ uploadUrl: nonEmpty, mediaPath: nonEmpty }),
|
|
418
|
+
listMyVideos: z.object({ maxResults: z.number().int().positive().optional() }),
|
|
419
|
+
updateVideoMetadata: z.object({
|
|
420
|
+
videoId: nonEmpty,
|
|
421
|
+
title: optionalNonEmpty,
|
|
422
|
+
description: optionalNonEmpty,
|
|
423
|
+
privacyStatus: z.enum(["private", "public", "unlisted"]).optional()
|
|
424
|
+
}),
|
|
425
|
+
deleteVideo: z.object({ videoId: nonEmpty }),
|
|
426
|
+
commentOnVideo: z.object({ videoId: nonEmpty, text: nonEmpty }),
|
|
427
|
+
replyToComment: z.object({ parentCommentId: nonEmpty, text: nonEmpty }),
|
|
428
|
+
likeVideo: z.object({ videoId: nonEmpty }),
|
|
429
|
+
unlikeVideo: z.object({ videoId: nonEmpty }),
|
|
430
|
+
createPlaylist: z.object({
|
|
431
|
+
title: nonEmpty,
|
|
432
|
+
description: optionalNonEmpty,
|
|
433
|
+
privacyStatus: z.enum(["private", "public", "unlisted"]).optional()
|
|
434
|
+
}),
|
|
435
|
+
addVideoToPlaylist: z.object({ playlistId: nonEmpty, videoId: nonEmpty }),
|
|
436
|
+
getChannelAnalytics: z.object({
|
|
437
|
+
startDate: nonEmpty,
|
|
438
|
+
endDate: nonEmpty,
|
|
439
|
+
metrics: optionalNonEmpty
|
|
440
|
+
}),
|
|
441
|
+
scheduleVideoMetadataUpdate: z.object({
|
|
442
|
+
videoId: nonEmpty,
|
|
443
|
+
title: optionalNonEmpty,
|
|
444
|
+
description: optionalNonEmpty,
|
|
445
|
+
privacyStatus: z.enum(["private", "public", "unlisted"]).optional(),
|
|
446
|
+
publishAt: dateLike
|
|
447
|
+
})
|
|
448
|
+
},
|
|
449
|
+
tiktok: {
|
|
450
|
+
createPost: z.object({
|
|
451
|
+
text: nonEmpty,
|
|
452
|
+
visibility: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "SELF_ONLY"]).optional()
|
|
453
|
+
}),
|
|
454
|
+
createVideoPost: z.object({
|
|
455
|
+
title: nonEmpty,
|
|
456
|
+
videoUrl: nonEmpty,
|
|
457
|
+
visibility: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "SELF_ONLY"]).optional()
|
|
458
|
+
}),
|
|
459
|
+
getPostStatus: z.object({ publishId: nonEmpty }),
|
|
460
|
+
listVideos: z.object({ maxCount: z.number().int().positive().optional() }),
|
|
461
|
+
deleteVideo: z.object({ videoId: nonEmpty }),
|
|
462
|
+
commentOnVideo: z.object({ videoId: nonEmpty, text: nonEmpty }),
|
|
463
|
+
replyToComment: z.object({ commentId: nonEmpty, text: nonEmpty }),
|
|
464
|
+
likeVideo: z.object({ videoId: nonEmpty }),
|
|
465
|
+
unlikeVideo: z.object({ videoId: nonEmpty }),
|
|
466
|
+
getVideoAnalytics: z.object({ videoIds: z.array(nonEmpty).min(1) }),
|
|
467
|
+
getProfileAnalytics: z.object({ fields: z.array(nonEmpty).optional() }),
|
|
468
|
+
scheduleVideoPost: z.object({
|
|
469
|
+
title: nonEmpty,
|
|
470
|
+
videoUrl: nonEmpty,
|
|
471
|
+
publishAt: dateLike
|
|
472
|
+
})
|
|
473
|
+
},
|
|
474
|
+
pinterest: {
|
|
475
|
+
createPin: z.object({
|
|
476
|
+
boardId: optionalNonEmpty,
|
|
477
|
+
title: nonEmpty,
|
|
478
|
+
description: optionalNonEmpty,
|
|
479
|
+
link: optionalNonEmpty,
|
|
480
|
+
mediaSourceUrl: nonEmpty
|
|
481
|
+
}),
|
|
482
|
+
createVideoPin: z.object({
|
|
483
|
+
boardId: optionalNonEmpty,
|
|
484
|
+
title: nonEmpty,
|
|
485
|
+
description: optionalNonEmpty,
|
|
486
|
+
mediaSourceUrl: nonEmpty
|
|
487
|
+
}),
|
|
488
|
+
updatePin: z.object({
|
|
489
|
+
pinId: nonEmpty,
|
|
490
|
+
title: optionalNonEmpty,
|
|
491
|
+
description: optionalNonEmpty,
|
|
492
|
+
link: optionalNonEmpty
|
|
493
|
+
}),
|
|
494
|
+
deletePin: z.object({ pinId: nonEmpty }),
|
|
495
|
+
listPins: z.object({
|
|
496
|
+
boardId: optionalNonEmpty,
|
|
497
|
+
pageSize: z.number().int().positive().optional()
|
|
498
|
+
}),
|
|
499
|
+
createBoard: z.object({
|
|
500
|
+
name: nonEmpty,
|
|
501
|
+
description: optionalNonEmpty,
|
|
502
|
+
privacy: z.enum(["PUBLIC", "PROTECTED", "SECRET"]).optional()
|
|
503
|
+
}),
|
|
504
|
+
listBoards: z.object({ pageSize: z.number().int().positive().optional() }),
|
|
505
|
+
commentOnPin: z.object({ pinId: nonEmpty, text: nonEmpty }),
|
|
506
|
+
replyToComment: z.object({
|
|
507
|
+
pinId: nonEmpty,
|
|
508
|
+
commentId: nonEmpty,
|
|
509
|
+
text: nonEmpty
|
|
510
|
+
}),
|
|
511
|
+
getPinAnalytics: z.object({
|
|
512
|
+
pinId: nonEmpty,
|
|
513
|
+
startDate: nonEmpty,
|
|
514
|
+
endDate: nonEmpty
|
|
515
|
+
}),
|
|
516
|
+
getAccountAnalytics: z.object({ startDate: nonEmpty, endDate: nonEmpty }),
|
|
517
|
+
schedulePin: z.object({
|
|
518
|
+
title: nonEmpty,
|
|
519
|
+
mediaSourceUrl: nonEmpty,
|
|
520
|
+
boardId: optionalNonEmpty,
|
|
521
|
+
publishAt: dateLike
|
|
522
|
+
})
|
|
523
|
+
},
|
|
524
|
+
bluesky: {
|
|
525
|
+
postText: z.object({ text: nonEmpty }),
|
|
526
|
+
postWithLink: z.object({ text: nonEmpty, url: nonEmpty }),
|
|
527
|
+
replyToPost: z.object({
|
|
528
|
+
text: nonEmpty,
|
|
529
|
+
rootUri: nonEmpty,
|
|
530
|
+
rootCid: nonEmpty,
|
|
531
|
+
parentUri: nonEmpty,
|
|
532
|
+
parentCid: nonEmpty
|
|
533
|
+
}),
|
|
534
|
+
likePost: z.object({ subjectUri: nonEmpty, subjectCid: nonEmpty }),
|
|
535
|
+
repost: z.object({ subjectUri: nonEmpty, subjectCid: nonEmpty }),
|
|
536
|
+
deleteRecord: z.object({ uri: nonEmpty }),
|
|
537
|
+
getAuthorFeed: z.object({
|
|
538
|
+
actorDidOrHandle: nonEmpty,
|
|
539
|
+
limit: z.number().int().positive().optional()
|
|
540
|
+
}),
|
|
541
|
+
searchPosts: z.object({
|
|
542
|
+
query: nonEmpty,
|
|
543
|
+
limit: z.number().int().positive().optional()
|
|
544
|
+
}),
|
|
545
|
+
getPostThread: z.object({
|
|
546
|
+
uri: nonEmpty,
|
|
547
|
+
depth: z.number().int().positive().optional()
|
|
548
|
+
}),
|
|
549
|
+
getNotificationFeed: z.object({
|
|
550
|
+
limit: z.number().int().positive().optional()
|
|
551
|
+
}),
|
|
552
|
+
schedulePost: z.object({ text: nonEmpty, publishAt: dateLike })
|
|
553
|
+
},
|
|
554
|
+
mastodon: {
|
|
555
|
+
createStatus: z.object({
|
|
556
|
+
text: nonEmpty,
|
|
557
|
+
visibility: z.enum(["public", "unlisted", "private", "direct"]).optional()
|
|
558
|
+
}),
|
|
559
|
+
uploadMedia: z.object({ mediaPath: nonEmpty, description: optionalNonEmpty }),
|
|
560
|
+
createMediaStatus: z.object({
|
|
561
|
+
text: nonEmpty,
|
|
562
|
+
mediaIds: z.array(nonEmpty).min(1),
|
|
563
|
+
visibility: z.enum(["public", "unlisted", "private", "direct"]).optional()
|
|
564
|
+
}),
|
|
565
|
+
replyToStatus: z.object({ statusId: nonEmpty, text: nonEmpty }),
|
|
566
|
+
deleteStatus: z.object({ statusId: nonEmpty }),
|
|
567
|
+
favouriteStatus: z.object({ statusId: nonEmpty }),
|
|
568
|
+
unfavouriteStatus: z.object({ statusId: nonEmpty }),
|
|
569
|
+
boostStatus: z.object({ statusId: nonEmpty }),
|
|
570
|
+
unboostStatus: z.object({ statusId: nonEmpty }),
|
|
571
|
+
listMyStatuses: z.object({ limit: z.number().int().positive().optional() }),
|
|
572
|
+
getStatusContext: z.object({ statusId: nonEmpty }),
|
|
573
|
+
getAccountAnalytics: z.object({
|
|
574
|
+
instanceScope: z.enum(["day", "week", "month"]).optional()
|
|
575
|
+
}),
|
|
576
|
+
scheduleStatus: z.object({ text: nonEmpty, publishAt: dateLike })
|
|
577
|
+
},
|
|
578
|
+
threads: {
|
|
579
|
+
postText: z.object({ threadsUserId: optionalNonEmpty, text: nonEmpty }),
|
|
580
|
+
postImage: z.object({
|
|
581
|
+
threadsUserId: optionalNonEmpty,
|
|
582
|
+
text: optionalNonEmpty,
|
|
583
|
+
imageUrl: nonEmpty
|
|
584
|
+
}),
|
|
585
|
+
postVideo: z.object({
|
|
586
|
+
threadsUserId: optionalNonEmpty,
|
|
587
|
+
text: optionalNonEmpty,
|
|
588
|
+
videoUrl: nonEmpty
|
|
589
|
+
}),
|
|
590
|
+
replyToThread: z.object({
|
|
591
|
+
threadsUserId: optionalNonEmpty,
|
|
592
|
+
threadId: nonEmpty,
|
|
593
|
+
text: nonEmpty
|
|
594
|
+
}),
|
|
595
|
+
deleteThread: z.object({ threadId: nonEmpty }),
|
|
596
|
+
getThread: z.object({
|
|
597
|
+
threadId: nonEmpty,
|
|
598
|
+
fields: z.array(nonEmpty).optional()
|
|
599
|
+
}),
|
|
600
|
+
listMyThreads: z.object({
|
|
601
|
+
threadsUserId: optionalNonEmpty,
|
|
602
|
+
limit: z.number().int().positive().optional()
|
|
603
|
+
}),
|
|
604
|
+
getThreadInsights: z.object({
|
|
605
|
+
threadId: nonEmpty,
|
|
606
|
+
metrics: z.array(nonEmpty).optional()
|
|
607
|
+
}),
|
|
608
|
+
getAccountInsights: z.object({
|
|
609
|
+
threadsUserId: optionalNonEmpty,
|
|
610
|
+
metrics: z.array(nonEmpty).optional(),
|
|
611
|
+
period: z.enum(["day", "week", "days_28"]).optional()
|
|
612
|
+
}),
|
|
613
|
+
likeThread: z.object({ threadId: nonEmpty }),
|
|
614
|
+
unlikeThread: z.object({ threadId: nonEmpty }),
|
|
615
|
+
scheduleTextPost: z.object({
|
|
616
|
+
threadsUserId: optionalNonEmpty,
|
|
617
|
+
text: nonEmpty,
|
|
618
|
+
publishAt: dateLike
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
function validatePlatformInput(platform, method, input) {
|
|
623
|
+
const schema = schemaMap[platform][method];
|
|
624
|
+
const result = schema.safeParse(input);
|
|
625
|
+
if (result.success) {
|
|
626
|
+
return result.data;
|
|
627
|
+
}
|
|
628
|
+
throw new SocialError({
|
|
629
|
+
platform,
|
|
630
|
+
endpoint: `${platform}.${method}`,
|
|
631
|
+
message: `Invalid input for ${platform}.${method}`,
|
|
632
|
+
details: result.error.flatten()
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/platforms/instagram.ts
|
|
637
|
+
function igUserIdOrThrow(inputUserId) {
|
|
638
|
+
const igUserId = inputUserId ?? env.meta.igUserId;
|
|
639
|
+
if (!igUserId) {
|
|
640
|
+
throw new SocialError({
|
|
641
|
+
platform: "instagram",
|
|
642
|
+
endpoint: "ig-user-id",
|
|
643
|
+
message: "Missing IG_USER_ID (or pass igUserId explicitly)."
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return igUserId;
|
|
647
|
+
}
|
|
648
|
+
async function createMediaContainer(params) {
|
|
649
|
+
return metaCall({
|
|
650
|
+
platform: "instagram",
|
|
651
|
+
method: "POST",
|
|
652
|
+
endpoint: `/${params.igUserId}/media`,
|
|
653
|
+
body: {
|
|
654
|
+
image_url: params.imageUrl,
|
|
655
|
+
video_url: params.videoUrl,
|
|
656
|
+
caption: params.caption,
|
|
657
|
+
media_type: params.mediaType,
|
|
658
|
+
is_carousel_item: params.isCarouselItem,
|
|
659
|
+
children: params.children
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
async function publishContainer(igUserId, creationId) {
|
|
664
|
+
return metaCall({
|
|
665
|
+
platform: "instagram",
|
|
666
|
+
method: "POST",
|
|
667
|
+
endpoint: `/${igUserId}/media_publish`,
|
|
668
|
+
body: { creation_id: creationId }
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
var Instagram = class _Instagram {
|
|
672
|
+
static async uploadPhoto(input) {
|
|
673
|
+
validatePlatformInput("instagram", "uploadPhoto", input);
|
|
674
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
675
|
+
const container = await createMediaContainer({
|
|
676
|
+
igUserId,
|
|
677
|
+
imageUrl: input.imageUrl,
|
|
678
|
+
caption: input.caption,
|
|
679
|
+
mediaType: "IMAGE"
|
|
680
|
+
});
|
|
681
|
+
return publishContainer(igUserId, container.id);
|
|
682
|
+
}
|
|
683
|
+
static async uploadVideo(input) {
|
|
684
|
+
validatePlatformInput("instagram", "uploadVideo", input);
|
|
685
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
686
|
+
const container = await createMediaContainer({
|
|
687
|
+
igUserId,
|
|
688
|
+
videoUrl: input.videoUrl,
|
|
689
|
+
caption: input.caption,
|
|
690
|
+
mediaType: "VIDEO"
|
|
691
|
+
});
|
|
692
|
+
return publishContainer(igUserId, container.id);
|
|
693
|
+
}
|
|
694
|
+
static async uploadReel(input) {
|
|
695
|
+
validatePlatformInput("instagram", "uploadReel", input);
|
|
696
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
697
|
+
const container = await createMediaContainer({
|
|
698
|
+
igUserId,
|
|
699
|
+
videoUrl: input.videoUrl,
|
|
700
|
+
caption: input.caption,
|
|
701
|
+
mediaType: "REELS"
|
|
702
|
+
});
|
|
703
|
+
return publishContainer(igUserId, container.id);
|
|
704
|
+
}
|
|
705
|
+
static async uploadStoryPhoto(input) {
|
|
706
|
+
validatePlatformInput("instagram", "uploadStoryPhoto", input);
|
|
707
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
708
|
+
const container = await createMediaContainer({
|
|
709
|
+
igUserId,
|
|
710
|
+
imageUrl: input.imageUrl,
|
|
711
|
+
mediaType: "STORIES"
|
|
712
|
+
});
|
|
713
|
+
return publishContainer(igUserId, container.id);
|
|
714
|
+
}
|
|
715
|
+
static async uploadStoryVideo(input) {
|
|
716
|
+
validatePlatformInput("instagram", "uploadStoryVideo", input);
|
|
717
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
718
|
+
const container = await createMediaContainer({
|
|
719
|
+
igUserId,
|
|
720
|
+
videoUrl: input.videoUrl,
|
|
721
|
+
mediaType: "STORIES"
|
|
722
|
+
});
|
|
723
|
+
return publishContainer(igUserId, container.id);
|
|
724
|
+
}
|
|
725
|
+
static async publishCarousel(input) {
|
|
726
|
+
validatePlatformInput("instagram", "publishCarousel", input);
|
|
727
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
728
|
+
const children = [];
|
|
729
|
+
for (const item of input.items) {
|
|
730
|
+
const child = await createMediaContainer({
|
|
731
|
+
igUserId,
|
|
732
|
+
imageUrl: item.imageUrl,
|
|
733
|
+
videoUrl: item.videoUrl,
|
|
734
|
+
isCarouselItem: true
|
|
735
|
+
});
|
|
736
|
+
children.push(child.id);
|
|
737
|
+
}
|
|
738
|
+
const parent = await createMediaContainer({
|
|
739
|
+
igUserId,
|
|
740
|
+
caption: input.caption,
|
|
741
|
+
mediaType: "IMAGE",
|
|
742
|
+
children
|
|
743
|
+
});
|
|
744
|
+
return publishContainer(igUserId, parent.id);
|
|
745
|
+
}
|
|
746
|
+
static async commentOnMedia(input) {
|
|
747
|
+
validatePlatformInput("instagram", "commentOnMedia", input);
|
|
748
|
+
return metaCall({
|
|
749
|
+
platform: "instagram",
|
|
750
|
+
method: "POST",
|
|
751
|
+
endpoint: `/${input.mediaId}/comments`,
|
|
752
|
+
body: { message: input.message }
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
static async replyToComment(input) {
|
|
756
|
+
validatePlatformInput("instagram", "replyToComment", input);
|
|
757
|
+
return metaCall({
|
|
758
|
+
platform: "instagram",
|
|
759
|
+
method: "POST",
|
|
760
|
+
endpoint: `/${input.commentId}/replies`,
|
|
761
|
+
body: { message: input.message }
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
static async hideComment(input) {
|
|
765
|
+
validatePlatformInput("instagram", "hideComment", input);
|
|
766
|
+
return metaCall({
|
|
767
|
+
platform: "instagram",
|
|
768
|
+
method: "POST",
|
|
769
|
+
endpoint: `/${input.commentId}`,
|
|
770
|
+
body: { hidden: input.hide }
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
static async deleteComment(input) {
|
|
774
|
+
validatePlatformInput("instagram", "deleteComment", input);
|
|
775
|
+
return metaCall({
|
|
776
|
+
platform: "instagram",
|
|
777
|
+
method: "DELETE",
|
|
778
|
+
endpoint: `/${input.commentId}`
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
static async deleteMedia(input) {
|
|
782
|
+
validatePlatformInput("instagram", "deleteMedia", input);
|
|
783
|
+
return metaCall({
|
|
784
|
+
platform: "instagram",
|
|
785
|
+
method: "DELETE",
|
|
786
|
+
endpoint: `/${input.mediaId}`
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
static async sendPrivateReply(input) {
|
|
790
|
+
validatePlatformInput("instagram", "sendPrivateReply", input);
|
|
791
|
+
return metaCall({
|
|
792
|
+
platform: "instagram",
|
|
793
|
+
method: "POST",
|
|
794
|
+
endpoint: `/${input.commentId}/private_replies`,
|
|
795
|
+
body: { message: input.message }
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
static async getMediaInsights(input) {
|
|
799
|
+
validatePlatformInput("instagram", "getMediaInsights", input);
|
|
800
|
+
return metaCall({
|
|
801
|
+
platform: "instagram",
|
|
802
|
+
method: "GET",
|
|
803
|
+
endpoint: `/${input.mediaId}/insights`,
|
|
804
|
+
query: { metric: (input.metrics ?? ["impressions", "reach", "saved", "video_views"]).join(",") }
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
static async getAccountInsights(input) {
|
|
808
|
+
validatePlatformInput("instagram", "getAccountInsights", input);
|
|
809
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
810
|
+
return metaCall({
|
|
811
|
+
platform: "instagram",
|
|
812
|
+
method: "GET",
|
|
813
|
+
endpoint: `/${igUserId}/insights`,
|
|
814
|
+
query: {
|
|
815
|
+
metric: (input.metrics ?? ["impressions", "reach", "profile_views"]).join(","),
|
|
816
|
+
period: input.period ?? "day"
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
static async getPublishingLimit(input) {
|
|
821
|
+
validatePlatformInput("instagram", "getPublishingLimit", input);
|
|
822
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
823
|
+
return metaCall({
|
|
824
|
+
platform: "instagram",
|
|
825
|
+
method: "GET",
|
|
826
|
+
endpoint: `/${igUserId}/content_publishing_limit`,
|
|
827
|
+
query: { fields: "quota_usage,config" }
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
static async scheduleReel(input) {
|
|
831
|
+
validatePlatformInput("instagram", "scheduleReel", input);
|
|
832
|
+
const igUserId = igUserIdOrThrow(input.igUserId);
|
|
833
|
+
return scheduleTask({
|
|
834
|
+
id: `ig-schedule-${Date.now()}`,
|
|
835
|
+
runAt: input.publishAt,
|
|
836
|
+
task: async () => _Instagram.uploadReel({
|
|
837
|
+
igUserId,
|
|
838
|
+
videoUrl: input.videoUrl,
|
|
839
|
+
caption: input.caption
|
|
840
|
+
})
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// src/platforms/x.ts
|
|
846
|
+
import { TwitterApi } from "twitter-api-v2";
|
|
847
|
+
|
|
848
|
+
// src/utils/file.ts
|
|
849
|
+
import { createReadStream, statSync } from "fs";
|
|
850
|
+
import path from "path";
|
|
851
|
+
function getFileMeta(filePath) {
|
|
852
|
+
const stat = statSync(filePath);
|
|
853
|
+
return {
|
|
854
|
+
fileName: path.basename(filePath),
|
|
855
|
+
fileSize: stat.size
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
function createUploadStream(filePath) {
|
|
859
|
+
return createReadStream(filePath);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/platforms/x.ts
|
|
863
|
+
var client = null;
|
|
864
|
+
function getClient() {
|
|
865
|
+
if (client) {
|
|
866
|
+
return client;
|
|
867
|
+
}
|
|
868
|
+
if (!env.x.apiKey || !env.x.apiSecret || !env.x.accessToken || !env.x.accessSecret) {
|
|
869
|
+
throw new SocialError({
|
|
870
|
+
platform: "x",
|
|
871
|
+
endpoint: "auth",
|
|
872
|
+
message: "Missing X credentials. Required: X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_SECRET."
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
client = new TwitterApi({
|
|
876
|
+
appKey: env.x.apiKey,
|
|
877
|
+
appSecret: env.x.apiSecret,
|
|
878
|
+
accessToken: env.x.accessToken,
|
|
879
|
+
accessSecret: env.x.accessSecret
|
|
880
|
+
});
|
|
881
|
+
return client;
|
|
882
|
+
}
|
|
883
|
+
async function uploadMediaInternal(mediaPath) {
|
|
884
|
+
const rw = getClient().readWrite;
|
|
885
|
+
return withRetries({
|
|
886
|
+
platform: "x",
|
|
887
|
+
endpoint: "v1.1/media/upload",
|
|
888
|
+
execute: async () => rw.v1.uploadMedia(createUploadStream(mediaPath))
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
var X = class _X {
|
|
892
|
+
static async postTweet(input) {
|
|
893
|
+
validatePlatformInput("x", "postTweet", input);
|
|
894
|
+
const rw = getClient().readWrite;
|
|
895
|
+
return withRetries({
|
|
896
|
+
platform: "x",
|
|
897
|
+
endpoint: "POST /2/tweets",
|
|
898
|
+
execute: async () => rw.v2.tweet({ text: input.text, media: input.mediaIds ? { media_ids: input.mediaIds } : void 0 })
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
static async postThread(input) {
|
|
902
|
+
validatePlatformInput("x", "postThread", input);
|
|
903
|
+
let previousId;
|
|
904
|
+
const published = [];
|
|
905
|
+
for (const text of input.tweets) {
|
|
906
|
+
const payload = previousId ? { text, reply: { in_reply_to_tweet_id: previousId } } : { text };
|
|
907
|
+
const rw = getClient().readWrite;
|
|
908
|
+
const result = await rw.v2.tweet(payload);
|
|
909
|
+
published.push(result);
|
|
910
|
+
previousId = result?.data?.id;
|
|
911
|
+
}
|
|
912
|
+
return published;
|
|
913
|
+
}
|
|
914
|
+
static async replyTweet(input) {
|
|
915
|
+
validatePlatformInput("x", "replyTweet", input);
|
|
916
|
+
const rw = getClient().readWrite;
|
|
917
|
+
return withRetries({
|
|
918
|
+
platform: "x",
|
|
919
|
+
endpoint: "POST /2/tweets",
|
|
920
|
+
execute: async () => rw.v2.tweet({ text: input.text, reply: { in_reply_to_tweet_id: input.inReplyToTweetId } })
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
static async quoteTweet(input) {
|
|
924
|
+
validatePlatformInput("x", "quoteTweet", input);
|
|
925
|
+
const rw = getClient().readWrite;
|
|
926
|
+
return withRetries({
|
|
927
|
+
platform: "x",
|
|
928
|
+
endpoint: "POST /2/tweets",
|
|
929
|
+
execute: async () => rw.v2.tweet({ text: input.text, quote_tweet_id: input.quoteTweetId })
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
static async deleteTweet(input) {
|
|
933
|
+
validatePlatformInput("x", "deleteTweet", input);
|
|
934
|
+
const rw = getClient().readWrite;
|
|
935
|
+
return withRetries({
|
|
936
|
+
platform: "x",
|
|
937
|
+
endpoint: "DELETE /2/tweets/:id",
|
|
938
|
+
execute: async () => rw.v2.deleteTweet(input.tweetId)
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
static async retweet(input) {
|
|
942
|
+
validatePlatformInput("x", "retweet", input);
|
|
943
|
+
const rw = getClient().readWrite;
|
|
944
|
+
return withRetries({
|
|
945
|
+
platform: "x",
|
|
946
|
+
endpoint: "POST /2/users/:id/retweets",
|
|
947
|
+
execute: async () => rw.v2.retweet(input.userId, input.tweetId)
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
static async unretweet(input) {
|
|
951
|
+
validatePlatformInput("x", "unretweet", input);
|
|
952
|
+
const rw = getClient().readWrite;
|
|
953
|
+
return withRetries({
|
|
954
|
+
platform: "x",
|
|
955
|
+
endpoint: "DELETE /2/users/:id/retweets/:tweet_id",
|
|
956
|
+
execute: async () => rw.v2.unretweet(input.userId, input.tweetId)
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
static async likeTweet(input) {
|
|
960
|
+
validatePlatformInput("x", "likeTweet", input);
|
|
961
|
+
const rw = getClient().readWrite;
|
|
962
|
+
return withRetries({
|
|
963
|
+
platform: "x",
|
|
964
|
+
endpoint: "POST /2/users/:id/likes",
|
|
965
|
+
execute: async () => rw.v2.like(input.userId, input.tweetId)
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
static async unlikeTweet(input) {
|
|
969
|
+
validatePlatformInput("x", "unlikeTweet", input);
|
|
970
|
+
const rw = getClient().readWrite;
|
|
971
|
+
return withRetries({
|
|
972
|
+
platform: "x",
|
|
973
|
+
endpoint: "DELETE /2/users/:id/likes/:tweet_id",
|
|
974
|
+
execute: async () => rw.v2.unlike(input.userId, input.tweetId)
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
static async uploadMedia(input) {
|
|
978
|
+
validatePlatformInput("x", "uploadMedia", input);
|
|
979
|
+
return uploadMediaInternal(input.mediaPath);
|
|
980
|
+
}
|
|
981
|
+
static async postPhoto(input) {
|
|
982
|
+
validatePlatformInput("x", "postPhoto", input);
|
|
983
|
+
const mediaId = await uploadMediaInternal(input.mediaPath);
|
|
984
|
+
return _X.postTweet({ text: input.text, mediaIds: [mediaId] });
|
|
985
|
+
}
|
|
986
|
+
static async postVideo(input) {
|
|
987
|
+
validatePlatformInput("x", "postVideo", input);
|
|
988
|
+
const mediaId = await uploadMediaInternal(input.mediaPath);
|
|
989
|
+
return _X.postTweet({ text: input.text, mediaIds: [mediaId] });
|
|
990
|
+
}
|
|
991
|
+
static async postPoll(input) {
|
|
992
|
+
validatePlatformInput("x", "postPoll", input);
|
|
993
|
+
const rw = getClient().readWrite;
|
|
994
|
+
return withRetries({
|
|
995
|
+
platform: "x",
|
|
996
|
+
endpoint: "POST /2/tweets",
|
|
997
|
+
execute: async () => rw.v2.tweet({
|
|
998
|
+
text: input.text,
|
|
999
|
+
poll: {
|
|
1000
|
+
options: input.options,
|
|
1001
|
+
duration_minutes: input.durationMinutes
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
static async sendDirectMessage(input) {
|
|
1007
|
+
validatePlatformInput("x", "sendDirectMessage", input);
|
|
1008
|
+
const rw = getClient().readWrite;
|
|
1009
|
+
return withRetries({
|
|
1010
|
+
platform: "x",
|
|
1011
|
+
endpoint: "POST /2/dm_conversations/with/:participant_id/messages",
|
|
1012
|
+
execute: async () => rw.v2.post(`dm_conversations/with/${input.recipientId}/messages`, {
|
|
1013
|
+
text: input.text
|
|
1014
|
+
})
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
static async getTweetAnalytics(input) {
|
|
1018
|
+
validatePlatformInput("x", "getTweetAnalytics", input);
|
|
1019
|
+
const ro = getClient().readOnly;
|
|
1020
|
+
return withRetries({
|
|
1021
|
+
platform: "x",
|
|
1022
|
+
endpoint: "GET /2/tweets/:id",
|
|
1023
|
+
execute: async () => ro.v2.singleTweet(input.tweetId, {
|
|
1024
|
+
"tweet.fields": ["public_metrics", "created_at", "organic_metrics", "non_public_metrics"]
|
|
1025
|
+
})
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
static async scheduleTweet(input) {
|
|
1029
|
+
validatePlatformInput("x", "scheduleTweet", input);
|
|
1030
|
+
const jobId = `x-schedule-${Date.now()}`;
|
|
1031
|
+
return scheduleTask({
|
|
1032
|
+
id: jobId,
|
|
1033
|
+
runAt: input.publishAt,
|
|
1034
|
+
task: async () => _X.postTweet({ text: input.text })
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
// src/platforms/facebook.ts
|
|
1040
|
+
function pageIdOrThrow(inputPageId) {
|
|
1041
|
+
const pageId = inputPageId ?? env.meta.fbPageId;
|
|
1042
|
+
if (!pageId) {
|
|
1043
|
+
throw new SocialError({
|
|
1044
|
+
platform: "facebook",
|
|
1045
|
+
endpoint: "page-id",
|
|
1046
|
+
message: "Missing FB_PAGE_ID (or pass pageId explicitly)."
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
return pageId;
|
|
1050
|
+
}
|
|
1051
|
+
var Facebook = class {
|
|
1052
|
+
static async publishToPage(input) {
|
|
1053
|
+
validatePlatformInput("facebook", "publishToPage", input);
|
|
1054
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1055
|
+
return metaCall({
|
|
1056
|
+
platform: "facebook",
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
endpoint: `/${pageId}/feed`,
|
|
1059
|
+
body: {
|
|
1060
|
+
message: input.message,
|
|
1061
|
+
link: input.link,
|
|
1062
|
+
picture: input.photoUrl
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
static async publishPhoto(input) {
|
|
1067
|
+
validatePlatformInput("facebook", "publishPhoto", input);
|
|
1068
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1069
|
+
return metaCall({
|
|
1070
|
+
platform: "facebook",
|
|
1071
|
+
method: "POST",
|
|
1072
|
+
endpoint: `/${pageId}/photos`,
|
|
1073
|
+
body: { url: input.url, caption: input.caption }
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
static async publishVideo(input) {
|
|
1077
|
+
validatePlatformInput("facebook", "publishVideo", input);
|
|
1078
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1079
|
+
return metaCall({
|
|
1080
|
+
platform: "facebook",
|
|
1081
|
+
method: "POST",
|
|
1082
|
+
endpoint: `/${pageId}/videos`,
|
|
1083
|
+
body: {
|
|
1084
|
+
file_url: input.fileUrl,
|
|
1085
|
+
description: input.description,
|
|
1086
|
+
title: input.title
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
static async publishCarousel(input) {
|
|
1091
|
+
validatePlatformInput("facebook", "publishCarousel", input);
|
|
1092
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1093
|
+
const mediaIds = [];
|
|
1094
|
+
for (const url of input.photoUrls) {
|
|
1095
|
+
const media = await metaCall({
|
|
1096
|
+
platform: "facebook",
|
|
1097
|
+
method: "POST",
|
|
1098
|
+
endpoint: `/${pageId}/photos`,
|
|
1099
|
+
body: { url, published: false }
|
|
1100
|
+
});
|
|
1101
|
+
mediaIds.push(media.id);
|
|
1102
|
+
}
|
|
1103
|
+
return metaCall({
|
|
1104
|
+
platform: "facebook",
|
|
1105
|
+
method: "POST",
|
|
1106
|
+
endpoint: `/${pageId}/feed`,
|
|
1107
|
+
body: {
|
|
1108
|
+
message: input.message,
|
|
1109
|
+
attached_media: mediaIds.map((id) => ({ media_fbid: id }))
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
static async publishStory(input) {
|
|
1114
|
+
validatePlatformInput("facebook", "publishStory", input);
|
|
1115
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1116
|
+
return metaCall({
|
|
1117
|
+
platform: "facebook",
|
|
1118
|
+
method: "POST",
|
|
1119
|
+
endpoint: `/${pageId}/photo_stories`,
|
|
1120
|
+
body: { url: input.photoUrl }
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
static async schedulePost(input) {
|
|
1124
|
+
validatePlatformInput("facebook", "schedulePost", input);
|
|
1125
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1126
|
+
const publishTime = Math.floor(new Date(input.publishAt).getTime() / 1e3);
|
|
1127
|
+
return metaCall({
|
|
1128
|
+
platform: "facebook",
|
|
1129
|
+
method: "POST",
|
|
1130
|
+
endpoint: `/${pageId}/feed`,
|
|
1131
|
+
body: {
|
|
1132
|
+
message: input.message,
|
|
1133
|
+
published: false,
|
|
1134
|
+
scheduled_publish_time: publishTime
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
static async commentOnPost(input) {
|
|
1139
|
+
validatePlatformInput("facebook", "commentOnPost", input);
|
|
1140
|
+
return metaCall({
|
|
1141
|
+
platform: "facebook",
|
|
1142
|
+
method: "POST",
|
|
1143
|
+
endpoint: `/${input.postId}/comments`,
|
|
1144
|
+
body: { message: input.message }
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
static async replyToComment(input) {
|
|
1148
|
+
validatePlatformInput("facebook", "replyToComment", input);
|
|
1149
|
+
return metaCall({
|
|
1150
|
+
platform: "facebook",
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
endpoint: `/${input.commentId}/comments`,
|
|
1153
|
+
body: { message: input.message }
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
static async likeObject(input) {
|
|
1157
|
+
validatePlatformInput("facebook", "likeObject", input);
|
|
1158
|
+
return metaCall({
|
|
1159
|
+
platform: "facebook",
|
|
1160
|
+
method: "POST",
|
|
1161
|
+
endpoint: `/${input.objectId}/likes`
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
static async deletePost(input) {
|
|
1165
|
+
validatePlatformInput("facebook", "deletePost", input);
|
|
1166
|
+
return metaCall({
|
|
1167
|
+
platform: "facebook",
|
|
1168
|
+
method: "DELETE",
|
|
1169
|
+
endpoint: `/${input.objectId}`
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
static async sendPageMessage(input) {
|
|
1173
|
+
validatePlatformInput("facebook", "sendPageMessage", input);
|
|
1174
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1175
|
+
return metaCall({
|
|
1176
|
+
platform: "facebook",
|
|
1177
|
+
method: "POST",
|
|
1178
|
+
endpoint: `/${pageId}/messages`,
|
|
1179
|
+
body: {
|
|
1180
|
+
recipient: { id: input.recipientPsid },
|
|
1181
|
+
message: { text: input.message },
|
|
1182
|
+
messaging_type: "RESPONSE"
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
static async getPostInsights(input) {
|
|
1187
|
+
validatePlatformInput("facebook", "getPostInsights", input);
|
|
1188
|
+
return metaCall({
|
|
1189
|
+
platform: "facebook",
|
|
1190
|
+
method: "GET",
|
|
1191
|
+
endpoint: `/${input.postId}/insights`,
|
|
1192
|
+
query: { metric: (input.metrics ?? ["post_impressions", "post_engaged_users"]).join(",") }
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
static async getPageInsights(input) {
|
|
1196
|
+
validatePlatformInput("facebook", "getPageInsights", input);
|
|
1197
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1198
|
+
return metaCall({
|
|
1199
|
+
platform: "facebook",
|
|
1200
|
+
method: "GET",
|
|
1201
|
+
endpoint: `/${pageId}/insights`,
|
|
1202
|
+
query: {
|
|
1203
|
+
metric: (input.metrics ?? ["page_impressions", "page_engaged_users"]).join(","),
|
|
1204
|
+
period: input.period ?? "day"
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
static async uploadResumableVideo(input) {
|
|
1209
|
+
validatePlatformInput("facebook", "uploadResumableVideo", input);
|
|
1210
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1211
|
+
return metaCall({
|
|
1212
|
+
platform: "facebook",
|
|
1213
|
+
method: "POST",
|
|
1214
|
+
endpoint: `/${pageId}/videos`,
|
|
1215
|
+
body: {
|
|
1216
|
+
upload_phase: "start",
|
|
1217
|
+
file_size: input.fileSize,
|
|
1218
|
+
start_offset: input.startOffset ?? 0
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
static async listPublishedPosts(input) {
|
|
1223
|
+
validatePlatformInput("facebook", "listPublishedPosts", input);
|
|
1224
|
+
const pageId = pageIdOrThrow(input.pageId);
|
|
1225
|
+
return metaCall({
|
|
1226
|
+
platform: "facebook",
|
|
1227
|
+
method: "GET",
|
|
1228
|
+
endpoint: `/${pageId}/published_posts`,
|
|
1229
|
+
query: { limit: input.limit ?? 25 }
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
static async scheduleInProcess(input) {
|
|
1233
|
+
return scheduleTask({
|
|
1234
|
+
id: `facebook-schedule-${Date.now()}`,
|
|
1235
|
+
runAt: input.publishAt,
|
|
1236
|
+
task: input.action
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
// src/platforms/linkedin.ts
|
|
1242
|
+
import axios2 from "axios";
|
|
1243
|
+
|
|
1244
|
+
// src/platforms/shared/linkedinAuth.ts
|
|
1245
|
+
import axios from "axios";
|
|
1246
|
+
var LINKEDIN_TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken";
|
|
1247
|
+
var accessTokenCache = env.linkedin.accessToken;
|
|
1248
|
+
var expiryEpochMs = 0;
|
|
1249
|
+
function getLinkedInHeaders() {
|
|
1250
|
+
return {
|
|
1251
|
+
Authorization: `Bearer ${accessTokenCache}`,
|
|
1252
|
+
"Content-Type": "application/json",
|
|
1253
|
+
"Linkedin-Version": env.linkedin.apiVersion,
|
|
1254
|
+
"X-Restli-Protocol-Version": "2.0.0"
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
async function getLinkedInAccessToken() {
|
|
1258
|
+
const stillValid = accessTokenCache.length > 0 && Date.now() < expiryEpochMs - 3e4;
|
|
1259
|
+
if (stillValid) {
|
|
1260
|
+
return accessTokenCache;
|
|
1261
|
+
}
|
|
1262
|
+
if (!env.linkedin.refreshToken) {
|
|
1263
|
+
if (!accessTokenCache) {
|
|
1264
|
+
throw new SocialError({
|
|
1265
|
+
platform: "linkedin",
|
|
1266
|
+
endpoint: "oauth/token",
|
|
1267
|
+
message: "Missing LinkedIn credentials. Provide LINKEDIN_ACCESS_TOKEN or refresh-token credentials."
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
return accessTokenCache;
|
|
1271
|
+
}
|
|
1272
|
+
if (!env.linkedin.clientId || !env.linkedin.clientSecret) {
|
|
1273
|
+
throw new SocialError({
|
|
1274
|
+
platform: "linkedin",
|
|
1275
|
+
endpoint: "oauth/token",
|
|
1276
|
+
message: "Missing LINKEDIN_CLIENT_ID or LINKEDIN_CLIENT_SECRET for token refresh."
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
const body = new URLSearchParams({
|
|
1280
|
+
grant_type: "refresh_token",
|
|
1281
|
+
refresh_token: env.linkedin.refreshToken,
|
|
1282
|
+
client_id: env.linkedin.clientId,
|
|
1283
|
+
client_secret: env.linkedin.clientSecret
|
|
1284
|
+
});
|
|
1285
|
+
try {
|
|
1286
|
+
const response = await axios.post(LINKEDIN_TOKEN_URL, body.toString(), {
|
|
1287
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" }
|
|
1288
|
+
});
|
|
1289
|
+
accessTokenCache = response.data.access_token;
|
|
1290
|
+
expiryEpochMs = Date.now() + response.data.expires_in * 1e3;
|
|
1291
|
+
return accessTokenCache;
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
throw SocialError.normalize({
|
|
1294
|
+
platform: "linkedin",
|
|
1295
|
+
endpoint: "oauth/token",
|
|
1296
|
+
error
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/platforms/linkedin.ts
|
|
1302
|
+
var BASE_URL = "https://api.linkedin.com/rest";
|
|
1303
|
+
function authorOrThrow(author) {
|
|
1304
|
+
const resolved = author ?? env.linkedin.orgUrn ?? env.linkedin.personUrn;
|
|
1305
|
+
if (!resolved) {
|
|
1306
|
+
throw new SocialError({
|
|
1307
|
+
platform: "linkedin",
|
|
1308
|
+
endpoint: "author",
|
|
1309
|
+
message: "Missing LinkedIn author URN. Set LINKEDIN_ORG_URN or LINKEDIN_PERSON_URN or pass author."
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return resolved;
|
|
1313
|
+
}
|
|
1314
|
+
async function linkedInRequest(params) {
|
|
1315
|
+
await getLinkedInAccessToken();
|
|
1316
|
+
return withRetries({
|
|
1317
|
+
platform: "linkedin",
|
|
1318
|
+
endpoint: params.endpoint,
|
|
1319
|
+
execute: async () => {
|
|
1320
|
+
try {
|
|
1321
|
+
const response = await axios2.request({
|
|
1322
|
+
url: `${BASE_URL}${params.endpoint}`,
|
|
1323
|
+
method: params.method,
|
|
1324
|
+
params: params.query,
|
|
1325
|
+
data: params.data,
|
|
1326
|
+
headers: { ...getLinkedInHeaders(), ...params.headers ?? {} }
|
|
1327
|
+
});
|
|
1328
|
+
return response.data;
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
throw SocialError.normalize({
|
|
1331
|
+
platform: "linkedin",
|
|
1332
|
+
endpoint: params.endpoint,
|
|
1333
|
+
error
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
var LinkedIn = class _LinkedIn {
|
|
1340
|
+
static async createTextPost(input) {
|
|
1341
|
+
validatePlatformInput("linkedin", "createTextPost", input);
|
|
1342
|
+
const author = authorOrThrow(input.author);
|
|
1343
|
+
return linkedInRequest({
|
|
1344
|
+
endpoint: "/posts",
|
|
1345
|
+
method: "POST",
|
|
1346
|
+
data: {
|
|
1347
|
+
author,
|
|
1348
|
+
commentary: input.text,
|
|
1349
|
+
visibility: input.visibility ?? "PUBLIC",
|
|
1350
|
+
distribution: { feedDistribution: "MAIN_FEED", targetEntities: [], thirdPartyDistributionChannels: [] },
|
|
1351
|
+
lifecycleState: "PUBLISHED",
|
|
1352
|
+
isReshareDisabledByAuthor: false
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
static async createImagePost(input) {
|
|
1357
|
+
validatePlatformInput("linkedin", "createImagePost", input);
|
|
1358
|
+
const author = authorOrThrow(input.author);
|
|
1359
|
+
return linkedInRequest({
|
|
1360
|
+
endpoint: "/posts",
|
|
1361
|
+
method: "POST",
|
|
1362
|
+
data: {
|
|
1363
|
+
author,
|
|
1364
|
+
commentary: input.text,
|
|
1365
|
+
visibility: "PUBLIC",
|
|
1366
|
+
content: { media: { id: input.mediaUrn } },
|
|
1367
|
+
lifecycleState: "PUBLISHED"
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
static async createVideoPost(input) {
|
|
1372
|
+
validatePlatformInput("linkedin", "createVideoPost", input);
|
|
1373
|
+
return _LinkedIn.createImagePost(input);
|
|
1374
|
+
}
|
|
1375
|
+
static async createCarouselPost(input) {
|
|
1376
|
+
validatePlatformInput("linkedin", "createCarouselPost", input);
|
|
1377
|
+
const author = authorOrThrow(input.author);
|
|
1378
|
+
return linkedInRequest({
|
|
1379
|
+
endpoint: "/posts",
|
|
1380
|
+
method: "POST",
|
|
1381
|
+
data: {
|
|
1382
|
+
author,
|
|
1383
|
+
commentary: input.text,
|
|
1384
|
+
visibility: "PUBLIC",
|
|
1385
|
+
content: {
|
|
1386
|
+
multiImage: {
|
|
1387
|
+
images: input.mediaUrns.map((urn) => ({ id: urn }))
|
|
1388
|
+
}
|
|
1389
|
+
},
|
|
1390
|
+
lifecycleState: "PUBLISHED"
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
static async schedulePost(input) {
|
|
1395
|
+
validatePlatformInput("linkedin", "schedulePost", input);
|
|
1396
|
+
return scheduleTask({
|
|
1397
|
+
id: `linkedin-schedule-${Date.now()}`,
|
|
1398
|
+
runAt: input.publishAt,
|
|
1399
|
+
task: async () => _LinkedIn.createTextPost({ author: input.author, text: input.text })
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
static async commentOnPost(input) {
|
|
1403
|
+
validatePlatformInput("linkedin", "commentOnPost", input);
|
|
1404
|
+
const actor = authorOrThrow(input.actor);
|
|
1405
|
+
return linkedInRequest({
|
|
1406
|
+
endpoint: "/socialActions/comments",
|
|
1407
|
+
method: "POST",
|
|
1408
|
+
data: {
|
|
1409
|
+
actor,
|
|
1410
|
+
object: input.objectUrn,
|
|
1411
|
+
message: { text: input.message }
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
static async replyToComment(input) {
|
|
1416
|
+
validatePlatformInput("linkedin", "replyToComment", input);
|
|
1417
|
+
const actor = authorOrThrow(input.actor);
|
|
1418
|
+
return linkedInRequest({
|
|
1419
|
+
endpoint: "/socialActions/comments",
|
|
1420
|
+
method: "POST",
|
|
1421
|
+
data: {
|
|
1422
|
+
actor,
|
|
1423
|
+
object: input.parentCommentUrn,
|
|
1424
|
+
message: { text: input.message }
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
static async deleteComment(input) {
|
|
1429
|
+
validatePlatformInput("linkedin", "deleteComment", input);
|
|
1430
|
+
return linkedInRequest({
|
|
1431
|
+
endpoint: `/socialActions/comments/${encodeURIComponent(input.encodedCommentUrn)}`,
|
|
1432
|
+
method: "DELETE"
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
static async likePost(input) {
|
|
1436
|
+
validatePlatformInput("linkedin", "likePost", input);
|
|
1437
|
+
const actor = authorOrThrow(input.actor);
|
|
1438
|
+
return linkedInRequest({
|
|
1439
|
+
endpoint: "/socialActions/likes",
|
|
1440
|
+
method: "POST",
|
|
1441
|
+
data: { actor, object: input.objectUrn }
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
static async unlikePost(input) {
|
|
1445
|
+
validatePlatformInput("linkedin", "unlikePost", input);
|
|
1446
|
+
const actorUrn = authorOrThrow(input.actorUrn);
|
|
1447
|
+
const encodedActor = encodeURIComponent(actorUrn);
|
|
1448
|
+
return linkedInRequest({
|
|
1449
|
+
endpoint: `/socialActions/${encodeURIComponent(input.encodedObjectUrn)}/likes/${encodedActor}`,
|
|
1450
|
+
method: "DELETE"
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
static async sendDirectMessage(input) {
|
|
1454
|
+
validatePlatformInput("linkedin", "sendDirectMessage", input);
|
|
1455
|
+
const actor = authorOrThrow(input.actor);
|
|
1456
|
+
return linkedInRequest({
|
|
1457
|
+
endpoint: "/messages",
|
|
1458
|
+
method: "POST",
|
|
1459
|
+
data: {
|
|
1460
|
+
from: actor,
|
|
1461
|
+
recipients: [input.recipientUrn],
|
|
1462
|
+
body: input.text
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
static async getPostAnalytics(input) {
|
|
1467
|
+
validatePlatformInput("linkedin", "getPostAnalytics", input);
|
|
1468
|
+
return linkedInRequest({
|
|
1469
|
+
endpoint: "/organizationalEntityShareStatistics",
|
|
1470
|
+
method: "GET",
|
|
1471
|
+
query: {
|
|
1472
|
+
q: "organizationalEntity",
|
|
1473
|
+
organizationalEntity: input.postUrn
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
static async getOrganizationAnalytics(input) {
|
|
1478
|
+
validatePlatformInput("linkedin", "getOrganizationAnalytics", input);
|
|
1479
|
+
const orgUrn = input.orgUrn ?? env.linkedin.orgUrn;
|
|
1480
|
+
if (!orgUrn) {
|
|
1481
|
+
throw new SocialError({
|
|
1482
|
+
platform: "linkedin",
|
|
1483
|
+
endpoint: "organization-analytics",
|
|
1484
|
+
message: "Missing LINKEDIN_ORG_URN or orgUrn input."
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
return linkedInRequest({
|
|
1488
|
+
endpoint: "/organizationalEntityFollowerStatistics",
|
|
1489
|
+
method: "GET",
|
|
1490
|
+
query: { q: "organizationalEntity", organizationalEntity: orgUrn }
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
static async registerUpload(input) {
|
|
1494
|
+
validatePlatformInput("linkedin", "registerUpload", input);
|
|
1495
|
+
const owner = authorOrThrow(input.owner);
|
|
1496
|
+
return linkedInRequest({
|
|
1497
|
+
endpoint: "/assets?action=registerUpload",
|
|
1498
|
+
method: "POST",
|
|
1499
|
+
data: {
|
|
1500
|
+
registerUploadRequest: {
|
|
1501
|
+
recipes: input.mediaType === "image" ? ["urn:li:digitalmediaRecipe:feedshare-image"] : ["urn:li:digitalmediaRecipe:feedshare-video"],
|
|
1502
|
+
owner,
|
|
1503
|
+
serviceRelationships: [
|
|
1504
|
+
{
|
|
1505
|
+
relationshipType: "OWNER",
|
|
1506
|
+
identifier: "urn:li:userGeneratedContent"
|
|
1507
|
+
}
|
|
1508
|
+
],
|
|
1509
|
+
supportedUploadMechanism: ["SYNCHRONOUS_UPLOAD"]
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
static async uploadBinary(input) {
|
|
1515
|
+
validatePlatformInput("linkedin", "uploadBinary", input);
|
|
1516
|
+
const { fileSize } = getFileMeta(input.mediaPath);
|
|
1517
|
+
await getLinkedInAccessToken();
|
|
1518
|
+
return withRetries({
|
|
1519
|
+
platform: "linkedin",
|
|
1520
|
+
endpoint: "uploadBinary",
|
|
1521
|
+
execute: async () => axios2.put(input.uploadUrl, createUploadStream(input.mediaPath), {
|
|
1522
|
+
maxBodyLength: Infinity,
|
|
1523
|
+
headers: {
|
|
1524
|
+
Authorization: getLinkedInHeaders().Authorization,
|
|
1525
|
+
"Content-Length": String(fileSize)
|
|
1526
|
+
}
|
|
1527
|
+
})
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
static async deletePost(input) {
|
|
1531
|
+
validatePlatformInput("linkedin", "deletePost", input);
|
|
1532
|
+
return linkedInRequest({
|
|
1533
|
+
endpoint: `/posts/${encodeURIComponent(input.encodedPostUrn)}`,
|
|
1534
|
+
method: "DELETE"
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
// src/platforms/youtube.ts
|
|
1540
|
+
import axios3 from "axios";
|
|
1541
|
+
var YT_API_BASE = "https://www.googleapis.com/youtube/v3";
|
|
1542
|
+
var YT_UPLOAD_BASE = "https://www.googleapis.com/upload/youtube/v3";
|
|
1543
|
+
var YT_ANALYTICS_BASE = "https://youtubeanalytics.googleapis.com/v2";
|
|
1544
|
+
function authHeader() {
|
|
1545
|
+
if (!env.youtube.accessToken) {
|
|
1546
|
+
throw new SocialError({
|
|
1547
|
+
platform: "youtube",
|
|
1548
|
+
endpoint: "auth",
|
|
1549
|
+
message: "Missing YOUTUBE_ACCESS_TOKEN."
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
return { Authorization: `Bearer ${env.youtube.accessToken}` };
|
|
1553
|
+
}
|
|
1554
|
+
async function youtubeRequest(params) {
|
|
1555
|
+
return withRetries({
|
|
1556
|
+
platform: "youtube",
|
|
1557
|
+
endpoint: params.endpoint,
|
|
1558
|
+
execute: async () => {
|
|
1559
|
+
try {
|
|
1560
|
+
const response = await axios3.request({
|
|
1561
|
+
baseURL: params.upload ? YT_UPLOAD_BASE : YT_API_BASE,
|
|
1562
|
+
url: params.endpoint,
|
|
1563
|
+
method: params.method,
|
|
1564
|
+
params: params.query,
|
|
1565
|
+
data: params.data,
|
|
1566
|
+
headers: {
|
|
1567
|
+
...authHeader(),
|
|
1568
|
+
"Content-Type": "application/json"
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
return response.data;
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
throw SocialError.normalize({
|
|
1574
|
+
platform: "youtube",
|
|
1575
|
+
endpoint: params.endpoint,
|
|
1576
|
+
error
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
var YouTube = class _YouTube {
|
|
1583
|
+
static async createVideoUploadSession(input) {
|
|
1584
|
+
validatePlatformInput("youtube", "createVideoUploadSession", input);
|
|
1585
|
+
return youtubeRequest({
|
|
1586
|
+
endpoint: "/videos",
|
|
1587
|
+
method: "POST",
|
|
1588
|
+
query: { part: "snippet,status", uploadType: "resumable" },
|
|
1589
|
+
data: {
|
|
1590
|
+
snippet: {
|
|
1591
|
+
title: input.title,
|
|
1592
|
+
description: input.description ?? ""
|
|
1593
|
+
},
|
|
1594
|
+
status: { privacyStatus: input.privacyStatus ?? "private" }
|
|
1595
|
+
},
|
|
1596
|
+
upload: true
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
static async uploadBinary(input) {
|
|
1600
|
+
validatePlatformInput("youtube", "uploadBinary", input);
|
|
1601
|
+
const { fileSize } = getFileMeta(input.mediaPath);
|
|
1602
|
+
return withRetries({
|
|
1603
|
+
platform: "youtube",
|
|
1604
|
+
endpoint: "uploadBinary",
|
|
1605
|
+
execute: async () => axios3.put(input.uploadUrl, createUploadStream(input.mediaPath), {
|
|
1606
|
+
maxBodyLength: Infinity,
|
|
1607
|
+
headers: {
|
|
1608
|
+
...authHeader(),
|
|
1609
|
+
"Content-Length": String(fileSize),
|
|
1610
|
+
"Content-Type": "application/octet-stream"
|
|
1611
|
+
}
|
|
1612
|
+
})
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
static async listMyVideos(input) {
|
|
1616
|
+
validatePlatformInput("youtube", "listMyVideos", input);
|
|
1617
|
+
return youtubeRequest({
|
|
1618
|
+
endpoint: "/search",
|
|
1619
|
+
method: "GET",
|
|
1620
|
+
query: {
|
|
1621
|
+
part: "snippet",
|
|
1622
|
+
forMine: true,
|
|
1623
|
+
type: "video",
|
|
1624
|
+
maxResults: input.maxResults ?? 25
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
static async updateVideoMetadata(input) {
|
|
1629
|
+
validatePlatformInput("youtube", "updateVideoMetadata", input);
|
|
1630
|
+
return youtubeRequest({
|
|
1631
|
+
endpoint: "/videos",
|
|
1632
|
+
method: "PUT",
|
|
1633
|
+
query: { part: "snippet,status" },
|
|
1634
|
+
data: {
|
|
1635
|
+
id: input.videoId,
|
|
1636
|
+
snippet: {
|
|
1637
|
+
title: input.title,
|
|
1638
|
+
description: input.description
|
|
1639
|
+
},
|
|
1640
|
+
status: {
|
|
1641
|
+
privacyStatus: input.privacyStatus
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
static async deleteVideo(input) {
|
|
1647
|
+
validatePlatformInput("youtube", "deleteVideo", input);
|
|
1648
|
+
return youtubeRequest({
|
|
1649
|
+
endpoint: "/videos",
|
|
1650
|
+
method: "DELETE",
|
|
1651
|
+
query: { id: input.videoId }
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
static async commentOnVideo(input) {
|
|
1655
|
+
validatePlatformInput("youtube", "commentOnVideo", input);
|
|
1656
|
+
return youtubeRequest({
|
|
1657
|
+
endpoint: "/commentThreads",
|
|
1658
|
+
method: "POST",
|
|
1659
|
+
query: { part: "snippet" },
|
|
1660
|
+
data: {
|
|
1661
|
+
snippet: {
|
|
1662
|
+
videoId: input.videoId,
|
|
1663
|
+
topLevelComment: {
|
|
1664
|
+
snippet: { textOriginal: input.text }
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
static async replyToComment(input) {
|
|
1671
|
+
validatePlatformInput("youtube", "replyToComment", input);
|
|
1672
|
+
return youtubeRequest({
|
|
1673
|
+
endpoint: "/comments",
|
|
1674
|
+
method: "POST",
|
|
1675
|
+
query: { part: "snippet" },
|
|
1676
|
+
data: {
|
|
1677
|
+
snippet: {
|
|
1678
|
+
parentId: input.parentCommentId,
|
|
1679
|
+
textOriginal: input.text
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
static async likeVideo(input) {
|
|
1685
|
+
validatePlatformInput("youtube", "likeVideo", input);
|
|
1686
|
+
return youtubeRequest({
|
|
1687
|
+
endpoint: "/videos/rate",
|
|
1688
|
+
method: "POST",
|
|
1689
|
+
query: { id: input.videoId, rating: "like" }
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
static async unlikeVideo(input) {
|
|
1693
|
+
validatePlatformInput("youtube", "unlikeVideo", input);
|
|
1694
|
+
return youtubeRequest({
|
|
1695
|
+
endpoint: "/videos/rate",
|
|
1696
|
+
method: "POST",
|
|
1697
|
+
query: { id: input.videoId, rating: "none" }
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
static async createPlaylist(input) {
|
|
1701
|
+
validatePlatformInput("youtube", "createPlaylist", input);
|
|
1702
|
+
return youtubeRequest({
|
|
1703
|
+
endpoint: "/playlists",
|
|
1704
|
+
method: "POST",
|
|
1705
|
+
query: { part: "snippet,status" },
|
|
1706
|
+
data: {
|
|
1707
|
+
snippet: {
|
|
1708
|
+
title: input.title,
|
|
1709
|
+
description: input.description ?? ""
|
|
1710
|
+
},
|
|
1711
|
+
status: { privacyStatus: input.privacyStatus ?? "private" }
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
static async addVideoToPlaylist(input) {
|
|
1716
|
+
validatePlatformInput("youtube", "addVideoToPlaylist", input);
|
|
1717
|
+
return youtubeRequest({
|
|
1718
|
+
endpoint: "/playlistItems",
|
|
1719
|
+
method: "POST",
|
|
1720
|
+
query: { part: "snippet" },
|
|
1721
|
+
data: {
|
|
1722
|
+
snippet: {
|
|
1723
|
+
playlistId: input.playlistId,
|
|
1724
|
+
resourceId: {
|
|
1725
|
+
kind: "youtube#video",
|
|
1726
|
+
videoId: input.videoId
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
static async getChannelAnalytics(input) {
|
|
1733
|
+
validatePlatformInput("youtube", "getChannelAnalytics", input);
|
|
1734
|
+
return withRetries({
|
|
1735
|
+
platform: "youtube",
|
|
1736
|
+
endpoint: "/reports",
|
|
1737
|
+
execute: async () => {
|
|
1738
|
+
const response = await axios3.get(`${YT_ANALYTICS_BASE}/reports`, {
|
|
1739
|
+
params: {
|
|
1740
|
+
ids: `channel==${env.youtube.channelId}`,
|
|
1741
|
+
startDate: input.startDate,
|
|
1742
|
+
endDate: input.endDate,
|
|
1743
|
+
metrics: input.metrics ?? "views,likes,comments,estimatedMinutesWatched"
|
|
1744
|
+
},
|
|
1745
|
+
headers: authHeader()
|
|
1746
|
+
});
|
|
1747
|
+
return response.data;
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
static async scheduleVideoMetadataUpdate(input) {
|
|
1752
|
+
validatePlatformInput("youtube", "scheduleVideoMetadataUpdate", input);
|
|
1753
|
+
return scheduleTask({
|
|
1754
|
+
id: `youtube-schedule-${Date.now()}`,
|
|
1755
|
+
runAt: input.publishAt,
|
|
1756
|
+
task: async () => _YouTube.updateVideoMetadata({
|
|
1757
|
+
videoId: input.videoId,
|
|
1758
|
+
title: input.title,
|
|
1759
|
+
description: input.description,
|
|
1760
|
+
privacyStatus: input.privacyStatus
|
|
1761
|
+
})
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
// src/platforms/tiktok.ts
|
|
1767
|
+
import axios4 from "axios";
|
|
1768
|
+
var TIKTOK_API_BASE = "https://open.tiktokapis.com/v2";
|
|
1769
|
+
function tiktokHeaders() {
|
|
1770
|
+
if (!env.tiktok.accessToken) {
|
|
1771
|
+
throw new SocialError({
|
|
1772
|
+
platform: "tiktok",
|
|
1773
|
+
endpoint: "auth",
|
|
1774
|
+
message: "Missing TIKTOK_ACCESS_TOKEN."
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
return {
|
|
1778
|
+
Authorization: `Bearer ${env.tiktok.accessToken}`,
|
|
1779
|
+
"Content-Type": "application/json"
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
async function tikTokRequest(params) {
|
|
1783
|
+
return withRetries({
|
|
1784
|
+
platform: "tiktok",
|
|
1785
|
+
endpoint: params.endpoint,
|
|
1786
|
+
execute: async () => {
|
|
1787
|
+
try {
|
|
1788
|
+
const response = await axios4.request({
|
|
1789
|
+
baseURL: TIKTOK_API_BASE,
|
|
1790
|
+
url: params.endpoint,
|
|
1791
|
+
method: params.method,
|
|
1792
|
+
params: params.query,
|
|
1793
|
+
data: params.data,
|
|
1794
|
+
headers: tiktokHeaders()
|
|
1795
|
+
});
|
|
1796
|
+
return response.data;
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
throw SocialError.normalize({
|
|
1799
|
+
platform: "tiktok",
|
|
1800
|
+
endpoint: params.endpoint,
|
|
1801
|
+
error
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
var TikTok = class _TikTok {
|
|
1808
|
+
static async createPost(input) {
|
|
1809
|
+
validatePlatformInput("tiktok", "createPost", input);
|
|
1810
|
+
return tikTokRequest({
|
|
1811
|
+
endpoint: "/post/publish/inbox/video/init/",
|
|
1812
|
+
method: "POST",
|
|
1813
|
+
data: {
|
|
1814
|
+
post_info: {
|
|
1815
|
+
title: input.text,
|
|
1816
|
+
privacy_level: input.visibility ?? "PUBLIC_TO_EVERYONE"
|
|
1817
|
+
},
|
|
1818
|
+
source_info: {
|
|
1819
|
+
source: "PULL_FROM_URL",
|
|
1820
|
+
video_url: ""
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
static async createVideoPost(input) {
|
|
1826
|
+
validatePlatformInput("tiktok", "createVideoPost", input);
|
|
1827
|
+
return tikTokRequest({
|
|
1828
|
+
endpoint: "/post/publish/video/init/",
|
|
1829
|
+
method: "POST",
|
|
1830
|
+
data: {
|
|
1831
|
+
post_info: {
|
|
1832
|
+
title: input.title,
|
|
1833
|
+
privacy_level: input.visibility ?? "PUBLIC_TO_EVERYONE"
|
|
1834
|
+
},
|
|
1835
|
+
source_info: {
|
|
1836
|
+
source: "PULL_FROM_URL",
|
|
1837
|
+
video_url: input.videoUrl
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
static async getPostStatus(input) {
|
|
1843
|
+
validatePlatformInput("tiktok", "getPostStatus", input);
|
|
1844
|
+
return tikTokRequest({
|
|
1845
|
+
endpoint: "/post/publish/status/fetch/",
|
|
1846
|
+
method: "POST",
|
|
1847
|
+
data: { publish_id: input.publishId }
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
static async listVideos(input) {
|
|
1851
|
+
validatePlatformInput("tiktok", "listVideos", input);
|
|
1852
|
+
return tikTokRequest({
|
|
1853
|
+
endpoint: "/video/list/",
|
|
1854
|
+
method: "POST",
|
|
1855
|
+
data: { max_count: input.maxCount ?? 20 }
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
static async deleteVideo(input) {
|
|
1859
|
+
validatePlatformInput("tiktok", "deleteVideo", input);
|
|
1860
|
+
return tikTokRequest({
|
|
1861
|
+
endpoint: "/video/delete/",
|
|
1862
|
+
method: "POST",
|
|
1863
|
+
data: { video_id: input.videoId }
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
static async commentOnVideo(input) {
|
|
1867
|
+
validatePlatformInput("tiktok", "commentOnVideo", input);
|
|
1868
|
+
return tikTokRequest({
|
|
1869
|
+
endpoint: "/video/comment/create/",
|
|
1870
|
+
method: "POST",
|
|
1871
|
+
data: { video_id: input.videoId, text: input.text }
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
static async replyToComment(input) {
|
|
1875
|
+
validatePlatformInput("tiktok", "replyToComment", input);
|
|
1876
|
+
return tikTokRequest({
|
|
1877
|
+
endpoint: "/video/comment/reply/",
|
|
1878
|
+
method: "POST",
|
|
1879
|
+
data: { comment_id: input.commentId, text: input.text }
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
static async likeVideo(input) {
|
|
1883
|
+
validatePlatformInput("tiktok", "likeVideo", input);
|
|
1884
|
+
return tikTokRequest({
|
|
1885
|
+
endpoint: "/video/like/",
|
|
1886
|
+
method: "POST",
|
|
1887
|
+
data: { video_id: input.videoId }
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
static async unlikeVideo(input) {
|
|
1891
|
+
validatePlatformInput("tiktok", "unlikeVideo", input);
|
|
1892
|
+
return tikTokRequest({
|
|
1893
|
+
endpoint: "/video/unlike/",
|
|
1894
|
+
method: "POST",
|
|
1895
|
+
data: { video_id: input.videoId }
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
static async getVideoAnalytics(input) {
|
|
1899
|
+
validatePlatformInput("tiktok", "getVideoAnalytics", input);
|
|
1900
|
+
return tikTokRequest({
|
|
1901
|
+
endpoint: "/research/video/query/",
|
|
1902
|
+
method: "POST",
|
|
1903
|
+
data: { filters: { video_ids: input.videoIds } }
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
static async getProfileAnalytics(input) {
|
|
1907
|
+
validatePlatformInput("tiktok", "getProfileAnalytics", input);
|
|
1908
|
+
return tikTokRequest({
|
|
1909
|
+
endpoint: "/user/info/",
|
|
1910
|
+
method: "GET",
|
|
1911
|
+
query: {
|
|
1912
|
+
fields: (input.fields ?? ["display_name", "follower_count", "video_count"]).join(",")
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
static async scheduleVideoPost(input) {
|
|
1917
|
+
validatePlatformInput("tiktok", "scheduleVideoPost", input);
|
|
1918
|
+
return scheduleTask({
|
|
1919
|
+
id: `tiktok-schedule-${Date.now()}`,
|
|
1920
|
+
runAt: input.publishAt,
|
|
1921
|
+
task: async () => _TikTok.createVideoPost({
|
|
1922
|
+
title: input.title,
|
|
1923
|
+
videoUrl: input.videoUrl
|
|
1924
|
+
})
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1929
|
+
// src/platforms/pinterest.ts
|
|
1930
|
+
import axios5 from "axios";
|
|
1931
|
+
var PINTEREST_BASE = "https://api.pinterest.com/v5";
|
|
1932
|
+
function pinterestHeaders() {
|
|
1933
|
+
if (!env.pinterest.accessToken) {
|
|
1934
|
+
throw new SocialError({
|
|
1935
|
+
platform: "pinterest",
|
|
1936
|
+
endpoint: "auth",
|
|
1937
|
+
message: "Missing PINTEREST_ACCESS_TOKEN."
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
return {
|
|
1941
|
+
Authorization: `Bearer ${env.pinterest.accessToken}`,
|
|
1942
|
+
"Content-Type": "application/json"
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
async function pinterestRequest(params) {
|
|
1946
|
+
return withRetries({
|
|
1947
|
+
platform: "pinterest",
|
|
1948
|
+
endpoint: params.endpoint,
|
|
1949
|
+
execute: async () => {
|
|
1950
|
+
try {
|
|
1951
|
+
const response = await axios5.request({
|
|
1952
|
+
baseURL: PINTEREST_BASE,
|
|
1953
|
+
url: params.endpoint,
|
|
1954
|
+
method: params.method,
|
|
1955
|
+
params: params.query,
|
|
1956
|
+
data: params.data,
|
|
1957
|
+
headers: pinterestHeaders()
|
|
1958
|
+
});
|
|
1959
|
+
return response.data;
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
throw SocialError.normalize({
|
|
1962
|
+
platform: "pinterest",
|
|
1963
|
+
endpoint: params.endpoint,
|
|
1964
|
+
error
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
var Pinterest = class _Pinterest {
|
|
1971
|
+
static async createPin(input) {
|
|
1972
|
+
validatePlatformInput("pinterest", "createPin", input);
|
|
1973
|
+
return pinterestRequest({
|
|
1974
|
+
endpoint: "/pins",
|
|
1975
|
+
method: "POST",
|
|
1976
|
+
data: {
|
|
1977
|
+
board_id: input.boardId ?? env.pinterest.boardId,
|
|
1978
|
+
title: input.title,
|
|
1979
|
+
description: input.description,
|
|
1980
|
+
link: input.link,
|
|
1981
|
+
media_source: {
|
|
1982
|
+
source_type: "image_url",
|
|
1983
|
+
url: input.mediaSourceUrl
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
static async createVideoPin(input) {
|
|
1989
|
+
validatePlatformInput("pinterest", "createVideoPin", input);
|
|
1990
|
+
return pinterestRequest({
|
|
1991
|
+
endpoint: "/pins",
|
|
1992
|
+
method: "POST",
|
|
1993
|
+
data: {
|
|
1994
|
+
board_id: input.boardId ?? env.pinterest.boardId,
|
|
1995
|
+
title: input.title,
|
|
1996
|
+
description: input.description,
|
|
1997
|
+
media_source: {
|
|
1998
|
+
source_type: "video_url",
|
|
1999
|
+
url: input.mediaSourceUrl
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
static async updatePin(input) {
|
|
2005
|
+
validatePlatformInput("pinterest", "updatePin", input);
|
|
2006
|
+
return pinterestRequest({
|
|
2007
|
+
endpoint: `/pins/${input.pinId}`,
|
|
2008
|
+
method: "PATCH",
|
|
2009
|
+
data: {
|
|
2010
|
+
title: input.title,
|
|
2011
|
+
description: input.description,
|
|
2012
|
+
link: input.link
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
static async deletePin(input) {
|
|
2017
|
+
validatePlatformInput("pinterest", "deletePin", input);
|
|
2018
|
+
return pinterestRequest({
|
|
2019
|
+
endpoint: `/pins/${input.pinId}`,
|
|
2020
|
+
method: "DELETE"
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
static async listPins(input) {
|
|
2024
|
+
validatePlatformInput("pinterest", "listPins", input);
|
|
2025
|
+
return pinterestRequest({
|
|
2026
|
+
endpoint: "/pins",
|
|
2027
|
+
method: "GET",
|
|
2028
|
+
query: {
|
|
2029
|
+
board_id: input.boardId ?? env.pinterest.boardId,
|
|
2030
|
+
page_size: input.pageSize ?? 25
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
static async createBoard(input) {
|
|
2035
|
+
validatePlatformInput("pinterest", "createBoard", input);
|
|
2036
|
+
return pinterestRequest({
|
|
2037
|
+
endpoint: "/boards",
|
|
2038
|
+
method: "POST",
|
|
2039
|
+
data: {
|
|
2040
|
+
name: input.name,
|
|
2041
|
+
description: input.description,
|
|
2042
|
+
privacy: input.privacy ?? "PUBLIC"
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
static async listBoards(input) {
|
|
2047
|
+
validatePlatformInput("pinterest", "listBoards", input);
|
|
2048
|
+
return pinterestRequest({
|
|
2049
|
+
endpoint: "/boards",
|
|
2050
|
+
method: "GET",
|
|
2051
|
+
query: { page_size: input.pageSize ?? 25 }
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
static async commentOnPin(input) {
|
|
2055
|
+
validatePlatformInput("pinterest", "commentOnPin", input);
|
|
2056
|
+
return pinterestRequest({
|
|
2057
|
+
endpoint: `/pins/${input.pinId}/comments`,
|
|
2058
|
+
method: "POST",
|
|
2059
|
+
data: { text: input.text }
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
static async replyToComment(input) {
|
|
2063
|
+
validatePlatformInput("pinterest", "replyToComment", input);
|
|
2064
|
+
return pinterestRequest({
|
|
2065
|
+
endpoint: `/pins/${input.pinId}/comments/${input.commentId}/replies`,
|
|
2066
|
+
method: "POST",
|
|
2067
|
+
data: { text: input.text }
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
static async getPinAnalytics(input) {
|
|
2071
|
+
validatePlatformInput("pinterest", "getPinAnalytics", input);
|
|
2072
|
+
return pinterestRequest({
|
|
2073
|
+
endpoint: `/pins/${input.pinId}/analytics`,
|
|
2074
|
+
method: "GET",
|
|
2075
|
+
query: { start_date: input.startDate, end_date: input.endDate }
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
static async getAccountAnalytics(input) {
|
|
2079
|
+
validatePlatformInput("pinterest", "getAccountAnalytics", input);
|
|
2080
|
+
return pinterestRequest({
|
|
2081
|
+
endpoint: "/user_account/analytics",
|
|
2082
|
+
method: "GET",
|
|
2083
|
+
query: { start_date: input.startDate, end_date: input.endDate }
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
static async schedulePin(input) {
|
|
2087
|
+
validatePlatformInput("pinterest", "schedulePin", input);
|
|
2088
|
+
return scheduleTask({
|
|
2089
|
+
id: `pinterest-schedule-${Date.now()}`,
|
|
2090
|
+
runAt: input.publishAt,
|
|
2091
|
+
task: async () => _Pinterest.createPin({
|
|
2092
|
+
title: input.title,
|
|
2093
|
+
mediaSourceUrl: input.mediaSourceUrl,
|
|
2094
|
+
boardId: input.boardId
|
|
2095
|
+
})
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
|
|
2100
|
+
// src/platforms/bluesky.ts
|
|
2101
|
+
import axios6 from "axios";
|
|
2102
|
+
var DEFAULT_SERVICE = "https://bsky.social";
|
|
2103
|
+
function serviceBase() {
|
|
2104
|
+
return `${env.bluesky.serviceUrl || DEFAULT_SERVICE}/xrpc`;
|
|
2105
|
+
}
|
|
2106
|
+
var accessJwt = env.bluesky.accessJwt;
|
|
2107
|
+
var did = "";
|
|
2108
|
+
async function ensureSession() {
|
|
2109
|
+
if (accessJwt && did) {
|
|
2110
|
+
return { accessJwt, did };
|
|
2111
|
+
}
|
|
2112
|
+
if (!env.bluesky.identifier || !env.bluesky.appPassword) {
|
|
2113
|
+
throw new SocialError({
|
|
2114
|
+
platform: "bluesky",
|
|
2115
|
+
endpoint: "createSession",
|
|
2116
|
+
message: "Missing Bluesky session credentials. Set BLUESKY_IDENTIFIER and BLUESKY_APP_PASSWORD, or provide BLUESKY_ACCESS_JWT."
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
const response = await axios6.post(`${serviceBase()}/com.atproto.server.createSession`, {
|
|
2120
|
+
identifier: env.bluesky.identifier,
|
|
2121
|
+
password: env.bluesky.appPassword
|
|
2122
|
+
});
|
|
2123
|
+
accessJwt = response.data.accessJwt;
|
|
2124
|
+
did = response.data.did;
|
|
2125
|
+
return { accessJwt, did };
|
|
2126
|
+
}
|
|
2127
|
+
async function bskyRequest(params) {
|
|
2128
|
+
const session = await ensureSession();
|
|
2129
|
+
return withRetries({
|
|
2130
|
+
platform: "bluesky",
|
|
2131
|
+
endpoint: params.endpoint,
|
|
2132
|
+
execute: async () => {
|
|
2133
|
+
try {
|
|
2134
|
+
const response = await axios6.request({
|
|
2135
|
+
baseURL: serviceBase(),
|
|
2136
|
+
url: params.endpoint,
|
|
2137
|
+
method: params.method,
|
|
2138
|
+
params: params.query,
|
|
2139
|
+
data: params.data,
|
|
2140
|
+
headers: {
|
|
2141
|
+
Authorization: `Bearer ${session.accessJwt}`,
|
|
2142
|
+
"Content-Type": "application/json"
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
return response.data;
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
throw SocialError.normalize({
|
|
2148
|
+
platform: "bluesky",
|
|
2149
|
+
endpoint: params.endpoint,
|
|
2150
|
+
error
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
async function createRecord(collection, record) {
|
|
2157
|
+
const session = await ensureSession();
|
|
2158
|
+
return bskyRequest({
|
|
2159
|
+
endpoint: "/com.atproto.repo.createRecord",
|
|
2160
|
+
method: "POST",
|
|
2161
|
+
data: {
|
|
2162
|
+
repo: session.did,
|
|
2163
|
+
collection,
|
|
2164
|
+
record
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
var Bluesky = class _Bluesky {
|
|
2169
|
+
static async postText(input) {
|
|
2170
|
+
validatePlatformInput("bluesky", "postText", input);
|
|
2171
|
+
return createRecord("app.bsky.feed.post", {
|
|
2172
|
+
$type: "app.bsky.feed.post",
|
|
2173
|
+
text: input.text,
|
|
2174
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
static async postWithLink(input) {
|
|
2178
|
+
validatePlatformInput("bluesky", "postWithLink", input);
|
|
2179
|
+
return createRecord("app.bsky.feed.post", {
|
|
2180
|
+
$type: "app.bsky.feed.post",
|
|
2181
|
+
text: `${input.text}
|
|
2182
|
+
${input.url}`,
|
|
2183
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
static async replyToPost(input) {
|
|
2187
|
+
validatePlatformInput("bluesky", "replyToPost", input);
|
|
2188
|
+
return createRecord("app.bsky.feed.post", {
|
|
2189
|
+
$type: "app.bsky.feed.post",
|
|
2190
|
+
text: input.text,
|
|
2191
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2192
|
+
reply: {
|
|
2193
|
+
root: { uri: input.rootUri, cid: input.rootCid },
|
|
2194
|
+
parent: { uri: input.parentUri, cid: input.parentCid }
|
|
2195
|
+
}
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
static async likePost(input) {
|
|
2199
|
+
validatePlatformInput("bluesky", "likePost", input);
|
|
2200
|
+
return createRecord("app.bsky.feed.like", {
|
|
2201
|
+
$type: "app.bsky.feed.like",
|
|
2202
|
+
subject: { uri: input.subjectUri, cid: input.subjectCid },
|
|
2203
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
static async repost(input) {
|
|
2207
|
+
validatePlatformInput("bluesky", "repost", input);
|
|
2208
|
+
return createRecord("app.bsky.feed.repost", {
|
|
2209
|
+
$type: "app.bsky.feed.repost",
|
|
2210
|
+
subject: { uri: input.subjectUri, cid: input.subjectCid },
|
|
2211
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
static async deleteRecord(input) {
|
|
2215
|
+
validatePlatformInput("bluesky", "deleteRecord", input);
|
|
2216
|
+
const session = await ensureSession();
|
|
2217
|
+
const parsed = new URL(input.uri.replace("at://", "https://"));
|
|
2218
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
2219
|
+
const [collection, rkey] = parts.slice(-2);
|
|
2220
|
+
return bskyRequest({
|
|
2221
|
+
endpoint: "/com.atproto.repo.deleteRecord",
|
|
2222
|
+
method: "POST",
|
|
2223
|
+
data: {
|
|
2224
|
+
repo: session.did,
|
|
2225
|
+
collection,
|
|
2226
|
+
rkey
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
static async getAuthorFeed(input) {
|
|
2231
|
+
validatePlatformInput("bluesky", "getAuthorFeed", input);
|
|
2232
|
+
return bskyRequest({
|
|
2233
|
+
endpoint: "/app.bsky.feed.getAuthorFeed",
|
|
2234
|
+
method: "GET",
|
|
2235
|
+
query: {
|
|
2236
|
+
actor: input.actorDidOrHandle,
|
|
2237
|
+
limit: input.limit ?? 25
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
static async searchPosts(input) {
|
|
2242
|
+
validatePlatformInput("bluesky", "searchPosts", input);
|
|
2243
|
+
return bskyRequest({
|
|
2244
|
+
endpoint: "/app.bsky.feed.searchPosts",
|
|
2245
|
+
method: "GET",
|
|
2246
|
+
query: { q: input.query, limit: input.limit ?? 25 }
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
static async getPostThread(input) {
|
|
2250
|
+
validatePlatformInput("bluesky", "getPostThread", input);
|
|
2251
|
+
return bskyRequest({
|
|
2252
|
+
endpoint: "/app.bsky.feed.getPostThread",
|
|
2253
|
+
method: "GET",
|
|
2254
|
+
query: { uri: input.uri, depth: input.depth ?? 6 }
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
static async getNotificationFeed(input) {
|
|
2258
|
+
validatePlatformInput("bluesky", "getNotificationFeed", input);
|
|
2259
|
+
return bskyRequest({
|
|
2260
|
+
endpoint: "/app.bsky.notification.listNotifications",
|
|
2261
|
+
method: "GET",
|
|
2262
|
+
query: { limit: input.limit ?? 25 }
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
static async schedulePost(input) {
|
|
2266
|
+
validatePlatformInput("bluesky", "schedulePost", input);
|
|
2267
|
+
return scheduleTask({
|
|
2268
|
+
id: `bluesky-schedule-${Date.now()}`,
|
|
2269
|
+
runAt: input.publishAt,
|
|
2270
|
+
task: async () => _Bluesky.postText({ text: input.text })
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2275
|
+
// src/platforms/mastodon.ts
|
|
2276
|
+
import axios7 from "axios";
|
|
2277
|
+
function baseUrl() {
|
|
2278
|
+
if (!env.mastodon.baseUrl) {
|
|
2279
|
+
throw new SocialError({
|
|
2280
|
+
platform: "mastodon",
|
|
2281
|
+
endpoint: "baseUrl",
|
|
2282
|
+
message: "Missing MASTODON_BASE_URL."
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
return env.mastodon.baseUrl.replace(/\/$/, "");
|
|
2286
|
+
}
|
|
2287
|
+
function mastodonHeaders(extra) {
|
|
2288
|
+
if (!env.mastodon.accessToken) {
|
|
2289
|
+
throw new SocialError({
|
|
2290
|
+
platform: "mastodon",
|
|
2291
|
+
endpoint: "auth",
|
|
2292
|
+
message: "Missing MASTODON_ACCESS_TOKEN."
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
return {
|
|
2296
|
+
Authorization: `Bearer ${env.mastodon.accessToken}`,
|
|
2297
|
+
...extra
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
async function mastodonRequest(params) {
|
|
2301
|
+
return withRetries({
|
|
2302
|
+
platform: "mastodon",
|
|
2303
|
+
endpoint: params.endpoint,
|
|
2304
|
+
execute: async () => {
|
|
2305
|
+
try {
|
|
2306
|
+
const response = await axios7.request({
|
|
2307
|
+
baseURL: `${baseUrl()}/api/v1`,
|
|
2308
|
+
url: params.endpoint,
|
|
2309
|
+
method: params.method,
|
|
2310
|
+
params: params.query,
|
|
2311
|
+
data: params.data,
|
|
2312
|
+
headers: mastodonHeaders(params.headers)
|
|
2313
|
+
});
|
|
2314
|
+
return response.data;
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
throw SocialError.normalize({
|
|
2317
|
+
platform: "mastodon",
|
|
2318
|
+
endpoint: params.endpoint,
|
|
2319
|
+
error
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
var Mastodon = class _Mastodon {
|
|
2326
|
+
static async createStatus(input) {
|
|
2327
|
+
validatePlatformInput("mastodon", "createStatus", input);
|
|
2328
|
+
return mastodonRequest({
|
|
2329
|
+
endpoint: "/statuses",
|
|
2330
|
+
method: "POST",
|
|
2331
|
+
data: {
|
|
2332
|
+
status: input.text,
|
|
2333
|
+
visibility: input.visibility ?? "public"
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
static async uploadMedia(input) {
|
|
2338
|
+
validatePlatformInput("mastodon", "uploadMedia", input);
|
|
2339
|
+
const { fileSize } = getFileMeta(input.mediaPath);
|
|
2340
|
+
return withRetries({
|
|
2341
|
+
platform: "mastodon",
|
|
2342
|
+
endpoint: "/media",
|
|
2343
|
+
execute: async () => axios7.post(`${baseUrl()}/api/v2/media`, createUploadStream(input.mediaPath), {
|
|
2344
|
+
headers: mastodonHeaders({
|
|
2345
|
+
"Content-Length": String(fileSize),
|
|
2346
|
+
"Content-Type": "application/octet-stream"
|
|
2347
|
+
}),
|
|
2348
|
+
maxBodyLength: Infinity
|
|
2349
|
+
})
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
static async createMediaStatus(input) {
|
|
2353
|
+
validatePlatformInput("mastodon", "createMediaStatus", input);
|
|
2354
|
+
return mastodonRequest({
|
|
2355
|
+
endpoint: "/statuses",
|
|
2356
|
+
method: "POST",
|
|
2357
|
+
data: {
|
|
2358
|
+
status: input.text,
|
|
2359
|
+
media_ids: input.mediaIds,
|
|
2360
|
+
visibility: input.visibility ?? "public"
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
static async replyToStatus(input) {
|
|
2365
|
+
validatePlatformInput("mastodon", "replyToStatus", input);
|
|
2366
|
+
return mastodonRequest({
|
|
2367
|
+
endpoint: "/statuses",
|
|
2368
|
+
method: "POST",
|
|
2369
|
+
data: {
|
|
2370
|
+
status: input.text,
|
|
2371
|
+
in_reply_to_id: input.statusId
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
static async deleteStatus(input) {
|
|
2376
|
+
validatePlatformInput("mastodon", "deleteStatus", input);
|
|
2377
|
+
return mastodonRequest({
|
|
2378
|
+
endpoint: `/statuses/${input.statusId}`,
|
|
2379
|
+
method: "DELETE"
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
static async favouriteStatus(input) {
|
|
2383
|
+
validatePlatformInput("mastodon", "favouriteStatus", input);
|
|
2384
|
+
return mastodonRequest({
|
|
2385
|
+
endpoint: `/statuses/${input.statusId}/favourite`,
|
|
2386
|
+
method: "POST"
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
static async unfavouriteStatus(input) {
|
|
2390
|
+
validatePlatformInput("mastodon", "unfavouriteStatus", input);
|
|
2391
|
+
return mastodonRequest({
|
|
2392
|
+
endpoint: `/statuses/${input.statusId}/unfavourite`,
|
|
2393
|
+
method: "POST"
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
static async boostStatus(input) {
|
|
2397
|
+
validatePlatformInput("mastodon", "boostStatus", input);
|
|
2398
|
+
return mastodonRequest({
|
|
2399
|
+
endpoint: `/statuses/${input.statusId}/reblog`,
|
|
2400
|
+
method: "POST"
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
static async unboostStatus(input) {
|
|
2404
|
+
validatePlatformInput("mastodon", "unboostStatus", input);
|
|
2405
|
+
return mastodonRequest({
|
|
2406
|
+
endpoint: `/statuses/${input.statusId}/unreblog`,
|
|
2407
|
+
method: "POST"
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
static async listMyStatuses(input) {
|
|
2411
|
+
validatePlatformInput("mastodon", "listMyStatuses", input);
|
|
2412
|
+
const accountId = env.mastodon.accountId;
|
|
2413
|
+
if (!accountId) {
|
|
2414
|
+
throw new SocialError({
|
|
2415
|
+
platform: "mastodon",
|
|
2416
|
+
endpoint: "accountId",
|
|
2417
|
+
message: "Missing MASTODON_ACCOUNT_ID."
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
return mastodonRequest({
|
|
2421
|
+
endpoint: `/accounts/${accountId}/statuses`,
|
|
2422
|
+
method: "GET",
|
|
2423
|
+
query: { limit: input.limit ?? 20 }
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
static async getStatusContext(input) {
|
|
2427
|
+
validatePlatformInput("mastodon", "getStatusContext", input);
|
|
2428
|
+
return mastodonRequest({
|
|
2429
|
+
endpoint: `/statuses/${input.statusId}/context`,
|
|
2430
|
+
method: "GET"
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
static async getAccountAnalytics(input) {
|
|
2434
|
+
validatePlatformInput("mastodon", "getAccountAnalytics", input);
|
|
2435
|
+
return mastodonRequest({
|
|
2436
|
+
endpoint: "/accounts/verify_credentials",
|
|
2437
|
+
method: "GET",
|
|
2438
|
+
query: { scope: input.instanceScope ?? "day" }
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
static async scheduleStatus(input) {
|
|
2442
|
+
validatePlatformInput("mastodon", "scheduleStatus", input);
|
|
2443
|
+
return scheduleTask({
|
|
2444
|
+
id: `mastodon-schedule-${Date.now()}`,
|
|
2445
|
+
runAt: input.publishAt,
|
|
2446
|
+
task: async () => _Mastodon.createStatus({ text: input.text })
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
};
|
|
2450
|
+
|
|
2451
|
+
// src/platforms/threads.ts
|
|
2452
|
+
import axios8 from "axios";
|
|
2453
|
+
function graphBase() {
|
|
2454
|
+
return `https://graph.facebook.com/${env.meta.graphVersion}`;
|
|
2455
|
+
}
|
|
2456
|
+
function threadsUserIdOrThrow(inputUserId) {
|
|
2457
|
+
const userId = inputUserId ?? env.threads.userId;
|
|
2458
|
+
if (!userId) {
|
|
2459
|
+
throw new SocialError({
|
|
2460
|
+
platform: "threads",
|
|
2461
|
+
endpoint: "threads-user-id",
|
|
2462
|
+
message: "Missing THREADS_USER_ID (or pass threadsUserId)."
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
return userId;
|
|
2466
|
+
}
|
|
2467
|
+
function threadsTokenOrThrow() {
|
|
2468
|
+
const token = env.threads.accessToken || env.meta.igToken;
|
|
2469
|
+
if (!token) {
|
|
2470
|
+
throw new SocialError({
|
|
2471
|
+
platform: "threads",
|
|
2472
|
+
endpoint: "threads-token",
|
|
2473
|
+
message: "Missing THREADS_ACCESS_TOKEN (or IG_ACCESS_TOKEN fallback)."
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
return token;
|
|
2477
|
+
}
|
|
2478
|
+
async function threadsRequest(params) {
|
|
2479
|
+
return withRetries({
|
|
2480
|
+
platform: "threads",
|
|
2481
|
+
endpoint: params.endpoint,
|
|
2482
|
+
execute: async () => {
|
|
2483
|
+
try {
|
|
2484
|
+
const response = await axios8.request({
|
|
2485
|
+
baseURL: graphBase(),
|
|
2486
|
+
url: params.endpoint,
|
|
2487
|
+
method: params.method,
|
|
2488
|
+
params: {
|
|
2489
|
+
...params.query,
|
|
2490
|
+
access_token: threadsTokenOrThrow()
|
|
2491
|
+
},
|
|
2492
|
+
data: params.data
|
|
2493
|
+
});
|
|
2494
|
+
return response.data;
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
throw SocialError.normalize({
|
|
2497
|
+
platform: "threads",
|
|
2498
|
+
endpoint: params.endpoint,
|
|
2499
|
+
error
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
async function createContainer(params) {
|
|
2506
|
+
return threadsRequest({
|
|
2507
|
+
endpoint: `/${params.threadsUserId}/threads`,
|
|
2508
|
+
method: "POST",
|
|
2509
|
+
data: {
|
|
2510
|
+
media_type: params.mediaType ?? "TEXT",
|
|
2511
|
+
text: params.text,
|
|
2512
|
+
image_url: params.imageUrl,
|
|
2513
|
+
video_url: params.videoUrl,
|
|
2514
|
+
reply_to_id: params.replyToId
|
|
2515
|
+
}
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
async function publishContainer2(threadsUserId, creationId) {
|
|
2519
|
+
return threadsRequest({
|
|
2520
|
+
endpoint: `/${threadsUserId}/threads_publish`,
|
|
2521
|
+
method: "POST",
|
|
2522
|
+
data: { creation_id: creationId }
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
var Threads = class _Threads {
|
|
2526
|
+
static async postText(input) {
|
|
2527
|
+
validatePlatformInput("threads", "postText", input);
|
|
2528
|
+
const threadsUserId = threadsUserIdOrThrow(input.threadsUserId);
|
|
2529
|
+
const container = await createContainer({
|
|
2530
|
+
threadsUserId,
|
|
2531
|
+
mediaType: "TEXT",
|
|
2532
|
+
text: input.text
|
|
2533
|
+
});
|
|
2534
|
+
return publishContainer2(threadsUserId, container.id);
|
|
2535
|
+
}
|
|
2536
|
+
static async postImage(input) {
|
|
2537
|
+
validatePlatformInput("threads", "postImage", input);
|
|
2538
|
+
const threadsUserId = threadsUserIdOrThrow(input.threadsUserId);
|
|
2539
|
+
const container = await createContainer({
|
|
2540
|
+
threadsUserId,
|
|
2541
|
+
mediaType: "IMAGE",
|
|
2542
|
+
text: input.text,
|
|
2543
|
+
imageUrl: input.imageUrl
|
|
2544
|
+
});
|
|
2545
|
+
return publishContainer2(threadsUserId, container.id);
|
|
2546
|
+
}
|
|
2547
|
+
static async postVideo(input) {
|
|
2548
|
+
validatePlatformInput("threads", "postVideo", input);
|
|
2549
|
+
const threadsUserId = threadsUserIdOrThrow(input.threadsUserId);
|
|
2550
|
+
const container = await createContainer({
|
|
2551
|
+
threadsUserId,
|
|
2552
|
+
mediaType: "VIDEO",
|
|
2553
|
+
text: input.text,
|
|
2554
|
+
videoUrl: input.videoUrl
|
|
2555
|
+
});
|
|
2556
|
+
return publishContainer2(threadsUserId, container.id);
|
|
2557
|
+
}
|
|
2558
|
+
static async replyToThread(input) {
|
|
2559
|
+
validatePlatformInput("threads", "replyToThread", input);
|
|
2560
|
+
const threadsUserId = threadsUserIdOrThrow(input.threadsUserId);
|
|
2561
|
+
const container = await createContainer({
|
|
2562
|
+
threadsUserId,
|
|
2563
|
+
mediaType: "TEXT",
|
|
2564
|
+
text: input.text,
|
|
2565
|
+
replyToId: input.threadId
|
|
2566
|
+
});
|
|
2567
|
+
return publishContainer2(threadsUserId, container.id);
|
|
2568
|
+
}
|
|
2569
|
+
static async deleteThread(input) {
|
|
2570
|
+
validatePlatformInput("threads", "deleteThread", input);
|
|
2571
|
+
return threadsRequest({
|
|
2572
|
+
endpoint: `/${input.threadId}`,
|
|
2573
|
+
method: "DELETE"
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
static async getThread(input) {
|
|
2577
|
+
validatePlatformInput("threads", "getThread", input);
|
|
2578
|
+
return threadsRequest({
|
|
2579
|
+
endpoint: `/${input.threadId}`,
|
|
2580
|
+
method: "GET",
|
|
2581
|
+
query: {
|
|
2582
|
+
fields: (input.fields ?? ["id", "text", "timestamp", "permalink"]).join(",")
|
|
2583
|
+
}
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
static async listMyThreads(input) {
|
|
2587
|
+
validatePlatformInput("threads", "listMyThreads", input);
|
|
2588
|
+
const threadsUserId = threadsUserIdOrThrow(input.threadsUserId);
|
|
2589
|
+
return threadsRequest({
|
|
2590
|
+
endpoint: `/${threadsUserId}/threads`,
|
|
2591
|
+
method: "GET",
|
|
2592
|
+
query: { limit: input.limit ?? 25 }
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
static async getThreadInsights(input) {
|
|
2596
|
+
validatePlatformInput("threads", "getThreadInsights", input);
|
|
2597
|
+
return threadsRequest({
|
|
2598
|
+
endpoint: `/${input.threadId}/insights`,
|
|
2599
|
+
method: "GET",
|
|
2600
|
+
query: {
|
|
2601
|
+
metric: (input.metrics ?? ["views", "likes", "replies", "reposts"]).join(",")
|
|
2602
|
+
}
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
static async getAccountInsights(input) {
|
|
2606
|
+
validatePlatformInput("threads", "getAccountInsights", input);
|
|
2607
|
+
const threadsUserId = threadsUserIdOrThrow(input.threadsUserId);
|
|
2608
|
+
return threadsRequest({
|
|
2609
|
+
endpoint: `/${threadsUserId}/threads_insights`,
|
|
2610
|
+
method: "GET",
|
|
2611
|
+
query: {
|
|
2612
|
+
metric: (input.metrics ?? ["views", "followers_count", "likes"]).join(","),
|
|
2613
|
+
period: input.period ?? "day"
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
static async likeThread(input) {
|
|
2618
|
+
validatePlatformInput("threads", "likeThread", input);
|
|
2619
|
+
return threadsRequest({
|
|
2620
|
+
endpoint: `/${input.threadId}/likes`,
|
|
2621
|
+
method: "POST"
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
static async unlikeThread(input) {
|
|
2625
|
+
validatePlatformInput("threads", "unlikeThread", input);
|
|
2626
|
+
return threadsRequest({
|
|
2627
|
+
endpoint: `/${input.threadId}/likes`,
|
|
2628
|
+
method: "DELETE"
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
static async scheduleTextPost(input) {
|
|
2632
|
+
validatePlatformInput("threads", "scheduleTextPost", input);
|
|
2633
|
+
return scheduleTask({
|
|
2634
|
+
id: `threads-schedule-${Date.now()}`,
|
|
2635
|
+
runAt: input.publishAt,
|
|
2636
|
+
task: async () => _Threads.postText({
|
|
2637
|
+
threadsUserId: input.threadsUserId,
|
|
2638
|
+
text: input.text
|
|
2639
|
+
})
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
export {
|
|
2644
|
+
Bluesky,
|
|
2645
|
+
Facebook,
|
|
2646
|
+
Instagram,
|
|
2647
|
+
LinkedIn,
|
|
2648
|
+
Mastodon,
|
|
2649
|
+
Pinterest,
|
|
2650
|
+
SocialError,
|
|
2651
|
+
Threads,
|
|
2652
|
+
TikTok,
|
|
2653
|
+
X,
|
|
2654
|
+
YouTube
|
|
2655
|
+
};
|
|
2656
|
+
//# sourceMappingURL=index.js.map
|