Kick Mesaj Fix

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();