Flashii Chat Userscripts
#20033
For now I'm calling this thing gaataa, even though it's probably gonna be more in the future. But hey, we've got world's mediocrest and least secure typing indicators for people using the script when my wonderful little NUC is online.

Edit 1: I have updated (fucking rewrote) it to use websockets and also ban fun (upside down RCE is real and can hurt you).


// ==UserScript==
// @name         Gaataa
// @namespace    https://saikuru.net/
// @version      2.0
// @description  Gaataa typing indicators
// @author       saikuru0
// @match        *://chat.flashii.net/*
// @connect      gt.nii.so
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  const WS_URL = "wss://gt.nii.so/typing";
  let ws = null;
  let indicator = null;
  let uid = null;
  let frame = 0;
  let animTimer = null;

  const frames = ["○○○", "●○○", "○●○", "○○●", "○○○"];

  function createIndicator() {
    const el = document.createElement("div");
    el.style.cssText = `
      position: fixed;
      color: white;
      opacity: 0.8;
      font-size: 12px;
      font-family: Tahoma,Geneva,Arial,Helvetica,sans-serif;
      pointer-events: none;
      display: none;
      z-index: 4444;
      max-width: 100%;
    `;
    document.body.appendChild(el);
    return el;
  }

  function updatePos() {
    if (!indicator) return;
    const input = document.querySelector(
      '.input__main, [class*="input__main"], input[type="text"], textarea',
    );
    if (input) {
      const rect = input.getBoundingClientRect();
      indicator.style.left = `${rect.right - indicator.offsetWidth - 10}px`;
      indicator.style.top = `${rect.top - indicator.offsetHeight - 10}px`;
    }
  }

  function getUser(id) {
    try {
      const user = Umi.Users.Get(id);
      return { name: user.name, color: user.colour };
    } catch (error) {
      return { name: id, color: "white" };
    }
  }

  function updateIndicator(users) {
    if (!indicator) indicator = createIndicator();

    const filtered = users.filter((u) => u !== uid);

    if (!filtered.length) {
      indicator.style.display = "none";
      if (animTimer) {
        clearInterval(animTimer);
        animTimer = null;
      }
      return;
    }

    if (!animTimer) {
      animTimer = setInterval(() => {
        frame = (frame + 1) % frames.length;
        updateContent(filtered);
      }, 444);
    }

    updateContent(filtered);
    indicator.style.display = "block";
    updatePos();
  }

  function updateContent(filtered) {
    const anim = frames[frame];
    const displays = filtered.map(getUser);
    const colorTag = (u) => `<b style="color: ${u.color}">${u.name}</b>`;

    let userText;
    if (displays.length <= 2) {
      userText = displays.map(colorTag).join(" and ");
    } else if (displays.length === 3) {
      const tags = displays.map(colorTag);
      userText = `${tags.slice(0, -1).join(", ")}, and ${tags[tags.length - 1]}`;
    } else {
      const visible = displays.slice(0, 2).map(colorTag);
      userText = `${visible.join(", ")} and ${displays.length - 2} others`;
    }

    const verb = displays.length === 1 ? "is" : "are";
    indicator.innerHTML = `<span style="font-size: 12px">${anim}</span> ${userText} ${verb} typing...`;
  }

  function connect() {
    if (ws?.readyState === WebSocket.OPEN) return;

    console.log("connecting to gaataa...");
    ws = new WebSocket(WS_URL);
    ws.onopen = () => console.log("connected to gaataa");
    ws.onmessage = (e) => {
      try {
        const data = JSON.parse(e.data);
        if (data.users) updateIndicator(data.users);
      } catch (err) {
        console.log("failed to parse message:", err);
      }
    };
    ws.onclose = () => {
      console.log("gaataa disconnected, reconnecting in 4.444s...");
      updateIndicator([]);
      setTimeout(connect, 4444);
    };
    ws.onerror = (error) => console.log("gaataa error:", error);
  }

  function send(action) {
    if (ws?.readyState === WebSocket.OPEN && uid) {
      const msg = { action, uid };
      ws.send(JSON.stringify(msg));
    } else {
      console.log("cannot send:", {
        wsOpen: ws?.readyState === WebSocket.OPEN,
        uid,
      });
    }
  }

  function setupTextarea() {
    const textarea = document.querySelector(
      "textarea.input__text, textarea[class*='input'], .input__text textarea, textarea",
    );
    console.log("textarea found:", !!textarea);
    if (textarea) {
      console.log("setting up textarea event listeners");
      const typing = () => {
        if (!textarea.value.trim()) {
          return;
        }
        send("type");
      };
      textarea.addEventListener("input", typing);
      textarea.addEventListener("keyup", typing);
      return true;
    }
    return false;
  }

  function setupSubmit() {
    const form = document.querySelector("form");
    if (form) {
      form.addEventListener("submit", () => send("stop"));
      return true;
    }

    let buttons = [
      ...document.querySelectorAll(
        'button[type="submit"], input[type="submit"]',
      ),
    ];
    document.querySelectorAll("button").forEach((btn) => {
      if (btn.textContent.toLowerCase().includes("send")) buttons.push(btn);
    });

    buttons.forEach((btn) => btn.addEventListener("click", () => send("stop")));
    return buttons.length > 0;
  }

  function setup() {
    const textareaOk = setupTextarea();
    const submitOk = setupSubmit();
    return textareaOk;
  }

  function init() {
    window.addEventListener("umi:connect", () => {
      console.log("umi:connect event fired");
      try {
        uid = Number(Umi.User.getCurrentUser().id);
        console.log("got uid:", uid);
        connect();
      } catch (e) {
        console.log("failed to get uid:", e);
      }
    });

    if (!setup()) {
      console.log("setup failed, retrying every 4.444s");
      const retry = setInterval(() => {
        console.log("retrying setup...");
        if (setup()) {
          console.log("setup successful on retry");
          clearInterval(retry);
        }
      }, 4444);
    } else {
      console.log("setup successful");
    }

    window.addEventListener("scroll", updatePos);
    window.addEventListener("resize", updatePos);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }

  window.addEventListener("beforeunload", () => {
    if (animTimer) clearInterval(animTimer);
    if (ws) ws.close();
  });
})();
https://saikuru.net/sig
#20035
Wrote this to fix an issue with chat where you can see peoples messages

