Kick Mesaj Fix

Kick mesaj gönderme 403 hatası için fetch retry fix

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Kick Mesaj Fix
// @namespace    wichlost-kick-message-fix
// @version      6.0.0
// @description  Kick mesaj gönderme 403 hatası için fetch retry fix
// @match        https://kick.com/*
// @match        https://www.kick.com/*
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// ==/UserScript==

(function () {
  'use strict';

  const W = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

  const APP = 'Kick Mesaj Fix';
  const MAKER = 'wichlost';

  const SEND_REGEX = /\/api\/v2\/messages\/send\/\d+/i;

  const DEFAULTS = {
    enabled: true,
    retryCount: 1,
    retryDelay: 400,
    refreshBeforeRetry: true,
    notifySuccess: true,
    keepLogs: true,
    maxLogs: 80,
    debug: false
  };

  const state = {
    retrying: false,
    panelOpen: false,
    panelCreated: false,
    lastStatus: null,
    last403: null,
    lastFix: null,
    logs: GM_getValue('kmf_logs', []),
    total403: GM_getValue('kmf_total403', 0),
    totalFixed: GM_getValue('kmf_totalFixed', 0)
  };

  const originalFetch = W.fetch.bind(W);

  function opt(key) {
    return GM_getValue('kmf_' + key, DEFAULTS[key]);
  }

  function setOpt(key, value) {
    GM_setValue('kmf_' + key, value);
  }

  function now() {
    return new Date().toLocaleTimeString('tr-TR', {
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit'
    });
  }

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

  function esc(v) {
    return String(v ?? '')
      .replaceAll('&', '&')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#039;');
  }

  function saveLogs() {
    const max = Number(opt('maxLogs')) || 80;
    state.logs = state.logs.slice(-max);
    GM_setValue('kmf_logs', state.logs);
  }

  function log(text, data) {
    const row = {
      time: now(),
      text,
      data: data || null
    };

    if (opt('keepLogs')) {
      state.logs.push(row);
      saveLogs();
    }

    if (opt('debug')) {
      console.log(`[${APP}] ${row.time} - ${text}`, data || '');
    }

    updatePanel();
  }

  function bump(key) {
    const storeKey = key === '403' ? 'kmf_total403' : 'kmf_totalFixed';
    const next = Number(GM_getValue(storeKey, 0)) + 1;

    GM_setValue(storeKey, next);

    if (key === '403') state.total403 = next;
    if (key === 'fixed') state.totalFixed = next;

    updatePanel();
  }

  function isSendUrl(url) {
    try {
      const u = new URL(url, W.location.origin);
      return SEND_REGEX.test(u.pathname);
    } catch {
      return false;
    }
  }

  function getCookie(name) {
    const safe = name.replace(/[.$?*|{}()[\]\\/+^]/g, '\\$&');
    const match = W.document.cookie.match(new RegExp('(?:^|; )' + safe + '=([^;]*)'));
    return match ? decodeURIComponent(match[1]) : '';
  }

  function cloneHeaders(headers) {
    const out = new W.Headers(headers || {});

    out.set('Accept', out.get('Accept') || 'application/json, text/plain, */*');
    out.set('X-Requested-With', 'XMLHttpRequest');

    const xsrf = getCookie('XSRF-TOKEN');

    if (xsrf && !out.has('X-XSRF-TOKEN')) {
      out.set('X-XSRF-TOKEN', xsrf);
    }

    return out;
  }

  async function refreshSession() {
    const paths = [
      '/sanctum/csrf-cookie',
      '/api/v2/user',
      W.location.pathname
    ];

    log('Oturum/CSRF tazeleniyor');

    for (const path of paths) {
      try {
        const res = await originalFetch(path, {
          method: 'GET',
          credentials: 'include',
          cache: 'no-store',
          headers: {
            'Accept': 'application/json,text/html,*/*',
            'X-Requested-With': 'XMLHttpRequest'
          }
        });

        log(`Refresh: ${path} -> ${res.status}`);
        await sleep(220);
      } catch (err) {
        log(`Refresh hata: ${path}`, {
          error: err.message || String(err)
        });
      }
    }
  }

  async function buildRetryPayload(input, init) {
    if (input instanceof W.Request) {
      const req = input.clone();
      const method = req.method || 'GET';

      let body;

      if (!['GET', 'HEAD'].includes(method.toUpperCase())) {
        try {
          body = await req.clone().text();
        } catch {
          body = undefined;
        }
      }

      return {
        input: req.url,
        init: {
          method,
          body,
          credentials: 'include',
          cache: 'no-store',
          mode: req.mode,
          redirect: req.redirect,
          referrer: req.referrer,
          headers: cloneHeaders(req.headers)
        }
      };
    }

    return {
      input,
      init: {
        ...(init || {}),
        credentials: 'include',
        cache: 'no-store',
        headers: cloneHeaders(init && init.headers)
      }
    };
  }

  async function retryMessage(safeInput, safeInit, url) {
    if (state.retrying) {
      log('Retry zaten çalışıyor, pas geçildi');
      return null;
    }

    state.retrying = true;
    updatePanel();

    try {
      const max = Math.max(1, Number(opt('retryCount')) || 1);
      let lastRes = null;

      for (let i = 1; i <= max; i++) {
        if (opt('refreshBeforeRetry')) {
          await refreshSession();
        }

        await sleep(Number(opt('retryDelay')) || 400);

        const payload = await buildRetryPayload(safeInput, safeInit);

        log(`403 sonrası mesaj tekrar deneniyor: ${i}/${max}`);

        const retryRes = await originalFetch(payload.input, payload.init);

        lastRes = retryRes;
        state.lastStatus = retryRes.status;

        log(`Retry sonucu: ${retryRes.status}`);

        if (retryRes.status !== 403) {
          state.lastFix = {
            time: now(),
            status: retryRes.status,
            attempt: i,
            url
          };

          bump('fixed');

          log('Retry 403 dışında cevap verdi, mesaj düzelmiş olmalı', state.lastFix);

          if (retryRes.status >= 200 && retryRes.status < 300 && opt('notifySuccess')) {
            showToast('Mesaj gönderildi', `403 sonrası ${i}. denemede düzeldi`);
          }

          return retryRes;
        }
      }

      log('Retry sonrası 403 devam ediyor');
      return lastRes;
    } catch (err) {
      log('Retry hata verdi', {
        error: err.message || String(err)
      });

      return null;
    } finally {
      state.retrying = false;
      updatePanel();
    }
  }

  W.fetch = async function kickMessageFixPatchedFetch(input, init) {
    if (!opt('enabled')) {
      return originalFetch(input, init);
    }

    const url = input instanceof W.Request ? input.url : String(input || '');
    const isSend = isSendUrl(url);

    let safeInput = input;
    let safeInit = init ? { ...init } : init;

    if (isSend && input instanceof W.Request) {
      try {
        safeInput = input.clone();
      } catch {
        safeInput = input;
      }
    }

    let res;

    try {
      res = await originalFetch(input, init);
    } catch (err) {
      if (isSend) {
        log('Mesaj fetch hatası', {
          error: err.message || String(err)
        });
      }

      throw err;
    }

    if (!isSend) {
      return res;
    }

    state.lastStatus = res.status;
    log(`Mesaj gönderme isteği: ${res.status}`);

    if (res.status === 403) {
      state.last403 = {
        time: now(),
        url
      };

      bump('403');
      log('403 yakalandı', state.last403);

      const retried = await retryMessage(safeInput, safeInit, url);

      if (retried) {
        return retried;
      }
    }

    return res;
  };

  function ensureStyle() {
    if (W.document.getElementById('kmf-style')) return;

    const style = W.document.createElement('style');
    style.id = 'kmf-style';

    style.textContent = `
      #kmf-toast-wrap {
        position: fixed;
        left: 50%;
        bottom: 34px;
        transform: translateX(-50%);
        z-index: 2147483647;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
        pointer-events: none;
      }

      .kmf-toast {
        min-width: 260px;
        max-width: 360px;
        padding: 12px 15px;
        border-radius: 18px;
        background:
          radial-gradient(circle at top left, rgba(168,85,247,.30), transparent 42%),
          linear-gradient(145deg, rgba(12,9,20,.98), rgba(29,16,46,.98));
        color: #fff;
        border: 1px solid rgba(216,180,254,.28);
        box-shadow: 0 18px 55px rgba(0,0,0,.58), 0 0 34px rgba(168,85,247,.25);
        font-family: Arial, sans-serif;
        opacity: 0;
        transform: translateY(12px) scale(.98);
        text-align: center;
        animation: kmfToastIn .18s ease forwards, kmfToastOut .25s ease forwards 3.4s;
      }

      .kmf-toast-title {
        font-size: 13px;
        font-weight: 950;
      }

      .kmf-toast-msg {
        margin-top: 4px;
        font-size: 12px;
        color: #d8c6ff;
      }

      @keyframes kmfToastIn {
        to {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }

      @keyframes kmfToastOut {
        to {
          opacity: 0;
          transform: translateY(10px) scale(.98);
        }
      }

      #kmf-overlay {
        position: fixed;
        inset: 0;
        display: none;
        z-index: 2147483646;
        align-items: center;
        justify-content: center;
        background:
          radial-gradient(circle at 35% 25%, rgba(147,51,234,.22), transparent 34%),
          rgba(0,0,0,.62);
        backdrop-filter: blur(8px);
        font-family: Arial, sans-serif;
      }

      #kmf-overlay.kmf-show {
        display: flex;
      }

      #kmf-panel {
        width: 330px;
        border-radius: 22px;
        color: #fff;
        overflow: hidden;
        background:
          radial-gradient(circle at top left, rgba(168,85,247,.30), transparent 45%),
          linear-gradient(145deg, rgba(10,8,16,.98), rgba(26,14,42,.98));
        border: 1px solid rgba(216,180,254,.24);
        box-shadow: 0 28px 85px rgba(0,0,0,.68), 0 0 40px rgba(147,51,234,.20);
        transform: translateY(10px) scale(.97);
        opacity: 0;
        animation: kmfPanelIn .18s ease forwards;
      }

      @keyframes kmfPanelIn {
        to {
          transform: translateY(0) scale(1);
          opacity: 1;
        }
      }

      .kmf-head {
        padding: 14px 15px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        background: rgba(255,255,255,.045);
        border-bottom: 1px solid rgba(255,255,255,.08);
      }

      .kmf-title {
        display: flex;
        align-items: center;
        gap: 9px;
        font-size: 14px;
        font-weight: 950;
      }

      .kmf-dot {
        width: 10px;
        height: 10px;
        border-radius: 99px;
        background: #a855f7;
        box-shadow: 0 0 16px #a855f7;
      }

      .kmf-close {
        width: 31px;
        height: 31px;
        border: 0;
        border-radius: 12px;
        cursor: pointer;
        color: #fff;
        background: rgba(255,255,255,.09);
        font-size: 18px;
      }

      .kmf-body {
        padding: 13px;
      }

      .kmf-grid {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 8px;
      }

      .kmf-card {
        padding: 10px;
        border-radius: 15px;
        background: rgba(0,0,0,.25);
        border: 1px solid rgba(255,255,255,.075);
      }

      .kmf-card span {
        display: block;
        font-size: 10px;
        color: #bda7e8;
        margin-bottom: 6px;
      }

      .kmf-card b {
        display: block;
        font-size: 14px;
        color: #fff;
      }

      .kmf-row {
        margin-top: 10px;
        padding-top: 10px;
        border-top: 1px solid rgba(255,255,255,.08);
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 9px;
        color: #dbcaff;
        font-size: 12px;
      }

      .kmf-input {
        width: 58px;
        border: 1px solid rgba(255,255,255,.12);
        background: rgba(0,0,0,.28);
        color: #fff;
        border-radius: 11px;
        padding: 7px;
        text-align: center;
        font-weight: 900;
        outline: none;
      }

      .kmf-switch {
        position: relative;
        width: 44px;
        height: 25px;
        flex: 0 0 auto;
      }

      .kmf-switch input {
        display: none;
      }

      .kmf-slider {
        position: absolute;
        inset: 0;
        border-radius: 999px;
        cursor: pointer;
        background: rgba(255,255,255,.13);
      }

      .kmf-slider:before {
        content: "";
        position: absolute;
        width: 19px;
        height: 19px;
        left: 3px;
        top: 3px;
        border-radius: 50%;
        background: #fff;
        transition: .15s;
      }

      .kmf-switch input:checked + .kmf-slider {
        background: linear-gradient(135deg, #7c3aed, #d946ef);
      }

      .kmf-switch input:checked + .kmf-slider:before {
        transform: translateX(19px);
      }

      .kmf-actions {
        display: flex;
        gap: 7px;
        margin-top: 11px;
      }

      .kmf-btn {
        flex: 1;
        border: 0;
        border-radius: 13px;
        padding: 8px;
        cursor: pointer;
        color: #fff;
        font-size: 11px;
        font-weight: 900;
        background: rgba(255,255,255,.09);
        border: 1px solid rgba(255,255,255,.08);
      }

      .kmf-btn.primary {
        background: linear-gradient(135deg, #7c3aed, #c026d3);
        box-shadow: 0 0 20px rgba(192,38,211,.22);
      }

      .kmf-log {
        margin-top: 11px;
        max-height: 135px;
        overflow: auto;
        padding: 9px;
        border-radius: 13px;
        background: rgba(0,0,0,.28);
        color: #cbb8f7;
        font: 10px/1.45 Consolas, monospace;
        white-space: pre-wrap;
      }

      .kmf-maker {
        margin-top: 10px;
        text-align: center;
        color: #8b74bd;
        font-size: 10px;
        letter-spacing: .2px;
      }
    `;

    W.document.documentElement.appendChild(style);
  }

  function showToast(title, msg) {
    ensureStyle();

    let wrap = W.document.getElementById('kmf-toast-wrap');

    if (!wrap) {
      wrap = W.document.createElement('div');
      wrap.id = 'kmf-toast-wrap';
      W.document.documentElement.appendChild(wrap);
    }

    const box = W.document.createElement('div');
    box.className = 'kmf-toast';

    box.innerHTML = `
      <div class="kmf-toast-title">${esc(title)}</div>
      <div class="kmf-toast-msg">${esc(msg)}</div>
    `;

    wrap.appendChild(box);
    setTimeout(() => box.remove(), 3900);
  }

  function createPanel() {
    if (state.panelCreated) return;

    state.panelCreated = true;
    ensureStyle();

    const overlay = W.document.createElement('div');
    overlay.id = 'kmf-overlay';

    overlay.innerHTML = `
      <div id="kmf-panel">
        <div class="kmf-head">
          <div class="kmf-title">
            <span class="kmf-dot"></span>
            <span>Kick Mesaj Fix</span>
          </div>
          <button class="kmf-close" id="kmf-close">×</button>
        </div>

        <div class="kmf-body">
          <div class="kmf-grid">
            <div class="kmf-card">
              <span>Durum</span>
              <b id="kmf-status">Aktif</b>
            </div>
            <div class="kmf-card">
              <span>Son istek</span>
              <b id="kmf-last">Yok</b>
            </div>
            <div class="kmf-card">
              <span>403</span>
              <b id="kmf-403">0</b>
            </div>
            <div class="kmf-card">
              <span>Fix</span>
              <b id="kmf-fixed">0</b>
            </div>
          </div>

          <div class="kmf-row">
            <span>Fix aktif</span>
            <label class="kmf-switch">
              <input id="kmf-enabled" type="checkbox">
              <span class="kmf-slider"></span>
            </label>
          </div>

          <div class="kmf-row">
            <span>Retry</span>
            <input id="kmf-retry" class="kmf-input" type="number" min="1" max="5">
          </div>

          <div class="kmf-row">
            <span>Gecikme</span>
            <input id="kmf-delay" class="kmf-input" type="number" min="100" max="3000">
          </div>

          <div class="kmf-actions">
            <button class="kmf-btn primary" id="kmf-refresh">Oturum</button>
            <button class="kmf-btn" id="kmf-copy">Log</button>
            <button class="kmf-btn" id="kmf-clear">Temizle</button>
          </div>

          <div class="kmf-log" id="kmf-log"></div>
          <div class="kmf-maker">${MAKER}</div>
        </div>
      </div>
    `;

    W.document.documentElement.appendChild(overlay);

    overlay.addEventListener('click', e => {
      if (e.target === overlay) closePanel();
    });

    W.document.getElementById('kmf-close').onclick = closePanel;

    W.document.getElementById('kmf-refresh').onclick = async () => {
      await refreshSession();
      showToast('Oturum tazelendi', 'CSRF/oturum yenileme denendi');
    };

    W.document.getElementById('kmf-copy').onclick = async () => {
      const text = logsText();

      try {
        await navigator.clipboard.writeText(text);
        showToast('Log kopyalandı', 'Panoya aktarıldı');
      } catch {
        prompt('Loglar:', text);
      }
    };

    W.document.getElementById('kmf-clear').onclick = () => {
      state.logs = [];
      GM_setValue('kmf_logs', []);
      updatePanel();
      showToast('Log temizlendi', 'Kayıtlar silindi');
    };

    W.document.getElementById('kmf-enabled').onchange = e => {
      setOpt('enabled', e.target.checked);
      updatePanel();
    };

    W.document.getElementById('kmf-retry').onchange = e => {
      let val = Number(e.target.value);
      if (!Number.isFinite(val)) val = DEFAULTS.retryCount;
      val = Math.max(1, Math.min(5, val));
      setOpt('retryCount', val);
      e.target.value = val;
    };

    W.document.getElementById('kmf-delay').onchange = e => {
      let val = Number(e.target.value);
      if (!Number.isFinite(val)) val = DEFAULTS.retryDelay;
      val = Math.max(100, Math.min(3000, val));
      setOpt('retryDelay', val);
      e.target.value = val;
    };

    updatePanel();
  }

  function openPanel() {
    createPanel();
    state.panelOpen = true;

    const overlay = W.document.getElementById('kmf-overlay');
    if (overlay) overlay.classList.add('kmf-show');

    updatePanel();
  }

  function closePanel() {
    state.panelOpen = false;

    const overlay = W.document.getElementById('kmf-overlay');
    if (overlay) overlay.classList.remove('kmf-show');
  }

  function togglePanel() {
    if (state.panelOpen) closePanel();
    else openPanel();
  }

  function logsText() {
    if (!state.logs.length) return 'Log yok';

    return state.logs.map(x => {
      let line = `${x.time}  ${x.text}`;
      if (x.data) line += `\n${JSON.stringify(x.data, null, 2)}`;
      return line;
    }).join('\n\n');
  }

  function updatePanel() {
    if (!state.panelCreated) return;

    const status = W.document.getElementById('kmf-status');
    const last = W.document.getElementById('kmf-last');
    const c403 = W.document.getElementById('kmf-403');
    const fixed = W.document.getElementById('kmf-fixed');
    const enabled = W.document.getElementById('kmf-enabled');
    const retry = W.document.getElementById('kmf-retry');
    const delay = W.document.getElementById('kmf-delay');
    const logBox = W.document.getElementById('kmf-log');

    if (status) status.textContent = opt('enabled') ? (state.retrying ? 'Deniyor' : 'Aktif') : 'Kapalı';
    if (last) last.textContent = state.lastStatus || 'Yok';
    if (c403) c403.textContent = String(GM_getValue('kmf_total403', 0));
    if (fixed) fixed.textContent = String(GM_getValue('kmf_totalFixed', 0));

    if (enabled) enabled.checked = !!opt('enabled');
    if (retry) retry.value = opt('retryCount');
    if (delay) delay.value = opt('retryDelay');

    if (logBox) {
      logBox.textContent = state.logs
        .slice(-10)
        .map(x => `${x.time}  ${x.text}`)
        .join('\n') || 'Log yok';
    }
  }

  function bootPanelHotkey() {
    W.document.addEventListener('keydown', e => {
      if (e.altKey && e.shiftKey && e.key.toLowerCase() === 'k') {
        e.preventDefault();
        togglePanel();
      }
    });
  }

  GM_registerMenuCommand('Kick Mesaj Fix Paneli', togglePanel);

  GM_registerMenuCommand('Kick Mesaj Fix Aç/Kapat', () => {
    setOpt('enabled', !opt('enabled'));
    showToast('Kick Mesaj Fix', opt('enabled') ? 'Aktif' : 'Kapalı');
    updatePanel();
  });

  GM_registerMenuCommand('Kick Mesaj Fix Log Temizle', () => {
    state.logs = [];
    GM_setValue('kmf_logs', []);
    updatePanel();
    showToast('Log temizlendi', 'Kayıtlar silindi');
  });

  if (W.document.readyState === 'loading') {
    W.document.addEventListener('DOMContentLoaded', () => {
      ensureStyle();
      bootPanelHotkey();
      log('Script aktif');
    });
  } else {
    ensureStyle();
    bootPanelHotkey();
    log('Script aktif');
  }

})();