Consolidate schedule config, support multi-post per day
- Schedule config moved to nested schedule object in prompts.json - Scheduler picks 1-3 random times for personality with 4hr min gap - shouldRun reads from schedule object Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
14
bot.js
14
bot.js
@@ -28,18 +28,16 @@ function saveHistory(name, history) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldRun(promptConfig) {
|
function shouldRun(promptConfig) {
|
||||||
|
const schedule = promptConfig.schedule;
|
||||||
|
if (!schedule || schedule.type === "daily") return true;
|
||||||
|
|
||||||
|
if (schedule.type === "random" && schedule.minDays) {
|
||||||
const history = loadHistory(promptConfig.name);
|
const history = loadHistory(promptConfig.name);
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
if (promptConfig.frequency === "daily") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (promptConfig.frequency === "random" && promptConfig.minDays) {
|
|
||||||
if (history.length > 0) {
|
if (history.length > 0) {
|
||||||
|
const now = new Date();
|
||||||
const lastPost = new Date(history[history.length - 1].date);
|
const lastPost = new Date(history[history.length - 1].date);
|
||||||
const daysSinceLast = (now - lastPost) / (1000 * 60 * 60 * 24);
|
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
|
// Random chance so it doesn't always fire on the minimum day
|
||||||
return Math.random() < 0.5;
|
return Math.random() < 0.5;
|
||||||
|
|||||||
27
prompts.json
27
prompts.json
@@ -2,19 +2,32 @@
|
|||||||
{
|
{
|
||||||
"name": "tease",
|
"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 <link> on its own line.",
|
"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 <link> on its own line.",
|
||||||
"frequency": "random",
|
|
||||||
"startHour": 8,
|
|
||||||
"endHour": 20,
|
|
||||||
"minDays": 2,
|
|
||||||
"links": {
|
"links": {
|
||||||
"link": "https://onlyfans.com/juniper_sky"
|
"link": "https://onlyfans.com/juniper_sky"
|
||||||
|
},
|
||||||
|
"schedule": {
|
||||||
|
"type": "random",
|
||||||
|
"window": [
|
||||||
|
8,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"minDays": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "personality",
|
"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.",
|
"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",
|
"schedule": {
|
||||||
"startHour": 8,
|
"type": "daily",
|
||||||
"endHour": 20
|
"window": [
|
||||||
|
8,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"postsPerDay": [
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"minGapHours": 4
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
83
scheduler.js
83
scheduler.js
@@ -19,33 +19,72 @@ if (!config) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick a random time in the window
|
const schedule = config.schedule;
|
||||||
const windowMinutes = (config.endHour - config.startHour) * 60;
|
const [startHour, endHour] = schedule.window;
|
||||||
const delayMinutes = Math.floor(Math.random() * windowMinutes);
|
const minGap = (schedule.minGapHours || 4) * 60;
|
||||||
const hours = Math.floor(delayMinutes / 60) + config.startHour;
|
|
||||||
const minutes = delayMinutes % 60;
|
|
||||||
|
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
function randomMinuteInWindow() {
|
||||||
console.log(`[${promptName}] Scheduled for ${pad(hours)}:${pad(minutes)} PST`);
|
const windowMinutes = (endHour - startHour) * 60;
|
||||||
|
return Math.floor(Math.random() * windowMinutes);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
execFileSync("node", [join(__dirname, "bot.js"), promptName], { stdio: "inherit" });
|
execFileSync("node", [join(__dirname, "bot.js"), promptName], { stdio: "inherit" });
|
||||||
} catch {
|
} 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user