peak6-x-publishing-plugin 0.1.2 → 0.2.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/dist/manifest.js +46 -40
- package/dist/manifest.js.map +2 -2
- package/dist/worker.js +202 -122
- package/dist/worker.js.map +2 -2
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -929,38 +929,37 @@ var STATE_KEYS = {
|
|
|
929
929
|
};
|
|
930
930
|
var DEFAULT_CONFIG = {
|
|
931
931
|
company_id: "",
|
|
932
|
-
x_handle: "",
|
|
933
|
-
x_user_id: "",
|
|
934
932
|
oauth_client_id_ref: "X_OAUTH_CLIENT_ID",
|
|
935
933
|
oauth_client_secret_ref: "X_OAUTH_CLIENT_SECRET",
|
|
934
|
+
default_account: "",
|
|
935
|
+
accounts: {},
|
|
936
936
|
daily_post_limit: 25,
|
|
937
|
-
approval_modes: {
|
|
938
|
-
posts: "none",
|
|
939
|
-
replies: "none",
|
|
940
|
-
quotes: "none",
|
|
941
|
-
reposts: "none",
|
|
942
|
-
scheduled: "required"
|
|
943
|
-
},
|
|
944
937
|
engagement_milestones: [50, 100, 500, 1e3],
|
|
945
938
|
metrics_capture_lookback_days: 7,
|
|
946
939
|
alert_agents: []
|
|
947
940
|
};
|
|
941
|
+
function accountStateKey(base, handle) {
|
|
942
|
+
return `${base}:${handle}`;
|
|
943
|
+
}
|
|
948
944
|
|
|
949
945
|
// src/pipeline/oauth-manager.ts
|
|
950
946
|
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
951
947
|
function stateKey(key) {
|
|
952
948
|
return { scopeKind: "instance", stateKey: key };
|
|
953
949
|
}
|
|
954
|
-
async function getTokens(ctx) {
|
|
955
|
-
const raw = await ctx.state.get(stateKey(STATE_KEYS.oauthTokens));
|
|
956
|
-
if (!raw) throw new Error(
|
|
950
|
+
async function getTokens(ctx, handle) {
|
|
951
|
+
const raw = await ctx.state.get(stateKey(accountStateKey(STATE_KEYS.oauthTokens, handle)));
|
|
952
|
+
if (!raw) throw new Error(`No OAuth tokens in state for @${handle}`);
|
|
957
953
|
return raw;
|
|
958
954
|
}
|
|
959
|
-
async function setTokens(ctx, tokens) {
|
|
960
|
-
await ctx.state.set(
|
|
955
|
+
async function setTokens(ctx, handle, tokens) {
|
|
956
|
+
await ctx.state.set(
|
|
957
|
+
stateKey(accountStateKey(STATE_KEYS.oauthTokens, handle)),
|
|
958
|
+
tokens
|
|
959
|
+
);
|
|
961
960
|
}
|
|
962
|
-
async function refreshTokens(ctx, config) {
|
|
963
|
-
const current = await getTokens(ctx);
|
|
961
|
+
async function refreshTokens(ctx, config, handle) {
|
|
962
|
+
const current = await getTokens(ctx, handle);
|
|
964
963
|
const clientId = await ctx.secrets.resolve(config.oauth_client_id_ref);
|
|
965
964
|
const clientSecret = await ctx.secrets.resolve(config.oauth_client_secret_ref);
|
|
966
965
|
const resp = await ctx.http.fetch("https://api.x.com/2/oauth2/token", {
|
|
@@ -976,7 +975,7 @@ async function refreshTokens(ctx, config) {
|
|
|
976
975
|
});
|
|
977
976
|
if (!resp.ok) {
|
|
978
977
|
const text = await resp.text();
|
|
979
|
-
throw new Error(`Token refresh failed: HTTP ${resp.status} \u2014 ${text}`);
|
|
978
|
+
throw new Error(`Token refresh failed for @${handle}: HTTP ${resp.status} \u2014 ${text}`);
|
|
980
979
|
}
|
|
981
980
|
const body = await resp.json();
|
|
982
981
|
const newTokens = {
|
|
@@ -984,13 +983,13 @@ async function refreshTokens(ctx, config) {
|
|
|
984
983
|
refresh_token: body.refresh_token,
|
|
985
984
|
expires_at: Date.now() + body.expires_in * 1e3
|
|
986
985
|
};
|
|
987
|
-
await setTokens(ctx, newTokens);
|
|
986
|
+
await setTokens(ctx, handle, newTokens);
|
|
988
987
|
return newTokens;
|
|
989
988
|
}
|
|
990
|
-
async function getValidAccessToken(ctx, config) {
|
|
991
|
-
const tokens = await getTokens(ctx);
|
|
989
|
+
async function getValidAccessToken(ctx, config, handle) {
|
|
990
|
+
const tokens = await getTokens(ctx, handle);
|
|
992
991
|
if (tokens.expires_at - Date.now() < REFRESH_BUFFER_MS) {
|
|
993
|
-
const refreshed = await refreshTokens(ctx, config);
|
|
992
|
+
const refreshed = await refreshTokens(ctx, config, handle);
|
|
994
993
|
return refreshed.access_token;
|
|
995
994
|
}
|
|
996
995
|
return tokens.access_token;
|
|
@@ -1092,8 +1091,8 @@ var DEFAULT_STATE = {
|
|
|
1092
1091
|
daily_posts: 0,
|
|
1093
1092
|
daily_reset_at: 0
|
|
1094
1093
|
};
|
|
1095
|
-
async function getRateLimitState(ctx) {
|
|
1096
|
-
const raw = await ctx.state.get(stateKey2(STATE_KEYS.rateLimits));
|
|
1094
|
+
async function getRateLimitState(ctx, handle) {
|
|
1095
|
+
const raw = await ctx.state.get(stateKey2(accountStateKey(STATE_KEYS.rateLimits, handle)));
|
|
1097
1096
|
if (!raw) return { ...DEFAULT_STATE };
|
|
1098
1097
|
const state = raw;
|
|
1099
1098
|
if (state.daily_reset_at && Date.now() > state.daily_reset_at) {
|
|
@@ -1107,8 +1106,8 @@ function getNextMidnightMs() {
|
|
|
1107
1106
|
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
1108
1107
|
return tomorrow.getTime();
|
|
1109
1108
|
}
|
|
1110
|
-
async function updateRateLimits(ctx, endpoint, headers) {
|
|
1111
|
-
const state = await getRateLimitState(ctx);
|
|
1109
|
+
async function updateRateLimits(ctx, handle, endpoint, headers) {
|
|
1110
|
+
const state = await getRateLimitState(ctx, handle);
|
|
1112
1111
|
state[endpoint] = {
|
|
1113
1112
|
remaining: headers.remaining,
|
|
1114
1113
|
reset_at: headers.reset_at
|
|
@@ -1119,7 +1118,10 @@ async function updateRateLimits(ctx, endpoint, headers) {
|
|
|
1119
1118
|
state.daily_reset_at = getNextMidnightMs();
|
|
1120
1119
|
}
|
|
1121
1120
|
}
|
|
1122
|
-
await ctx.state.set(
|
|
1121
|
+
await ctx.state.set(
|
|
1122
|
+
stateKey2(accountStateKey(STATE_KEYS.rateLimits, handle)),
|
|
1123
|
+
state
|
|
1124
|
+
);
|
|
1123
1125
|
}
|
|
1124
1126
|
|
|
1125
1127
|
// src/pipeline/approval-gate.ts
|
|
@@ -1253,12 +1255,30 @@ async function getConfig(ctx) {
|
|
|
1253
1255
|
const raw = await ctx.config.get();
|
|
1254
1256
|
return { ...DEFAULT_CONFIG, ...raw };
|
|
1255
1257
|
}
|
|
1258
|
+
function resolveAccount(config, handle) {
|
|
1259
|
+
const accounts = config.accounts || {};
|
|
1260
|
+
const handles = Object.keys(accounts);
|
|
1261
|
+
if (handles.length === 0) {
|
|
1262
|
+
return { ok: false, error: "No accounts configured." };
|
|
1263
|
+
}
|
|
1264
|
+
const target = handle || config.default_account || handles[0];
|
|
1265
|
+
const account = accounts[target];
|
|
1266
|
+
if (!account) {
|
|
1267
|
+
return { ok: false, error: `Account @${target} not found in config. Available: ${handles.join(", ")}` };
|
|
1268
|
+
}
|
|
1269
|
+
return { ok: true, handle: target, account };
|
|
1270
|
+
}
|
|
1256
1271
|
function findEntityById(entities, id) {
|
|
1257
1272
|
const found = entities.find((e) => e.id === id);
|
|
1258
1273
|
if (!found) return null;
|
|
1259
1274
|
return { id: found.id, data: found.data, raw: found };
|
|
1260
1275
|
}
|
|
1261
1276
|
async function handleSetupOauth(ctx, params) {
|
|
1277
|
+
const handle = params.account;
|
|
1278
|
+
if (!handle) return { error: "account param is required (X handle to store tokens for)." };
|
|
1279
|
+
const config = await getConfig(ctx);
|
|
1280
|
+
const resolved = resolveAccount(config, handle);
|
|
1281
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1262
1282
|
const accessToken = params.access_token;
|
|
1263
1283
|
const refreshToken = params.refresh_token;
|
|
1264
1284
|
const expiresIn = params.expires_in || 7200;
|
|
@@ -1267,8 +1287,8 @@ async function handleSetupOauth(ctx, params) {
|
|
|
1267
1287
|
refresh_token: refreshToken,
|
|
1268
1288
|
expires_at: Date.now() + expiresIn * 1e3
|
|
1269
1289
|
};
|
|
1270
|
-
await setTokens(ctx, tokenState);
|
|
1271
|
-
return { content: `OAuth tokens stored. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
|
|
1290
|
+
await setTokens(ctx, handle, tokenState);
|
|
1291
|
+
return { content: `OAuth tokens stored for @${handle}. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
|
|
1272
1292
|
}
|
|
1273
1293
|
async function handleDraftPost(ctx, params) {
|
|
1274
1294
|
const text = params.text;
|
|
@@ -1355,6 +1375,9 @@ async function handleUpdateDraft(ctx, params) {
|
|
|
1355
1375
|
}
|
|
1356
1376
|
async function handlePublishPost(ctx, params) {
|
|
1357
1377
|
const config = await getConfig(ctx);
|
|
1378
|
+
const resolved = resolveAccount(config, params.account);
|
|
1379
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1380
|
+
const { handle, account } = resolved;
|
|
1358
1381
|
let text = params.text;
|
|
1359
1382
|
let metadata = params.metadata || {};
|
|
1360
1383
|
const draftId = params.draft_id;
|
|
@@ -1368,9 +1391,9 @@ async function handlePublishPost(ctx, params) {
|
|
|
1368
1391
|
if (!text) return { error: "No text provided and no draft_id." };
|
|
1369
1392
|
const approval = await checkApproval(ctx, config, "posts", draftId);
|
|
1370
1393
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1371
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1394
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1372
1395
|
const result = await createTweet(ctx.http, token, { text });
|
|
1373
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1396
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1374
1397
|
const published = {
|
|
1375
1398
|
tweet_id: result.data.id,
|
|
1376
1399
|
text: result.data.text,
|
|
@@ -1396,14 +1419,17 @@ async function handlePublishPost(ctx, params) {
|
|
|
1396
1419
|
}
|
|
1397
1420
|
async function handleReplyToTweet(ctx, params) {
|
|
1398
1421
|
const config = await getConfig(ctx);
|
|
1422
|
+
const resolved = resolveAccount(config, params.account);
|
|
1423
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1424
|
+
const { handle } = resolved;
|
|
1399
1425
|
const tweetId = params.tweet_id;
|
|
1400
1426
|
const text = params.text;
|
|
1401
1427
|
const metadata = params.metadata || {};
|
|
1402
1428
|
const approval = await checkApproval(ctx, config, "replies", params.draft_id);
|
|
1403
1429
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1404
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1430
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1405
1431
|
const result = await createTweet(ctx.http, token, { text, reply_to: tweetId });
|
|
1406
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1432
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1407
1433
|
const published = {
|
|
1408
1434
|
tweet_id: result.data.id,
|
|
1409
1435
|
text: result.data.text,
|
|
@@ -1429,14 +1455,17 @@ async function handleReplyToTweet(ctx, params) {
|
|
|
1429
1455
|
}
|
|
1430
1456
|
async function handleQuoteTweet(ctx, params) {
|
|
1431
1457
|
const config = await getConfig(ctx);
|
|
1458
|
+
const resolved = resolveAccount(config, params.account);
|
|
1459
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1460
|
+
const { handle } = resolved;
|
|
1432
1461
|
const tweetId = params.tweet_id;
|
|
1433
1462
|
const text = params.text;
|
|
1434
1463
|
const metadata = params.metadata || {};
|
|
1435
1464
|
const approval = await checkApproval(ctx, config, "quotes", params.draft_id);
|
|
1436
1465
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1437
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1466
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1438
1467
|
const result = await createTweet(ctx.http, token, { text, quote_tweet_id: tweetId });
|
|
1439
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1468
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1440
1469
|
const published = {
|
|
1441
1470
|
tweet_id: result.data.id,
|
|
1442
1471
|
text: result.data.text,
|
|
@@ -1462,12 +1491,15 @@ async function handleQuoteTweet(ctx, params) {
|
|
|
1462
1491
|
}
|
|
1463
1492
|
async function handleRepost(ctx, params) {
|
|
1464
1493
|
const config = await getConfig(ctx);
|
|
1494
|
+
const resolved = resolveAccount(config, params.account);
|
|
1495
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1496
|
+
const { handle, account } = resolved;
|
|
1465
1497
|
const tweetId = params.tweet_id;
|
|
1466
1498
|
const approval = await checkApproval(ctx, config, "reposts", params.draft_id);
|
|
1467
1499
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1468
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1469
|
-
const result = await repost(ctx.http, token,
|
|
1470
|
-
await updateRateLimits(ctx, "retweets", result.rateLimit);
|
|
1500
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1501
|
+
const result = await repost(ctx.http, token, account.x_user_id, tweetId);
|
|
1502
|
+
await updateRateLimits(ctx, handle, "retweets", result.rateLimit);
|
|
1471
1503
|
const published = {
|
|
1472
1504
|
tweet_id: tweetId,
|
|
1473
1505
|
text: "",
|
|
@@ -1528,6 +1560,9 @@ async function handleSchedulePost(ctx, params) {
|
|
|
1528
1560
|
}
|
|
1529
1561
|
async function handlePublishThread(ctx, params) {
|
|
1530
1562
|
const config = await getConfig(ctx);
|
|
1563
|
+
const resolved = resolveAccount(config, params.account);
|
|
1564
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1565
|
+
const { handle } = resolved;
|
|
1531
1566
|
let threadTweets = params.thread_tweets;
|
|
1532
1567
|
let metadata = params.metadata || {};
|
|
1533
1568
|
const draftId = params.draft_id;
|
|
@@ -1543,7 +1578,7 @@ async function handlePublishThread(ctx, params) {
|
|
|
1543
1578
|
}
|
|
1544
1579
|
const approval = await checkApproval(ctx, config, "posts", draftId);
|
|
1545
1580
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1546
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1581
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1547
1582
|
const result = await publishThread(ctx, token, threadTweets, metadata, config.company_id);
|
|
1548
1583
|
await ctx.events.emit(EVENT_NAMES.threadPublished, config.company_id, {
|
|
1549
1584
|
tweet_ids: result.tweetIds,
|
|
@@ -1596,60 +1631,76 @@ async function handleGetPostMetrics(ctx, params) {
|
|
|
1596
1631
|
metrics: data.metrics || null
|
|
1597
1632
|
}) };
|
|
1598
1633
|
}
|
|
1599
|
-
async function handleGetAccountStatus(ctx,
|
|
1634
|
+
async function handleGetAccountStatus(ctx, params) {
|
|
1600
1635
|
const config = await getConfig(ctx);
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
const tokens = await getTokens(ctx);
|
|
1604
|
-
if (tokens.expires_at > Date.now()) {
|
|
1605
|
-
const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
|
|
1606
|
-
tokenHealth = `valid (${minutesLeft}m remaining)`;
|
|
1607
|
-
} else {
|
|
1608
|
-
tokenHealth = "expired";
|
|
1609
|
-
}
|
|
1610
|
-
} catch {
|
|
1611
|
-
tokenHealth = "no tokens stored";
|
|
1612
|
-
}
|
|
1613
|
-
let rateLimits = null;
|
|
1614
|
-
try {
|
|
1615
|
-
const raw = await ctx.state.get(stateKey3(STATE_KEYS.rateLimits));
|
|
1616
|
-
if (raw) rateLimits = raw;
|
|
1617
|
-
} catch {
|
|
1618
|
-
}
|
|
1636
|
+
const accounts = config.accounts || {};
|
|
1637
|
+
const handles = params.account ? [params.account] : Object.keys(accounts);
|
|
1619
1638
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1620
1639
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1640
|
+
const statuses = [];
|
|
1641
|
+
for (const handle of handles) {
|
|
1642
|
+
let tokenHealth = "unknown";
|
|
1643
|
+
try {
|
|
1644
|
+
const tokens = await getTokens(ctx, handle);
|
|
1645
|
+
if (tokens.expires_at > Date.now()) {
|
|
1646
|
+
const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
|
|
1647
|
+
tokenHealth = `valid (${minutesLeft}m remaining)`;
|
|
1648
|
+
} else {
|
|
1649
|
+
tokenHealth = "expired";
|
|
1650
|
+
}
|
|
1651
|
+
} catch {
|
|
1652
|
+
tokenHealth = "no tokens stored";
|
|
1653
|
+
}
|
|
1654
|
+
let rateLimits = null;
|
|
1655
|
+
try {
|
|
1656
|
+
const raw = await ctx.state.get(stateKey3(accountStateKey(STATE_KEYS.rateLimits, handle)));
|
|
1657
|
+
if (raw) rateLimits = raw;
|
|
1658
|
+
} catch {
|
|
1659
|
+
}
|
|
1660
|
+
const todayCount = entities.filter((e) => {
|
|
1661
|
+
const data = e.data;
|
|
1662
|
+
return data.published_at.startsWith(today);
|
|
1663
|
+
}).length;
|
|
1664
|
+
statuses.push({
|
|
1665
|
+
x_handle: handle,
|
|
1666
|
+
role: accounts[handle]?.role || "unknown",
|
|
1667
|
+
token_health: tokenHealth,
|
|
1668
|
+
daily_posts: { count: todayCount, limit: config.daily_post_limit },
|
|
1669
|
+
rate_limits: rateLimits
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
return { content: JSON.stringify(handles.length === 1 ? statuses[0] : statuses) };
|
|
1631
1673
|
}
|
|
1632
1674
|
async function handleTokenRefresh(ctx, _job) {
|
|
1633
1675
|
const config = await getConfig(ctx);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1676
|
+
const handles = Object.keys(config.accounts || {});
|
|
1677
|
+
for (const handle of handles) {
|
|
1678
|
+
try {
|
|
1679
|
+
await refreshTokens(ctx, config, handle);
|
|
1680
|
+
ctx.logger.info(`Token refresh succeeded for @${handle}`);
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
ctx.logger.error(`Token refresh failed for @${handle}`, { error: String(err) });
|
|
1683
|
+
const issue = await ctx.issues.create({
|
|
1684
|
+
companyId: config.company_id,
|
|
1685
|
+
title: `OAuth token refresh failed for @${handle}`,
|
|
1686
|
+
description: `Token refresh failed for @${handle} at ${(/* @__PURE__ */ new Date()).toISOString()}.
|
|
1643
1687
|
|
|
1644
1688
|
Error: ${String(err)}
|
|
1645
1689
|
|
|
1646
1690
|
Manual re-auth may be required via setup-oauth tool.`
|
|
1647
|
-
|
|
1648
|
-
|
|
1691
|
+
});
|
|
1692
|
+
await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
|
|
1693
|
+
}
|
|
1649
1694
|
}
|
|
1650
1695
|
}
|
|
1651
1696
|
async function handlePublishScheduled(ctx, _job) {
|
|
1652
1697
|
const config = await getConfig(ctx);
|
|
1698
|
+
const resolved = resolveAccount(config);
|
|
1699
|
+
if (!resolved.ok) {
|
|
1700
|
+
ctx.logger.error(resolved.error);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const { handle } = resolved;
|
|
1653
1704
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1654
1705
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
1655
1706
|
const due = entities.filter((e) => {
|
|
@@ -1664,7 +1715,7 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1664
1715
|
continue;
|
|
1665
1716
|
}
|
|
1666
1717
|
try {
|
|
1667
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1718
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1668
1719
|
if (draft.format === "thread" && draft.thread_tweets) {
|
|
1669
1720
|
await publishThread(ctx, token, draft.thread_tweets, draft.metadata, config.company_id);
|
|
1670
1721
|
} else {
|
|
@@ -1673,7 +1724,7 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1673
1724
|
reply_to: draft.reply_to_tweet_id,
|
|
1674
1725
|
quote_tweet_id: draft.quote_tweet_id
|
|
1675
1726
|
});
|
|
1676
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1727
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1677
1728
|
await ctx.entities.upsert({
|
|
1678
1729
|
entityType: ENTITY_TYPES.publishedPost,
|
|
1679
1730
|
scopeKind: "instance",
|
|
@@ -1711,6 +1762,12 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1711
1762
|
}
|
|
1712
1763
|
async function handleMetricsCapture(ctx, _job) {
|
|
1713
1764
|
const config = await getConfig(ctx);
|
|
1765
|
+
const resolved = resolveAccount(config);
|
|
1766
|
+
if (!resolved.ok) {
|
|
1767
|
+
ctx.logger.error(resolved.error);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const { handle } = resolved;
|
|
1714
1771
|
const lookbackMs = config.metrics_capture_lookback_days * 864e5;
|
|
1715
1772
|
const cutoff = new Date(Date.now() - lookbackMs).toISOString();
|
|
1716
1773
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
@@ -1720,7 +1777,7 @@ async function handleMetricsCapture(ctx, _job) {
|
|
|
1720
1777
|
});
|
|
1721
1778
|
if (recent.length === 0) return;
|
|
1722
1779
|
const tweetIds = recent.map((e) => e.data.tweet_id);
|
|
1723
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1780
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1724
1781
|
const batchSize = 100;
|
|
1725
1782
|
for (let i = 0; i < tweetIds.length; i += batchSize) {
|
|
1726
1783
|
const batch = tweetIds.slice(i, i + batchSize);
|
|
@@ -1871,6 +1928,8 @@ var plugin = definePlugin({
|
|
|
1871
1928
|
);
|
|
1872
1929
|
ctx.data.register("dashboard-summary", async () => {
|
|
1873
1930
|
const config = await getConfig(ctx);
|
|
1931
|
+
const accounts = config.accounts || {};
|
|
1932
|
+
const handles = Object.keys(accounts);
|
|
1874
1933
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1875
1934
|
const published = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 100 });
|
|
1876
1935
|
const drafts = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
@@ -1881,19 +1940,24 @@ var plugin = definePlugin({
|
|
|
1881
1940
|
const d = e.data;
|
|
1882
1941
|
return d.status === "draft" || d.status === "in_review" || d.status === "approved";
|
|
1883
1942
|
});
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1943
|
+
const accountHealth = {};
|
|
1944
|
+
for (const handle of handles) {
|
|
1945
|
+
try {
|
|
1946
|
+
const tokens = await getTokens(ctx, handle);
|
|
1947
|
+
accountHealth[handle] = tokens.expires_at > Date.now() ? "valid" : "expired";
|
|
1948
|
+
} catch {
|
|
1949
|
+
accountHealth[handle] = "not configured";
|
|
1950
|
+
}
|
|
1890
1951
|
}
|
|
1952
|
+
const allValid = Object.values(accountHealth).every((h) => h === "valid");
|
|
1953
|
+
const tokenHealth = handles.length === 0 ? "no accounts" : allValid ? "valid" : "degraded";
|
|
1891
1954
|
const recentPosts = published.map((e) => e.data).sort((a, b) => a.published_at > b.published_at ? -1 : 1).slice(0, 5);
|
|
1892
1955
|
return {
|
|
1893
1956
|
today_post_count: todayPosts.length,
|
|
1894
1957
|
daily_limit: config.daily_post_limit,
|
|
1895
1958
|
pending_drafts: pendingDrafts.length,
|
|
1896
1959
|
token_health: tokenHealth,
|
|
1960
|
+
accounts: accountHealth,
|
|
1897
1961
|
recent_posts: recentPosts
|
|
1898
1962
|
};
|
|
1899
1963
|
});
|
|
@@ -1919,47 +1983,61 @@ var plugin = definePlugin({
|
|
|
1919
1983
|
},
|
|
1920
1984
|
async onHealth() {
|
|
1921
1985
|
if (!pluginCtx) return { status: "error", message: "Plugin not initialized" };
|
|
1922
|
-
const
|
|
1986
|
+
const config = await getConfig(pluginCtx);
|
|
1987
|
+
const accounts = config.accounts || {};
|
|
1988
|
+
const handles = Object.keys(accounts);
|
|
1989
|
+
if (handles.length === 0) {
|
|
1990
|
+
return { status: "error", message: "No accounts configured" };
|
|
1991
|
+
}
|
|
1923
1992
|
let status = "ok";
|
|
1924
1993
|
const issues = [];
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
const
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
} catch {
|
|
1935
|
-
status = "error";
|
|
1936
|
-
issues.push("No OAuth tokens configured");
|
|
1937
|
-
details.token = "not configured";
|
|
1938
|
-
}
|
|
1939
|
-
try {
|
|
1940
|
-
const raw = await pluginCtx.state.get(stateKey3(STATE_KEYS.rateLimits));
|
|
1941
|
-
if (raw) {
|
|
1942
|
-
const limits = raw;
|
|
1943
|
-
details.rate_limits = {
|
|
1944
|
-
post_tweets_remaining: limits.post_tweets?.remaining ?? "unknown",
|
|
1945
|
-
daily_posts: limits.daily_posts ?? 0
|
|
1946
|
-
};
|
|
1947
|
-
if (limits.post_tweets && limits.post_tweets.remaining === 0 && limits.post_tweets.reset_at > Date.now()) {
|
|
1994
|
+
const accountDetails = {};
|
|
1995
|
+
for (const handle of handles) {
|
|
1996
|
+
const detail = { role: accounts[handle].role };
|
|
1997
|
+
try {
|
|
1998
|
+
const tokens = await getTokens(pluginCtx, handle);
|
|
1999
|
+
const tokenOk = tokens.expires_at > Date.now();
|
|
2000
|
+
const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
|
|
2001
|
+
detail.token = tokenOk ? `valid (${minutesLeft}m remaining)` : "expired";
|
|
2002
|
+
if (!tokenOk) {
|
|
1948
2003
|
if (status === "ok") status = "degraded";
|
|
1949
|
-
issues.push(
|
|
2004
|
+
issues.push(`@${handle} token expired`);
|
|
1950
2005
|
}
|
|
2006
|
+
} catch {
|
|
2007
|
+
status = "error";
|
|
2008
|
+
issues.push(`@${handle} no tokens`);
|
|
2009
|
+
detail.token = "not configured";
|
|
1951
2010
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
2011
|
+
try {
|
|
2012
|
+
const raw = await pluginCtx.state.get(stateKey3(accountStateKey(STATE_KEYS.rateLimits, handle)));
|
|
2013
|
+
if (raw) {
|
|
2014
|
+
const limits = raw;
|
|
2015
|
+
detail.rate_limits = { remaining: limits.post_tweets?.remaining ?? "unknown", daily: limits.daily_posts ?? 0 };
|
|
2016
|
+
if (limits.post_tweets && limits.post_tweets.remaining === 0 && limits.post_tweets.reset_at > Date.now()) {
|
|
2017
|
+
if (status === "ok") status = "degraded";
|
|
2018
|
+
issues.push(`@${handle} rate limited`);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
} catch {
|
|
2022
|
+
}
|
|
2023
|
+
accountDetails[handle] = detail;
|
|
1954
2024
|
}
|
|
1955
|
-
const message = issues.length > 0 ? issues.join("; ") :
|
|
1956
|
-
return { status, message, details };
|
|
2025
|
+
const message = issues.length > 0 ? issues.join("; ") : `All ${handles.length} account(s) operational`;
|
|
2026
|
+
return { status, message, details: { accounts: accountDetails } };
|
|
1957
2027
|
},
|
|
1958
2028
|
async onValidateConfig(config) {
|
|
1959
2029
|
const warnings = [];
|
|
1960
2030
|
const errors = [];
|
|
1961
|
-
|
|
1962
|
-
|
|
2031
|
+
const accounts = config.accounts || {};
|
|
2032
|
+
const handles = Object.keys(accounts);
|
|
2033
|
+
if (handles.length === 0) {
|
|
2034
|
+
errors.push("At least one account is required in accounts map");
|
|
2035
|
+
}
|
|
2036
|
+
for (const handle of handles) {
|
|
2037
|
+
const acct = accounts[handle];
|
|
2038
|
+
if (!acct.x_handle) errors.push(`Account ${handle}: x_handle is required`);
|
|
2039
|
+
if (!acct.x_user_id) errors.push(`Account ${handle}: x_user_id is required`);
|
|
2040
|
+
}
|
|
1963
2041
|
if (pluginCtx) {
|
|
1964
2042
|
const clientIdRef = config.oauth_client_id_ref || DEFAULT_CONFIG.oauth_client_id_ref;
|
|
1965
2043
|
const clientSecretRef = config.oauth_client_secret_ref || DEFAULT_CONFIG.oauth_client_secret_ref;
|
|
@@ -1973,10 +2051,12 @@ var plugin = definePlugin({
|
|
|
1973
2051
|
} catch {
|
|
1974
2052
|
errors.push("Could not resolve oauth_client_secret_ref secret");
|
|
1975
2053
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2054
|
+
for (const handle of handles) {
|
|
2055
|
+
try {
|
|
2056
|
+
await getTokens(pluginCtx, handle);
|
|
2057
|
+
} catch {
|
|
2058
|
+
warnings.push(`No OAuth tokens for @${handle} \u2014 run setup-oauth tool`);
|
|
2059
|
+
}
|
|
1980
2060
|
}
|
|
1981
2061
|
}
|
|
1982
2062
|
return { ok: errors.length === 0, warnings, errors };
|