SnapScore Automater

Multi-step auto-snap (Cam, Picture, Send, Final). Drag/upload profiles. Delay applies between every step. X = emergency stop.

// ==UserScript==
// @name         SnapScore Automater
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Multi-step auto-snap (Cam, Picture, Send, Final). Drag/upload profiles. Delay applies between every step. X = emergency stop.
// @match        https://www.snapchat.com/web*
// @match        https://web.snapchat.com/*
// @grant        none
// @license      Unlicense
// ==/UserScript==

(function(){
  'use strict';

  /* ---------- UI ---------- */
  const overlay = document.createElement('div');
  Object.assign(overlay.style, {
    position:'fixed', top:'12px', right:'12px', zIndex:2147483647,
    background:'rgba(0,0,0,0.80)', color:'#fff', padding:'10px',
    borderRadius:'8px', fontFamily:'Inter, Arial, sans-serif', fontSize:'13px', minWidth:'460px',
    boxShadow:'0 6px 24px rgba(0,0,0,0.5)'
  });
  overlay.innerHTML = `
    <div style="display:flex;justify-content:space-between;align-items:center">
      <strong>Snap AutoClick</strong>
      <div style="display:flex;gap:6px;align-items:center">
        <button id="ac-close" title="Remove overlay" style="background:transparent;border:0;color:#fff;cursor:pointer">✕</button>
      </div>
    </div>

    <div style="margin-top:8px;display:flex;gap:6px;flex-wrap:wrap">
      <button class="ac-set-btn" data-type="cam">Cam ON</button>
      <button class="ac-set-btn" data-type="picture">Picture</button>
      <button class="ac-set-btn" data-type="send">Send</button>
      <button class="ac-set-btn" data-type="final">Final Send</button>
    </div>

    <div style="margin-top:8px;display:flex;gap:8px;align-items:center">
      <label style="font-size:12px">Delay (s):</label>
      <input id="ac-delay" type="text" value="0.2" style="width:70px;padding:4px;border-radius:4px;border:none"/>
      <button id="ac-start">Start</button>
      <button id="ac-stop">Stop</button>
      <button id="ac-clear">Clear All</button>
      <button id="ac-upload">Upload</button>
    </div>

    <div style="margin-top:8px;font-size:12px;color:#ddd">Profiles (drag images here or use Upload):</div>
    <div id="ac-drop" style="border:1px dashed #888;min-height:80px;padding:6px;margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;align-items:flex-start;overflow:auto;max-height:160px;"></div>

    <div id="ac-preview" style="margin-top:8px;font-size:12px;color:#9f9;max-height:80px;overflow:auto;border:1px solid #555;padding:6px">Preview: <i>none</i></div>
    <div id="ac-status" style="margin-top:8px;font-size:12px;color:#9f9">Status: idle</div>

    <div style="margin-top:8px;font-size:11px;opacity:0.85">
      Notes: X = emergency stop. Click a Set button then click the page element to save it. Script ignores left 1/3 of screen.
    </div>
  `;
  document.documentElement.appendChild(overlay);

  /* ---------- elements ---------- */
  const dropEl = document.getElementById('ac-drop');
  const startBtn = document.getElementById('ac-start');
  const stopBtn = document.getElementById('ac-stop');
  const clearBtn = document.getElementById('ac-clear');
  const uploadBtn = document.getElementById('ac-upload');
  const delayInput = document.getElementById('ac-delay');
  const statusEl = document.getElementById('ac-status');
  const previewEl = document.getElementById('ac-preview');

  /* ---------- state ---------- */
  // setButtons: each is { sel: string|null, el: Element|null }
  const setButtons = { cam:{sel:null,el:null}, picture:{sel:null,el:null}, send:{sel:null,el:null}, final:{sel:null,el:null} };
  let captureMode = null;
  let running = false;
  let emergency = false;
  // profiles
  let profileList = []; // {id, type:'data'|'url', src, name, w?, h?, enabled:true}
  let profileIdCounter = 1;

  /* ---------- utilities & safety fixes ---------- */
  function buildSelector(el){
    if(!el || el.nodeType !== 1) return null;
    if(el.id) return `#${CSS.escape(el.id)}`;
    const parts = [];
    let node = el;
    while(node && node.nodeType === 1 && node.tagName.toLowerCase() !== 'html'){
      let part = node.tagName.toLowerCase();
      if(node.className && typeof node.className === 'string'){
        const cls = node.className.split(/\s+/).filter(Boolean)[0];
        if(cls) part += `.${CSS.escape(cls)}`;
      }
      const parent = node.parentElement;
      if(parent){
        const siblings = Array.from(parent.children).filter(ch => ch.tagName === node.tagName);
        if(siblings.length > 1) part += `:nth-child(${Array.from(parent.children).indexOf(node)+1})`;
      }
      parts.unshift(part);
      node = node.parentElement;
      if(parts.length > 8) break;
    }
    return parts.length ? parts.join(' > ') : null;
  }

  function safeQuery(sel){
    try { return sel ? document.querySelector(sel) : null; } catch(e){ return null; }
  }

  function updateStatus(msg){
    if(msg) statusEl.textContent = msg;
    else statusEl.textContent = running ? `Running (delay ${delayInput.value}s)` : 'Status: idle';
    highlightSetButtons();
  }

  function highlightSetButtons(){
    document.querySelectorAll('.ac-set-btn').forEach(btn=>{
      const t = btn.dataset.type;
      const s = setButtons[t];
      if(s && (s.sel || s.el)){
        btn.style.border = '2px solid #0f0';
        btn.style.boxShadow = '0 0 6px rgba(0,255,0,0.12)';
      } else {
        btn.style.border = '1px solid #888';
        btn.style.boxShadow = 'none';
      }
    });
  }

  function updatePreview(){
    previewEl.innerHTML = profileList.length ? ('Profiles: ' + profileList.filter(p=>p.enabled).map(p=>p.name).join(', ') + (profileList.filter(p=>!p.enabled).length? ' <span style="color:#f88">(some disabled)</span>':'') ) : 'Preview: <i>none</i>';
  }

  function minDelayMs(){ return 20; } // minimum 20ms for fast but realistic timing

  function getDelayMs(){
    let d = parseFloat(delayInput.value);
    if(isNaN(d) || d < 0) d = 0.02;
    const ms = Math.round(d * 1000);
    return Math.max(ms, minDelayMs());
  }

  /* ---------- profile UI and drag/drop ---------- */
  function makeProfileCard(profile){
    const div = document.createElement('div');
    Object.assign(div.style, {display:'flex', flexDirection:'column', alignItems:'center', gap:'4px', width:'96px', padding:'6px', background:'rgba(255,255,255,0.03)', borderRadius:'6px'});
    const img = document.createElement('img'); img.src = profile.src; img.style.width='48px'; img.style.height='48px'; img.style.objectFit='cover'; img.style.borderRadius='6px';
    const label = document.createElement('div'); label.textContent = profile.name; label.style.fontSize='11px'; label.style.width='100%'; label.style.whiteSpace='nowrap'; label.style.overflow='hidden'; label.style.textOverflow='ellipsis'; label.style.color='#fff';
    const row = document.createElement('div'); row.style.display='flex'; row.style.gap='6px'; row.style.alignItems='center';
    const checkbox = document.createElement('input'); checkbox.type='checkbox'; checkbox.checked = !!profile.enabled; checkbox.addEventListener('change', ()=>{ profile.enabled = checkbox.checked; updatePreview(); });
    const del = document.createElement('button'); del.textContent='X'; Object.assign(del.style,{background:'#900', color:'#fff', border:'0', padding:'2px 6px', borderRadius:'6px', cursor:'pointer'}); del.addEventListener('click', ()=>{ profileList = profileList.filter(p=>p.id!==profile.id); renderProfiles(); updatePreview(); });
    row.appendChild(checkbox); row.appendChild(del);
    div.appendChild(img); div.appendChild(label); div.appendChild(row);
    return div;
  }

  function renderProfiles(){
    dropEl.innerHTML = '';
    profileList.forEach(p => dropEl.appendChild(makeProfileCard(p)));
  }

  // drag/drop handlers
  dropEl.addEventListener('dragover', e => { e.preventDefault(); dropEl.style.borderColor='#fff'; });
  dropEl.addEventListener('dragleave', e => { dropEl.style.borderColor='#888'; });
  dropEl.addEventListener('drop', e => {
    e.preventDefault(); dropEl.style.borderColor='#888';
    // files
    if(e.dataTransfer.files && e.dataTransfer.files.length > 0){
      Array.from(e.dataTransfer.files).forEach(file => {
        if(!file.type.startsWith('image/')) return;
        const reader = new FileReader();
        reader.onload = ev => {
          const src = ev.target.result;
          const id = profileIdCounter++;
          const profile = { id, type:'data', src, name: file.name, enabled:true };
          const img = new Image();
          img.onload = () => { profile.w = img.naturalWidth; profile.h = img.naturalHeight; profileList.push(profile); renderProfiles(); updatePreview(); };
          img.src = src;
        };
        reader.readAsDataURL(file);
      });
    }
    // dragged-from-page URL fallback
    const url = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain');
    if(url){
      const first = url.split('\n')[0].trim();
      if(first){
        const id = profileIdCounter++;
        const profile = { id, type:'url', src:first, name: first.split('/').pop()||'dragged-image', enabled:true };
        const img = new Image(); img.crossOrigin='anonymous';
        img.onload = () => { profile.w = img.naturalWidth; profile.h = img.naturalHeight; profileList.push(profile); renderProfiles(); updatePreview(); };
        img.onerror = () => { profileList.push(profile); renderProfiles(); updatePreview(); };
        img.src = first;
      }
    }
  });

  // explicit Upload button (prevents accidental file opens)
  uploadBtn.addEventListener('click', () => {
    const inp = document.createElement('input');
    inp.type = 'file'; inp.accept = 'image/*'; inp.multiple = true;
    inp.onchange = () => {
      Array.from(inp.files || []).forEach(file => {
        if(!file.type.startsWith('image/')) return;
        const reader = new FileReader();
        reader.onload = ev => {
          const src = ev.target.result;
          const id = profileIdCounter++;
          const profile = { id, type:'data', src, name:file.name, enabled:true };
          const img = new Image(); img.onload = () => { profile.w = img.naturalWidth; profile.h = img.naturalHeight; profileList.push(profile); renderProfiles(); updatePreview(); };
          img.src = src;
        };
        reader.readAsDataURL(file);
      });
    };
    inp.click();
  });

  /* ---------- click helpers: ignore left 1/3, find smallest clickable ---------- */
  function inRightArea(rect){ return rect.left >= (window.innerWidth / 3); }

  function findSmallestClickableWithin(el){
    if(!el) return null;
    const candidates = [el].concat(Array.from(el.querySelectorAll('button, a, [role="button"], [onclick], img')));
    let best = null; let bestArea = Infinity;
    candidates.forEach(c => {
      try{
        const r = c.getBoundingClientRect();
        if(r.width <= 0 || r.height <= 0) return;
        if(!inRightArea(r)) return;
        const area = r.width * r.height;
        if(area > 0 && area < bestArea) { bestArea = area; best = c; }
      }catch(e){}
    });
    return best;
  }

  function clickElement(el){
    if(!el) return false;
    const target = findSmallestClickableWithin(el) || el;
    if(!target) return false;
    const rect = target.getBoundingClientRect();
    if(!inRightArea(rect)) return false;
    const props = { bubbles:true, cancelable:true, view:window, clientX: Math.round(rect.left + rect.width/2), clientY: Math.round(rect.top + rect.height/2) };
    try{
      target.dispatchEvent(new PointerEvent('pointerdown', props));
      target.dispatchEvent(new PointerEvent('pointerup', props));
      target.dispatchEvent(new MouseEvent('click', props));
      // flash highlight (safe restore)
      const prev = target.style.boxShadow;
      try{ target.style.boxShadow = '0 0 0 4px rgba(0,255,0,0.25)'; }catch(e){}
      setTimeout(()=>{ try{ target.style.boxShadow = prev; }catch(e){} }, 120);
      return true;
    }catch(e){ console.warn('click failed', e); return false; }
  }

  /* ---------- profile matching ---------- */
  function lastSegment(url){
    try{ return (new URL(url)).pathname.split('/').filter(Boolean).pop() || url; }catch(e){ return url.split('/').pop() || url; }
  }

  function findMatchingThumbnail(profile){
    const allImgs = Array.from(document.querySelectorAll('img'));
    const candidates = allImgs.filter(img=>{
      try{
        const r = img.getBoundingClientRect();
        if(r.width <= 0 || r.height <= 0) return false;
        if(!inRightArea(r)) return false;
        if(r.width <= 48 && r.height <= 48) return true;
        return false;
      }catch(e){ return false; }
    });
    if(candidates.length === 0) return null;

    let best = null; let bestScore = Infinity;
    candidates.forEach(img=>{
      try{
        const src = (img.currentSrc || img.src || '').toString();
        const r = img.getBoundingClientRect();
        const area = r.width * r.height;
        let score = 1000;
        if(profile.src && src === profile.src) score = 0;
        else if(profile.src && lastSegment(src) && lastSegment(profile.src) && lastSegment(src) === lastSegment(profile.src)) score = 10;
        if(profile.w && profile.h){
          const w = img.naturalWidth || img.width || r.width;
          const h = img.naturalHeight || img.height || r.height;
          score += Math.abs((w||0) - profile.w) + Math.abs((h||0) - profile.h);
        }
        score += area / 100;
        if(profile.type === 'data' && src.startsWith('data:')) score -= 5;
        if(score < bestScore){ bestScore = score; best = img; }
      }catch(e){}
    });
    return best;
  }

  /* ---------- capture logic for set buttons ---------- */
  document.querySelectorAll('.ac-set-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      captureMode = btn.dataset.type;
      updateStatus(`Capture: click page element for "${captureMode}"`);
      const handler = e => {
        if(e.composedPath && e.composedPath().includes(overlay)) return;
        e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
        const el = e.target;
        const sel = buildSelector(el);
        setButtons[captureMode] = { sel, el };
        captureMode = null;
        document.removeEventListener('click', handler, true);
        updateStatus();
      };
      document.addEventListener('click', handler, true);
    });
  });

  /* ---------- runner (async, respects delay for every step) ---------- */
  async function runSequenceOnce(delayMs){
    // 1) main buttons
    for(const k of ['cam','picture','send']){
      if(emergency) return false;
      const s = setButtons[k];
      const el = s && (s.sel ? safeQuery(s.sel) : s.el) || null;
      if(el) clickElement(el);
      await sleep(delayMs);
    }

    // 2) profiles in order
    for(const p of profileList){
      if(emergency) return false;
      if(!p.enabled) continue;
      const match = findMatchingThumbnail(p);
      if(match) clickElement(match);
      await sleep(delayMs);
    }

    // 3) final
    if(setButtons.final && (setButtons.final.sel || setButtons.final.el)){
      const sf = setButtons.final;
      const fe = sf.sel ? safeQuery(sf.sel) : sf.el;
      if(fe) clickElement(fe);
      await sleep(delayMs);
    }
    return true;
  }

  function sleep(ms){ return new Promise(res => setTimeout(res, ms)); }

  async function startRunner(){
    if(running) return;
    running = true;
    emergency = false;
    updateStatus();
    const delayMs = getDelayMs();
    // loop until stopped or emergency
    while(running && !emergency){
      const ok = await runSequenceOnce(delayMs);
      if(!ok) break;
      // short micro-yield between sequences to avoid lock, but main pacing is delayMs inside
      await sleep(2);
    }
    running = false;
    updateStatus();
  }

  function stopRunner(){
    running = false;
  }

  /* ---------- UI wiring ---------- */
  startBtn.addEventListener('click', () => {
    const anyBtn = Object.values(setButtons).some(s => s && (s.sel || s.el));
    if(!anyBtn && profileList.length===0){ updateStatus('Set a button or add profiles first'); setTimeout(()=>updateStatus(), 1200); return; }
    startRunner();
  });

  stopBtn.addEventListener('click', ()=>{ emergency = true; stopRunner(); updateStatus('Stopped'); setTimeout(()=>updateStatus(), 400); });

  clearBtn.addEventListener('click', ()=>{
    Object.keys(setButtons).forEach(k => setButtons[k] = { sel:null, el:null });
    profileList = [];
    profileIdCounter = 1;
    renderProfiles();
    updatePreview();
    updateStatus('Cleared');
    setTimeout(()=>updateStatus(), 800);
  });

  document.getElementById('ac-close').addEventListener('click', ()=>{ emergency=true; stopRunner(); overlay.remove(); });

  document.addEventListener('keydown', e => {
    if(e.key && e.key.toLowerCase() === 'x'){ emergency = true; stopRunner(); updateStatus('EMERGENCY STOP'); setTimeout(()=>{ emergency=false; updateStatus(); }, 700); }
  });

  /* ---------- init ---------- */
  renderProfiles();
  updatePreview();
  updateStatus();

  // debugging handle
  window.__SnapAutoClick = { startRunner, stopRunner, profileList, setButtons };

})();