InvadedLands Reactions

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

目前為 2025-07-17 提交的版本,檢視 最新版本

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

(function() {
  'use strict';
  const reactionMap = {
    "1": "👍", "2": "❤️", "3": "😂",
    "4": "😮", "5": "😢", "6": "😡"
  };

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

  // packets are very cool i do not hate them
  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(/\/posts\/(\d+)\/react/);
      hackedPostId = postIdMatch ? postIdMatch[1] : null;

      console.log('[invadedlands is the best] Blocked reaction packet 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);
  };

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

  function sendCustomReaction(postId, reactionId) {
    isHacking = false;

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

    const formData = new FormData();
    formData.append('_xfToken', xfToken);
    formData.append('_xfRequestUri', window.location.pathname);
    formData.append('_xfWithData', '1');
    formData.append('_xfResponseType', 'json');

    fetch(`/posts/${postId}/react?reaction_id=${reactionId}`, {
      method: 'POST',
      body: new URLSearchParams(formData),
      headers: { 'X-Requested-With': 'XMLHttpRequest' }
    })
    .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*="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);
    });
  }

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