Kick Mesaj Fix

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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');
  }

})();