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).
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();
});
})();
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.
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!" 💪✡

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