// ==UserScript==
// @name         Delete Chat
// @namespace    http://tampermonkey.net/
// @version      v2
// @description  It Deletes Chat
// @author       Chat Delete Pro
// @match        https://chat.flashii.net/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=flashii.net
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    setTimeout(() => {
        const element = document.getElementById('umi-chat');
        element.parentNode.removeChild(element);
    }, 1000);
})();
I once shot a man in Reno, just to watch him die.
#20049
BUG REPORT: please reduce timeout to 1ms i can still see peoples messages and it is upsetting me
"I know what I must do, be strong lest I shall perish!" 💪✡
https://i.fii.moe/uYgszrmMmZJ5PQz6UXu3x7TZL6m1hmZV
#20065
Markdown to bbcode on ctrl+m because i failed to stop the message from sending before it converts :')

// ==UserScript==
// @name         md2bb
// @namespace    https://saikuru.net/
// @version      1.0
// @description  Markdown to bbcode on Ctrl+M
// @author       saikuru0
// @match        *://chat.flashii.net/*
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  function convert(text) {
    text = text.replace(/```([\s\S]*?)```/g, "[code]$1[\/code]");
    text = text.replace(/`([^`]+)`/g, "[code]$1[\/code]");
    text = text.replace(/\*\*\*([^*]+)\*\*\*/g, "[i][b]$1[/b][/i]");
    text = text.replace(/\*\*([^*]+)\*\*/g, "[b]$1[/b]");
    text = text.replace(/\*([^*]+)\*/g, "[i]$1[/i]");
    text = text.replace(/__([^_]+)__/g, "[u]$1[/u]");
    text = text.replace(/~~([^~]+)~~/g, "[s]$1[/s]");
    text = text.replace(/\|\|([^|]+)\|\|/g, "[spoiler]$1[/spoiler]");
    return text;
  }

  function findInput() {
    const selectors = [
      "textarea.input__text",
      'textarea[class*="input"]',
      ".input__text textarea",
      "textarea",
      ".input__main",
      '[class*="input__main"]',
      'input[type="text"]',
    ];

    for (const s of selectors) {
      const el = document.querySelector(s);
      if (el) return el;
    }
    return null;
  }

  function setup() {
    const input = findInput();
    if (!input) return false;

    input.addEventListener("keydown", function (e) {
      if (e.ctrlKey && e.key === "m") {
        e.preventDefault();
        const converted = convert(input.value);
        input.value = converted;
        input.dispatchEvent(new Event("input", { bubbles: true }));
      }
    });

    return true;
  }

  function init() {
    if (!setup()) {
      const retry = setInterval(() => {
        if (setup()) clearInterval(retry);
      }, 4444);
    }
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }

  window.addEventListener("umi:connect", setup);
})();
https://saikuru.net/sig