Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions bot/spellblock-mentions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* 4. Save last-seen tweet ID so we don't re-process
*/

import { readFileSync, writeFileSync, existsSync } from 'fs';
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
Expand All @@ -23,6 +23,50 @@ const LOG_PREFIX = '[spellblock-mentions]';
const PAYMENT_BASE = 'https://spellblock.app/enter';
const BOT_HANDLE = 'ClawdiaBotAI';

// ── Twitter Usage Cap Guard ───────────────────────────────────────────
const CAP_FLAG = join(process.env.HOME, 'clawd/data/twitter-cap-exceeded.flag');

/**
* Returns true if the monthly usage cap flag is set and not yet expired.
*/
function isCapFlagActive() {
if (!existsSync(CAP_FLAG)) return false;
try {
const expiry = new Date(readFileSync(CAP_FLAG, 'utf8').trim());
if (isNaN(expiry.getTime())) return false; // malformed — ignore
if (new Date() < expiry) return true;
// Flag expired — clear it
unlinkSync(CAP_FLAG);
return false;
} catch {
return false;
}
}

/**
* Write the cap flag with expiry = 1st of next month, 00:00 UTC.
*/
function writeCapFlag() {
const now = new Date();
const expiry = new Date(Date.UTC(
now.getUTCMonth() === 11 ? now.getUTCFullYear() + 1 : now.getUTCFullYear(),
now.getUTCMonth() === 11 ? 0 : now.getUTCMonth() + 1,
1
));
writeFileSync(CAP_FLAG, expiry.toISOString());
log(`⚠️ UsageCapExceeded — flag written, skipping until ${expiry.toISOString().slice(0, 10)}`);
}

/**
* Clear the cap flag on a successful API call.
*/
function clearCapFlag() {
if (existsSync(CAP_FLAG)) {
unlinkSync(CAP_FLAG);
log('✅ Cap flag cleared (successful API response)');
}
}

// ── Auth ──────────────────────────────────────────────────────────────

function getToken() {
Expand Down Expand Up @@ -73,11 +117,28 @@ async function apiGet(path, params = {}) {
const res2 = await fetch(url, {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!res2.ok) throw new Error(`Twitter API ${res2.status}: ${await res2.text()}`);
if (!res2.ok) {
const body2 = await res2.text();
if (body2.includes('UsageCapExceeded')) {
writeCapFlag();
throw new Error(`UsageCapExceeded: Twitter monthly cap hit — ${body2.slice(0, 120)}`);
}
throw new Error(`Twitter API ${res2.status}: ${body2}`);
}
clearCapFlag();
return res2.json();
}

if (!res.ok) throw new Error(`Twitter API ${res.status}: ${await res.text()}`);
if (!res.ok) {
const body = await res.text();
// Detect monthly usage cap — write flag so future runs skip immediately
if (body.includes('UsageCapExceeded')) {
writeCapFlag();
throw new Error(`UsageCapExceeded: Twitter monthly cap hit — ${body.slice(0, 120)}`);
}
throw new Error(`Twitter API ${res.status}: ${body}`);
}
clearCapFlag();
return res.json();
}

Expand Down Expand Up @@ -120,6 +181,12 @@ function log(...args) {
async function main() {
log('Starting mention scan');

// 0. Pre-flight: skip if Twitter monthly cap is active
if (isCapFlagActive()) {
log('⏭️ Twitter monthly cap flag active — skipping scan (resets on 1st of month)');
return;
}

// 1. Load current round
const round = await db.getCurrentRound();
if (!round) {
Expand Down