peak6-x-publishing-plugin 0.1.2 → 0.2.1
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 +218 -130
- package/dist/worker.js.map +2 -2
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -927,40 +927,46 @@ var STATE_KEYS = {
|
|
|
927
927
|
oauthTokens: "oauth_tokens",
|
|
928
928
|
rateLimits: "rate_limits"
|
|
929
929
|
};
|
|
930
|
+
var DEFAULT_APPROVAL_MODES = {
|
|
931
|
+
posts: "none",
|
|
932
|
+
replies: "none",
|
|
933
|
+
quotes: "none",
|
|
934
|
+
reposts: "none",
|
|
935
|
+
scheduled: "required"
|
|
936
|
+
};
|
|
930
937
|
var DEFAULT_CONFIG = {
|
|
931
938
|
company_id: "",
|
|
932
|
-
x_handle: "",
|
|
933
|
-
x_user_id: "",
|
|
934
939
|
oauth_client_id_ref: "X_OAUTH_CLIENT_ID",
|
|
935
940
|
oauth_client_secret_ref: "X_OAUTH_CLIENT_SECRET",
|
|
941
|
+
default_account: "",
|
|
942
|
+
accounts: {},
|
|
936
943
|
daily_post_limit: 25,
|
|
937
|
-
approval_modes: {
|
|
938
|
-
posts: "none",
|
|
939
|
-
replies: "none",
|
|
940
|
-
quotes: "none",
|
|
941
|
-
reposts: "none",
|
|
942
|
-
scheduled: "required"
|
|
943
|
-
},
|
|
944
944
|
engagement_milestones: [50, 100, 500, 1e3],
|
|
945
945
|
metrics_capture_lookback_days: 7,
|
|
946
946
|
alert_agents: []
|
|
947
947
|
};
|
|
948
|
+
function accountStateKey(base, handle) {
|
|
949
|
+
return `${base}:${handle}`;
|
|
950
|
+
}
|
|
948
951
|
|
|
949
952
|
// src/pipeline/oauth-manager.ts
|
|
950
953
|
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
951
954
|
function stateKey(key) {
|
|
952
955
|
return { scopeKind: "instance", stateKey: key };
|
|
953
956
|
}
|
|
954
|
-
async function getTokens(ctx) {
|
|
955
|
-
const raw = await ctx.state.get(stateKey(STATE_KEYS.oauthTokens));
|
|
956
|
-
if (!raw) throw new Error(
|
|
957
|
+
async function getTokens(ctx, handle) {
|
|
958
|
+
const raw = await ctx.state.get(stateKey(accountStateKey(STATE_KEYS.oauthTokens, handle)));
|
|
959
|
+
if (!raw) throw new Error(`No OAuth tokens in state for @${handle}`);
|
|
957
960
|
return raw;
|
|
958
961
|
}
|
|
959
|
-
async function setTokens(ctx, tokens) {
|
|
960
|
-
await ctx.state.set(
|
|
962
|
+
async function setTokens(ctx, handle, tokens) {
|
|
963
|
+
await ctx.state.set(
|
|
964
|
+
stateKey(accountStateKey(STATE_KEYS.oauthTokens, handle)),
|
|
965
|
+
tokens
|
|
966
|
+
);
|
|
961
967
|
}
|
|
962
|
-
async function refreshTokens(ctx, config) {
|
|
963
|
-
const current = await getTokens(ctx);
|
|
968
|
+
async function refreshTokens(ctx, config, handle) {
|
|
969
|
+
const current = await getTokens(ctx, handle);
|
|
964
970
|
const clientId = await ctx.secrets.resolve(config.oauth_client_id_ref);
|
|
965
971
|
const clientSecret = await ctx.secrets.resolve(config.oauth_client_secret_ref);
|
|
966
972
|
const resp = await ctx.http.fetch("https://api.x.com/2/oauth2/token", {
|
|
@@ -976,7 +982,7 @@ async function refreshTokens(ctx, config) {
|
|
|
976
982
|
});
|
|
977
983
|
if (!resp.ok) {
|
|
978
984
|
const text = await resp.text();
|
|
979
|
-
throw new Error(`Token refresh failed: HTTP ${resp.status} \u2014 ${text}`);
|
|
985
|
+
throw new Error(`Token refresh failed for @${handle}: HTTP ${resp.status} \u2014 ${text}`);
|
|
980
986
|
}
|
|
981
987
|
const body = await resp.json();
|
|
982
988
|
const newTokens = {
|
|
@@ -984,13 +990,13 @@ async function refreshTokens(ctx, config) {
|
|
|
984
990
|
refresh_token: body.refresh_token,
|
|
985
991
|
expires_at: Date.now() + body.expires_in * 1e3
|
|
986
992
|
};
|
|
987
|
-
await setTokens(ctx, newTokens);
|
|
993
|
+
await setTokens(ctx, handle, newTokens);
|
|
988
994
|
return newTokens;
|
|
989
995
|
}
|
|
990
|
-
async function getValidAccessToken(ctx, config) {
|
|
991
|
-
const tokens = await getTokens(ctx);
|
|
996
|
+
async function getValidAccessToken(ctx, config, handle) {
|
|
997
|
+
const tokens = await getTokens(ctx, handle);
|
|
992
998
|
if (tokens.expires_at - Date.now() < REFRESH_BUFFER_MS) {
|
|
993
|
-
const refreshed = await refreshTokens(ctx, config);
|
|
999
|
+
const refreshed = await refreshTokens(ctx, config, handle);
|
|
994
1000
|
return refreshed.access_token;
|
|
995
1001
|
}
|
|
996
1002
|
return tokens.access_token;
|
|
@@ -1092,8 +1098,8 @@ var DEFAULT_STATE = {
|
|
|
1092
1098
|
daily_posts: 0,
|
|
1093
1099
|
daily_reset_at: 0
|
|
1094
1100
|
};
|
|
1095
|
-
async function getRateLimitState(ctx) {
|
|
1096
|
-
const raw = await ctx.state.get(stateKey2(STATE_KEYS.rateLimits));
|
|
1101
|
+
async function getRateLimitState(ctx, handle) {
|
|
1102
|
+
const raw = await ctx.state.get(stateKey2(accountStateKey(STATE_KEYS.rateLimits, handle)));
|
|
1097
1103
|
if (!raw) return { ...DEFAULT_STATE };
|
|
1098
1104
|
const state = raw;
|
|
1099
1105
|
if (state.daily_reset_at && Date.now() > state.daily_reset_at) {
|
|
@@ -1107,8 +1113,8 @@ function getNextMidnightMs() {
|
|
|
1107
1113
|
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
1108
1114
|
return tomorrow.getTime();
|
|
1109
1115
|
}
|
|
1110
|
-
async function updateRateLimits(ctx, endpoint, headers) {
|
|
1111
|
-
const state = await getRateLimitState(ctx);
|
|
1116
|
+
async function updateRateLimits(ctx, handle, endpoint, headers) {
|
|
1117
|
+
const state = await getRateLimitState(ctx, handle);
|
|
1112
1118
|
state[endpoint] = {
|
|
1113
1119
|
remaining: headers.remaining,
|
|
1114
1120
|
reset_at: headers.reset_at
|
|
@@ -1119,12 +1125,16 @@ async function updateRateLimits(ctx, endpoint, headers) {
|
|
|
1119
1125
|
state.daily_reset_at = getNextMidnightMs();
|
|
1120
1126
|
}
|
|
1121
1127
|
}
|
|
1122
|
-
await ctx.state.set(
|
|
1128
|
+
await ctx.state.set(
|
|
1129
|
+
stateKey2(accountStateKey(STATE_KEYS.rateLimits, handle)),
|
|
1130
|
+
state
|
|
1131
|
+
);
|
|
1123
1132
|
}
|
|
1124
1133
|
|
|
1125
1134
|
// src/pipeline/approval-gate.ts
|
|
1126
|
-
async function checkApproval(ctx, config, contentType, draftId) {
|
|
1127
|
-
const
|
|
1135
|
+
async function checkApproval(ctx, config, contentType, draftId, approvalModes) {
|
|
1136
|
+
const modes = approvalModes || DEFAULT_APPROVAL_MODES;
|
|
1137
|
+
const mode = modes[contentType];
|
|
1128
1138
|
if (mode === "none") {
|
|
1129
1139
|
return { allowed: true };
|
|
1130
1140
|
}
|
|
@@ -1253,12 +1263,30 @@ async function getConfig(ctx) {
|
|
|
1253
1263
|
const raw = await ctx.config.get();
|
|
1254
1264
|
return { ...DEFAULT_CONFIG, ...raw };
|
|
1255
1265
|
}
|
|
1266
|
+
function resolveAccount(config, handle) {
|
|
1267
|
+
const accounts = config.accounts || {};
|
|
1268
|
+
const handles = Object.keys(accounts);
|
|
1269
|
+
if (handles.length === 0) {
|
|
1270
|
+
return { ok: false, error: "No accounts configured." };
|
|
1271
|
+
}
|
|
1272
|
+
const target = handle || config.default_account || handles[0];
|
|
1273
|
+
const account = accounts[target];
|
|
1274
|
+
if (!account) {
|
|
1275
|
+
return { ok: false, error: `Account @${target} not found in config. Available: ${handles.join(", ")}` };
|
|
1276
|
+
}
|
|
1277
|
+
return { ok: true, handle: target, account };
|
|
1278
|
+
}
|
|
1256
1279
|
function findEntityById(entities, id) {
|
|
1257
1280
|
const found = entities.find((e) => e.id === id);
|
|
1258
1281
|
if (!found) return null;
|
|
1259
1282
|
return { id: found.id, data: found.data, raw: found };
|
|
1260
1283
|
}
|
|
1261
1284
|
async function handleSetupOauth(ctx, params) {
|
|
1285
|
+
const handle = params.account;
|
|
1286
|
+
if (!handle) return { error: "account param is required (X handle to store tokens for)." };
|
|
1287
|
+
const config = await getConfig(ctx);
|
|
1288
|
+
const resolved = resolveAccount(config, handle);
|
|
1289
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1262
1290
|
const accessToken = params.access_token;
|
|
1263
1291
|
const refreshToken = params.refresh_token;
|
|
1264
1292
|
const expiresIn = params.expires_in || 7200;
|
|
@@ -1267,8 +1295,8 @@ async function handleSetupOauth(ctx, params) {
|
|
|
1267
1295
|
refresh_token: refreshToken,
|
|
1268
1296
|
expires_at: Date.now() + expiresIn * 1e3
|
|
1269
1297
|
};
|
|
1270
|
-
await setTokens(ctx, tokenState);
|
|
1271
|
-
return { content: `OAuth tokens stored. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
|
|
1298
|
+
await setTokens(ctx, handle, tokenState);
|
|
1299
|
+
return { content: `OAuth tokens stored for @${handle}. Expires at ${new Date(tokenState.expires_at).toISOString()}.` };
|
|
1272
1300
|
}
|
|
1273
1301
|
async function handleDraftPost(ctx, params) {
|
|
1274
1302
|
const text = params.text;
|
|
@@ -1355,6 +1383,9 @@ async function handleUpdateDraft(ctx, params) {
|
|
|
1355
1383
|
}
|
|
1356
1384
|
async function handlePublishPost(ctx, params) {
|
|
1357
1385
|
const config = await getConfig(ctx);
|
|
1386
|
+
const resolved = resolveAccount(config, params.account);
|
|
1387
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1388
|
+
const { handle, account } = resolved;
|
|
1358
1389
|
let text = params.text;
|
|
1359
1390
|
let metadata = params.metadata || {};
|
|
1360
1391
|
const draftId = params.draft_id;
|
|
@@ -1366,11 +1397,11 @@ async function handlePublishPost(ctx, params) {
|
|
|
1366
1397
|
metadata = { ...match.data.metadata, ...metadata };
|
|
1367
1398
|
}
|
|
1368
1399
|
if (!text) return { error: "No text provided and no draft_id." };
|
|
1369
|
-
const approval = await checkApproval(ctx, config, "posts", draftId);
|
|
1400
|
+
const approval = await checkApproval(ctx, config, "posts", draftId, account.approval_modes);
|
|
1370
1401
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1371
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1402
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1372
1403
|
const result = await createTweet(ctx.http, token, { text });
|
|
1373
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1404
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1374
1405
|
const published = {
|
|
1375
1406
|
tweet_id: result.data.id,
|
|
1376
1407
|
text: result.data.text,
|
|
@@ -1396,14 +1427,17 @@ async function handlePublishPost(ctx, params) {
|
|
|
1396
1427
|
}
|
|
1397
1428
|
async function handleReplyToTweet(ctx, params) {
|
|
1398
1429
|
const config = await getConfig(ctx);
|
|
1430
|
+
const resolved = resolveAccount(config, params.account);
|
|
1431
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1432
|
+
const { handle, account } = resolved;
|
|
1399
1433
|
const tweetId = params.tweet_id;
|
|
1400
1434
|
const text = params.text;
|
|
1401
1435
|
const metadata = params.metadata || {};
|
|
1402
|
-
const approval = await checkApproval(ctx, config, "replies", params.draft_id);
|
|
1436
|
+
const approval = await checkApproval(ctx, config, "replies", params.draft_id, account.approval_modes);
|
|
1403
1437
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1404
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1438
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1405
1439
|
const result = await createTweet(ctx.http, token, { text, reply_to: tweetId });
|
|
1406
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1440
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1407
1441
|
const published = {
|
|
1408
1442
|
tweet_id: result.data.id,
|
|
1409
1443
|
text: result.data.text,
|
|
@@ -1429,14 +1463,17 @@ async function handleReplyToTweet(ctx, params) {
|
|
|
1429
1463
|
}
|
|
1430
1464
|
async function handleQuoteTweet(ctx, params) {
|
|
1431
1465
|
const config = await getConfig(ctx);
|
|
1466
|
+
const resolved = resolveAccount(config, params.account);
|
|
1467
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1468
|
+
const { handle, account } = resolved;
|
|
1432
1469
|
const tweetId = params.tweet_id;
|
|
1433
1470
|
const text = params.text;
|
|
1434
1471
|
const metadata = params.metadata || {};
|
|
1435
|
-
const approval = await checkApproval(ctx, config, "quotes", params.draft_id);
|
|
1472
|
+
const approval = await checkApproval(ctx, config, "quotes", params.draft_id, account.approval_modes);
|
|
1436
1473
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1437
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1474
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1438
1475
|
const result = await createTweet(ctx.http, token, { text, quote_tweet_id: tweetId });
|
|
1439
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1476
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1440
1477
|
const published = {
|
|
1441
1478
|
tweet_id: result.data.id,
|
|
1442
1479
|
text: result.data.text,
|
|
@@ -1462,12 +1499,15 @@ async function handleQuoteTweet(ctx, params) {
|
|
|
1462
1499
|
}
|
|
1463
1500
|
async function handleRepost(ctx, params) {
|
|
1464
1501
|
const config = await getConfig(ctx);
|
|
1502
|
+
const resolved = resolveAccount(config, params.account);
|
|
1503
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1504
|
+
const { handle, account } = resolved;
|
|
1465
1505
|
const tweetId = params.tweet_id;
|
|
1466
|
-
const approval = await checkApproval(ctx, config, "reposts", params.draft_id);
|
|
1506
|
+
const approval = await checkApproval(ctx, config, "reposts", params.draft_id, account.approval_modes);
|
|
1467
1507
|
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);
|
|
1508
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1509
|
+
const result = await repost(ctx.http, token, account.x_user_id, tweetId);
|
|
1510
|
+
await updateRateLimits(ctx, handle, "retweets", result.rateLimit);
|
|
1471
1511
|
const published = {
|
|
1472
1512
|
tweet_id: tweetId,
|
|
1473
1513
|
text: "",
|
|
@@ -1528,6 +1568,9 @@ async function handleSchedulePost(ctx, params) {
|
|
|
1528
1568
|
}
|
|
1529
1569
|
async function handlePublishThread(ctx, params) {
|
|
1530
1570
|
const config = await getConfig(ctx);
|
|
1571
|
+
const resolved = resolveAccount(config, params.account);
|
|
1572
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1573
|
+
const { handle, account } = resolved;
|
|
1531
1574
|
let threadTweets = params.thread_tweets;
|
|
1532
1575
|
let metadata = params.metadata || {};
|
|
1533
1576
|
const draftId = params.draft_id;
|
|
@@ -1541,9 +1584,9 @@ async function handlePublishThread(ctx, params) {
|
|
|
1541
1584
|
if (!threadTweets || threadTweets.length < 2) {
|
|
1542
1585
|
return { error: "Thread must have at least 2 tweets." };
|
|
1543
1586
|
}
|
|
1544
|
-
const approval = await checkApproval(ctx, config, "posts", draftId);
|
|
1587
|
+
const approval = await checkApproval(ctx, config, "posts", draftId, account.approval_modes);
|
|
1545
1588
|
if (!approval.allowed) return { error: `Blocked: ${approval.reason}` };
|
|
1546
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1589
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1547
1590
|
const result = await publishThread(ctx, token, threadTweets, metadata, config.company_id);
|
|
1548
1591
|
await ctx.events.emit(EVENT_NAMES.threadPublished, config.company_id, {
|
|
1549
1592
|
tweet_ids: result.tweetIds,
|
|
@@ -1596,60 +1639,76 @@ async function handleGetPostMetrics(ctx, params) {
|
|
|
1596
1639
|
metrics: data.metrics || null
|
|
1597
1640
|
}) };
|
|
1598
1641
|
}
|
|
1599
|
-
async function handleGetAccountStatus(ctx,
|
|
1642
|
+
async function handleGetAccountStatus(ctx, params) {
|
|
1600
1643
|
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
|
-
}
|
|
1644
|
+
const accounts = config.accounts || {};
|
|
1645
|
+
const handles = params.account ? [params.account] : Object.keys(accounts);
|
|
1619
1646
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1620
1647
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1648
|
+
const statuses = [];
|
|
1649
|
+
for (const handle of handles) {
|
|
1650
|
+
let tokenHealth = "unknown";
|
|
1651
|
+
try {
|
|
1652
|
+
const tokens = await getTokens(ctx, handle);
|
|
1653
|
+
if (tokens.expires_at > Date.now()) {
|
|
1654
|
+
const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
|
|
1655
|
+
tokenHealth = `valid (${minutesLeft}m remaining)`;
|
|
1656
|
+
} else {
|
|
1657
|
+
tokenHealth = "expired";
|
|
1658
|
+
}
|
|
1659
|
+
} catch {
|
|
1660
|
+
tokenHealth = "no tokens stored";
|
|
1661
|
+
}
|
|
1662
|
+
let rateLimits = null;
|
|
1663
|
+
try {
|
|
1664
|
+
const raw = await ctx.state.get(stateKey3(accountStateKey(STATE_KEYS.rateLimits, handle)));
|
|
1665
|
+
if (raw) rateLimits = raw;
|
|
1666
|
+
} catch {
|
|
1667
|
+
}
|
|
1668
|
+
const todayCount = entities.filter((e) => {
|
|
1669
|
+
const data = e.data;
|
|
1670
|
+
return data.published_at.startsWith(today);
|
|
1671
|
+
}).length;
|
|
1672
|
+
statuses.push({
|
|
1673
|
+
x_handle: handle,
|
|
1674
|
+
role: accounts[handle]?.role || "unknown",
|
|
1675
|
+
token_health: tokenHealth,
|
|
1676
|
+
daily_posts: { count: todayCount, limit: config.daily_post_limit },
|
|
1677
|
+
rate_limits: rateLimits
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
return { content: JSON.stringify(handles.length === 1 ? statuses[0] : statuses) };
|
|
1631
1681
|
}
|
|
1632
1682
|
async function handleTokenRefresh(ctx, _job) {
|
|
1633
1683
|
const config = await getConfig(ctx);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1684
|
+
const handles = Object.keys(config.accounts || {});
|
|
1685
|
+
for (const handle of handles) {
|
|
1686
|
+
try {
|
|
1687
|
+
await refreshTokens(ctx, config, handle);
|
|
1688
|
+
ctx.logger.info(`Token refresh succeeded for @${handle}`);
|
|
1689
|
+
} catch (err) {
|
|
1690
|
+
ctx.logger.error(`Token refresh failed for @${handle}`, { error: String(err) });
|
|
1691
|
+
const issue = await ctx.issues.create({
|
|
1692
|
+
companyId: config.company_id,
|
|
1693
|
+
title: `OAuth token refresh failed for @${handle}`,
|
|
1694
|
+
description: `Token refresh failed for @${handle} at ${(/* @__PURE__ */ new Date()).toISOString()}.
|
|
1643
1695
|
|
|
1644
1696
|
Error: ${String(err)}
|
|
1645
1697
|
|
|
1646
1698
|
Manual re-auth may be required via setup-oauth tool.`
|
|
1647
|
-
|
|
1648
|
-
|
|
1699
|
+
});
|
|
1700
|
+
await ctx.issues.update(issue.id, { status: "todo" }, config.company_id);
|
|
1701
|
+
}
|
|
1649
1702
|
}
|
|
1650
1703
|
}
|
|
1651
1704
|
async function handlePublishScheduled(ctx, _job) {
|
|
1652
1705
|
const config = await getConfig(ctx);
|
|
1706
|
+
const resolved = resolveAccount(config);
|
|
1707
|
+
if (!resolved.ok) {
|
|
1708
|
+
ctx.logger.error(resolved.error);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const { handle } = resolved;
|
|
1653
1712
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1654
1713
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
1655
1714
|
const due = entities.filter((e) => {
|
|
@@ -1658,13 +1717,13 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1658
1717
|
});
|
|
1659
1718
|
for (const entity of due) {
|
|
1660
1719
|
const draft = entity.data;
|
|
1661
|
-
const approval = await checkApproval(ctx, config, "scheduled", entity.id);
|
|
1720
|
+
const approval = await checkApproval(ctx, config, "scheduled", entity.id, resolved.account.approval_modes);
|
|
1662
1721
|
if (!approval.allowed) {
|
|
1663
1722
|
ctx.logger.info(`Scheduled draft ${entity.id} blocked: ${approval.reason}`);
|
|
1664
1723
|
continue;
|
|
1665
1724
|
}
|
|
1666
1725
|
try {
|
|
1667
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1726
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1668
1727
|
if (draft.format === "thread" && draft.thread_tweets) {
|
|
1669
1728
|
await publishThread(ctx, token, draft.thread_tweets, draft.metadata, config.company_id);
|
|
1670
1729
|
} else {
|
|
@@ -1673,7 +1732,7 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1673
1732
|
reply_to: draft.reply_to_tweet_id,
|
|
1674
1733
|
quote_tweet_id: draft.quote_tweet_id
|
|
1675
1734
|
});
|
|
1676
|
-
await updateRateLimits(ctx, "post_tweets", result.rateLimit);
|
|
1735
|
+
await updateRateLimits(ctx, handle, "post_tweets", result.rateLimit);
|
|
1677
1736
|
await ctx.entities.upsert({
|
|
1678
1737
|
entityType: ENTITY_TYPES.publishedPost,
|
|
1679
1738
|
scopeKind: "instance",
|
|
@@ -1711,6 +1770,12 @@ async function handlePublishScheduled(ctx, _job) {
|
|
|
1711
1770
|
}
|
|
1712
1771
|
async function handleMetricsCapture(ctx, _job) {
|
|
1713
1772
|
const config = await getConfig(ctx);
|
|
1773
|
+
const resolved = resolveAccount(config);
|
|
1774
|
+
if (!resolved.ok) {
|
|
1775
|
+
ctx.logger.error(resolved.error);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
const { handle } = resolved;
|
|
1714
1779
|
const lookbackMs = config.metrics_capture_lookback_days * 864e5;
|
|
1715
1780
|
const cutoff = new Date(Date.now() - lookbackMs).toISOString();
|
|
1716
1781
|
const entities = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 500 });
|
|
@@ -1720,7 +1785,7 @@ async function handleMetricsCapture(ctx, _job) {
|
|
|
1720
1785
|
});
|
|
1721
1786
|
if (recent.length === 0) return;
|
|
1722
1787
|
const tweetIds = recent.map((e) => e.data.tweet_id);
|
|
1723
|
-
const token = await getValidAccessToken(ctx, config);
|
|
1788
|
+
const token = await getValidAccessToken(ctx, config, handle);
|
|
1724
1789
|
const batchSize = 100;
|
|
1725
1790
|
for (let i = 0; i < tweetIds.length; i += batchSize) {
|
|
1726
1791
|
const batch = tweetIds.slice(i, i + batchSize);
|
|
@@ -1871,6 +1936,8 @@ var plugin = definePlugin({
|
|
|
1871
1936
|
);
|
|
1872
1937
|
ctx.data.register("dashboard-summary", async () => {
|
|
1873
1938
|
const config = await getConfig(ctx);
|
|
1939
|
+
const accounts = config.accounts || {};
|
|
1940
|
+
const handles = Object.keys(accounts);
|
|
1874
1941
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1875
1942
|
const published = await ctx.entities.list({ entityType: ENTITY_TYPES.publishedPost, limit: 100 });
|
|
1876
1943
|
const drafts = await ctx.entities.list({ entityType: ENTITY_TYPES.draft, limit: 100 });
|
|
@@ -1881,19 +1948,24 @@ var plugin = definePlugin({
|
|
|
1881
1948
|
const d = e.data;
|
|
1882
1949
|
return d.status === "draft" || d.status === "in_review" || d.status === "approved";
|
|
1883
1950
|
});
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1951
|
+
const accountHealth = {};
|
|
1952
|
+
for (const handle of handles) {
|
|
1953
|
+
try {
|
|
1954
|
+
const tokens = await getTokens(ctx, handle);
|
|
1955
|
+
accountHealth[handle] = tokens.expires_at > Date.now() ? "valid" : "expired";
|
|
1956
|
+
} catch {
|
|
1957
|
+
accountHealth[handle] = "not configured";
|
|
1958
|
+
}
|
|
1890
1959
|
}
|
|
1960
|
+
const allValid = Object.values(accountHealth).every((h) => h === "valid");
|
|
1961
|
+
const tokenHealth = handles.length === 0 ? "no accounts" : allValid ? "valid" : "degraded";
|
|
1891
1962
|
const recentPosts = published.map((e) => e.data).sort((a, b) => a.published_at > b.published_at ? -1 : 1).slice(0, 5);
|
|
1892
1963
|
return {
|
|
1893
1964
|
today_post_count: todayPosts.length,
|
|
1894
1965
|
daily_limit: config.daily_post_limit,
|
|
1895
1966
|
pending_drafts: pendingDrafts.length,
|
|
1896
1967
|
token_health: tokenHealth,
|
|
1968
|
+
accounts: accountHealth,
|
|
1897
1969
|
recent_posts: recentPosts
|
|
1898
1970
|
};
|
|
1899
1971
|
});
|
|
@@ -1919,47 +1991,61 @@ var plugin = definePlugin({
|
|
|
1919
1991
|
},
|
|
1920
1992
|
async onHealth() {
|
|
1921
1993
|
if (!pluginCtx) return { status: "error", message: "Plugin not initialized" };
|
|
1922
|
-
const
|
|
1994
|
+
const config = await getConfig(pluginCtx);
|
|
1995
|
+
const accounts = config.accounts || {};
|
|
1996
|
+
const handles = Object.keys(accounts);
|
|
1997
|
+
if (handles.length === 0) {
|
|
1998
|
+
return { status: "error", message: "No accounts configured" };
|
|
1999
|
+
}
|
|
1923
2000
|
let status = "ok";
|
|
1924
2001
|
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()) {
|
|
2002
|
+
const accountDetails = {};
|
|
2003
|
+
for (const handle of handles) {
|
|
2004
|
+
const detail = { role: accounts[handle].role };
|
|
2005
|
+
try {
|
|
2006
|
+
const tokens = await getTokens(pluginCtx, handle);
|
|
2007
|
+
const tokenOk = tokens.expires_at > Date.now();
|
|
2008
|
+
const minutesLeft = Math.round((tokens.expires_at - Date.now()) / 6e4);
|
|
2009
|
+
detail.token = tokenOk ? `valid (${minutesLeft}m remaining)` : "expired";
|
|
2010
|
+
if (!tokenOk) {
|
|
1948
2011
|
if (status === "ok") status = "degraded";
|
|
1949
|
-
issues.push(
|
|
2012
|
+
issues.push(`@${handle} token expired`);
|
|
1950
2013
|
}
|
|
2014
|
+
} catch {
|
|
2015
|
+
status = "error";
|
|
2016
|
+
issues.push(`@${handle} no tokens`);
|
|
2017
|
+
detail.token = "not configured";
|
|
1951
2018
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
2019
|
+
try {
|
|
2020
|
+
const raw = await pluginCtx.state.get(stateKey3(accountStateKey(STATE_KEYS.rateLimits, handle)));
|
|
2021
|
+
if (raw) {
|
|
2022
|
+
const limits = raw;
|
|
2023
|
+
detail.rate_limits = { remaining: limits.post_tweets?.remaining ?? "unknown", daily: limits.daily_posts ?? 0 };
|
|
2024
|
+
if (limits.post_tweets && limits.post_tweets.remaining === 0 && limits.post_tweets.reset_at > Date.now()) {
|
|
2025
|
+
if (status === "ok") status = "degraded";
|
|
2026
|
+
issues.push(`@${handle} rate limited`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
} catch {
|
|
2030
|
+
}
|
|
2031
|
+
accountDetails[handle] = detail;
|
|
1954
2032
|
}
|
|
1955
|
-
const message = issues.length > 0 ? issues.join("; ") :
|
|
1956
|
-
return { status, message, details };
|
|
2033
|
+
const message = issues.length > 0 ? issues.join("; ") : `All ${handles.length} account(s) operational`;
|
|
2034
|
+
return { status, message, details: { accounts: accountDetails } };
|
|
1957
2035
|
},
|
|
1958
2036
|
async onValidateConfig(config) {
|
|
1959
2037
|
const warnings = [];
|
|
1960
2038
|
const errors = [];
|
|
1961
|
-
|
|
1962
|
-
|
|
2039
|
+
const accounts = config.accounts || {};
|
|
2040
|
+
const handles = Object.keys(accounts);
|
|
2041
|
+
if (handles.length === 0) {
|
|
2042
|
+
errors.push("At least one account is required in accounts map");
|
|
2043
|
+
}
|
|
2044
|
+
for (const handle of handles) {
|
|
2045
|
+
const acct = accounts[handle];
|
|
2046
|
+
if (!acct.x_handle) errors.push(`Account ${handle}: x_handle is required`);
|
|
2047
|
+
if (!acct.x_user_id) errors.push(`Account ${handle}: x_user_id is required`);
|
|
2048
|
+
}
|
|
1963
2049
|
if (pluginCtx) {
|
|
1964
2050
|
const clientIdRef = config.oauth_client_id_ref || DEFAULT_CONFIG.oauth_client_id_ref;
|
|
1965
2051
|
const clientSecretRef = config.oauth_client_secret_ref || DEFAULT_CONFIG.oauth_client_secret_ref;
|
|
@@ -1973,10 +2059,12 @@ var plugin = definePlugin({
|
|
|
1973
2059
|
} catch {
|
|
1974
2060
|
errors.push("Could not resolve oauth_client_secret_ref secret");
|
|
1975
2061
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2062
|
+
for (const handle of handles) {
|
|
2063
|
+
try {
|
|
2064
|
+
await getTokens(pluginCtx, handle);
|
|
2065
|
+
} catch {
|
|
2066
|
+
warnings.push(`No OAuth tokens for @${handle} \u2014 run setup-oauth tool`);
|
|
2067
|
+
}
|
|
1980
2068
|
}
|
|
1981
2069
|
}
|
|
1982
2070
|
return { ok: errors.length === 0, warnings, errors };
|