InvadedLands Reactions (Updated for fetch JSON)

Blocks reaction fetch packets, shows GUI, resends with chosen emoji, updates client UI

当前为 2025-07-19 提交的版本,查看 最新版本

// ==UserScript==
// @name         InvadedLands Reactions (Updated for fetch JSON)
// @namespace    http://tampermonkey.net/
// @version      7.1
// @description  Blocks reaction fetch packets, shows GUI, resends with chosen emoji, updates client UI
// @author       PillowPB (updated by EclipseMaster)
// @match        *https://invadedlands.net/*
// @grant        none
// @license      Creative Commons
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict';
  // This update patches fetch() to block reactions and show custom menu as before.
  // Annoying update, but here we are...

  const reactionMap = {
    "1": "👍", "2": "❤️", "3": "😂",
    "4": "😮", "5": "😢", "6": "😡"
  };

  let isHacking = true;
  let hackedPostId = null;
  let lastClickEvent = null;

  // Hook the fetch() to intercept reaction requests (new method)
  const originalFetch = window.fetch;
  window.fetch = function(input, init) {
    let url = input;
    if (input instanceof Request) url = input.url;

    if (typeof url === 'string' && url.includes('/react?reaction_id=')) {
      //URL pattern: /profile-posts/{id}/react
      const postIdMatch = url.match(/\/profile-posts\/(\d+)\/react/);
      hackedPostId = postIdMatch ? postIdMatch[1] : null;

      console.log('[invadedlands] Blocked reaction fetch for post', hackedPostId);

      const x = lastClickEvent?.clientX || window.innerWidth / 2;
      const y = lastClickEvent?.clientY || window.innerHeight / 2;
      if (hackedPostId) showReactionMenu(hackedPostId, x, y);

      // Block  fetch
      return new Promise(() => {});
    }

    return originalFetch.apply(this, arguments);
  };

  // Keep XHR hook for any other reaction packets
  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function(method, url) {
    this._method = method;
    this._url = url;
    return originalOpen.apply(this, arguments);
  };
  XMLHttpRequest.prototype.send = function(body) {
    if (isHacking && this._url?.includes('/react?reaction_id=')) {
      const postIdMatch = this._url.match(/\/profile-posts\/(\d+)\/react/);
      hackedPostId = postIdMatch ? postIdMatch[1] : null;

      console.log('[invadedlands] Blocked reaction XHR for post', hackedPostId);
      const x = lastClickEvent?.clientX || window.innerWidth / 2;
      const y = lastClickEvent?.clientY || window.innerHeight / 2;

      if (hackedPostId) showReactionMenu(hackedPostId, x, y);
      return; // block original packet
    }
    return originalSend.apply(this, arguments);
  };

  //menu positioning
  document.addEventListener('click', e => lastClickEvent = e, true);

  // Send reaction with JSON payload
  function sendCustomReaction(postId, reactionId) {
    isHacking = false;

    const xfToken = document.querySelector('input[name="_xfToken"]')?.value;
    if (!xfToken) {
      console.warn('Missing _xfToken');
      return;
    }

    const payload = {
      _xfResponseType: "json",
      _xfWithData: 1,
      _xfRequestUri: window.location.pathname,
      _xfToken: xfToken,
    };

    fetch(`/profile-posts/${postId}/react?reaction_id=${reactionId}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
      },
      body: JSON.stringify(payload),
    })
    .then(response => {
      if (!response.ok) throw new Error('Network response was not ok');
      return response.json();
    })
    .then(data => {
      console.log(`[omg] Sent reaction ${reactionId} to post ${postId}`, data);

      // Update reaction count UI
      const postElem = document.querySelector(`[data-content*="profile-post-${postId}"]`);
      if (postElem) {
        const reactionBtn = postElem.querySelector(`a.messageReactionLink[href*="reaction_id=${reactionId}"]`);
        if (reactionBtn) {
          let countSpan = reactionBtn.querySelector('.reactionCount');
          if (countSpan) {
            countSpan.textContent = (parseInt(countSpan.textContent) || 0) + 1;
          } else {
            const span = document.createElement('span');
            span.className = 'reactionCount';
            span.textContent = '1';
            reactionBtn.appendChild(span);
          }
          reactionBtn.classList.add('reactionSelected');
          setTimeout(() => reactionBtn.classList.remove('reactionSelected'), 1500);
        }
      }
      document.dispatchEvent(new CustomEvent('reactionSent', { detail: { postId, reactionId } }));
    })
    .catch(error => {
      console.error('[WTF] Failed to send reaction:', error);
    })
    .finally(() => {
      setTimeout(() => isHacking = true, 100);
    });
  }

  // Show reaction selection menu near click
  function showReactionMenu(postId, x, y) {
    document.querySelectorAll('.reaction-menu-container').forEach(el => el.remove());

    const menu = document.createElement('div');
    menu.className = 'reaction-menu-container';
    menu.style.cssText = `
      position: fixed;
      left: ${Math.min(x, window.innerWidth - 220)}px;
      top: ${Math.min(y, window.innerHeight - 120)}px;
      background: #2d2d2d;
      border-radius: 8px;
      padding: 10px;
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 8px;
      z-index: 99999;
      box-shadow: 0 2px 10px rgba(0,0,0,0.5);
      border: 1px solid #444;
    `;

    Object.entries(reactionMap).forEach(([id, emoji]) => {
      const btn = document.createElement('div');
      btn.className = 'reaction-emoji-btn';
      btn.textContent = emoji;
      btn.title = `Reaction ID: ${id}`;
      btn.style.cssText = `
        font-size: 24px;
        cursor: pointer;
        padding: 4px;
        border-radius: 4px;
        transition: all 0.2s;
        text-align: center;
      `;

      btn.addEventListener('mouseenter', () => {
        btn.style.background = '#3e3e3e';
        btn.style.transform = 'scale(1.2)';
      });
      btn.addEventListener('mouseleave', () => {
        btn.style.background = 'transparent';
        btn.style.transform = 'scale(1)';
      });
      btn.addEventListener('click', () => {
        sendCustomReaction(postId, id);
        menu.remove();
      });

      menu.appendChild(btn);
    });

    document.body.appendChild(menu);

    setTimeout(() => {
      const clickHandler = (e) => {
        if (!menu.contains(e.target)) {
          menu.remove();
          document.removeEventListener('click', clickHandler);
        }
      };
      document.addEventListener('click', clickHandler);
    }, 10);
  }

  console.log('Reaction invadedlands initialized (fetch patched with JSON payload)');
})();