CS Group Maker

Create Chicken Smoothie groups with trading setting

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         CS Group Maker
// @namespace    cs-tools
// @version      1.0
// @description  Create Chicken Smoothie groups with trading setting
// @match        https://chickensmoothie.com/*
// @match        https://www.chickensmoothie.com/*
// @run-at       document-end
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  const CFG = {
    WAIT_AFTER_SUBMIT_MS: 8000,
    FORM_READY_RETRIES: 60,
    FORM_READY_INTERVAL: 200,
    DELAY_BETWEEN_GROUPS_MS: 200,
    DEFAULT_USER_ID: '299578',
    VERSION: '1.0',
  };

  const SS = window.sessionStorage;
  const qs = (sel, root=document) => root.querySelector(sel);
  const qsa = (sel, root=document) => Array.from(root.querySelectorAll(sel));
  const sleep = (ms)=> new Promise(r=>setTimeout(r,ms));
  const norm = (s)=> (s||'').replace(/\s+/g,' ').trim().toLowerCase();
  const onCreatePage = () => /\/accounts\/creategroup\.php\b/i.test(location.pathname);
  const createUrl = () => 'https://www.chickensmoothie.com/accounts/creategroup.php';

  let logEl = null;
  const log = (msg) => {
    const t = new Date().toLocaleTimeString();
    if (logEl) {
      logEl.textContent += `[${t}] ${msg}\n`;
      logEl.scrollTop = logEl.scrollHeight;
    }
  };

  const SKEY = {
    running:'csgm_running', queue:'csgm_queue', userId:'csgm_user',
    current:'csgm_current', attempts:'csgm_attempts', trade:'csgm_trade'
  };
  const getRunning = ()=> SS.getItem(SKEY.running) === '1';
  const setRunning = (b)=> SS.setItem(SKEY.running, b ? '1' : '0');
  const getQueue = ()=> { try { return JSON.parse(SS.getItem(SKEY.queue)||'[]'); } catch { return []; } };
  const setQueue = (arr)=> SS.setItem(SKEY.queue, JSON.stringify(arr||[]));
  const getUserId = ()=> SS.getItem(SKEY.userId) || '';
  const setUserId = (v)=> SS.setItem(SKEY.userId, v||'');
  const getCurrent = ()=> SS.getItem(SKEY.current) || '';
  const setCurrent = (v)=> SS.setItem(SKEY.current, v||'');
  const getAttempts = ()=> { try { return JSON.parse(SS.getItem(SKEY.attempts)||'{}'); } catch { return {}; } };
  const setAttempts= (obj)=> SS.setItem(SKEY.attempts, JSON.stringify(obj||{}));
  const getAttempt = (name)=> getAttempts()[name] || 0;
  const incAttempt = (name)=> { const a=getAttempts(); a[name]=(a[name]||0)+1; setAttempts(a); return a[name]; };
  const clearAttempt = (name)=> { const a=getAttempts(); if(name in a){ delete a[name]; setAttempts(a); } };
  const getTrade = ()=> SS.getItem(SKEY.trade) || 'disable';
  const setTrade = (v)=> SS.setItem(SKEY.trade, v || 'disable');

  // ---------------- Site messages ----------------
  function duplicateErrorPresent(expectedName){
    const want = norm(expectedName);
    const redSpans = qsa('span[style*="color:red"]');
    const pool = redSpans.length ? redSpans.map(n => n.innerText || '') : [ (document.body?.innerText || '') ];
    const re = /A\s+group\s+with\s+the\s+name\s+['"'‘’“”]([^"'‘’“”]+)['"'‘’“”]\s+already\s+exists\./i;
    for (const txt of pool) {
      const m = (txt || '').match(re);
      if (m && (!expectedName || norm(m[1]) === want)) return true;
    }
    return false;
  }

  function creationSuccessPresent(expectedName){
    const txt = document.body?.innerText || '';
    const m = txt.match(/A\s+new\s+group\s+called\s+['"'‘’“”]([^"'‘’“”]+)['"'‘’“”]\s+has\s+been\s+created\./i);
    if (!m) return false;
    return !expectedName || norm(m[1]) === norm(expectedName);
  }

  function waitForOutcome(prevURL, expectedName, timeout=CFG.WAIT_AFTER_SUBMIT_MS){
    return new Promise(resolve=>{
      let done=false; const finish=v=>{ if(!done){ done=true; resolve(v); } };
      const start=Date.now();
      (function tick(){
        if (location.href !== prevURL) return finish('navigated');
        if (duplicateErrorPresent(expectedName)) return finish('duplicate');
        if (creationSuccessPresent(expectedName)) return finish('created-inline');
        if (Date.now()-start >= timeout) return finish('timeout');
        setTimeout(tick,150);
      })();
      window.addEventListener('load', ()=>finish('navigated'), { once:true });
    });
  }

  async function waitForCreateFormReady(){
    for (let i=0;i<CFG.FORM_READY_RETRIES;i++){
      if (creationSuccessPresent()) return 'success-page';
      const nameInput = qs('input#groupname, input[name="groupname"], input[name="name"]');
      if (nameInput){
        const form = nameInput.closest('form') ||
          qs('form[action*="creategroup"]') || qs('form');
        if (form){
          const selectEl  = qs('#tradingsettings, select[name="tradingsettings"], select[name="trading"], select[id*="trading"]', form);
          const submitBtn = qs('#submit-button, button[type="submit"], input[type="submit"], input[name="submit"]', form);
          if (selectEl && submitBtn) return { nameInput, selectEl, submitBtn, form };
        }
      }
      await sleep(CFG.FORM_READY_INTERVAL);
    }
    return null;
  }

  // ---------------- UI ----------------
  if (typeof GM_addStyle === 'function') {
    GM_addStyle([
      '.csgm-toggle{position:fixed;left:10px;top:10px;z-index:999999;padding:6px 10px;background:#111;color:#fff;border-radius:6px;cursor:pointer;font:12px/1 ui-monospace,monospace;opacity:.9}',
      '.csgm-toggle:hover{opacity:1}',
      '.csg-panel{position:fixed;right:16px;bottom:16px;width:340px;z-index:999998;background:#fff;border:1px solid #ccc;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);font:13px/1.35 system-ui,sans-serif}',
      '.csg-header{padding:10px 12px;font-weight:700;background:#f7f7f7;border-bottom:1px solid #e5e5e5;display:flex;justify-content:space-between;align-items:center;border-radius:10px 10px 0 0}',
      '.csg-body{padding:10px 12px}',
      '.csg-row{margin-bottom:8px}',
      '.csg-row input[type="text"], .csg-row select{width:100%;padding:6px 8px;border:1px solid #ccc;border-radius:6px}',
      '.csg-row textarea{width:100%;height:140px;padding:8px;border:1px solid #ccc;border-radius:6px;resize:vertical;font-family:ui-monospace,monospace}',
      '.csg-actions{display:flex;gap:8px;margin-top:10px}',
      '.csg-actions button{flex:1;padding:8px 10px;border:1px solid #bbb;border-radius:8px;cursor:pointer;background:#fafafa}',
      '.csg-actions button:hover{background:#f0f0f0}',
      '.csg-log{background:#0b1220;color:#d4e0ff;font-family:ui-monospace,monospace;font-size:12px;padding:8px;border-radius:8px;max-height:160px;overflow:auto;white-space:pre-wrap}',
      '.csg-close{cursor:pointer;color:#666}',
      '.csg-hidden{display:none!important}'
    ].join(''));
  }

  function el(tag, cls, text){
    const n = document.createElement(tag);
    if (cls) n.className = cls;
    if (text != null) n.textContent = text;
    return n;
  }

  const toggle = el('button','csgm-toggle','CSGM');
  toggle.title = 'Toggle CS Group Maker';
  document.documentElement.appendChild(toggle);

  const panel = el('div','csg-panel csg-hidden');
  const header = el('div','csg-header');
  const title = el('div','', 'CS Group Maker ');
  const ver = el('span',''); ver.style.fontSize='11px'; ver.style.color='#777'; ver.textContent='v'+CFG.VERSION;
  title.appendChild(ver);
  const closeBtn = el('div','csg-close','✕'); closeBtn.title = 'Hide';
  header.appendChild(title); header.appendChild(closeBtn);
  panel.appendChild(header);

  const body = el('div','csg-body');
  const uidInput = el('input');
    uidInput.type='text';
    uidInput.placeholder='User ID (e.g., 299578)';
    uidInput.id='csg-userid';
    // save id
    uidInput.value = getUserId() || CFG.DEFAULT_USER_ID;

    uidInput.addEventListener('change', () => setUserId(uidInput.value.trim()));
    uidInput.addEventListener('input', () => setUserId(uidInput.value.trim()));

  const namesTA = el('textarea'); namesTA.placeholder='Group Name 1\nGroup Name 2'; namesTA.id='csg-names';
  const tradeSel = el('select'); tradeSel.id='csg-trade';
  [['enable','Everybody (default)'],
   ['wishlist-any','Wishlist: pets or items'],
   ['wishlist-pets','Wishlist: pets'],
   ['wishlist-items','Wishlist: items'],
   ['friends','Friends only'],
   ['disable','Nobody']].forEach(([v,t])=>{
     const o = el('option'); o.value=v; o.textContent=t; tradeSel.appendChild(o);
  });

  function row(label, node){ const r = el('div','csg-row'); if (label) r.appendChild(el('label','',label)); r.appendChild(node); return r; }
  body.appendChild(row('User ID', uidInput));
  body.appendChild(row('Group names (one per line)', namesTA));
  body.appendChild(row('Trading setting', tradeSel));

  const actions = el('div','csg-actions');
  const startBtn = el('button','', 'Start');
  const stopBtn = el('button','', 'Stop');
  actions.appendChild(startBtn); actions.appendChild(stopBtn);
  body.appendChild(actions);

  logEl = el('div','csg-log');
  body.appendChild(row('', logEl));

  panel.appendChild(body);
  document.documentElement.appendChild(panel);

  const show = ()=>panel.classList.remove('csg-hidden');
  const hide = ()=>panel.classList.add('csg-hidden');
  toggle.addEventListener('click', ()=> panel.classList.contains('csg-hidden') ? show() : hide());
  closeBtn.addEventListener('click', hide);
  setTimeout(()=>toggle.click(), 600); // show once

  // persist trade choice
  tradeSel.value = getTrade();
  tradeSel.addEventListener('change', ()=> setTrade(tradeSel.value));

  // ---------------- Controls & Flow ----------------
  async function resume(){
    if (!getRunning()) return;

    let current = getCurrent();
    let queue = getQueue();

    if (!current){
      current = (queue.shift() || '');
      setQueue(queue);
      setCurrent(current);
      if (!current){
        log('All groups created.');
        setRunning(false);
        setAttempts({});
        return;
      }
      log(`Working on: ${current}`);
    }

    if (!onCreatePage()){ location.href = createUrl(); return; }

    const ready = await waitForCreateFormReady();

    if (ready === 'success-page') {
      log('Continuing to next group');
      setCurrent('');
      await sleep(CFG.DELAY_BETWEEN_GROUPS_MS);
      location.href = createUrl();
      return;
    }

    if (!ready){
      const tries = incAttempt(current);
      if (tries <= 2){
        log('Form not ready');
        location.href = createUrl();
        return;
      }
      log('Skipping this name due to error.');
      clearAttempt(current);
      setCurrent('');
      setTimeout(resume, 50);
      return;
    }

    await createGroup(current, ready);
  }

  async function createGroup(groupName, { nameInput, selectEl, submitBtn, form }){
    incAttempt(groupName);

    // Fill names
    nameInput.value = '';
    for (const ch of groupName){ nameInput.value += ch; nameInput.dispatchEvent(new Event('input',{bubbles:true})); }
    nameInput.dispatchEvent(new Event('change',{bubbles:true}));

    // Set trading
    const pref = norm(getTrade() || 'disable');
    let opt = Array.from(selectEl.options || []).find(o => norm(o.value||'') === pref);
    if (!opt) {
      const map = { enable:'everybody', 'wishlist-any':'wishlist', 'wishlist-pets':'wishlist', 'wishlist-items':'wishlist', friends:'friends', disable:'nobody' };
      const hint = map[pref] || pref;
      opt = Array.from(selectEl.options || []).find(o => norm(o.textContent||'').includes(hint));
    }
    if (opt){ selectEl.value = opt.value; selectEl.dispatchEvent(new Event('change',{bubbles:true})); }

    const prevURL = location.href;
    try { (typeof form.requestSubmit === 'function') ? form.requestSubmit(submitBtn) : submitBtn.click(); }
    catch { submitBtn.click(); }

    const outcome = await waitForOutcome(prevURL, groupName, CFG.WAIT_AFTER_SUBMIT_MS);

    if (outcome === 'duplicate') {
      log(`Duplicate: "${groupName}" skipping`);
      clearAttempt(groupName);
      setCurrent('');
      await sleep(CFG.DELAY_BETWEEN_GROUPS_MS);
      setTimeout(resume, 50);
      return;
    }

    if (outcome === 'created-inline' || !onCreatePage()) {
      log(`✅ Created: ${groupName} (trade=${pref})`);
      clearAttempt(groupName);
      setCurrent('');
      await sleep(CFG.DELAY_BETWEEN_GROUPS_MS);
      location.href = createUrl(); // always start next
      return;
    }

    // Still on create page with no outcome then retry
    if (outcome === 'timeout' && onCreatePage()) {
      const tries = getAttempt(groupName);
      if (tries <= 1) {
        log('No navigation after submit retrying once.');
        location.href = createUrl();
      } else {
        log('Still on create page - skipping this name.');
        clearAttempt(groupName);
        setCurrent('');
        await sleep(CFG.DELAY_BETWEEN_GROUPS_MS);
        setTimeout(resume, 50);
      }
    }
  }

  async function start(){
    if (getRunning()) return;
    const userId = (uidInput.value || '').trim() || CFG.DEFAULT_USER_ID;
    const names = (namesTA.value || '').split('\n').map(s=>s.trim()).filter(Boolean);
    if (!names.length){ log('No group names.'); return; }
    setUserId(userId);
    setTrade(tradeSel.value || getTrade());
    setQueue(names);
    setCurrent('');
    setAttempts({});
    setRunning(true);
    log(`Starting… (${names.length} names), trading=${getTrade()}`);
    await resume();
  }
  function stop(){ setRunning(false); setCurrent(''); log('Stopping after current step...'); }

  startBtn.addEventListener('click', start);
  stopBtn.addEventListener('click', stop);

  (function bootstrap(){
    // Don’t run on login pages, you have to log in
    if (qs('input[type="password"]')) { setRunning(false); setCurrent(''); return; }
    setTimeout(resume, 150);
  })();

})();