// ==UserScript==
// @name Instagram Sample Bot (Adaptive Scroll, Sample & Like)
// @namespace http://tampermonkey.net/
// @version 1.32
// @description Automates Instagram scrolling: fluid in foreground, jumpy in background. Detects and filters sample offers by popular beauty brands, and auto-likes relevant posts. Includes customizable settings and improved UI. Now with in-app Help and Update options in settings, improved modal visibility, fluid UI panel animations, intelligent loading detection, and draggable UI confined to viewport.
// @author dprits419
// @match https://www.instagram.com/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(function() {
'use strict';
// IMPORTANT: Make sure this version matches the @version in the header!
const SCRIPT_VERSION = "1.32";
// --- IMPORTANT: CONFIGURE THESE URLs IF YOU HOST YOUR SCRIPT ---
// This is the URL where your raw .user.js script is hosted.
// When the "Update Userscript" button is clicked, it opens this URL.
// Tampermonkey will detect the .user.js file and prompt for update if a newer version is available.
const SCRIPT_INSTALL_URL = 'https://raw.githubusercontent.com/dprits419/Tampermonkey-Scripts/main/instagram_bot.user.js'; // EXAMPLE: Replace with your actual GitHub Gist raw URL or Greasy Fork URL
// This is the URL for a help page or README for your script.
// NOTE: This URL is now primarily for reference, as help is shown in-app.
const SCRIPT_HELP_URL = 'https://github.com/dprits419/Tampermonkey-Scripts/blob/main/instagram_bot_README.md'; // EXAMPLE: Replace with your actual help/documentation URL
// --- END CONFIGURATION ---
// Processing parameters (these are not customizable via UI for now)
const processPostsInterval = 2500; // How often to process posts (liking, sample detection)
const endOfFeedConfirmDelay = 1500; // Milliseconds to confirm end of feed after hitting bottom
console.log(`Instagram Bot script loaded! (Version ${SCRIPT_VERSION} - Draggable UI Confined to Viewport)`);
// Global settings object with defaults
let settings = {
pixelsPerFrame: 4, // For fluid scrolling (foreground)
jumpyScrollInterval: 1000, // How often to jump in jumpy mode (background)
enableLiking: true, // Toggle auto-liking
enableSampleDetection: true, // Toggle sample offer detection
minConfidenceThreshold: 1.5, // Confidence required for sample detection
acceptOnlyPopularBrands: false, // Filter sample offers by popular brands
};
// Global state for bot activity
let scrolling = false;
// References to the two types of scrolling mechanisms
let fluidAnimationFrameId = null; // For requestAnimationFrame
let jumpyScrollIntervalId = null; // For setInterval (background)
let processPostsIntervalId = null; // Separate interval for processing posts
// Track last known scroll positions for end-of-feed detection
let lastScrollY = 0;
let lastScrollHeight = 0;
// --- Variables for Stuck/Loading Detection ---
let stuckDetectionIntervalId = null; // For hard refresh
let lastEffectiveScrollTime = Date.now(); // Timestamp of last *effective* scroll change (for hard stuck detection)
const STUCK_TIMEOUT_MS = 30 * 1000; // 30 seconds without *any* scroll progress before full refresh
const AUTO_START_FLAG = 'instagramBotAutoStartAfterReload'; // Flag for localStorage
let noScrollProgressTimeout = null; // Timeout for initial detection of no scroll progress
const NO_SCROLL_PROGRESS_WARN_DELAY = 3 * 1000; // 3 seconds without scroll progress to warn/slow down initially
let isTemporarilyStuckLoading = false; // Flag to indicate if we're in the slow-down state
let originalPixelsPerFrame = settings.pixelsPerFrame; // Store original for restoration
let originalJumpyScrollInterval = settings.jumpyScrollInterval; // Store original for restoration
const SLOW_SCROLL_FACTOR_FLUID = 0.5; // Reduce fluid speed to 50%
const SLOW_SCROLL_FACTOR_JUMPY = 2.0; // Double jumpy interval (half speed)
// Keywords for Sample Detection
// General sample-related keywords (will be used with negative phrases for context)
const sampleKeywords = ['free', 'sample', 'samples', 'offer', 'deal', 'giveaway', 'trial', 'complimentary', 'discount', 'coupon', 'win', 'contest', 'promo', 'gift'];
// Phrases that strongly indicate an offer (higher confidence)
const strongOfferPhrases = [
'get your free', 'claim your free', 'win a free', 'enter to win',
'get your sample', 'claim your sample', 'free product', 'free gift',
'limited time offer', 'exclusive offer', 'sign up for free', 'redeem your', 'grab your'
];
const sampleActionTexts = ['sign up', 'learn more', 'shop now', 'claim here', 'get sample', 'redeem', 'click here', 'get offer', 'apply now'];
// Negative keywords/phrases to reduce false positives by penalizing confidence
// These are very specific non-offer contexts for 'free' or 'sample'.
const negativeSamplePhrases = [
'cruelty-free', 'free from', 'gluten-free', 'sugar-free', 'dairy-free', 'ad-free',
'sample size', 'sample of work', 'free to use', 'free to download', 'free trial',
'sample chapter', 'sample lesson', 'sample video', 'sample audio', 'sample data',
'sample image', 'sample text', 'free consultation', 'free estimate'
];
// Keywords for Auto-Liking
const cologneKeywords = ['cologne', 'fragrance', 'perfume', 'scent', 'eau de parfum', 'eau de toilette'];
const beautyKeywords = [
'skincare', 'makeup', 'cosmetics', 'beauty', 'haircare',
'dior', 'chanel', 'sephora', 'ulta', 'fenty', 'kylie cosmetics', 'nars', 'mac cosmetics',
'glossier', 'rare beauty', 'lancome', 'este lauder', 'clinique', 'shiseido',
'hourglass', 'charlotte tilly', 'tatcha', 'kiehl\'s', 'cerave', 'la roche-posay',
'olaplex', 'sol de janeiro', 'lip gloss', 'mascara', 'eyeshadow', 'blush', 'foundation', 'concealer',
'serum', 'moisturizer', 'cleanser', 'sunscreen'
];
// List of popular beauty brands for filtering
const popularBeautyBrands = [
'sephora', 'ulta', 'dior', 'chanel', 'fenty beauty', 'kylie cosmetics', 'nars', 'mac cosmetics',
'glossier', 'rare beauty', 'lancome', 'este lauder', 'clinique', 'shiseido', 'hourglass',
'charlotte tilly', 'tatcha', 'kiehl\'s', 'cerave', 'la roche-posay', 'olaplex', 'sol de janeiro',
'anastasia beverly hills', 'too faced', 'urban decay', 'benefit cosmetics', 'tarte', 'it cosmetics',
'fresh', 'sunday riley', 'drunk elephant', 'summer fridays', 'glow recipe', 'paula\'s choice',
'the ordinary', 'skinfix', 'dr. jart+', 'biossance', 'supergoop', 'milk makeup', 'kosas', 'saie',
'tower 28', 'innisfree', 'laneige', 'dr. brandt', 'peter thomas roth', 'first aid beauty', 'farmacy',
'youth to the people', 'herbivore botanicals', 'origins', 'clarins', 'guerlain', 'sisley', 'la mer',
'skinceuticals', 'obagi', 'elizabeth arden', 'aveeno', 'neutrogena', 'cetaphil', 'vichy', 'laroche-posay'
];
let statusBarElement; // Reference to the status bar element
// --- Settings Management ---
function loadSettings() {
try {
const savedSettings = JSON.parse(localStorage.getItem('instagramBotSettings'));
if (savedSettings) {
// Merge saved settings with defaults to ensure new settings are added but old ones persist
settings = { ...settings, ...savedSettings };
}
// Update original values based on loaded settings
originalPixelsPerFrame = settings.pixelsPerFrame;
originalJumpyScrollInterval = settings.jumpyScrollInterval;
console.log("Settings loaded:", settings);
} catch (e) {
console.error("Error loading settings from localStorage:", e);
}
}
function saveSettings() {
try {
localStorage.setItem('instagramBotSettings', JSON.stringify(settings));
updateStatus("Settings saved!", 'var(--primary-color)');
console.log("Settings saved:", settings);
// Also update original values if settings are saved
originalPixelsPerFrame = settings.pixelsPerFrame;
originalJumpyScrollInterval = settings.jumpyScrollInterval;
} catch (e) {
console.error("Error saving settings to localStorage:", e);
updateStatus("Failed to save settings!", 'var(--danger-color)');
}
}
// --- Utility Functions ---
// Helper to update the status bar
function updateStatus(message, color = 'var(--text-color, #333)') {
if (statusBarElement) {
statusBarElement.innerText = message;
statusBarElement.style.color = color;
}
console.log("STATUS: " + message);
}
// Helper for delays
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Helper to detect if a post is sponsored
function isPostSponsored(postElement) {
const sponsoredIndicator = postElement.querySelector('span[aria-label="Sponsored"], div[aria-label="Sponsored"], [data-testid="sponsored-label"], span:-webkit-any(span, div)[tabindex="-1"][role="button"]');
if (sponsoredIndicator && (sponsoredIndicator.innerText.toLowerCase().includes('sponsored') || sponsoredIndicator.getAttribute('aria-label') === 'Sponsored')) {
return true;
}
return false;
}
// --- Liking Logic ---
async function likePost(postElement) {
const unlikeButton = postElement.querySelector('svg[aria-label="Unlike"]')?.closest('button');
if (unlikeButton) {
return false; // Already liked
}
const likeButton = postElement.querySelector('svg[aria-label="Like"]')?.closest('button');
if (likeButton) {
likeButton.click();
return true; // Successfully clicked like button
}
return false; // Like button not found
}
// --- Scrolling Mode Management ---
// Starts continuous, fluid scrolling using requestAnimationFrame
function startFluidScrolling() {
if (fluidAnimationFrameId) return; // Already fluid scrolling
// Ensure jumpy scrolling is stopped
if (jumpyScrollIntervalId) {
clearInterval(jumpyScrollIntervalId);
jumpyScrollIntervalId = null;
}
function animateScroll() {
if (!scrolling) return; // Stop if scrolling is cancelled
const currentScrollY = window.scrollY;
const currentScrollHeight = document.documentElement.scrollHeight;
if (currentScrollY === lastScrollY && currentScrollHeight === lastScrollHeight) {
// No scroll progress
if (!noScrollProgressTimeout) {
// Start a timeout to detect prolonged lack of scroll progress
noScrollProgressTimeout = setTimeout(() => {
if (window.scrollY === lastScrollY && document.documentElement.scrollHeight === lastScrollHeight) {
// Still no scroll progress after initial delay, slow down
isTemporarilyStuckLoading = true;
settings.pixelsPerFrame = Math.max(1, Math.round(originalPixelsPerFrame * SLOW_SCROLL_FACTOR_FLUID));
updateStatus(`Slowing for loading (${settings.pixelsPerFrame}px/frame)...`, 'orange');
}
noScrollProgressTimeout = null; // Clear this timeout regardless if it triggered slow down or not
}, NO_SCROLL_PROGRESS_WARN_DELAY);
}
// If we are in a slowed-down state, or just detected no scroll progress, don't scroll further yet.
// We wait for scrollHeight to change or the stuck detection to trigger a refresh.
if (isTemporarilyStuckLoading) {
fluidAnimationFrameId = requestAnimationFrame(animateScroll); // Keep animation loop alive but don't scroll
return;
}
} else {
// Scroll progress made
clearTimeout(noScrollProgressTimeout);
noScrollProgressTimeout = null;
// If we were temporarily stuck, reset to original speed
if (isTemporarilyStuckLoading) {
isTemporarilyStuckLoading = false;
settings.pixelsPerFrame = originalPixelsPerFrame;
updateStatus("Resuming normal scroll speed...", 'var(--primary-color, #4CAF50)');
}
}
// Only scroll if not in a temporarily stuck loading state
if (!isTemporarilyStuckLoading) {
window.scrollBy(0, settings.pixelsPerFrame); // Use current (potentially modified) setting
}
lastScrollY = currentScrollY;
lastScrollHeight = currentScrollHeight;
fluidAnimationFrameId = requestAnimationFrame(animateScroll);
}
fluidAnimationFrameId = requestAnimationFrame(animateScroll);
updateStatus("Scrolling feed fluidly...", 'var(--primary-color, #4CAF50)');
}
// Stops fluid scrolling
function stopFluidScrolling() {
if (fluidAnimationFrameId) {
cancelAnimationFrame(fluidAnimationFrameId);
fluidAnimationFrameId = null;
}
clearTimeout(noScrollProgressTimeout); // Clear loading timeout if stopping
noScrollProgressTimeout = null;
if (isTemporarilyStuckLoading) { // Reset speed if it was slowed down
settings.pixelsPerFrame = originalPixelsPerFrame;
isTemporarilyStuckLoading = false;
}
}
// Starts jumpy scrolling using setInterval (for background tabs)
function startJumpyScrolling() {
if (jumpyScrollIntervalId) return; // Already jumpy scrolling
// Ensure fluid scrolling is stopped
if (fluidAnimationFrameId) {
cancelAnimationFrame(fluidAnimationFrameId);
fluidAnimationFrameId = null;
}
jumpyScrollIntervalId = setInterval(() => {
if (!scrolling) {
clearInterval(jumpyScrollIntervalId);
jumpyScrollIntervalId = null;
return;
}
const currentScrollHeight = document.documentElement.scrollHeight;
if (currentScrollHeight === lastScrollHeight) {
// No scroll progress
if (!noScrollProgressTimeout) {
noScrollProgressTimeout = setTimeout(() => {
if (document.documentElement.scrollHeight === lastScrollHeight) {
isTemporarilyStuckLoading = true;
settings.jumpyScrollInterval = Math.round(originalJumpyScrollInterval * SLOW_SCROLL_FACTOR_JUMPY);
updateStatus(`Slowing for loading (${settings.jumpyScrollInterval}ms interval)...`, 'orange');
// To apply new interval, restart the interval
clearInterval(jumpyScrollIntervalId);
jumpyScrollIntervalId = null;
startJumpyScrolling(); // Call itself to restart with new interval
}
noScrollProgressTimeout = null;
}, NO_SCROLL_PROGRESS_WARN_DELAY);
}
// If we are in a slowed-down state, or just detected no scroll progress, don't jump further yet.
if (isTemporarilyStuckLoading) {
// Do nothing, the interval will just keep firing at the new (slower) rate, waiting for scrollHeight to change.
return;
}
} else {
// Scroll progress made
clearTimeout(noScrollProgressTimeout);
noScrollProgressTimeout = null;
// If we were temporarily stuck, reset to original speed
if (isTemporarilyStuckLoading) {
isTemporarilyStuckLoading = false;
settings.jumpyScrollInterval = originalJumpyScrollInterval;
updateStatus("Resuming normal scroll speed...", 'var(--primary-color, #4CAF50)');
// To apply new interval, restart the interval
clearInterval(jumpyScrollIntervalId);
jumpyScrollIntervalId = null;
startJumpyScrolling(); // Call itself to restart with original interval
}
}
// Only scroll if not in a temporarily stuck loading state
if (!isTemporarilyStuckLoading) {
window.scrollTo({ top: currentScrollHeight, behavior: 'instant' });
}
lastScrollHeight = currentScrollHeight;
}, settings.jumpyScrollInterval); // Use current (potentially modified) setting
updateStatus("Scrolling feed (jumpy in background)...", 'var(--primary-color, #4CAF50)');
}
// Stops jumpy scrolling
function stopJumpyScrolling() {
if (jumpyScrollIntervalId) {
clearInterval(jumpyScrollIntervalId);
jumpyScrollIntervalId = null;
}
clearTimeout(noScrollProgressTimeout); // Clear loading timeout if stopping
noScrollProgressTimeout = null;
if (isTemporarilyStuckLoading) { // Reset speed if it was slowed down
settings.jumpyScrollInterval = originalJumpyScrollInterval;
isTemporarilyStuckLoading = false;
}
}
// --- Stuck Detection Logic (for full page refresh - ultimate fallback) ---
let lastKnownScrollYForStuckDetection = 0;
let lastKnownScrollHeightForStuckDetection = 0;
function startStuckDetection() {
if (stuckDetectionIntervalId) clearInterval(stuckDetectionIntervalId); // Clear any existing interval
// Initialize last known scroll positions and time when detection starts
lastKnownScrollYForStuckDetection = window.scrollY;
lastKnownScrollHeightForStuckDetection = document.documentElement.scrollHeight;
lastEffectiveScrollTime = Date.now(); // Reset time when detection starts
stuckDetectionIntervalId = setInterval(() => {
if (!scrolling) { // Only check if the bot is actively running
clearInterval(stuckDetectionIntervalId);
stuckDetectionIntervalId = null;
return;
}
const currentScrollY = window.scrollY;
const currentScrollHeight = document.documentElement.scrollHeight;
// Check if *any* scroll progress has been made since the last check by THIS interval
if (currentScrollY !== lastKnownScrollYForStuckDetection || currentScrollHeight !== lastKnownScrollHeightForStuckDetection) {
lastEffectiveScrollTime = Date.now(); // Scroll position has changed, reset the "stuck" timer
}
// If no effective scroll progress for STUCK_TIMEOUT_MS, consider it truly stuck and reload
if (Date.now() - lastEffectiveScrollTime > STUCK_TIMEOUT_MS) {
console.warn(`[Instagram Bot] Bot appears truly stuck (no scroll progress for ${STUCK_TIMEOUT_MS / 1000}s). Initiating page refresh.`);
updateStatus("Bot stuck! Refreshing page...", 'red'); // Use red for critical error/refresh
stopBot(); // Stop cleanly before forcing a reload
localStorage.setItem(AUTO_START_FLAG, 'true'); // Set flag to auto-start after reload
location.reload(); // Reload the page
}
// Update for the next check by this interval
lastKnownScrollYForStuckDetection = currentScrollY;
lastKnownScrollHeightForStuckDetection = currentScrollHeight;
}, 5000); // Check every 5 seconds for stuck state
}
// Main function to start the bot
function startBot() {
if (scrolling) return; // Bot is already active
scrolling = true;
// Ensure original speeds are correct before starting
originalPixelsPerFrame = settings.pixelsPerFrame;
originalJumpyScrollInterval = settings.jumpyScrollInterval;
isTemporarilyStuckLoading = false; // Reset loading state
// Start the post processing interval, which runs regardless of scroll mode
if (!processPostsIntervalId) {
processPostsIntervalId = setInterval(async () => {
await processPostsBatch();
}, processPostsInterval);
}
// Determine initial scrolling mode based on visibility
if (document.visibilityState === 'visible') {
startFluidScrolling();
} else {
startJumpyScrolling();
}
// Start monitoring for stuck state when the bot starts
startStuckDetection();
}
// Main function to stop the bot
function stopBot() {
if (!scrolling) return; // Bot is already stopped
scrolling = false;
stopFluidScrolling(); // Stop fluid scrolling if active
stopJumpyScrolling(); // Stop jumpy scrolling if active
// Stop the post processing interval
if (processPostsIntervalId) {
clearInterval(processPostsIntervalId);
processPostsIntervalId = null;
}
// Stop the stuck detection interval
if (stuckDetectionIntervalId) {
clearInterval(stuckDetectionIntervalId);
stuckDetectionIntervalId = null;
}
clearTimeout(noScrollProgressTimeout); // Clear any pending no-scroll checks
noScrollProgressTimeout = null;
updateStatus("Stopped.", 'var(--warning-color, #f44336)');
}
// --- Event Listener for Tab Visibility Change ---
document.addEventListener('visibilitychange', () => {
if (scrolling) { // Only change mode if bot is currently active
// Stop current scrolling mode cleanly
stopFluidScrolling();
stopJumpyScrolling();
// Restart scrolling in the appropriate new mode
if (document.visibilityState === 'visible') {
startFluidScrolling();
} else {
startJumpyScrolling();
}
// The stuck detection interval continues running, as it monitors overall scroll progress
}
});
// --- Custom Confirmation Modal Function ---
function showCustomConfirmation(postElement, postSnippet, isSponsored, targetLinkHref) {
console.log("Attempting to show custom confirmation modal.");
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.id = 'tm-custom-modal-overlay';
Object.assign(overlay.style, {
position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '100000', display: 'flex',
justifyContent: 'center', alignItems: 'center', opacity: '1', // Set opacity directly
});
const modal = document.createElement('div');
modal.id = 'tm-custom-modal';
Object.assign(modal.style, {
backgroundColor: '#262626', color: '#F0F0F0', borderRadius: '12px', padding: '25px',
boxShadow: '0 8px 30px rgba(0, 0, 0, 0.5)', maxWidth: '450px', width: '90%',
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif',
display: 'flex', flexDirection: 'column', gap: '15px', transform: 'scale(1)', // Set transform directly
});
const header = document.createElement('div');
Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '10px', marginBottom: '10px' });
const title = document.createElement('h3');
title.innerText = 'Potential Offer Found!';
Object.assign(title.style, { margin: '0', fontSize: '18px', fontWeight: 'bold', color: '#66B3FF' });
const closeBtn = document.createElement('button');
closeBtn.innerText = '✕';
Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#F0F0F0', fontSize: '20px', cursor: 'pointer', padding: '5px', lineHeight: '1' });
closeBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); };
header.appendChild(title); header.appendChild(closeBtn); modal.appendChild(header);
const body = document.createElement('div');
Object.assign(body.style, { fontSize: '15px', lineHeight: '1.6', color: '#E0E0E0' });
const infoText = document.createElement('p');
infoText.innerHTML = `An offer has been detected based on your criteria.` + (isSponsored ? ` <span style="font-weight: bold; color: #FFEB3B;">(Sponsored Post)</span>` : '');
Object.assign(infoText.style, { margin: '0 0 10px 0' });
const snippetHeader = document.createElement('p');
snippetHeader.innerText = 'Post Snippet:';
Object.assign(snippetHeader.style, { margin: '0', fontWeight: 'bold', color: '#B3B3FF' });
const snippetContent = document.createElement('code');
snippetContent.innerText = postSnippet;
Object.assign(snippetContent.style, {
display: 'block', backgroundColor: 'rgba(0,0,0,0.2)', borderRadius: '5px', padding: '10px',
fontSize: '13px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: '100px',
overflowY: 'auto', marginTop: '5px', marginBottom: '10px', color: '#C0C0C0'
});
body.appendChild(infoText); body.appendChild(snippetHeader); body.appendChild(snippetContent);
if (targetLinkHref) {
const linkP = document.createElement('p');
linkP.innerText = 'Detected Link:';
Object.assign(linkP.style, { margin: '0', fontWeight: 'bold', color: '#B3B3FF' });
const linkElement = document.createElement('a');
linkElement.href = targetLinkHref; linkElement.target = '_blank';
linkElement.innerText = targetLinkHref.length > 60 ? targetLinkHref.substring(0, 57) + '...' : targetLinkHref;
Object.assign(linkElement.style, { display: 'block', marginTop: '5px', color: '#66B3FF', wordBreak: 'break-all', textDecoration: 'underline', fontSize: '13px' });
// Note: The click handler for this link element itself should not resolve the promise,
// as the user might click it and then still want to dismiss the modal.
body.appendChild(linkP); body.appendChild(linkElement);
} else {
const noLinkP = document.createElement('p');
noLinkP.innerText = "No direct link found. You'll need to manually inspect this post.";
Object.assign(noLinkP.style, { margin: '0', fontStyle: 'italic', color: '#A0A0A0' });
body.appendChild(noLinkP);
}
modal.appendChild(body);
const footer = document.createElement('div');
Object.assign(footer.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px', borderTop: '1px solid rgba(255,255,255,0.1)', marginTop: '10px' });
// Button styling template
const buttonStyle = {
padding: '10px 20px', borderRadius: '8px', border: 'none',
fontWeight: 'bold', cursor: 'pointer',
transition: 'background-color 0.2s ease, transform 0.1s ease',
};
const primaryButtonStyle = {
backgroundColor: '#66B3FF', color: 'white',
onmouseover: (btn) => btn.style.backgroundColor = '#4DA8FF',
onmouseout: (btn) => btn.style.backgroundColor = '#66B3FF',
onmousedown: (btn) => btn.style.transform = 'translateY(1px)',
onmouseup: (btn) => btn.style.transform = 'translateY(0)',
};
const secondaryButtonStyle = {
border: '1px solid rgba(255,255,255,0.3)', backgroundColor: 'transparent', color: '#E0E0E0',
onmouseover: (btn) => { btn.style.backgroundColor = 'rgba(255,255,255,0.1)'; btn.style.color = '#FFFFFF'; },
onmouseout: (btn) => { btn.style.backgroundColor = 'transparent'; btn.style.color = '#E0E0E0'; },
onmousedown: (btn) => btn.style.transform = 'translateY(1px)',
onmouseup: (btn) => btn.style.transform = 'translateY(0)',
};
// "Open Link" button (if targetLinkHref exists)
if (targetLinkHref) {
const openLinkBtn = document.createElement('button');
openLinkBtn.innerText = 'Open Link';
Object.assign(openLinkBtn.style, buttonStyle, primaryButtonStyle);
openLinkBtn.onmouseover = () => primaryButtonStyle.onmouseover(openLinkBtn);
openLinkBtn.onmouseout = () => primaryButtonStyle.onmouseout(openLinkBtn);
openLinkBtn.onmousedown = () => primaryButtonStyle.onmousedown(openLinkBtn);
openLinkBtn.onmouseup = () => primaryButtonStyle.onmouseup(openLinkBtn);
openLinkBtn.onclick = () => {
document.removeEventListener('keydown', handleEscape);
document.body.removeChild(overlay);
resolve('open_link'); // Resolve with 'open_link'
};
footer.appendChild(openLinkBtn);
}
// "Jump to Post" button (always available, but style changes)
const jumpToPostBtn = document.createElement('button');
jumpToPostBtn.innerText = 'Jump to Post';
// If there's a direct link, Jump to Post becomes a secondary action. Otherwise, it's primary.
const jumpBtnStyle = targetLinkHref ? secondaryButtonStyle : primaryButtonStyle;
Object.assign(jumpToPostBtn.style, buttonStyle, jumpBtnStyle);
jumpToPostBtn.onmouseover = () => jumpBtnStyle.onmouseover(jumpToPostBtn);
jumpToPostBtn.onmouseout = () => jumpBtnStyle.onmouseout(jumpToPostBtn);
jumpToPostBtn.onmousedown = () => jumpBtnStyle.onmousedown(jumpToPostBtn);
jumpToPostBtn.onmouseup = () => jumpBtnStyle.onmouseup(jumpToPostBtn);
jumpToPostBtn.onclick = () => {
document.removeEventListener('keydown', handleEscape);
document.body.removeChild(overlay);
if (postElement) {
postElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
console.log("Attempted to scroll to post.");
}
resolve('jump'); // Resolve with 'jump'
};
footer.appendChild(jumpToPostBtn);
// "Dismiss & Resume" button (always available)
const dismissBtn = document.createElement('button');
dismissBtn.innerText = 'Dismiss & Resume';
Object.assign(dismissBtn.style, buttonStyle, secondaryButtonStyle);
dismissBtn.onmouseover = () => secondaryButtonStyle.onmouseover(dismissBtn);
dismissBtn.onmouseout = () => secondaryButtonStyle.onmouseout(dismissBtn);
dismissBtn.onmousedown = () => secondaryButtonStyle.onmousedown(dismissBtn);
dismissBtn.onmouseup = () => secondaryButtonStyle.onmouseup(dismissBtn);
dismissBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve('dismiss'); }; // Resolve with 'dismiss'
footer.appendChild(dismissBtn);
modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay);
const handleEscape = (e) => {
if (e.key === 'Escape') {
document.removeEventListener('keydown', handleEscape);
document.body.removeChild(overlay);
resolve('dismiss'); // Escape key also dismisses and resumes
}
};
document.addEventListener('keydown', handleEscape);
});
}
// --- Help Modal Function ---
async function showHelpModal() {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.id = 'tm-help-overlay';
Object.assign(overlay.style, {
position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '100001', display: 'flex',
justifyContent: 'center', alignItems: 'center', opacity: '0', transition: 'opacity 0.3s ease-in-out'
});
const modal = document.createElement('div');
modal.id = 'tm-help-modal';
Object.assign(modal.style, {
backgroundColor: '#262626', color: '#F0F0F0', borderRadius: '12px', padding: '25px',
boxShadow: '0 8px 30px rgba(0, 0, 0, 0.5)', maxWidth: '450px', width: '90%',
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif',
display: 'flex', flexDirection: 'column', gap: '15px', transform: 'scale(0.9)', transition: 'transform 0.3s ease-in-out'
});
const header = document.createElement('div');
Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '10px', marginBottom: '10px' });
const title = document.createElement('h3');
title.innerText = 'Bot Help & Troubleshooting';
Object.assign(title.style, { margin: '0', fontSize: '18px', fontWeight: 'bold', color: '#66B3FF' });
const closeBtn = document.createElement('button');
closeBtn.innerText = '✕';
Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#F0F0F0', fontSize: '20px', cursor: 'pointer', padding: '5px', lineHeight: '1' });
closeBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); };
header.appendChild(title); header.appendChild(closeBtn); modal.appendChild(header);
const content = document.createElement('div');
Object.assign(content.style, { fontSize: '14px', lineHeight: '1.6', color: '#E0E0E0', maxHeight: '300px', overflowY: 'auto' });
content.innerHTML = `
<p>If the Instagram Bot isn't working as expected, try these steps:</p>
<ul>
<li><strong>Ensure Tampermonkey is active:</strong> Check your browser's Tampermonkey extension icon; it should be enabled.</li>
<li><strong>Script Enabled:</strong> In the Tampermonkey dashboard, make sure 'Instagram Sample Bot' is toggled ON.</li>
<li><strong>Disable Ad Blockers:</strong> Ad blockers (e.g., uBlock Origin, AdBlock Plus) or privacy extensions can interfere. Try disabling them for instagram.com.</li>
<li><strong>Refresh Page:</strong> A simple page refresh (F5 or Ctrl+R / Cmd+R) can often resolve minor issues.</li>
<li><strong>Restart Bot:</strong> If the bot stops, try clicking the 'Start Bot' button in the UI.</li>
<li><strong>Check Console for Errors:</strong> Open your browser's developer tools (F12 or Ctrl+Shift+I / Cmd+Option+I), go to the 'Console' tab, and look for any red error messages. Report them if you need further help.</li>
<li><strong>Update Script:</strong> Click 'Update Userscript' in the settings to check for the latest version.</li>
<li><strong>Clear Cache/Cookies:</strong> As a last resort, clearing browser cache and cookies for instagram.com can sometimes fix persistent issues (note: this will log you out).</li>
</ul>
<p>If you still face problems, please contact the script developer with details!</p>
`;
modal.appendChild(content);
const footer = document.createElement('div');
Object.assign(footer.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px', borderTop: '1px solid rgba(255,255,255,0.1)', marginTop: '10px' });
const closeButton = document.createElement('button');
closeButton.innerText = 'Close';
Object.assign(closeButton.style, { padding: '10px 20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.3)', backgroundColor: 'transparent', color: '#E0E0E0', cursor: 'pointer', transition: 'background-color 0.2s ease, color 0.2s ease, transform 0.1s ease' });
closeButton.onmouseover = () => { closeButton.style.backgroundColor = 'rgba(255,255,255,0.1)'; closeButton.style.color = '#FFFFFF'; };
closeButton.onmouseout = () => { closeButton.style.backgroundColor = 'transparent'; closeButton.style.color = '#E0E0E0'; };
closeButton.onmousedown = () => closeButton.style.transform = 'translateY(1px)';
closeButton.onmouseup = () => closeButton.style.transform = 'translateY(0)';
closeButton.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); };
footer.appendChild(closeButton);
modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay);
setTimeout(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1)'; }, 10);
const handleEscape = (e) => {
if (e.key === 'Escape') {
document.removeEventListener('keydown', handleEscape);
document.body.removeChild(overlay);
resolve(false);
}
};
document.addEventListener('keydown', handleEscape);
});
}
// --- Settings Modal Function ---
async function showSettingsModal() {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.id = 'tm-settings-overlay';
Object.assign(overlay.style, {
position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '100001', display: 'flex',
justifyContent: 'center', alignItems: 'center', opacity: '0', transition: 'opacity 0.3s ease-in-out'
});
const modal = document.createElement('div');
modal.id = 'tm-settings-modal';
Object.assign(modal.style, {
backgroundColor: '#262626', color: '#F0F0F0', borderRadius: '12px', padding: '25px',
boxShadow: '0 8px 30px rgba(0, 0, 0, 0.5)', maxWidth: '400px', width: '90%',
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif',
display: 'flex', flexDirection: 'column', gap: '15px', transform: 'scale(0.9)', transition: 'transform 0.3s ease-in-out'
});
const header = document.createElement('div');
Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '10px', marginBottom: '10px' });
const title = document.createElement('h3');
title.innerText = 'Bot Settings';
Object.assign(title.style, { margin: '0', fontSize: '18px', fontWeight: 'bold', color: '#66B3FF' });
const closeBtn = document.createElement('button');
closeBtn.innerText = '✕';
Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#F0F0F0', fontSize: '20px', cursor: 'pointer', padding: '5px', lineHeight: '1' });
closeBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); };
header.appendChild(title); header.appendChild(closeBtn); modal.appendChild(header);
const settingsForm = document.createElement('div');
Object.assign(settingsForm.style, { display: 'flex', flexDirection: 'column', gap: '12px' });
// Helper to create a setting row
const createSettingRow = (labelText, inputElement) => {
const row = document.createElement('div');
Object.assign(row.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center' });
const label = document.createElement('label');
label.innerText = labelText;
Object.assign(label.style, { flexShrink: '0', marginRight: '10px', fontSize: '14px', color: '#E0E0E0' });
row.appendChild(label);
row.appendChild(inputElement);
return row;
};
// Scrolling Speed (Fluid)
const fluidSpeedInput = document.createElement('input');
fluidSpeedInput.type = 'number';
fluidSpeedInput.min = '1'; fluidSpeedInput.max = '20'; fluidSpeedInput.step = '1';
fluidSpeedInput.value = settings.pixelsPerFrame;
Object.assign(fluidSpeedInput.style, { width: '80px', padding: '8px', borderRadius: '5px', border: '1px solid #555', backgroundColor: '#333', color: '#F0F0F0', fontSize: '14px' });
settingsForm.appendChild(createSettingRow('Fluid Scroll Speed (px/frame):', fluidSpeedInput));
// Scrolling Interval (Jumpy)
const jumpyIntervalInput = document.createElement('input');
jumpyIntervalInput.type = 'number';
jumpyIntervalInput.min = '100'; jumpyIntervalInput.max = '5000'; jumpyIntervalInput.step = '100';
jumpyIntervalInput.value = settings.jumpyScrollInterval;
Object.assign(jumpyIntervalInput.style, { width: '80px', padding: '8px', borderRadius: '5px', border: '1px solid #555', backgroundColor: '#333', color: '#F0F0F0', fontSize: '14px' });
settingsForm.appendChild(createSettingRow('Jumpy Scroll Interval (ms):', jumpyIntervalInput));
// Auto-Liking Toggle
const autoLikeToggle = document.createElement('input');
autoLikeToggle.type = 'checkbox';
autoLikeToggle.checked = settings.enableLiking;
Object.assign(autoLikeToggle.style, { width: '20px', height: '20px', cursor: 'pointer' });
settingsForm.appendChild(createSettingRow('Enable Auto-Liking:', autoLikeToggle));
// Sample Detection Toggle
const sampleDetectToggle = document.createElement('input');
sampleDetectToggle.type = 'checkbox';
sampleDetectToggle.checked = settings.enableSampleDetection;
Object.assign(sampleDetectToggle.style, { width: '20px', height: '20px', cursor: 'pointer' });
settingsForm.appendChild(createSettingRow('Enable Sample Detection:', sampleDetectToggle));
// Confidence Threshold for Sample Detection
const confidenceThresholdInput = document.createElement('input');
confidenceThresholdInput.type = 'number';
confidenceThresholdInput.min = '0.0'; confidenceThresholdInput.max = '5.0'; confidenceThresholdInput.step = '0.1'; // Max increased to accommodate new scoring
confidenceThresholdInput.value = settings.minConfidenceThreshold;
Object.assign(confidenceThresholdInput.style, { width: '80px', padding: '8px', borderRadius: '5px', border: '1px solid #555', backgroundColor: '#333', color: '#F0F0F0', fontSize: '14px' });
settingsForm.appendChild(createSettingRow('Sample Confidence Threshold:', confidenceThresholdInput));
// Accept Only Popular Brands Toggle
const popularBrandsToggle = document.createElement('input');
popularBrandsToggle.type = 'checkbox';
popularBrandsToggle.checked = settings.acceptOnlyPopularBrands;
Object.assign(popularBrandsToggle.style, { width: '20px', height: '20px', cursor: 'pointer' });
settingsForm.appendChild(createSettingRow('Only Popular Beauty Brands:', popularBrandsToggle));
modal.appendChild(settingsForm);
const footer = document.createElement('div');
Object.assign(footer.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px', borderTop: '1px solid rgba(255,255,255,0.1)', marginTop: '10px', flexWrap: 'wrap' }); // Added flexWrap
// UPDATED: Help Button now opens in-app modal
const helpBtn = document.createElement('button');
helpBtn.innerText = 'Help';
Object.assign(helpBtn.style, {
padding: '10px 15px', borderRadius: '8px', border: '1px solid rgba(102, 179, 255, 0.5)',
backgroundColor: 'transparent', color: '#66B3FF', fontWeight: 'bold', cursor: 'pointer',
transition: 'background-color 0.2s ease, color 0.2s ease, transform 0.1s ease',
flexShrink: '0' // Prevent shrinking
});
helpBtn.onmouseover = () => { helpBtn.style.backgroundColor = 'rgba(102, 179, 255, 0.1)'; };
helpBtn.onmouseout = () => { helpBtn.style.backgroundColor = 'transparent'; };
helpBtn.onmousedown = () => helpBtn.style.transform = 'translateY(1px)';
helpBtn.onmouseup = () => helpBtn.style.transform = 'translateY(0)';
helpBtn.onclick = async () => {
console.log("Help button clicked from settings modal.");
try {
// Do NOT close the settings modal here. Just show the help modal.
await showHelpModal();
} catch (e) {
console.error("Error executing showHelpModal from settings:", e);
// No status update here, as the settings modal is still open
}
};
footer.appendChild(helpBtn);
// Update Userscript Button
const updateBtn = document.createElement('button');
updateBtn.innerText = 'Update Userscript';
Object.assign(updateBtn.style, {
padding: '10px 15px', borderRadius: '8px', border: '1px solid rgba(144, 238, 144, 0.5)',
backgroundColor: 'transparent', color: '#90EE90', fontWeight: 'bold', cursor: 'pointer',
transition: 'background-color 0.2s ease, color 0.2s ease, transform 0.1s ease',
flexShrink: '0' // Prevent shrinking
});
updateBtn.onmouseover = () => { updateBtn.style.backgroundColor = 'rgba(144, 238, 144, 0.1)'; };
updateBtn.onmouseout = () => { updateBtn.style.backgroundColor = 'transparent'; };
updateBtn.onmousedown = () => updateBtn.style.transform = 'translateY(1px)';
updateBtn.onmouseup = () => updateBtn.style.transform = 'translateY(0)';
updateBtn.onclick = () => {
window.open(SCRIPT_INSTALL_URL, '_blank');
};
footer.appendChild(updateBtn);
const saveBtn = document.createElement('button');
saveBtn.innerText = 'Save Settings';
Object.assign(saveBtn.style, { padding: '10px 20px', borderRadius: '8px', border: 'none', backgroundColor: '#66B3FF', color: 'white', fontWeight: 'bold', cursor: 'pointer', transition: 'background-color 0.2s ease, transform 0.1s ease', flexShrink: '0' });
saveBtn.onmouseover = () => saveBtn.style.backgroundColor = '#4DA8FF';
saveBtn.onmouseout = () => saveBtn.style.backgroundColor = '#66B3FF';
saveBtn.onmousedown = () => saveBtn.style.transform = 'translateY(1px)';
saveBtn.onmouseup = () => saveBtn.style.transform = 'translateY(0)';
saveBtn.onclick = () => {
settings.pixelsPerFrame = parseInt(fluidSpeedInput.value);
settings.jumpyScrollInterval = parseInt(jumpyIntervalInput.value);
settings.enableLiking = autoLikeToggle.checked;
settings.enableSampleDetection = sampleDetectToggle.checked;
settings.minConfidenceThreshold = parseFloat(confidenceThresholdInput.value);
settings.acceptOnlyPopularBrands = popularBrandsToggle.checked;
saveSettings();
document.removeEventListener('keydown', handleEscape);
document.body.removeChild(overlay);
resolve(true); // Indicate settings were saved
};
footer.appendChild(saveBtn);
const cancelBtn = document.createElement('button');
cancelBtn.innerText = 'Cancel';
Object.assign(cancelBtn.style, { padding: '10px 20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.3)', backgroundColor: 'transparent', color: '#E0E0E0', cursor: 'pointer', transition: 'background-color 0.2s ease, color 0.2s ease, transform 0.1s ease' });
cancelBtn.onmouseover = () => { cancelBtn.style.backgroundColor = 'rgba(255,255,255,0.1)'; cancelBtn.style.color = '#FFFFFF'; };
cancelBtn.onmouseout = () => { cancelBtn.style.backgroundColor = 'transparent'; cancelBtn.style.color = '#E0E0E0'; };
cancelBtn.onmousedown = () => cancelBtn.style.transform = 'translateY(1px)';
cancelBtn.onmouseup = () => cancelBtn.style.transform = 'translateY(0)';
cancelBtn.onclick = () => { document.removeEventListener('keydown', handleEscape); document.body.removeChild(overlay); resolve(false); };
footer.appendChild(cancelBtn);
modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay);
setTimeout(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1)'; }, 10);
const handleEscape = (e) => {
if (e.key === 'Escape') {
document.removeEventListener('keydown', handleEscape);
document.body.removeChild(overlay);
resolve(false);
}
};
document.addEventListener('keydown', handleEscape);
});
}
async function processPostsBatch() {
const posts = document.querySelectorAll('article');
for (const post of posts) {
const postText = post.innerText.toLowerCase();
// --- LIKING LOGIC ---
if (settings.enableLiking && !post.dataset.likedChecked) {
post.dataset.likedChecked = 'true';
const shouldLike = cologneKeywords.some(kw => postText.includes(kw)) ||
beautyKeywords.some(kw => postText.includes(kw));
if (shouldLike) {
const likedSuccessfully = await likePost(post);
if (likedSuccessfully) {
updateStatus("Liked a post!", 'var(--primary-color)');
await delay(1000 + Math.random() * 1000); // Wait 1-2 seconds after liking
}
}
}
// --- SAMPLE DETECTION LOGIC ---
if (!settings.enableSampleDetection || post.dataset.sampleChecked) {
continue;
}
// Filter by popular brands if setting is enabled
if (settings.acceptOnlyPopularBrands) {
const foundPopularBrand = popularBeautyBrands.some(brand => postText.includes(brand.toLowerCase()));
if (!foundPopularBrand) {
continue; // Skip this post if it's not from a popular brand
}
}
let confidence = 0;
let targetLinkHref = null; // Changed to store href directly
const isSponsored = isPostSponsored(post);
let hasAnySampleKeyword = false;
let hasNegativePhrase = false;
// 1. Boost for strong offer phrases (highest priority)
for (const phrase of strongOfferPhrases) {
if (postText.includes(phrase)) {
confidence += 3.0; // High boost for direct offer phrases
}
}
// 2. Boost for action texts (strong indicator of an interactive offer)
const buttonsAndLinks = post.querySelectorAll('a, button');
for (const element of buttonsAndLinks) {
const elementText = element.innerText.toLowerCase();
if (sampleActionTexts.some(action => elementText.includes(action))) {
if (element.tagName === 'A' && element.href && element.href !== '#' && !element.href.startsWith('javascript:')) {
// Prioritize action-associated links if found, or if no direct text link yet
if (!targetLinkHref || element.href.includes('bit.ly') || element.href.includes('tinyurl')) { // Prioritize short links or if no other link
targetLinkHref = element.href;
}
}
confidence += 2.0; // Strong boost for action buttons/links
break; // Only need to find one action text
}
}
// NEW: Extract direct links from post text (more robust URL detection)
const urlRegex = /(https?:\/\/[^\s]+)/g; // Basic URL regex
const matches = postText.match(urlRegex);
if (matches && matches.length > 0) {
// If we found a direct URL in the text, use it if targetLinkHref isn't already set by an action button
if (!targetLinkHref) {
targetLinkHref = matches[0]; // Take the first URL found
}
confidence += 1.0; // Add confidence for finding a direct link
}
// 3. Boost for sponsored posts
if (isSponsored) {
confidence += 1.0; // Bonus for sponsored posts
}
// Identify presence of any generic sample keyword
for (const keyword of sampleKeywords) {
if (postText.includes(keyword)) {
hasAnySampleKeyword = true;
break;
}
}
// Identify presence of any negative sample phrase
for (const negPhrase of negativeSamplePhrases) {
if (postText.includes(negPhrase)) {
hasNegativePhrase = true;
break;
}
}
// 4. Conditional boost/penalty for general sample keywords based on negative context
if (hasAnySampleKeyword) {
if (!hasNegativePhrase) {
confidence += 1.5; // Significant boost if general keyword is clean
} else {
confidence -= 2.5; // Strong penalty if a negative context is present
}
}
// Ensure confidence doesn't go below zero after penalties
confidence = Math.max(0, confidence);
if (confidence >= settings.minConfidenceThreshold) {
updateStatus("Potential offer found! Waiting for your decision...", 'var(--highlight-color, #FFD700)');
// Stop all bot activity (scrolling and processing)
stopBot();
post.dataset.sampleChecked = 'true';
const postSnippet = postText.substring(0, Math.min(postText.length, 300));
// targetLinkHref is already determined above
// Pass the actual post element to the confirmation modal
const userChoice = await showCustomConfirmation(post, postSnippet, isSponsored, targetLinkHref);
if (userChoice === 'open_link') { // User chose to open the link
if (targetLinkHref) { // Double check link exists before opening
window.open(targetLinkHref, '_blank');
updateStatus("Opened link. Resuming bot...", 'var(--primary-color, #4CAF50)');
} else {
updateStatus("No link to open. Resuming bot...", 'var(--warning-color, #FFA500)');
}
// Automatically restart the bot after user interaction
setTimeout(() => {
startBot();
}, 0);
} else if (userChoice === 'jump') { // User chose to jump to the post
updateStatus("Jumped to post. Bot stopped for inspection.", 'var(--warning-color, #FFA500)');
// The bot remains stopped here. No startBot() call.
} else { // userChoice === 'dismiss' (User chose to dismiss and resume)
updateStatus("User chose to dismiss. Resuming bot...", 'var(--primary-color, #4CAF50)');
// Automatically restart the bot after user interaction
setTimeout(() => {
startBot();
}, 0);
}
return; // Stop processing other posts in this batch to avoid immediate re-trigger
}
}
}
// --- UI Creation ---
function createUI() {
const rootStyle = document.documentElement.style;
rootStyle.setProperty('--primary-color', '#4CAF50');
rootStyle.setProperty('--danger-color', '#f44336');
rootStyle.setProperty('--text-color', '#333');
rootStyle.setProperty('--bg-color', 'rgba(255, 255, 255, 0.7)');
rootStyle.setProperty('--border-color', 'rgba(200, 200, 200, 0.5)');
rootStyle.setProperty('--shadow-color', 'rgba(0, 0, 0, 0.1)');
rootStyle.setProperty('--highlight-color', '#FFD700');
rootStyle.setProperty('--warning-color', '#FFA500');
rootStyle.setProperty('--settings-icon-color', '#8A2BE2'); // A vibrant blue-violet for contrast
const uiContainer = document.createElement('div');
Object.assign(uiContainer.style, {
position: 'fixed', top: '20px', left: '20px',
backgroundColor: 'var(--bg-color)', backdropFilter: 'blur(8px)',
padding: '15px', border: '1px solid var(--border-color)', borderRadius: '12px',
boxShadow: '0 4px 15px var(--shadow-color)', zIndex: '99999',
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif',
display: 'flex', flexDirection: 'column', gap: '15px',
alignItems: 'center', justifyContent: 'center', minWidth: '240px',
// Initial state for fluid entry
opacity: '0',
transform: 'translateY(-20px)',
transition: 'opacity 0.5s ease-out, transform 0.5s ease-out',
// Draggable styles
cursor: 'grab', // Indicate it's draggable
userSelect: 'none', // Prevent text selection during drag
});
// --- Draggable UI Logic ---
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
uiContainer.addEventListener("mousedown", dragStart);
document.addEventListener("mouseup", dragEnd);
document.addEventListener("mousemove", drag); // Listen on document for smoother dragging outside element
function dragStart(e) {
initialX = e.clientX - uiContainer.getBoundingClientRect().left; // Use getBoundingClientRect for initial position
initialY = e.clientY - uiContainer.getBoundingClientRect().top; // Use getBoundingClientRect for initial position
if (e.target === uiContainer || uiContainer.contains(e.target)) { // Only drag if click is on container or its children
isDragging = true;
uiContainer.style.cursor = 'grabbing'; // Change cursor to indicate dragging
}
}
function dragEnd(e) {
isDragging = false;
uiContainer.style.cursor = 'grab'; // Reset cursor
}
function drag(e) {
if (isDragging) {
e.preventDefault(); // Prevent default browser drag behavior
// Calculate new position
let newX = e.clientX - initialX;
let newY = e.clientY - initialY;
// Get viewport dimensions
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Get UI container dimensions
const uiWidth = uiContainer.offsetWidth;
const uiHeight = uiContainer.offsetHeight;
// Clamp X position within viewport
newX = Math.max(0, Math.min(newX, viewportWidth - uiWidth));
// Clamp Y position within viewport
newY = Math.max(0, Math.min(newY, viewportHeight - uiHeight));
// Apply new position
uiContainer.style.left = newX + "px";
uiContainer.style.top = newY + "px";
// Update offsets for next drag event
xOffset = newX;
yOffset = newY;
}
}
// --- End Draggable UI Logic ---
const headerRow = document.createElement('div');
Object.assign(headerRow.style, {
display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%',
});
uiContainer.appendChild(headerRow);
const titleElement = document.createElement('h3');
titleElement.innerText = `Instagram Bot v${SCRIPT_VERSION}`;
Object.assign(titleElement.style, {
margin: '0', color: 'var(--text-color)', fontSize: '20px',
fontWeight: 'bold', flexGrow: '1', textAlign: 'center'
});
headerRow.appendChild(titleElement);
// Settings Button
const settingsButton = document.createElement('button');
settingsButton.innerHTML = '⚙️';
Object.assign(settingsButton.style, {
background: 'none', border: 'none', fontSize: '28px',
cursor: 'pointer', color: 'var(--settings-icon-color)',
padding: '0',
lineHeight: '1',
transition: 'transform 0.1s ease, color 0.2s ease',
});
settingsButton.onmouseover = () => settingsButton.style.transform = 'rotate(20deg)';
settingsButton.onmouseout = () => settingsButton.style.transform = 'rotate(0deg)';
settingsButton.onclick = async () => {
console.log("Settings button clicked.");
try {
await showSettingsModal();
} catch (e) {
console.error("Error executing showSettingsModal:", e);
updateStatus("Error opening settings!", 'red');
}
};
headerRow.appendChild(settingsButton);
const buttonsContainer = document.createElement('div');
Object.assign(buttonsContainer.style, {
display: 'flex', gap: '12px', width: '100%', justifyContent: 'center',
});
uiContainer.appendChild(buttonsContainer);
const buttonStyle = {
padding: '10px 18px', border: 'none', borderRadius: '8px',
fontSize: '14px', fontWeight: '600', cursor: 'pointer',
transition: 'background-color 0.2s ease, transform 0.1s ease',
boxShadow: '0 2px 5px var(--shadow-color)', flexGrow: '1',
};
const startButton = document.createElement('button');
startButton.innerText = 'Start Bot';
Object.assign(startButton.style, buttonStyle, { backgroundColor: 'var(--primary-color)', color: 'white' });
startButton.onmouseover = () => startButton.style.backgroundColor = '#45a049';
startButton.onmouseout = () => startButton.style.backgroundColor = 'var(--primary-color)';
startButton.onmousedown = () => startButton.style.transform = 'translateY(1px)';
startButton.onmouseup = () => startButton.style.transform = 'translateY(0)';
startButton.onclick = () => {
console.log("Start Bot button clicked.");
try {
startBot();
} catch (e) {
console.error("Error executing startBot:", e);
updateStatus("Error starting bot!", 'red');
}
};
buttonsContainer.appendChild(startButton);
const stopButton = document.createElement('button');
stopButton.innerText = 'Stop Bot';
Object.assign(stopButton.style, buttonStyle, { backgroundColor: 'var(--danger-color)', color: 'white' });
stopButton.onmouseover = () => stopButton.style.backgroundColor = '#da190b';
stopButton.onmouseout = () => stopButton.style.backgroundColor = 'var(--danger-color)';
stopButton.onmousedown = () => stopButton.style.transform = 'translateY(1px)';
stopButton.onmouseup = () => stopButton.style.transform = 'translateY(0)';
stopButton.onclick = () => {
console.log("Stop Bot button clicked.");
try {
stopBot();
} catch (e) {
console.error("Error executing stopBot:", e);
updateStatus("Error stopping bot!", 'red');
}
};
buttonsContainer.appendChild(stopButton);
statusBarElement = document.createElement('div');
Object.assign(statusBarElement.style, {
marginTop: '0px',
fontSize: '12px', color: 'var(--text-color)',
textAlign: 'center', width: '100%', paddingTop: '0px',
});
uiContainer.appendChild(statusBarElement);
document.body.appendChild(uiContainer);
// Animate the UI container into view
requestAnimationFrame(() => {
uiContainer.style.opacity = '1';
uiContainer.style.transform = 'translateY(0)';
});
updateStatus("Ready. Click 'Start Bot'!", 'var(--text-color)');
}
// --- Initialization ---
window.addEventListener('load', () => {
loadSettings(); // Load settings first
createUI(); // Then create the UI
// Check if we should auto-start the bot after a previous forced reload (due to being stuck)
if (localStorage.getItem(AUTO_START_FLAG) === 'true') {
localStorage.removeItem(AUTO_START_FLAG); // Clear the flag immediately
updateStatus("Restarting after recovery refresh...", 'green'); // Inform user
// Give the page a moment to fully render before restarting the bot
setTimeout(() => {
startBot();
}, 2000); // 2-second delay before auto-starting
}
});
})();