@hasna/experts 0.0.4 → 0.0.6
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/cli/index.js +65 -9
- package/dist/connectors.d.ts +3 -0
- package/dist/connectors.d.ts.map +1 -1
- package/dist/enrich.d.ts +18 -0
- package/dist/enrich.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -8
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -983,13 +983,14 @@ function defaultRunner(bin = "connectors") {
|
|
|
983
983
|
]);
|
|
984
984
|
const blob = `${out}
|
|
985
985
|
${err}`;
|
|
986
|
-
const rateLimited = /429|rate
|
|
986
|
+
const rateLimited = /429|rate.?limit|too many requests/i.test(blob);
|
|
987
|
+
const quotaExhausted = /credit|quota|usage ?cap|usagecap|monthly cap|capexceeded/i.test(blob);
|
|
987
988
|
if (code !== 0) {
|
|
988
|
-
return { success: false, error: (err || out || `exit ${code}`).trim(), rateLimited };
|
|
989
|
+
return { success: false, error: (err || out || `exit ${code}`).trim(), rateLimited, quotaExhausted };
|
|
989
990
|
}
|
|
990
991
|
const parsed = extractJson(out);
|
|
991
992
|
if (parsed === undefined) {
|
|
992
|
-
return { success: false, error: `unparseable output: ${out.slice(0, 200)}`, rateLimited };
|
|
993
|
+
return { success: false, error: `unparseable output: ${out.slice(0, 200)}`, rateLimited, quotaExhausted };
|
|
993
994
|
}
|
|
994
995
|
return { success: true, data: parsed };
|
|
995
996
|
};
|
|
@@ -1119,11 +1120,13 @@ async function enrichExpert(db, e, opts) {
|
|
|
1119
1120
|
const handle = handleFromSocial(e.socials.twitter || "");
|
|
1120
1121
|
const now = new Date().toISOString();
|
|
1121
1122
|
if (!handle)
|
|
1122
|
-
return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false };
|
|
1123
|
+
return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: false };
|
|
1123
1124
|
const { user, result } = await client.xUser(handle);
|
|
1124
1125
|
if (!user) {
|
|
1126
|
+
if (result.quotaExhausted)
|
|
1127
|
+
return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: true };
|
|
1125
1128
|
if (result.rateLimited)
|
|
1126
|
-
return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: true };
|
|
1129
|
+
return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: true, quotaExhausted: false };
|
|
1127
1130
|
db.upsertXProfile({
|
|
1128
1131
|
source: e.source,
|
|
1129
1132
|
sourceId: e.sourceId,
|
|
@@ -1140,7 +1143,7 @@ async function enrichExpert(db, e, opts) {
|
|
|
1140
1143
|
worksOn: "",
|
|
1141
1144
|
enrichedAt: now
|
|
1142
1145
|
});
|
|
1143
|
-
return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false };
|
|
1146
|
+
return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: false };
|
|
1144
1147
|
}
|
|
1145
1148
|
const profile = {
|
|
1146
1149
|
source: e.source,
|
|
@@ -1175,8 +1178,11 @@ async function enrichExpert(db, e, opts) {
|
|
|
1175
1178
|
max: opts.tweetMax ?? 100,
|
|
1176
1179
|
replies: false
|
|
1177
1180
|
});
|
|
1181
|
+
if (tlResult.quotaExhausted && tweets.length === 0) {
|
|
1182
|
+
return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: false, quotaExhausted: true };
|
|
1183
|
+
}
|
|
1178
1184
|
if (tlResult.rateLimited && tweets.length === 0) {
|
|
1179
|
-
return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: true };
|
|
1185
|
+
return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: true, quotaExhausted: false };
|
|
1180
1186
|
}
|
|
1181
1187
|
const cutoff = Date.now() - (opts.sinceDays ?? 30) * 86400000;
|
|
1182
1188
|
const recent = tweets.filter((t) => {
|
|
@@ -1200,7 +1206,35 @@ async function enrichExpert(db, e, opts) {
|
|
|
1200
1206
|
db.replaceTweets(e.source, e.sourceId, rows);
|
|
1201
1207
|
tweetCount = rows.length;
|
|
1202
1208
|
}
|
|
1203
|
-
return { ok: true, notFound: false, tweets: tweetCount, avatar, rateLimited: false };
|
|
1209
|
+
return { ok: true, notFound: false, tweets: tweetCount, avatar, rateLimited: false, quotaExhausted: false };
|
|
1210
|
+
}
|
|
1211
|
+
async function backfillAvatars(db, opts = {}) {
|
|
1212
|
+
const log = opts.onLog ?? (() => {});
|
|
1213
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
1214
|
+
const delayMs = opts.delayMs ?? 150;
|
|
1215
|
+
const experts = db.list({ source: opts.source });
|
|
1216
|
+
const res = { downloaded: 0, skipped: 0, failed: 0 };
|
|
1217
|
+
for (const e of experts) {
|
|
1218
|
+
if (e.avatarLocal || !e.avatar) {
|
|
1219
|
+
res.skipped++;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
try {
|
|
1223
|
+
const path = await downloadAvatar(e.avatar, e, fetchFn);
|
|
1224
|
+
if (path) {
|
|
1225
|
+
db.setAvatarLocal(e.source, e.sourceId, path);
|
|
1226
|
+
res.downloaded++;
|
|
1227
|
+
if (res.downloaded % 100 === 0)
|
|
1228
|
+
log(` avatars: ${res.downloaded} downloaded`);
|
|
1229
|
+
} else {
|
|
1230
|
+
res.failed++;
|
|
1231
|
+
}
|
|
1232
|
+
} catch {
|
|
1233
|
+
res.failed++;
|
|
1234
|
+
}
|
|
1235
|
+
await sleep2(delayMs);
|
|
1236
|
+
}
|
|
1237
|
+
return res;
|
|
1204
1238
|
}
|
|
1205
1239
|
async function enrichX(db, opts = {}) {
|
|
1206
1240
|
const log = opts.onLog ?? (() => {});
|
|
@@ -1226,6 +1260,13 @@ async function enrichX(db, opts = {}) {
|
|
|
1226
1260
|
await sleep2(delayMs);
|
|
1227
1261
|
continue;
|
|
1228
1262
|
}
|
|
1263
|
+
if (outcome.quotaExhausted) {
|
|
1264
|
+
res.stoppedEarly = true;
|
|
1265
|
+
res.reason = "X API credits/quota exhausted \u2014 top up or wait for the monthly reset, then re-run the same command to finish the rest";
|
|
1266
|
+
res.attempted--;
|
|
1267
|
+
log(`X API quota exhausted; stopping at ${i}/${targets.length}. ${res.enriched} enriched so far.`);
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1229
1270
|
if (outcome.rateLimited) {
|
|
1230
1271
|
consecutiveRateLimits++;
|
|
1231
1272
|
if (consecutiveRateLimits >= 3) {
|
|
@@ -1256,7 +1297,7 @@ async function enrichX(db, opts = {}) {
|
|
|
1256
1297
|
}
|
|
1257
1298
|
|
|
1258
1299
|
// src/cli/index.ts
|
|
1259
|
-
var VERSION = "0.0.
|
|
1300
|
+
var VERSION = "0.0.6";
|
|
1260
1301
|
function openDb() {
|
|
1261
1302
|
const opts = program.opts();
|
|
1262
1303
|
return new ExpertsDB(opts.db || defaultDbPath());
|
|
@@ -1320,6 +1361,21 @@ program.command("enrich [source]").description("Enrich experts via X/Twitter: pr
|
|
|
1320
1361
|
console.log(chalk2.dim(`progress: ${after.enriched}/${after.withHandle} enriched`));
|
|
1321
1362
|
db.close();
|
|
1322
1363
|
});
|
|
1364
|
+
program.command("avatars [source]").description("Download + properly name profile pictures for experts missing one").option("--delay <ms>", "delay between downloads", (v) => parseInt(v, 10), 150).action(async (source, cmdOpts) => {
|
|
1365
|
+
const db = openDb();
|
|
1366
|
+
requireData(db);
|
|
1367
|
+
console.error(chalk2.dim("Backfilling profile pictures from source media\u2026"));
|
|
1368
|
+
const res = await backfillAvatars(db, {
|
|
1369
|
+
source,
|
|
1370
|
+
delayMs: cmdOpts.delay,
|
|
1371
|
+
onLog: (m) => process.stderr.write(chalk2.dim(m + `
|
|
1372
|
+
`))
|
|
1373
|
+
});
|
|
1374
|
+
console.log(chalk2.green(`\u2713 ${res.downloaded} avatars downloaded`) + chalk2.dim(` (${res.skipped} already had one or no URL, ${res.failed} failed)`));
|
|
1375
|
+
const total = db.enrichmentStats(source).avatars;
|
|
1376
|
+
console.log(chalk2.dim(`total experts with a named avatar: ${total}`));
|
|
1377
|
+
db.close();
|
|
1378
|
+
});
|
|
1323
1379
|
program.command("tweets <idOrSlug>").description("Show an expert's stored recent tweets").option("-s, --source <name>", "disambiguate by source").option("-n, --limit <n>", "max tweets", (v) => parseInt(v, 10), 10).action((idOrSlug, cmdOpts) => {
|
|
1324
1380
|
const db = openDb();
|
|
1325
1381
|
const e = db.get(idOrSlug, cmdOpts.source);
|
package/dist/connectors.d.ts
CHANGED
|
@@ -10,7 +10,10 @@ export interface ConnectorResult {
|
|
|
10
10
|
success: boolean;
|
|
11
11
|
data?: any;
|
|
12
12
|
error?: string;
|
|
13
|
+
/** Transient 429 / rate-window limit — back off and retry. */
|
|
13
14
|
rateLimited?: boolean;
|
|
15
|
+
/** Hard quota/credit exhaustion — retrying won't help until reset/top-up. */
|
|
16
|
+
quotaExhausted?: boolean;
|
|
14
17
|
}
|
|
15
18
|
/** Runs `connectors run <connector> <args…> --format json`. */
|
|
16
19
|
export type ConnectorRunner = (connector: string, args: string[]) => Promise<ConnectorResult>;
|
package/dist/connectors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["../src/connectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["../src/connectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,+DAA+D;AAC/D,MAAM,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAE9F,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,GAAG,SAAe,GAAG,eAAe,CA0BjE;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAqB7C;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,GAAG,CAAkB;gBAEjB,IAAI,GAAE,uBAA4B;IAI9C,gEAAgE;IAC1D,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC;IAuBvF,oEAAoE;IAC9D,SAAS,CACb,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAO,GACjE,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC;CAwB1D"}
|
package/dist/enrich.d.ts
CHANGED
|
@@ -39,7 +39,25 @@ export declare function enrichExpert(db: ExpertsDB, e: Expert, opts: EnrichOptio
|
|
|
39
39
|
tweets: number;
|
|
40
40
|
avatar: boolean;
|
|
41
41
|
rateLimited: boolean;
|
|
42
|
+
quotaExhausted: boolean;
|
|
42
43
|
}>;
|
|
44
|
+
export interface AvatarBackfillResult {
|
|
45
|
+
downloaded: number;
|
|
46
|
+
skipped: number;
|
|
47
|
+
failed: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Download + properly name profile pictures for every expert that doesn't have
|
|
51
|
+
* a local one yet, using the source's own avatar URL (e.g. intro.co media). This
|
|
52
|
+
* needs no third-party API, so it completes "a named profile picture for every
|
|
53
|
+
* expert" even when X enrichment is unavailable.
|
|
54
|
+
*/
|
|
55
|
+
export declare function backfillAvatars(db: ExpertsDB, opts?: {
|
|
56
|
+
source?: string;
|
|
57
|
+
delayMs?: number;
|
|
58
|
+
fetchFn?: typeof fetch;
|
|
59
|
+
onLog?: (m: string) => void;
|
|
60
|
+
}): Promise<AvatarBackfillResult>;
|
|
43
61
|
/** Resumable, throttled enrichment over all experts with a Twitter handle. */
|
|
44
62
|
export declare function enrichX(db: ExpertsDB, opts?: EnrichOptions): Promise<EnrichResult>;
|
|
45
63
|
//# sourceMappingURL=enrich.d.ts.map
|
package/dist/enrich.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enrich.d.ts","sourceRoot":"","sources":["../src/enrich.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,SAAS,CAAC;AAIvD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC,GAAG,MAAM,CAQxF;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,mFAAmF;AACnF,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC,EAC5D,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,uEAAuE;AACvE,wBAAsB,YAAY,CAChC,EAAE,EAAE,SAAS,EACb,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"enrich.d.ts","sourceRoot":"","sources":["../src/enrich.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,SAAS,CAAC;AAIvD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC,GAAG,MAAM,CAQxF;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,mFAAmF;AACnF,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC,EAC5D,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,uEAAuE;AACvE,wBAAsB,YAAY,CAChC,EAAE,EAAE,SAAS,EACb,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,CAAC,CAyE7H;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,SAAS,EACb,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GACpG,OAAO,CAAC,oBAAoB,CAAC,CA0B/B;AAED,8EAA8E;AAC9E,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAqE5F"}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { crawlSource, type CrawlResult } from "./crawl";
|
|
|
10
10
|
export { getSource, listSources, registerSource, IntroSource } from "./sources";
|
|
11
11
|
export { IntroClient } from "./sources/intro-api";
|
|
12
12
|
export { normalizeIntroExpert, slugFromUrl } from "./sources/intro";
|
|
13
|
-
export { enrichX, enrichExpert, downloadAvatar, avatarBasename, handleFromSocial, type EnrichOptions, type EnrichResult } from "./enrich";
|
|
13
|
+
export { enrichX, enrichExpert, backfillAvatars, downloadAvatar, avatarBasename, handleFromSocial, type EnrichOptions, type EnrichResult, type AvatarBackfillResult } from "./enrich";
|
|
14
14
|
export { ConnectorsClient, defaultRunner, extractJson, type ConnectorRunner, type ConnectorResult } from "./connectors";
|
|
15
15
|
export { inferTags, expertText } from "./graph";
|
|
16
16
|
export * as format from "./format";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACtL,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACxH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -855,13 +855,14 @@ function defaultRunner(bin = "connectors") {
|
|
|
855
855
|
]);
|
|
856
856
|
const blob = `${out}
|
|
857
857
|
${err}`;
|
|
858
|
-
const rateLimited = /429|rate
|
|
858
|
+
const rateLimited = /429|rate.?limit|too many requests/i.test(blob);
|
|
859
|
+
const quotaExhausted = /credit|quota|usage ?cap|usagecap|monthly cap|capexceeded/i.test(blob);
|
|
859
860
|
if (code !== 0) {
|
|
860
|
-
return { success: false, error: (err || out || `exit ${code}`).trim(), rateLimited };
|
|
861
|
+
return { success: false, error: (err || out || `exit ${code}`).trim(), rateLimited, quotaExhausted };
|
|
861
862
|
}
|
|
862
863
|
const parsed = extractJson(out);
|
|
863
864
|
if (parsed === undefined) {
|
|
864
|
-
return { success: false, error: `unparseable output: ${out.slice(0, 200)}`, rateLimited };
|
|
865
|
+
return { success: false, error: `unparseable output: ${out.slice(0, 200)}`, rateLimited, quotaExhausted };
|
|
865
866
|
}
|
|
866
867
|
return { success: true, data: parsed };
|
|
867
868
|
};
|
|
@@ -991,11 +992,13 @@ async function enrichExpert(db, e, opts) {
|
|
|
991
992
|
const handle = handleFromSocial(e.socials.twitter || "");
|
|
992
993
|
const now = new Date().toISOString();
|
|
993
994
|
if (!handle)
|
|
994
|
-
return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false };
|
|
995
|
+
return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: false };
|
|
995
996
|
const { user, result } = await client.xUser(handle);
|
|
996
997
|
if (!user) {
|
|
998
|
+
if (result.quotaExhausted)
|
|
999
|
+
return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: true };
|
|
997
1000
|
if (result.rateLimited)
|
|
998
|
-
return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: true };
|
|
1001
|
+
return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: true, quotaExhausted: false };
|
|
999
1002
|
db.upsertXProfile({
|
|
1000
1003
|
source: e.source,
|
|
1001
1004
|
sourceId: e.sourceId,
|
|
@@ -1012,7 +1015,7 @@ async function enrichExpert(db, e, opts) {
|
|
|
1012
1015
|
worksOn: "",
|
|
1013
1016
|
enrichedAt: now
|
|
1014
1017
|
});
|
|
1015
|
-
return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false };
|
|
1018
|
+
return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: false };
|
|
1016
1019
|
}
|
|
1017
1020
|
const profile = {
|
|
1018
1021
|
source: e.source,
|
|
@@ -1047,8 +1050,11 @@ async function enrichExpert(db, e, opts) {
|
|
|
1047
1050
|
max: opts.tweetMax ?? 100,
|
|
1048
1051
|
replies: false
|
|
1049
1052
|
});
|
|
1053
|
+
if (tlResult.quotaExhausted && tweets.length === 0) {
|
|
1054
|
+
return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: false, quotaExhausted: true };
|
|
1055
|
+
}
|
|
1050
1056
|
if (tlResult.rateLimited && tweets.length === 0) {
|
|
1051
|
-
return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: true };
|
|
1057
|
+
return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: true, quotaExhausted: false };
|
|
1052
1058
|
}
|
|
1053
1059
|
const cutoff = Date.now() - (opts.sinceDays ?? 30) * 86400000;
|
|
1054
1060
|
const recent = tweets.filter((t) => {
|
|
@@ -1072,7 +1078,35 @@ async function enrichExpert(db, e, opts) {
|
|
|
1072
1078
|
db.replaceTweets(e.source, e.sourceId, rows);
|
|
1073
1079
|
tweetCount = rows.length;
|
|
1074
1080
|
}
|
|
1075
|
-
return { ok: true, notFound: false, tweets: tweetCount, avatar, rateLimited: false };
|
|
1081
|
+
return { ok: true, notFound: false, tweets: tweetCount, avatar, rateLimited: false, quotaExhausted: false };
|
|
1082
|
+
}
|
|
1083
|
+
async function backfillAvatars(db, opts = {}) {
|
|
1084
|
+
const log = opts.onLog ?? (() => {});
|
|
1085
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
1086
|
+
const delayMs = opts.delayMs ?? 150;
|
|
1087
|
+
const experts = db.list({ source: opts.source });
|
|
1088
|
+
const res = { downloaded: 0, skipped: 0, failed: 0 };
|
|
1089
|
+
for (const e of experts) {
|
|
1090
|
+
if (e.avatarLocal || !e.avatar) {
|
|
1091
|
+
res.skipped++;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
const path = await downloadAvatar(e.avatar, e, fetchFn);
|
|
1096
|
+
if (path) {
|
|
1097
|
+
db.setAvatarLocal(e.source, e.sourceId, path);
|
|
1098
|
+
res.downloaded++;
|
|
1099
|
+
if (res.downloaded % 100 === 0)
|
|
1100
|
+
log(` avatars: ${res.downloaded} downloaded`);
|
|
1101
|
+
} else {
|
|
1102
|
+
res.failed++;
|
|
1103
|
+
}
|
|
1104
|
+
} catch {
|
|
1105
|
+
res.failed++;
|
|
1106
|
+
}
|
|
1107
|
+
await sleep2(delayMs);
|
|
1108
|
+
}
|
|
1109
|
+
return res;
|
|
1076
1110
|
}
|
|
1077
1111
|
async function enrichX(db, opts = {}) {
|
|
1078
1112
|
const log = opts.onLog ?? (() => {});
|
|
@@ -1098,6 +1132,13 @@ async function enrichX(db, opts = {}) {
|
|
|
1098
1132
|
await sleep2(delayMs);
|
|
1099
1133
|
continue;
|
|
1100
1134
|
}
|
|
1135
|
+
if (outcome.quotaExhausted) {
|
|
1136
|
+
res.stoppedEarly = true;
|
|
1137
|
+
res.reason = "X API credits/quota exhausted \u2014 top up or wait for the monthly reset, then re-run the same command to finish the rest";
|
|
1138
|
+
res.attempted--;
|
|
1139
|
+
log(`X API quota exhausted; stopping at ${i}/${targets.length}. ${res.enriched} enriched so far.`);
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1101
1142
|
if (outcome.rateLimited) {
|
|
1102
1143
|
consecutiveRateLimits++;
|
|
1103
1144
|
if (consecutiveRateLimits >= 3) {
|
|
@@ -1768,6 +1809,7 @@ export {
|
|
|
1768
1809
|
defaultRunner,
|
|
1769
1810
|
defaultDbPath,
|
|
1770
1811
|
crawlSource,
|
|
1812
|
+
backfillAvatars,
|
|
1771
1813
|
avatarBasename,
|
|
1772
1814
|
IntroSource,
|
|
1773
1815
|
IntroClient,
|
package/package.json
CHANGED