diff --git a/bot.js b/bot.js index 890aad6..a669fa9 100644 --- a/bot.js +++ b/bot.js @@ -28,18 +28,16 @@ function saveHistory(name, history) { } function shouldRun(promptConfig) { - const history = loadHistory(promptConfig.name); - const now = new Date(); + const schedule = promptConfig.schedule; + if (!schedule || schedule.type === "daily") return true; - if (promptConfig.frequency === "daily") { - return true; - } - - if (promptConfig.frequency === "random" && promptConfig.minDays) { + if (schedule.type === "random" && schedule.minDays) { + const history = loadHistory(promptConfig.name); if (history.length > 0) { + const now = new Date(); const lastPost = new Date(history[history.length - 1].date); const daysSinceLast = (now - lastPost) / (1000 * 60 * 60 * 24); - if (daysSinceLast < promptConfig.minDays) return false; + if (daysSinceLast < schedule.minDays) return false; } // Random chance so it doesn't always fire on the minimum day return Math.random() < 0.5; diff --git a/prompts.json b/prompts.json index ba7c43c..d3f4285 100644 --- a/prompts.json +++ b/prompts.json @@ -2,19 +2,32 @@ { "name": "tease", "prompt": "You are writing a tweet for a woman who is quietly building a creative portfolio and wants people to visit her site out of curiosity.\n\nTone: casual, understated, real. Like she is just talking \u2014 not performing, not selling, not trying too hard. No hashtags. No emojis unless they feel completely natural. Never use the word \"model\" or any fashion/influencer language.\n\nThe goal is to make someone curious enough to click the link. The best tweets hint at something without explaining it. They feel personal but not oversharing. Confident but not loud.\n\nGenerate 1 tweet. Keep it under 280 characters. Just the tweet, nothing else.\n\nEnd the tweet with on its own line.", - "frequency": "random", - "startHour": 8, - "endHour": 20, - "minDays": 2, "links": { "link": "https://onlyfans.com/juniper_sky" + }, + "schedule": { + "type": "random", + "window": [ + 8, + 20 + ], + "minDays": 2 } }, { "name": "personality", "prompt": "You are writing a tweet for a woman in her late 20s to late 30s living in the Pacific Northwest. She reads constantly, makes pottery, spends time in the woods, and has a declining interest in being perceived.\n\nTone: dry, witty, slightly feral, cozy. Dark humor, sarcastic observations, occasional leftist commentary. Never preachy. Never try-hard. Feels like something she muttered to herself and decided to post.\n\nNo hashtags. No emojis unless they earn it. No links. No advertising. Nothing aspirational or girlboss. Just something real that makes someone think \"oh that is me.\"\n\nGenerate 1 tweet. Just the tweet. Nothing else.", - "frequency": "daily", - "startHour": 8, - "endHour": 20 + "schedule": { + "type": "daily", + "window": [ + 8, + 20 + ], + "postsPerDay": [ + 1, + 3 + ], + "minGapHours": 4 + } } ] diff --git a/scheduler.js b/scheduler.js index b91cebc..8c4ea32 100644 --- a/scheduler.js +++ b/scheduler.js @@ -19,33 +19,72 @@ if (!config) { process.exit(1); } -// Pick a random time in the window -const windowMinutes = (config.endHour - config.startHour) * 60; -const delayMinutes = Math.floor(Math.random() * windowMinutes); -const hours = Math.floor(delayMinutes / 60) + config.startHour; -const minutes = delayMinutes % 60; +const schedule = config.schedule; +const [startHour, endHour] = schedule.window; +const minGap = (schedule.minGapHours || 4) * 60; -const pad = (n) => String(n).padStart(2, "0"); -console.log(`[${promptName}] Scheduled for ${pad(hours)}:${pad(minutes)} PST`); - -const now = new Date( - new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" }) -); -const target = new Date(now); -target.setHours(hours, minutes, 0, 0); - -let delayMs = target.getTime() - now.getTime(); -if (delayMs <= 0) { - console.log("Target time passed. Posting now."); - delayMs = 0; +function randomMinuteInWindow() { + const windowMinutes = (endHour - startHour) * 60; + return Math.floor(Math.random() * windowMinutes); } -console.log(`Waiting ${Math.round(delayMs / 60000)} minutes...`); +function pickSpacedTimes(count) { + for (let attempt = 0; attempt < 100; attempt++) { + const times = Array.from({ length: count }, () => randomMinuteInWindow()).sort((a, b) => a - b); + let valid = true; + for (let i = 1; i < times.length; i++) { + if (times[i] - times[i - 1] < minGap) { + valid = false; + break; + } + } + if (valid) return times; + } + const window = (endHour - startHour) * 60; + const gap = Math.floor(window / (count + 1)); + return Array.from({ length: count }, (_, i) => gap * (i + 1)); +} -setTimeout(() => { +function nowPST() { + return new Date(new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" })); +} + +function runBot() { try { execFileSync("node", [join(__dirname, "bot.js"), promptName], { stdio: "inherit" }); } catch { - process.exit(1); + console.error(`[${promptName}] bot.js failed`); } -}, delayMs); +} + +const [minPerDay, maxPerDay] = schedule.postsPerDay || [1, 1]; +const count = Math.floor(Math.random() * (maxPerDay - minPerDay + 1)) + minPerDay; +const times = pickSpacedTimes(count); + +const pad = (n) => String(n).padStart(2, "0"); +const scheduled = times.map((m) => `${pad(Math.floor(m / 60) + startHour)}:${pad(m % 60)}`); +console.log(`[${promptName}] ${count} post(s) scheduled at: ${scheduled.join(", ")} PST`); + +function scheduleNext(index) { + if (index >= times.length) return; + + const minutes = times[index]; + const hours = Math.floor(minutes / 60) + startHour; + const mins = minutes % 60; + + const now = nowPST(); + const target = new Date(now); + target.setHours(hours, mins, 0, 0); + + let delayMs = target.getTime() - now.getTime(); + if (delayMs < 0) delayMs = 0; + + console.log(`[${promptName}] #${index + 1} in ${Math.round(delayMs / 60000)} minutes`); + + setTimeout(() => { + runBot(); + scheduleNext(index + 1); + }, delayMs); +} + +scheduleNext(0);