peak6-x-publishing-plugin 0.2.1 → 0.2.3
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/dist/manifest.js +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/worker.js +100 -87
- package/dist/worker.js.map +3 -3
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -965,10 +965,10 @@ async function setTokens(ctx, handle, tokens) {
|
|
|
965
965
|
tokens
|
|
966
966
|
);
|
|
967
967
|
}
|
|
968
|
-
async function refreshTokens(ctx,
|
|
968
|
+
async function refreshTokens(ctx, config2, handle) {
|
|
969
969
|
const current = await getTokens(ctx, handle);
|
|
970
|
-
const clientId = await ctx.secrets.resolve(
|
|
971
|
-
const clientSecret = await ctx.secrets.resolve(
|
|
970
|
+
const clientId = await ctx.secrets.resolve(config2.oauth_client_id_ref);
|
|
971
|
+
const clientSecret = await ctx.secrets.resolve(config2.oauth_client_secret_ref);
|
|
972
972
|
const resp = await ctx.http.fetch("https://api.x.com/2/oauth2/token", {
|
|
973
973
|
method: "POST",
|
|
974
974
|
headers: {
|
|
@@ -993,10 +993,10 @@ async function refreshTokens(ctx, config, handle) {
|
|
|
993
993
|
await setTokens(ctx, handle, newTokens);
|
|
994
994
|
return newTokens;
|
|
995
995
|
}
|
|
996
|
-
async function getValidAccessToken(ctx,
|
|
996
|
+
async function getValidAccessToken(ctx, config2, handle) {
|
|
997
997
|
const tokens = await getTokens(ctx, handle);
|
|
998
998
|
if (tokens.expires_at - Date.now() < REFRESH_BUFFER_MS) {
|
|
999
|
-
const refreshed = await refreshTokens(ctx,
|
|
999
|
+
const refreshed = await refreshTokens(ctx, config2, handle);
|
|
1000
1000
|
return refreshed.access_token;
|
|
1001
1001
|
}
|
|
1002
1002
|
return tokens.access_token;
|
|
@@ -1132,7 +1132,7 @@ async function updateRateLimits(ctx, handle, endpoint, headers) {
|
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
1134
|
// src/pipeline/approval-gate.ts
|
|
1135
|
-
async function checkApproval(ctx,
|
|
1135
|
+
async function checkApproval(ctx, config2, contentType, draftId, approvalModes) {
|
|
1136
1136
|
const modes = approvalModes || DEFAULT_APPROVAL_MODES;
|
|
1137
1137
|
const mode = modes[contentType];
|
|
1138
1138
|
if (mode === "none") {
|
|
@@ -1144,14 +1144,14 @@ async function checkApproval(ctx, config, contentType, draftId, approvalModes) {
|
|
|
1144
1144
|
}
|
|
1145
1145
|
if (!draftId) {
|
|
1146
1146
|
const issue = await ctx.issues.create({
|
|
1147
|
-
companyId:
|
|
1147
|
+
companyId: config2.company_id,
|
|
1148
1148
|
title: `Approval required for ${contentType}`,
|
|
1149
1149
|
description: `Content type "${contentType}" requires approval but no draft was provided for review.
|
|
1150
1150
|
|
|
1151
1151
|
Create a draft first, then have it approved before publishing.`
|
|
1152
1152
|
});
|
|
1153
|
-
await ctx.issues.update(issue.id, { status: "todo" },
|
|
1154
|
-
await ctx.events.emit(EVENT_NAMES.approvalRequired,
|
|
1153
|
+
await ctx.issues.update(issue.id, { status: "todo" }, config2.company_id);
|
|
1154
|
+
await ctx.events.emit(EVENT_NAMES.approvalRequired, config2.company_id, {
|
|
1155
1155
|
content_type: contentType,
|
|
1156
1156
|
reason: "No draft provided for required approval"
|
|
1157
1157
|
});
|
|
@@ -1169,7 +1169,7 @@ Create a draft first, then have it approved before publishing.`
|
|
|
1169
1169
|
if (draft.status !== "in_review") {
|
|
1170
1170
|
draft.status = "in_review";
|
|
1171
1171
|
const issue = await ctx.issues.create({
|
|
1172
|
-
companyId:
|
|
1172
|
+
companyId: config2.company_id,
|
|
1173
1173
|
title: `Review draft: ${draft.text.slice(0, 60)}`,
|
|
1174
1174
|
description: `Draft ID: ${draftId}
|
|
1175
1175
|
Format: ${draft.format}
|
|
@@ -1177,7 +1177,7 @@ Format: ${draft.format}
|
|
|
1177
1177
|
Text:
|
|
1178
1178
|
${draft.text}${draft.thread_tweets ? "\n\nThread:\n" + draft.thread_tweets.join("\n---\n") : ""}`
|
|
1179
1179
|
});
|
|
1180
|
-
await ctx.issues.update(issue.id, { status: "todo" },
|
|
1180
|
+
await ctx.issues.update(issue.id, { status: "todo" }, config2.company_id);
|
|
1181
1181
|
draft.review_issue_id = issue.id;
|
|
1182
1182
|
await ctx.entities.upsert({
|
|
1183
1183
|
entityType: ENTITY_TYPES.draft,
|
|
@@ -1186,7 +1186,7 @@ ${draft.text}${draft.thread_tweets ? "\n\nThread:\n" + draft.thread_tweets.join(
|
|
|
1186
1186
|
title: entity.title ?? draft.text.slice(0, 60),
|
|
1187
1187
|
data: draft
|
|
1188
1188
|
});
|
|
1189
|
-
await ctx.events.emit(EVENT_NAMES.approvalRequired,
|
|
1189
|
+
await ctx.events.emit(EVENT_NAMES.approvalRequired, config2.company_id, {
|
|
1190
1190
|
draft_id: draftId,
|
|
1191
1191
|
content_type: contentType,
|
|
1192
1192
|
reason: "Draft submitted for review"
|
|
@@ -1197,7 +1197,7 @@ ${draft.text}${draft.thread_tweets ? "\n\nThread:\n" + draft.thread_tweets.join(
|
|
|
1197
1197
|
|
|
1198
1198
|
// src/pipeline/thread-publisher.ts
|
|
1199
1199
|
var MAX_RETRIES = 3;
|
|
1200
|
-
async function publishThread(ctx, token, tweets, metadata, companyId) {
|
|
1200
|
+
async function publishThread(ctx, token, handle, tweets, metadata, companyId) {
|
|
1201
1201
|
const tweetIds = [];
|
|
1202
1202
|
let replyTo;
|
|
1203
1203
|
for (let i = 0; i < tweets.length; i++) {
|
|
@@ -1209,11 +1209,12 @@ async function publishThread(ctx, token, tweets, metadata, companyId) {
|
|
|
1209
1209
|
text,
|
|
1210
1210
|
reply_to: replyTo
|
|
1211
1211
|
});
|
|
1212
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1212
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1213
1213
|
tweetIds.push(result.data.id);
|
|
1214
1214
|
replyTo = result.data.id;
|
|
1215
1215
|
const published = {
|
|
1216
1216
|
tweet_id: result.data.id,
|
|
1217
|
+
account: handle,
|
|
1217
1218
|
text: result.data.text,
|
|
1218
1219
|
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1219
1220
|
format: "thread",
|
|
@@ -1263,13 +1264,13 @@ async function getConfig(ctx) {
|
|
|
1263
1264
|
const raw = await ctx.config.get();
|
|
1264
1265
|
return { ...DEFAULT_CONFIG, ...raw };
|
|
1265
1266
|
}
|
|
1266
|
-
function resolveAccount(
|
|
1267
|
-
const accounts =
|
|
1267
|
+
function resolveAccount(config2, handle) {
|
|
1268
|
+
const accounts = config2.accounts || {};
|
|
1268
1269
|
const handles = Object.keys(accounts);
|
|
1269
1270
|
if (handles.length === 0) {
|
|
1270
1271
|
return { ok: false, error: "No accounts configured." };
|
|
1271
1272
|
}
|
|
1272
|
-
const target = handle ||
|
|
1273
|
+
const target = handle || config2.default_account || handles[0];
|
|
1273
1274
|
const account = accounts[target];
|
|
1274
1275
|
if (!account) {
|
|
1275
1276
|
return { ok: false, error: `Account @${target} not found in config. Available: ${handles.join(", ")}` };
|
|
@@ -1284,8 +1285,8 @@ function findEntityById(entities, id) {
|
|
|
1284
1285
|
async function handleSetupOauth(ctx, params) {
|
|
1285
1286
|
const handle = params.account;
|
|
1286
1287
|
if (!handle) return { error: "account param is required (X handle to store tokens for)." };
|
|
1287
|
-
const
|
|
1288
|
-
const resolved = resolveAccount(
|
|
1288
|
+
const config2 = await getConfig(ctx);
|
|
1289
|
+
const resolved = resolveAccount(config2, handle);
|
|
1289
1290
|
if (!resolved.ok) return { error: resolved.error };
|
|
1290
1291
|
const accessToken = params.access_token;
|
|
1291
1292
|
const refreshToken = params.refresh_token;
|
|
@@ -1303,12 +1304,13 @@ async function handleDraftPost(ctx, params) {
|
|
|
1303
1304
|
if (text.length > 280) {
|
|
1304
1305
|
return { error: `Tweet text exceeds 280 characters (${text.length}).` };
|
|
1305
1306
|
}
|
|
1306
|
-
const
|
|
1307
|
+
const config2 = await getConfig(ctx);
|
|
1307
1308
|
const format = params.format || "single";
|
|
1308
1309
|
const metadata = params.metadata || {};
|
|
1309
1310
|
const draft = {
|
|
1310
1311
|
text,
|
|
1311
1312
|
format,
|
|
1313
|
+
account: params.account || config2.default_account || void 0,
|
|
1312
1314
|
reply_to_tweet_id: params.reply_to_tweet_id,
|
|
1313
1315
|
quote_tweet_id: params.quote_tweet_id,
|
|
1314
1316
|
schedule_at: params.schedule_at,
|
|
@@ -1321,7 +1323,7 @@ async function handleDraftPost(ctx, params) {
|
|
|
1321
1323
|
title: text.slice(0, 60),
|
|
1322
1324
|
data: draft
|
|
1323
1325
|
});
|
|
1324
|
-
await ctx.events.emit(EVENT_NAMES.draftCreated,
|
|
1326
|
+
await ctx.events.emit(EVENT_NAMES.draftCreated, config2.company_id, {
|
|
1325
1327
|
draft_id: entity.id,
|
|
1326
1328
|
format,
|
|
1327
1329
|
metadata
|
|
@@ -1338,11 +1340,12 @@ async function handleDraftThread(ctx, params) {
|
|
|
1338
1340
|
return { error: `Tweet ${i + 1} exceeds 280 characters (${threadTweets[i].length}).` };
|
|
1339
1341
|
}
|
|
1340
1342
|
}
|
|
1341
|
-
const
|
|
1343
|
+
const config2 = await getConfig(ctx);
|
|
1342
1344
|
const metadata = params.metadata || {};
|
|
1343
1345
|
const draft = {
|
|
1344
1346
|
text: threadTweets[0],
|
|
1345
1347
|
format: "thread",
|
|
1348
|
+
account: params.account || config2.default_account || void 0,
|
|
1346
1349
|
thread_tweets: threadTweets,
|
|
1347
1350
|
schedule_at: params.schedule_at,
|
|
1348
1351
|
status: "draft",
|
|
@@ -1354,7 +1357,7 @@ async function handleDraftThread(ctx, params) {
|
|
|
1354
1357
|
title: `Thread: ${threadTweets[0].slice(0, 50)}`,
|
|
1355
1358
|
data: draft
|
|
1356
1359
|
});
|
|
1357
|
-
await ctx.events.emit(EVENT_NAMES.draftCreated,
|
|
1360
|
+
await ctx.events.emit(EVENT_NAMES.draftCreated, config2.company_id, {
|
|
1358
1361
|
draft_id: entity.id,
|
|
1359
1362
|
format: "thread",
|
|
1360
1363
|
metadata
|
|
@@ -1382,8 +1385,8 @@ async function handleUpdateDraft(ctx, params) {
|
|
|
1382
1385
|
return { content: JSON.stringify({ draft_id: draftId, status: "draft", updated: true }) };
|
|
1383
1386
|
}
|
|
1384
1387
|
async function handlePublishPost(ctx, params) {
|
|
1385
|
-
const
|
|
1386
|
-
const resolved = resolveAccount(
|
|
1388
|
+
const config2 = await getConfig(ctx);
|
|
1389
|
+
const resolved = resolveAccount(config2, params.account);
|
|
1387
1390
|
if (!resolved.ok) return { error: resolved.error };
|
|
1388
1391
|
const { handle, account } = resolved;
|
|
1389
1392
|
let text = params.text;
|
|
@@ -1397,13 +1400,14 @@ async function handlePublishPost(ctx, params) {
|
|
|
1397
1400
|
metadata = { ...match.data.metadata, ...metadata };
|
|
1398
1401
|
}
|
|
1399
1402
|
if (!text) return { error: "No text provided and no draft_id." };
|
|
1400
|
-
const approval = await checkApproval(ctx,
|
|
1403
|
+
const approval = await checkApproval(ctx, config2, "posts", draftId, account.approval_modes);
|
|
1401
1404
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1402
|
-
const token = await getValidAccessToken(ctx,
|
|
1405
|
+
const token = await getValidAccessToken(ctx, config2, handle);
|
|
1403
1406
|
const result = await createTweet(ctx.http, token, { text });
|
|
1404
1407
|
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1405
1408
|
const published = {
|
|
1406
1409
|
tweet_id: result.data.id,
|
|
1410
|
+
account: handle,
|
|
1407
1411
|
text: result.data.text,
|
|
1408
1412
|
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1409
1413
|
draft_id: draftId,
|
|
@@ -1417,7 +1421,7 @@ async function handlePublishPost(ctx, params) {
|
|
|
1417
1421
|
title: text.slice(0, 60),
|
|
1418
1422
|
data: published
|
|
1419
1423
|
});
|
|
1420
|
-
await ctx.events.emit(EVENT_NAMES.postPublished,
|
|
1424
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config2.company_id, {
|
|
1421
1425
|
tweet_id: result.data.id,
|
|
1422
1426
|
text,
|
|
1423
1427
|
format: "single",
|
|
@@ -1426,20 +1430,21 @@ async function handlePublishPost(ctx, params) {
|
|
|
1426
1430
|
return { content: JSON.stringify({ tweet_id: result.data.id, text: result.data.text, status: "published" }) };
|
|
1427
1431
|
}
|
|
1428
1432
|
async function handleReplyToTweet(ctx, params) {
|
|
1429
|
-
const
|
|
1430
|
-
const resolved = resolveAccount(
|
|
1433
|
+
const config2 = await getConfig(ctx);
|
|
1434
|
+
const resolved = resolveAccount(config2, params.account);
|
|
1431
1435
|
if (!resolved.ok) return { error: resolved.error };
|
|
1432
1436
|
const { handle, account } = resolved;
|
|
1433
1437
|
const tweetId = params.tweet_id;
|
|
1434
1438
|
const text = params.text;
|
|
1435
1439
|
const metadata = params.metadata || {};
|
|
1436
|
-
const approval = await checkApproval(ctx,
|
|
1440
|
+
const approval = await checkApproval(ctx, config2, "replies", params.draft_id, account.approval_modes);
|
|
1437
1441
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1438
|
-
const token = await getValidAccessToken(ctx,
|
|
1442
|
+
const token = await getValidAccessToken(ctx, config2, handle);
|
|
1439
1443
|
const result = await createTweet(ctx.http, token, { text, reply_to: tweetId });
|
|
1440
1444
|
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1441
1445
|
const published = {
|
|
1442
1446
|
tweet_id: result.data.id,
|
|
1447
|
+
account: handle,
|
|
1443
1448
|
text: result.data.text,
|
|
1444
1449
|
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1445
1450
|
draft_id: params.draft_id,
|
|
@@ -1453,7 +1458,7 @@ async function handleReplyToTweet(ctx, params) {
|
|
|
1453
1458
|
title: `Reply: ${text.slice(0, 50)}`,
|
|
1454
1459
|
data: published
|
|
1455
1460
|
});
|
|
1456
|
-
await ctx.events.emit(EVENT_NAMES.postPublished,
|
|
1461
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config2.company_id, {
|
|
1457
1462
|
tweet_id: result.data.id,
|
|
1458
1463
|
text,
|
|
1459
1464
|
format: "reply",
|
|
@@ -1462,20 +1467,21 @@ async function handleReplyToTweet(ctx, params) {
|
|
|
1462
1467
|
return { content: JSON.stringify({ tweet_id: result.data.id, text: result.data.text, status: "published", reply_to: tweetId }) };
|
|
1463
1468
|
}
|
|
1464
1469
|
async function handleQuoteTweet(ctx, params) {
|
|
1465
|
-
const
|
|
1466
|
-
const resolved = resolveAccount(
|
|
1470
|
+
const config2 = await getConfig(ctx);
|
|
1471
|
+
const resolved = resolveAccount(config2, params.account);
|
|
1467
1472
|
if (!resolved.ok) return { error: resolved.error };
|
|
1468
1473
|
const { handle, account } = resolved;
|
|
1469
1474
|
const tweetId = params.tweet_id;
|
|
1470
1475
|
const text = params.text;
|
|
1471
1476
|
const metadata = params.metadata || {};
|
|
1472
|
-
const approval = await checkApproval(ctx,
|
|
1477
|
+
const approval = await checkApproval(ctx, config2, "quotes", params.draft_id, account.approval_modes);
|
|
1473
1478
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1474
|
-
const token = await getValidAccessToken(ctx,
|
|
1479
|
+
const token = await getValidAccessToken(ctx, config2, handle);
|
|
1475
1480
|
const result = await createTweet(ctx.http, token, { text, quote_tweet_id: tweetId });
|
|
1476
1481
|
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1477
1482
|
const published = {
|
|
1478
1483
|
tweet_id: result.data.id,
|
|
1484
|
+
account: handle,
|
|
1479
1485
|
text: result.data.text,
|
|
1480
1486
|
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1481
1487
|
draft_id: params.draft_id,
|
|
@@ -1489,7 +1495,7 @@ async function handleQuoteTweet(ctx, params) {
|
|
|
1489
1495
|
title: `Quote: ${text.slice(0, 50)}`,
|
|
1490
1496
|
data: published
|
|
1491
1497
|
});
|
|
1492
|
-
await ctx.events.emit(EVENT_NAMES.postPublished,
|
|
1498
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config2.company_id, {
|
|
1493
1499
|
tweet_id: result.data.id,
|
|
1494
1500
|
text,
|
|
1495
1501
|
format: "quote",
|
|
@@ -1498,18 +1504,19 @@ async function handleQuoteTweet(ctx, params) {
|
|
|
1498
1504
|
return { content: JSON.stringify({ tweet_id: result.data.id, text: result.data.text, status: "published", quoted: tweetId }) };
|
|
1499
1505
|
}
|
|
1500
1506
|
async function handleRepost(ctx, params) {
|
|
1501
|
-
const
|
|
1502
|
-
const resolved = resolveAccount(
|
|
1507
|
+
const config2 = await getConfig(ctx);
|
|
1508
|
+
const resolved = resolveAccount(config2, params.account);
|
|
1503
1509
|
if (!resolved.ok) return { error: resolved.error };
|
|
1504
1510
|
const { handle, account } = resolved;
|
|
1505
1511
|
const tweetId = params.tweet_id;
|
|
1506
|
-
const approval = await checkApproval(ctx,
|
|
1512
|
+
const approval = await checkApproval(ctx, config2, "reposts", params.draft_id, account.approval_modes);
|
|
1507
1513
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1508
|
-
const token = await getValidAccessToken(ctx,
|
|
1514
|
+
const token = await getValidAccessToken(ctx, config2, handle);
|
|
1509
1515
|
const result = await repost(ctx.http, token, account.x_user_id, tweetId);
|
|
1510
1516
|
await updateRateLimits(ctx, handle, "retweets", result.rateLimit);
|
|
1511
1517
|
const published = {
|
|
1512
1518
|
tweet_id: tweetId,
|
|
1519
|
+
account: handle,
|
|
1513
1520
|
text: "",
|
|
1514
1521
|
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1515
1522
|
draft_id: params.draft_id,
|
|
@@ -1523,7 +1530,7 @@ async function handleRepost(ctx, params) {
|
|
|
1523
1530
|
title: `Repost: ${tweetId}`,
|
|
1524
1531
|
data: published
|
|
1525
1532
|
});
|
|
1526
|
-
await ctx.events.emit(EVENT_NAMES.postPublished,
|
|
1533
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config2.company_id, {
|
|
1527
1534
|
tweet_id: tweetId,
|
|
1528
1535
|
text: "",
|
|
1529
1536
|
format: "repost",
|
|
@@ -1554,6 +1561,7 @@ async function handleSchedulePost(ctx, params) {
|
|
|
1554
1561
|
const draft = {
|
|
1555
1562
|
text,
|
|
1556
1563
|
format: "single",
|
|
1564
|
+
account: params.account || config.default_account || void 0,
|
|
1557
1565
|
schedule_at: scheduleAt,
|
|
1558
1566
|
status: "draft",
|
|
1559
1567
|
metadata
|
|
@@ -1567,8 +1575,8 @@ async function handleSchedulePost(ctx, params) {
|
|
|
1567
1575
|
return { content: JSON.stringify({ draft_id: entity.id, schedule_at: scheduleAt, status: "scheduled" }) };
|
|
1568
1576
|
}
|
|
1569
1577
|
async function handlePublishThread(ctx, params) {
|
|
1570
|
-
const
|
|
1571
|
-
const resolved = resolveAccount(
|
|
1578
|
+
const config2 = await getConfig(ctx);
|
|
1579
|
+
const resolved = resolveAccount(config2, params.account);
|
|
1572
1580
|
if (!resolved.ok) return { error: resolved.error };
|
|
1573
1581
|
const { handle, account } = resolved;
|
|
1574
1582
|
let threadTweets = params.thread_tweets;
|
|
@@ -1584,17 +1592,17 @@ async function handlePublishThread(ctx, params) {
|
|
|
1584
1592
|
if (!threadTweets || threadTweets.length < 2) {
|
|
1585
1593
|
return { error: "Thread must have at least 2 tweets." };
|
|
1586
1594
|
}
|
|
1587
|
-
const approval = await checkApproval(ctx,
|
|
1595
|
+
const approval = await checkApproval(ctx, config2, "posts", draftId, account.approval_modes);
|
|
1588
1596
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1589
|
-
const token = await getValidAccessToken(ctx,
|
|
1590
|
-
const result = await publishThread(ctx, token, threadTweets, metadata,
|
|
1591
|
-
await ctx.events.emit(EVENT_NAMES.threadPublished,
|
|
1597
|
+
const token = await getValidAccessToken(ctx, config2, handle);
|
|
1598
|
+
const result = await publishThread(ctx, token, handle, threadTweets, metadata, config2.company_id);
|
|
1599
|
+
await ctx.events.emit(EVENT_NAMES.threadPublished, config2.company_id, {
|
|
1592
1600
|
tweet_ids: result.tweetIds,
|
|
1593
1601
|
thread_length: threadTweets.length,
|
|
1594
1602
|
partial: result.partial
|
|
1595
1603
|
});
|
|
1596
1604
|
for (const tweetId of result.tweetIds) {
|
|
1597
|
-
await ctx.events.emit(EVENT_NAMES.postPublished,
|
|
1605
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config2.company_id, {
|
|
1598
1606
|
tweet_id: tweetId,
|
|
1599
1607
|
text: "",
|
|
1600
1608
|
format: "thread",
|
|
@@ -1640,8 +1648,8 @@ async function handleGetPostMetrics(ctx, params) {
|
|
|
1640
1648
|
}) };
|
|
1641
1649
|
}
|
|
1642
1650
|
async function handleGetAccountStatus(ctx, params) {
|
|
1643
|
-
const
|
|
1644
|
-
const accounts =
|
|
1651
|
+
const config2 = await getConfig(ctx);
|
|
1652
|
+
const accounts = config2.accounts || {};
|
|
1645
1653
|
const handles = params.account ? [params.account] : Object.keys(accounts);
|
|
1646
1654
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1647
1655
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
@@ -1667,29 +1675,29 @@ async function handleGetAccountStatus(ctx, params) {
|
|
|
1667
1675
|
}
|
|
1668
1676
|
const todayCount = entities.filter((e) => {
|
|
1669
1677
|
const data = e.data;
|
|
1670
|
-
return data.published_at.startsWith(today);
|
|
1678
|
+
return data.published_at.startsWith(today) && (!data.account || data.account === handle);
|
|
1671
1679
|
}).length;
|
|
1672
1680
|
statuses.push({
|
|
1673
1681
|
x_handle: handle,
|
|
1674
1682
|
role: accounts[handle]?.role || "unknown",
|
|
1675
1683
|
token_health: tokenHealth,
|
|
1676
|
-
daily_posts: { count: todayCount, limit:
|
|
1684
|
+
daily_posts: { count: todayCount, limit: config2.daily_post_limit },
|
|
1677
1685
|
rate_limits: rateLimits
|
|
1678
1686
|
});
|
|
1679
1687
|
}
|
|
1680
1688
|
return { content: JSON.stringify(handles.length === 1 ? statuses[0] : statuses) };
|
|
1681
1689
|
}
|
|
1682
1690
|
async function handleTokenRefresh(ctx, _job) {
|
|
1683
|
-
const
|
|
1684
|
-
const handles = Object.keys(
|
|
1691
|
+
const config2 = await getConfig(ctx);
|
|
1692
|
+
const handles = Object.keys(config2.accounts || {});
|
|
1685
1693
|
for (const handle of handles) {
|
|
1686
1694
|
try {
|
|
1687
|
-
await refreshTokens(ctx,
|
|
1695
|
+
await refreshTokens(ctx, config2, handle);
|
|
1688
1696
|
ctx.logger.info(`Token refresh succeeded for @${handle}`);
|
|
1689
1697
|
} catch (err) {
|
|
1690
1698
|
ctx.logger.error(`Token refresh failed for @${handle}`, { error: String(err) });
|
|
1691
1699
|
const issue = await ctx.issues.create({
|
|
1692
|
-
companyId:
|
|
1700
|
+
companyId: config2.company_id,
|
|
1693
1701
|
title: `OAuth token refresh failed for @${handle}`,
|
|
1694
1702
|
description: `Token refresh failed for @${handle} at ${(/* @__PURE__ */ new Date()).toISOString()}.
|
|
1695
1703
|
|
|
@@ -1697,18 +1705,12 @@ Error: ${String(err)}
|
|
|
1697
1705
|
|
|
1698
1706
|
Manual re-auth may be required via setup-oauth tool.`
|
|
1699
1707
|
});
|
|
1700
|
-
await ctx.issues.update(issue.id, { status: "todo" },
|
|
1708
|
+
await ctx.issues.update(issue.id, { status: "todo" }, config2.company_id);
|
|
1701
1709
|
}
|
|
1702
1710
|
}
|
|
1703
1711
|
}
|
|
1704
1712
|
async function handlePublishScheduled(ctx, _job) {
|
|
1705
|
-
const
|
|
1706
|
-
const resolved = resolveAccount(config);
|
|
1707
|
-
if (!resolved.ok) {
|
|
1708
|
-
ctx.logger.error(resolved.error);
|
|
1709
|
-
return;
|
|
1710
|
-
}
|
|
1711
|
-
const { handle } = resolved;
|
|
1713
|
+
const config2 = await getConfig(ctx);
|
|
1712
1714
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1713
1715
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
1714
1716
|
const due = entities.filter((e) => {
|
|
@@ -1717,15 +1719,21 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1717
1719
|
});
|
|
1718
1720
|
for (const entity of due) {
|
|
1719
1721
|
const draft = entity.data;
|
|
1720
|
-
const
|
|
1722
|
+
const resolved = resolveAccount(config2, draft.account);
|
|
1723
|
+
if (!resolved.ok) {
|
|
1724
|
+
ctx.logger.error(`Scheduled draft ${entity.id}: ${resolved.error}`);
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
const { handle, account } = resolved;
|
|
1728
|
+
const approval = await checkApproval(ctx, config2, "scheduled", entity.id, account.approval_modes);
|
|
1721
1729
|
if (!approval.allowed) {
|
|
1722
1730
|
ctx.logger.info(`Scheduled draft ${entity.id} blocked: ${approval.reason}`);
|
|
1723
1731
|
continue;
|
|
1724
1732
|
}
|
|
1725
1733
|
try {
|
|
1726
|
-
const token = await getValidAccessToken(ctx,
|
|
1734
|
+
const token = await getValidAccessToken(ctx, config2, handle);
|
|
1727
1735
|
if (draft.format === "thread" && draft.thread_tweets) {
|
|
1728
|
-
await publishThread(ctx, token, draft.thread_tweets, draft.metadata,
|
|
1736
|
+
await publishThread(ctx, token, handle, draft.thread_tweets, draft.metadata, config2.company_id);
|
|
1729
1737
|
} else {
|
|
1730
1738
|
const result = await createTweet(ctx.http, token, {
|
|
1731
1739
|
text: draft.text,
|
|
@@ -1740,6 +1748,7 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1740
1748
|
title: draft.text.slice(0, 60),
|
|
1741
1749
|
data: {
|
|
1742
1750
|
tweet_id: result.data.id,
|
|
1751
|
+
account: handle,
|
|
1743
1752
|
text: result.data.text,
|
|
1744
1753
|
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1745
1754
|
draft_id: entity.id,
|
|
@@ -1747,7 +1756,7 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1747
1756
|
metadata: draft.metadata
|
|
1748
1757
|
}
|
|
1749
1758
|
});
|
|
1750
|
-
await ctx.events.emit(EVENT_NAMES.postPublished,
|
|
1759
|
+
await ctx.events.emit(EVENT_NAMES.postPublished, config2.company_id, {
|
|
1751
1760
|
tweet_id: result.data.id,
|
|
1752
1761
|
text: draft.text,
|
|
1753
1762
|
format: draft.format,
|
|
@@ -1769,14 +1778,14 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1769
1778
|
}
|
|
1770
1779
|
}
|
|
1771
1780
|
async function handleMetricsCapture(ctx, _job) {
|
|
1772
|
-
const
|
|
1773
|
-
const resolved = resolveAccount(
|
|
1781
|
+
const config2 = await getConfig(ctx);
|
|
1782
|
+
const resolved = resolveAccount(config2);
|
|
1774
1783
|
if (!resolved.ok) {
|
|
1775
1784
|
ctx.logger.error(resolved.error);
|
|
1776
1785
|
return;
|
|
1777
1786
|
}
|
|
1778
1787
|
const { handle } = resolved;
|
|
1779
|
-
const lookbackMs =
|
|
1788
|
+
const lookbackMs = config2.metrics_capture_lookback_days * 864e5;
|
|
1780
1789
|
const cutoff = new Date(Date.now() - lookbackMs).toISOString();
|
|
1781
1790
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
1782
1791
|
const recent = entities.filter((e) => {
|
|
@@ -1785,7 +1794,7 @@ async function handleMetricsCapture(ctx, _job) {
|
|
|
1785
1794
|
});
|
|
1786
1795
|
if (recent.length === 0) return;
|
|
1787
1796
|
const tweetIds = recent.map((e) => e.data.tweet_id);
|
|
1788
|
-
const token = await getValidAccessToken(ctx,
|
|
1797
|
+
const token = await getValidAccessToken(ctx, config2, handle);
|
|
1789
1798
|
const batchSize = 100;
|
|
1790
1799
|
for (let i = 0; i < tweetIds.length; i += batchSize) {
|
|
1791
1800
|
const batch = tweetIds.slice(i, i + batchSize);
|
|
@@ -1812,9 +1821,9 @@ async function handleMetricsCapture(ctx, _job) {
|
|
|
1812
1821
|
};
|
|
1813
1822
|
const totalEngagement = newMetrics.likes + newMetrics.retweets + newMetrics.replies;
|
|
1814
1823
|
const prevTotal = data.metrics ? data.metrics.likes + data.metrics.retweets + data.metrics.replies : 0;
|
|
1815
|
-
for (const milestone of
|
|
1824
|
+
for (const milestone of config2.engagement_milestones) {
|
|
1816
1825
|
if (totalEngagement >= milestone && prevTotal < milestone) {
|
|
1817
|
-
await ctx.events.emit(EVENT_NAMES.postEngagementMilestone,
|
|
1826
|
+
await ctx.events.emit(EVENT_NAMES.postEngagementMilestone, config2.company_id, {
|
|
1818
1827
|
tweet_id: tweet.id,
|
|
1819
1828
|
milestone,
|
|
1820
1829
|
current_metrics: newMetrics
|
|
@@ -1935,8 +1944,8 @@ var plugin = definePlugin({
|
|
|
1935
1944
|
(params) => handleGetAccountStatus(ctx, p(params))
|
|
1936
1945
|
);
|
|
1937
1946
|
ctx.data.register("dashboard-summary", async () => {
|
|
1938
|
-
const
|
|
1939
|
-
const accounts =
|
|
1947
|
+
const config2 = await getConfig(ctx);
|
|
1948
|
+
const accounts = config2.accounts || {};
|
|
1940
1949
|
const handles = Object.keys(accounts);
|
|
1941
1950
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1942
1951
|
const published = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 100 });
|
|
@@ -1962,7 +1971,7 @@ var plugin = definePlugin({
|
|
|
1962
1971
|
const recentPosts = published.map((e) => e.data).sort((a, b) => a.published_at > b.published_at ? -1 : 1).slice(0, 5);
|
|
1963
1972
|
return {
|
|
1964
1973
|
today_post_count: todayPosts.length,
|
|
1965
|
-
daily_limit:
|
|
1974
|
+
daily_limit: config2.daily_post_limit,
|
|
1966
1975
|
pending_drafts: pendingDrafts.length,
|
|
1967
1976
|
token_health: tokenHealth,
|
|
1968
1977
|
accounts: accountHealth,
|
|
@@ -1970,8 +1979,8 @@ var plugin = definePlugin({
|
|
|
1970
1979
|
};
|
|
1971
1980
|
});
|
|
1972
1981
|
ctx.data.register("plugin-config", async () => {
|
|
1973
|
-
const
|
|
1974
|
-
return
|
|
1982
|
+
const config2 = await getConfig(ctx);
|
|
1983
|
+
return config2;
|
|
1975
1984
|
});
|
|
1976
1985
|
ctx.data.register("content-queue", async () => {
|
|
1977
1986
|
const allDrafts = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
@@ -1991,8 +2000,8 @@ var plugin = definePlugin({
|
|
|
1991
2000
|
},
|
|
1992
2001
|
async onHealth() {
|
|
1993
2002
|
if (!pluginCtx) return { status: "error", message: "Plugin not initialized" };
|
|
1994
|
-
const
|
|
1995
|
-
const accounts =
|
|
2003
|
+
const config2 = await getConfig(pluginCtx);
|
|
2004
|
+
const accounts = config2.accounts || {};
|
|
1996
2005
|
const handles = Object.keys(accounts);
|
|
1997
2006
|
if (handles.length === 0) {
|
|
1998
2007
|
return { status: "error", message: "No accounts configured" };
|
|
@@ -2033,22 +2042,26 @@ var plugin = definePlugin({
|
|
|
2033
2042
|
const message = issues.length > 0 ? issues.join("; ") : `All ${handles.length} account(s) operational`;
|
|
2034
2043
|
return { status, message, details: { accounts: accountDetails } };
|
|
2035
2044
|
},
|
|
2036
|
-
async onValidateConfig(
|
|
2045
|
+
async onValidateConfig(config2) {
|
|
2037
2046
|
const warnings = [];
|
|
2038
2047
|
const errors = [];
|
|
2039
|
-
const accounts =
|
|
2048
|
+
const accounts = config2.accounts || {};
|
|
2040
2049
|
const handles = Object.keys(accounts);
|
|
2041
2050
|
if (handles.length === 0) {
|
|
2042
2051
|
errors.push("At least one account is required in accounts map");
|
|
2043
2052
|
}
|
|
2053
|
+
const defaultAcct = config2.default_account;
|
|
2054
|
+
if (defaultAcct && handles.length > 0 && !accounts[defaultAcct]) {
|
|
2055
|
+
errors.push(`default_account "${defaultAcct}" is not in the accounts map. Available: ${handles.join(", ")}`);
|
|
2056
|
+
}
|
|
2044
2057
|
for (const handle of handles) {
|
|
2045
2058
|
const acct = accounts[handle];
|
|
2046
2059
|
if (!acct.x_handle) errors.push(`Account ${handle}: x_handle is required`);
|
|
2047
2060
|
if (!acct.x_user_id) errors.push(`Account ${handle}: x_user_id is required`);
|
|
2048
2061
|
}
|
|
2049
2062
|
if (pluginCtx) {
|
|
2050
|
-
const clientIdRef =
|
|
2051
|
-
const clientSecretRef =
|
|
2063
|
+
const clientIdRef = config2.oauth_client_id_ref || DEFAULT_CONFIG.oauth_client_id_ref;
|
|
2064
|
+
const clientSecretRef = config2.oauth_client_secret_ref || DEFAULT_CONFIG.oauth_client_secret_ref;
|
|
2052
2065
|
try {
|
|
2053
2066
|
await pluginCtx.secrets.resolve(clientIdRef);
|
|
2054
2067
|
} catch {
|