-
-
Save Radvylf/bdbaedb4c2bd1ba1127ea3251558bec6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Chat on Chat | |
// @namespace http://tampermonkey.net/ | |
// @version 1.8.0 | |
// @description It's like chat, but you use a chat program to chat :o | |
// @author Radvylf Programs | |
// @match https://chat.stackexchange.com/chatonchat | |
// @grant none | |
// ==/UserScript== | |
(() => { | |
var chat_on_chat = async () => { | |
var body = document.body; | |
while (body.firstChild) | |
body.removeChild(body.firstChild); | |
for (var stylsh of document.styleSheets) | |
stylsh.disabled = !0; | |
document.title = "Chat-on-Chat"; | |
var link = document.createElement("link"); | |
link.rel = "stylesheet"; | |
link.href = "https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&family=Roboto+Mono&display=swap"; | |
document.head.appendChild(link); | |
var stylsh = document.createElement("style"); | |
stylsh.innerHTML = ( | |
"body { margin: 0; font-family: 'Atkinson Hyperlegible', sans-serif; font-size: 12px; line-height: 14px; } h2 { margin: 0; margin-top: 6px; font-size: 10px; } .post { max-width: 100%; max-height: 480px; padding: 1px 4px; } " + | |
"p { margin: 0; } .post h1, .post h2, .post h3, .post h4, .post h5, .post h6 { margin: 0; padding: 6px 0; font-size: 12px; font-weight: bold; } " + | |
".post code { font-family: 'Roboto Mono', monospace; font-size: 10px; padding: 0 2px; background-color: #f0f0f0; } .gray { opacity: 0.6; } " + | |
".post a { color: #0000cc; } .post img { max-height: 240px; max-width: 100%; display: block; padding: 6px 0; } " + | |
".post a:visited { color: #440088; } " + | |
".post pre { margin: 0; padding: 6px; font-family: 'Roboto Mono', monospace; font-size: 10px; background-color: #f0f0f0; } " + | |
".post ol, .post ul { padding-left: 24px; margin-block-start: 6px; margin-block-end: 6px; margin-inline-start: 0; margin-inline-end: 0; } " + | |
".post blockquote { margin: 0; padding: 6px 0; padding-left: 24px; color: #444444; } " + | |
".post blockquote blockquote blockquote { display: none; } " + | |
".grow { display: grid; } .grow > * { display: block; } .grow::after { content: attr(data-copy) ' '; white-space: pre-wrap; visibility: hidden; } .grow > textarea { resize: none; overflow: hidden; } " + | |
"textarea, .grow::after { padding: 6px; border: 1px solid #888888; font-family: 'Roboto Mono', monospace; font-size: 10px; line-height: 12px; word-break: break-all; background-color: inherit; grid-area: 1 / 1 / 2 / 2; } " + | |
"textarea:focus { border: 1px solid #66b1ff; outline: 1px solid #66b1ff; } textarea::placeholder { color: #888888; } textarea:disabled { color: #666666; } " + | |
".grow { width: calc(100vw - 24px); position: absolute; left: 12px; bottom: 12px; } .posts { width: calc(100vw - 24px); height: calc(100vh - 56px); padding: 12px; padding-top: 6px; overflow-y: scroll; word-break: break-word; } " + | |
".thrown { padding: 6px 0; font-weight: bold; text-align: right; display: none; } " + | |
".post_cont { position: relative; } .post_cont:hover { outline: 1px solid #444444; } .tools { position: absolute; right: 0; bottom: 0; padding: 1px 4px; display: none; font-size: 10px; } .post_cont:hover .tools { display: block; } " + | |
".tools { user-select: none; cursor: initial; } .tools button { cursor: pointer; background: transparent; border: none; font: inherit; padding: 0; } .tools button:hover { font-weight: bold; }" | |
); | |
body.appendChild(stylsh); | |
var script; | |
script = document.createElement("script"); | |
script.src = "https://questwriting.org/chatonchat/commonmark.min.js"; | |
await new Promise((r) => { | |
script.onload = r; | |
body.appendChild(script); | |
}); | |
script = document.createElement("script"); | |
script.innerHTML = "MathJax = { tex: { inlineMath: [[\"\\\\$\", \"\\\\$\"]], displayMath: [[\"$$\", \"$$\"]], maxMacros: 24, maxBuffer: 1024 } }"; | |
body.appendChild(script); | |
await new Promise((r) => window.setTimeout(r, 0)); | |
script = document.createElement("script"); | |
script.id = "MathJax-script"; | |
script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"; | |
await new Promise((r) => { | |
script.onload = r; | |
body.appendChild(script); | |
}); | |
console.log("Scripts ins'd"); | |
var cm_p = new commonmark.Parser(); | |
var cm_r = new commonmark.HtmlRenderer({ | |
safe: true | |
}); | |
console.log("CommonMark"); | |
var rooms_body = document.createElement("div"); | |
rooms_body.innerHTML = (await (await fetch("https://chat.stackexchange.com/rooms")).text()); | |
var f_copy = rooms_body.querySelector("#fkey").value; | |
var who_id = rooms_body.querySelector(".topbar-menu-links a").href.match(/\d+/)[0]; | |
var who = rooms_body.querySelector(".topbar-menu-links a").textContent; | |
var coc_kon = window.localStorage.getItem("coc-konami") == "1"; | |
console.log("F_copy and who"); | |
var room = 136548; | |
var transcript = new Map(); | |
var chat_to_data = (chat) => { | |
if (chat.split("").some(c => c.charCodeAt(0) < 0x8000) || chat.length < 2) | |
return null; | |
return (chat.split("").map(c => c.charCodeAt(0).toString(2).slice(-14).padStart(14, "0")).join("") + "0000000").match(/.{8}/g).map(b => parseInt(b, 2)); | |
}; | |
var data_to_string = (data) => { | |
data = data.slice(2); | |
var string = ""; | |
for (var i = 0; i < data.length; i++) { | |
if (data[i] == 0) { | |
continue; | |
} else if (data[i] < 128) { | |
string += String.fromCodePoint(data[i]); | |
} else if (data[i] < 192) { | |
if (i >= data.length - 1) | |
return null; | |
else | |
string += String.fromCodePoint((data[i] % 64) * 256 + data[++i] + 128); | |
} else { | |
if (i >= data.length - 2) | |
return null; | |
else if ((data[i] % 32) * 65536 + data[i + 1] * 256 + data[i + 2] + 16512 > 1114111) | |
return null; | |
else | |
string += String.fromCodePoint((data[i] % 32) * 65536 + data[++i] * 256 + data[++i] + 16512); | |
} | |
} | |
return string; | |
}; | |
var chat_to_string = (chat) => { | |
var data = chat_to_data(chat); | |
if (!data || data[0] != 0xcc || data[1] != 0x01) | |
return null; | |
return data_to_string(data); | |
}; | |
var string_to_data = (string) => { | |
var data = [0xcc, 0x01]; | |
var point; | |
for (var char of string) { | |
point = char.codePointAt(); | |
if (point < 128) | |
data.push(point); | |
else if (point < 16512) | |
data.push(128 + ((point - 128) / 256 | 0), (point - 128) % 256); | |
else | |
data.push(192 + ((point - 16512) / 65536 | 0), ((point - 16512) / 256 | 0) % 256, (point - 16512) % 256); | |
} | |
return data; | |
}; | |
var data_to_chat = (data) => (data.map(d => d.toString(2).padStart(8, "0")).join("") + "0000000000000").match(/.{14}/g).map(x => String.fromCharCode(parseInt("10" + x, 2))).join(""); | |
var string_to_chat = (string) => data_to_chat(string_to_data(string)); | |
var gray = []; | |
var posts = document.createElement("div"); | |
posts.className = "posts"; | |
body.appendChild(posts); | |
var drop_post = async (id, tick = 1) => { | |
var fin = 0; | |
var mono, post_cont; | |
for (mono of [...posts.childNodes]) { | |
for (post_cont of [...mono.childNodes].slice(1)) { | |
if ("id" in post_cont.dataset && post_cont.dataset.id == id) { | |
if (mono.childNodes.length == 2) { | |
posts.removeChild(mono); | |
} else { | |
mono.removeChild(post_cont); | |
} | |
fin = 1; | |
break; | |
} | |
} | |
if (fin) | |
break; | |
} | |
transcript.delete(id); | |
}; | |
var modify_post = async (id, string, tick = 1) => { | |
var fin = 0; | |
var mono, post_cont, imgs = []; | |
for (var mono of [...posts.childNodes]) { | |
for (var post_cont of [...mono.childNodes].slice(1)) { | |
if ("id" in post_cont.dataset && post_cont.dataset.id == id) { | |
post_cont.firstChild.innerHTML = cm_r.render(cm_p.parse(string)); | |
imgs = post_cont.querySelectorAll("img"); | |
fin = 1; | |
break; | |
} | |
} | |
if (fin) | |
break; | |
} | |
transcript.set(id, [...transcript.get(id).slice(0, 2), string]); | |
if (tick) { | |
await Promise.all([...imgs].map(i => new Promise((r) => i.complete ? r() : (i.onload = r)))); | |
await MathJax.typesetPromise(); | |
} | |
return imgs; | |
}; | |
var ctr = 0; | |
var show_post = async (id, from_id, from, string, tick = 1, count = 0) => { | |
var p_scroll = (posts.scrollHeight - posts.offsetHeight) - posts.scrollTop; | |
if (string == null) { | |
if (transcript.has(id)) { | |
await drop_post(id, tick); | |
return []; | |
} | |
return []; | |
} | |
if (transcript.has(id)) | |
return modify_post(id, string, tick); | |
if (count && !document.hasFocus()) | |
document.title = "(" + ++ctr + ") Chat-on-Chat"; | |
var n_post_cont = document.createElement("div"); | |
n_post_cont.className = "post_cont"; | |
n_post_cont.dataset.id = id; | |
var n_post = document.createElement("div"); | |
n_post.className = "post"; | |
n_post.innerHTML = cm_r.render(cm_p.parse(string)); | |
var n_tools = document.createElement("div"); | |
n_tools.className = "tools"; | |
var mod = document.createElement("button"); | |
mod.textContent = (coc_kon ? "Modify" : "Edit"); | |
mod.onclick = () => { | |
// Modify | |
}; | |
var drop = document.createElement("button"); | |
drop.onclick = async () => { | |
var ab_ctrl, ab_st, data; | |
ab_ctrl = new AbortController(); | |
ab_st = window.setTimeout(() => ab_ctrl.abort(), 8000); | |
data = await fetch("https://chat.stackexchange.com/messages/" + id + "/delete", { | |
method: "POST", | |
body: new URLSearchParams({ | |
fkey: f_copy | |
}), | |
signal: ab_ctrl.signal | |
}); | |
window.clearTimeout(ab_st); | |
if (data.status == 200 && await data.text() == "ok") | |
return; | |
/* cc 02 drop | |
ab_ctrl = new AbortController(); | |
ab_st = window.setTimeout(() => ab_ctrl.abort(), 8000); | |
data = await fetch("https://chat.stackexchange.com/messages/" + id + "/delete", { | |
method: "POST", | |
body: new URLSearchParams({ | |
fkey: f_copy | |
}), | |
signal: ab_ctrl.signal | |
}); | |
window.clearTimeout(ab_st);*/ | |
}; | |
drop.textContent = (coc_kon ? "Drop" : "Delete"); | |
var quotify = document.createElement("button"); | |
quotify.onclick = () => { | |
// Quotify | |
}; | |
quotify.textContent = (coc_kon ? "Quotify" : "Reply"); | |
n_tools.appendChild(mod); | |
n_tools.appendChild(document.createTextNode(" \xb7 ")); | |
n_tools.appendChild(drop); | |
n_tools.appendChild(document.createTextNode(" \xb7 ")); | |
n_tools.appendChild(quotify); | |
n_post_cont.appendChild(n_post); | |
n_post_cont.appendChild(n_tools); | |
var imgs = n_post.querySelectorAll("img"); | |
var mono, p_mono, post_cont, fin, s_mono, a_mono, dst, d_post_cont, n_mono, h2; | |
fin = 0; | |
for (mono of [...posts.childNodes]) { | |
for (post_cont of [...mono.childNodes].slice(1)) { | |
if (!("id" in post_cont.dataset)) | |
continue; | |
if (+post_cont.dataset.id > id) { | |
if (mono.dataset.from == from_id) { | |
mono.insertBefore(n_post_cont, post_cont); | |
} else { | |
n_mono = document.createElement("div"); | |
n_mono.className = "mono"; | |
n_mono.dataset.from = from_id; | |
h2 = document.createElement("h2"); | |
h2.textContent = from; | |
n_mono.appendChild(h2); | |
n_mono.appendChild(n_post_cont); | |
if (mono.childNodes[1] == post_cont) { | |
if (p_mono && p_mono.dataset.from == from_id) { | |
p_mono.appendChild(n_post_cont); | |
} else { | |
posts.insertBefore(n_mono, mono); | |
} | |
} else { | |
s_mono = document.createElement("div"); | |
a_mono = document.createElement("div"); | |
s_mono.className = "mono"; | |
a_mono.className = "mono"; | |
s_mono.dataset.from = mono.dataset.from; | |
a_mono.dataset.from = mono.dataset.from; | |
h2 = document.createElement("h2"); | |
h2.textContent = mono.childNodes[0].textContent; | |
s_mono.appendChild(h2.cloneNode(true)); | |
a_mono.appendChild(h2.cloneNode(true)); | |
dst = 0; | |
for (d_post_cont of [...mono.childNodes].slice(1)) { | |
if (d_post_cont == post_cont) | |
dst = 1; | |
(dst == 0 ? s_mono : a_mono).appendChild(d_post_cont); | |
} | |
posts.insertBefore(a_mono, mono); | |
posts.insertBefore(n_mono, a_mono); | |
posts.insertBefore(s_mono, n_mono); | |
posts.removeChild(mono); | |
} | |
} | |
fin = 1; | |
break; | |
} | |
} | |
p_mono = mono; | |
if (fin) | |
break; | |
} | |
if (!fin) { | |
if (mono && mono.dataset.from == from_id) { | |
mono.appendChild(n_post_cont); | |
} else { | |
n_mono = document.createElement("div"); | |
n_mono.className = "mono"; | |
n_mono.dataset.from = from_id; | |
h2 = document.createElement("h2"); | |
h2.textContent = from; | |
n_mono.appendChild(h2); | |
n_mono.appendChild(n_post_cont); | |
posts.appendChild(n_mono); | |
} | |
} | |
transcript.set(id, [from_id, from, string]); | |
if (tick) { | |
await Promise.all([...imgs].map(i => new Promise((r) => i.complete ? r() : (i.onload = r)))); | |
await MathJax.typesetPromise(); | |
if (p_scroll < 24) | |
posts.scrollTo(0, posts.scrollHeight); | |
} | |
return n_post.querySelectorAll("img"); | |
}; | |
var show_copy = async (copy, count = 0) => { | |
var p_scroll = (posts.scrollHeight - posts.offsetHeight) - posts.scrollTop; | |
var sp = 0; | |
var imgs = []; | |
for (var info of copy) { | |
if (info.event_type == 1 || info.event_type == 2) { | |
imgs = imgs.concat([...(await show_post(info.message_id, info.user_id, info.user_name, chat_to_string(info.content || ""), 0, count))]); | |
sp = 1; | |
} else if (info.event_type == 10) { | |
await drop_post(info.message_id, 0); | |
} | |
} | |
if (sp) { | |
await Promise.all(imgs.map(i => new Promise((r) => i.complete ? r() : (i.onload = r)))); | |
await MathJax.typesetPromise(); | |
if (p_scroll < 24) | |
posts.scrollTo(0, posts.scrollHeight); | |
} | |
}; | |
var find_posts = async () => { | |
var copy = await (await fetch("https://chat.stackexchange.com/chats/" + room + "/events", { | |
method: "POST", | |
body: new URLSearchParams("since=0&mode=Messages&msgCount=500&fkey=" + f_copy) | |
})).json(); | |
await show_copy(copy.events); | |
}; | |
var post_string = async (string) => { | |
var data = string_to_data(string); | |
if (data.length > 832 + 2) | |
return { | |
chat: "", | |
status: 400, | |
body: "Too long (by " + (data.length - (832 + 2)) + "B)" | |
}; | |
var chat = data_to_chat(data); | |
var ab_ctrl = new AbortController(); | |
var ab_st = window.setTimeout(() => ab_ctrl.abort(), 8000); | |
var data = await fetch("https://chat.stackexchange.com/chats/" + room + "/messages/new", { | |
method: "POST", | |
body: new URLSearchParams({ | |
fkey: f_copy, | |
text: chat | |
}), | |
signal: ab_ctrl.signal | |
}); | |
window.clearTimeout(ab_st); | |
if (data.status != 200) | |
return { | |
chat: chat, | |
status: data.status, | |
body: await data.text() | |
}; | |
return { | |
chat: chat, | |
status: data.status, | |
body: await data.json() | |
}; | |
}; | |
var start_ws = async () => { | |
console.log("Starting ws"); | |
try { | |
var ws_auth = await (await fetch("https://chat.stackexchange.com/ws-auth", { | |
method: "POST", | |
body: new URLSearchParams("roomid=" + room + "&fkey=" + f_copy) | |
})).json(); | |
var hash = await (await fetch("https://chat.stackexchange.com/chats/" + room + "/events", { | |
method: "POST", | |
body: new URLSearchParams("since=0&mode=Messages&msgCount=100&fkey=" + f_copy) | |
})).json(); | |
var ws = new WebSocket(ws_auth.url + "?l=" + hash.time); | |
var sp = !1; | |
ws.onopen = () => console.log("Got ws"); | |
ws.onmessage = async (info) => { | |
var json = JSON.parse(info.data); | |
if (!json["r" + room] || !json["r" + room].e) | |
return; | |
await show_copy(json["r" + room].e, 1); | |
}; | |
ws.onclose = () => { | |
setTimeout(start_ws, 2000); | |
}; | |
} catch (info) { | |
setTimeout(start_ws, 2000); | |
} | |
}; | |
await find_posts(); | |
console.log("Got posts"); | |
start_ws(); | |
window.onfocus = () => { | |
ctr = 0; | |
document.title = "Chat-on-Chat"; | |
}; | |
var thrown = document.createElement("div"); | |
var t_int = 0; | |
var t_cool = 0; | |
thrown.className = "thrown"; | |
body.appendChild(thrown); | |
var grow = document.createElement("div"); | |
grow.className = "grow"; | |
var input = document.createElement("textarea"); | |
grow.appendChild(input); | |
input.rows = 1; | |
input.oninput = (info = null) => { | |
grow.dataset.copy = input.value; | |
var p_scroll = (posts.scrollHeight - posts.offsetHeight) - posts.scrollTop; | |
if (info && t_int && !t_cool) | |
t_int--; | |
if (t_int == 0) | |
thrown.style.display = ""; | |
var data_ct = string_to_data(input.value).length; | |
if (t_int == 0 && !t_cool && data_ct > 832 + 2) { | |
thrown.style.display = "block"; | |
thrown.style.opacity = ""; | |
thrown.textContent = "Too long (by " + (data_ct - (832 + 2)) + "B)"; | |
} else { | |
thrown.style.opacity = ((t_int / 4) * 100) + "%"; | |
} | |
posts.style.height = "calc(100vh - " + (30 + input.offsetHeight + (thrown.style.display == "block" ? 14 : 0)) + "px)"; | |
posts.scrollTo(0, (posts.scrollHeight - posts.offsetHeight) - p_scroll); | |
}; | |
var shift = [...Array(10)].fill(null); | |
var stat_shift = [...Array(10)].fill(null); | |
input.onkeydown = async (info) => { | |
if (info.ctrlKey || info.altKey || info.metaKey || info.shiftKey) | |
return; | |
if (info.code == "Enter") { | |
info.preventDefault(); | |
if (shift.join(":") == "ArrowUp:ArrowUp:ArrowDown:ArrowDown:ArrowLeft:ArrowRight:ArrowLeft:ArrowRight:KeyB:KeyA") { | |
coc_kon = !coc_kon; | |
window.localStorage.setItem("coc-konami", coc_kon ? "1" : "0"); | |
for (var tools of posts.querySelectorAll(".tools")) { | |
tools.childNodes[0].textContent = (coc_kon ? "Modify" : "Edit"); | |
tools.childNodes[2].textContent = (coc_kon ? "Drop" : "Delete"); | |
tools.childNodes[4].textContent = (coc_kon ? "Quotify" : "Reply"); | |
} | |
[input.value, input.selectionStart, input.selectionEnd] = stat_shift[0]; | |
return; | |
} | |
t_int = 0; | |
input.oninput(); | |
if (string_to_data(input.value).length > 832 + 2) | |
return; | |
input.disabled = true; | |
var post; | |
try { | |
post = await post_string(input.value); | |
} catch (info) { | |
console.error(info); | |
post = { | |
chat: "", | |
status: 400, | |
body: "Could not post" | |
}; | |
} | |
var si; | |
if (post.status == 200) { | |
await show_post(post.body.id, who_id, who, input.value); | |
input.value = ""; | |
input.disabled = false; | |
input.focus(); | |
} else if (typeof post.body == "string" && post.body.startsWith("Too long")) { | |
thrown.style.display = "block"; | |
thrown.textContent = post.body; | |
t_int = 4; | |
input.disabled = false; | |
input.focus(); | |
} else if (typeof post.body == "string" && post.body.match(/\d+/) && +post.body.match(/\d+/)[0] < 30) { | |
thrown.style.display = "block"; | |
thrown.textContent = "Cooldown: " + post.body.match(/\d+/)[0]; | |
t_int = 4; | |
t_cool = Date.now() + (+post.body.match(/\d+/)[0] * 1000); | |
si = window.setInterval(() => { | |
if (Date.now() > t_cool) { | |
t_int = 0; | |
t_cool = 0; | |
thrown.style.display = ""; | |
input.oninput(); | |
input.disabled = false; | |
input.focus(); | |
window.clearInterval(si); | |
return; | |
} | |
thrown.textContent = "Cooldown: " + Math.ceil((t_cool - Date.now()) / 1000) + "s"; | |
}, 100); | |
} else { | |
thrown.style.display = "block"; | |
thrown.textContent = "Could not post"; | |
t_int = 4; | |
input.disabled = false; | |
input.focus(); | |
} | |
input.oninput(); | |
} else { | |
shift = [...shift.slice(1), info.code]; | |
stat_shift = [...stat_shift.slice(1), [input.value, input.selectionStart, input.selectionEnd]]; | |
} | |
}; | |
document.body.appendChild(grow); | |
input.focus(); | |
console.log("Built body"); | |
}; | |
var script = document.createElement("script"); | |
script.innerHTML = "(" + chat_on_chat.toString() + ")()"; | |
document.body.appendChild(script); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment