Date: October 26th, 2026 12:28 AM
Author: The Penis
// ==UserScript==
// @name AutoAdmit / XO Tree Reply Rescue
// @namespace https://www.xoxohth.com/
// @version 0.3.0
// @description Restore per-poast reply links while XO's message pane is broken.
// @match *://www.xoxohth.com/thread.php*
// @match *://xoxohth.com/thread.php*
// @match *://www.autoadmit.com/thread.php*
// @match *://autoadmit.com/thread.php*
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
'use strict';
if (document.getElementById('xo-reply-rescue')) return;
const params = new URL(location.href).searchParams;
const forumId = params.get('forum_id') || '2';
const threadId = params.get('thread_id') || '';
const rowDate = /\b\d{1,2}\/\d{1,2}\/\d{2,4}\b/;
const tagged = new WeakMap();
let rows = [];
let selected = null;
let initialNative = null;
let panelOpen = true;
function validId(x) {
return /^\d{1,15}$/.test(String(x || '')) && Number(x) > 0;
}
function urlFromString(str) {
try {
return new URL(str, location.href);
} catch {
return null;
}
}
function nativeReply() {
for (const a of document.querySelectorAll('a[href]')) {
if (a.closest('#xo-reply-rescue') ||
a.classList.contains('xo-tree-rescue-link')) continue;
const u = urlFromString(a.getAttribute('href'));
if (!u || u.origin !== location.origin ||
!/\/post\.php$/i.test(u.pathname)) continue;
const id = u.searchParams.get('parent_message_id');
if (validId(id)) return { id, url: u.href };
}
return null;
}
function newMessageUrl() {
for (const a of document.querySelectorAll('a[href]')) {
if (a.closest('#xo-reply-rescue')) continue;
const u = urlFromString(a.getAttribute('href'));
if (!u || u.origin !== location.origin ||
!/\/post\.php$/i.test(u.pathname)) continue;
if (u.searchParams.get('parent_thread_id') === threadId)
return u.href;
}
if (!validId(threadId)) return null;
const u = new URL('post.php', location.href);
u.searchParams.set('parent_thread_id', threadId);
u.searchParams.set('forum_id', forumId);
return u.href;
}
function replyUrl(messageId) {
if (!validId(messageId)) return null;
const example = nativeReply()?.url;
const u = example
? new URL(example)
: new URL('post.php', location.href);
if (u.origin !== location.origin ||
!/\/post\.php$/i.test(u.pathname)) return null;
u.searchParams.delete('parent_thread_id');
u.searchParams.set('parent_message_id', String(messageId));
u.searchParams.set('forum_id', forumId);
return u.href;
}
function extractId(element, row) {
const items = [
element,
element?.parentElement,
element?.closest('td'),
row
].filter(Boolean);
const seen = new Set();
const hints = [];
for (const el of items) {
if (seen.has(el)) continue;
seen.add(el);
for (const attr of el.attributes || []) {
if (attr.name === 'style' || attr.name === 'class') continue;
hints.push(`${attr.name}=${attr.value}`);
}
}
const joined = hints.join(' \n ');
const named =
/(?:^|[^a-z0-9_])(?:parent[-_]?message[-_]?id|message[-_]?id|msg[-_]?id|post[-_]?id|messageId|msgId|postId)\s*(?:=|:)\s*['"\\]?\s*(\d{1,15})\b/gi;
let match;
const namedCandidates = [];
while ((match = named.exec(joined))) {
if (validId(match[1]) && match[1] !== threadId)
namedCandidates.push(match[1]);
}
const uniqueNamed = [...new Set(namedCandidates)];
if (uniqueNamed.length === 1) {
return {
id: uniqueNamed[0],
source: 'named attribute',
certain: true
};
}
const jsFn =
/\b(?:show|load|get|view|select|display|open|expand|reply)[_$]?(?:message|msg|post|poast)\s*\(\s*['"]?(\d{1,15})\b/gi;
const functionCandidates = [];
while ((match = jsFn.exec(joined))) {
if (validId(match[1]) && match[1] !== threadId)
functionCandidates.push(match[1]);
}
const domId =
/(?:\b(?:id|name)=)(?:msg|message|post)[_-](\d{1,15})\b/i.exec(joined);
if (domId && validId(domId[1]) && domId[1] !== threadId) {
return {
id: domId[1],
source: 'DOM element ID',
certain: true
};
}
const uniqueFunctions = [...new Set(functionCandidates)];
if (uniqueFunctions.length === 1) {
return {
id: uniqueFunctions[0],
source: 'click handler',
certain: true
};
}
const fromCode = hints.filter(h =>
/^(?:href|onclick|ondblclick|onmousedown|onmouseup|data-)/i.test(h)
);
const numericCandidates = [
...new Set(
(fromCode.join(' ').match(/(?<!\d)\d{8,15}(?!\d)/g) || [])
.filter(x => x !== threadId)
)
];
if (numericCandidates.length === 1) {
return {
id: numericCandidates[0],
source: 'numeric click hint',
certain: false
};
}
return null;
}
function treeRowFor(a) {
let tr = a.closest('tr');
for (let i = 0; tr && i < 5; i++,
tr = tr.parentElement?.closest('tr')) {
const cells = [...tr.children].filter(x => x.tagName === 'TD');
if (cells.length >= 3 &&
cells[0].contains(a) &&
rowDate.test(tr.textContent || '')) {
return tr;
}
}
return null;
}
function collectRows() {
const entries = [];
const found = new Set();
for (const a of document.querySelectorAll('a')) {
if (a.closest('#xo-reply-rescue') ||
a.classList.contains('xo-tree-rescue-link')) continue;
if (!a.textContent?.trim()) continue;
const tr = treeRowFor(a);
if (!tr || found.has(tr)) continue;
const cells = [...tr.children].filter(x => x.tagName === 'TD');
const text = a.textContent.trim();
if (/^(back|refresh|options|reply|top|favorite|poast new message)/i.test(text))
continue;
found.add(tr);
entries.push({
row: tr,
anchor: a,
title: text,
author: (cells[1]?.textContent || '').trim(),
idInfo: null
});
}
return entries;
}
function make(node, tag, text, css) {
const el = document.createElement(tag);
if (text !== undefined) el.textContent = text;
if (css) el.style.cssText = css;
node.appendChild(el);
return el;
}
const styles = document.createElement('style');
styles.textContent = `
#xo-reply-rescue {
position:fixed;
bottom:12px;
right:12px;
width:320px;
z-index:2147483647;
box-sizing:border-box;
padding:11px;
background:#fffff2;
color:#181818;
font:13px/1.45 Arial,sans-serif;
border:1px solid #666;
box-shadow:0 3px 16px #0003;
border-radius:4px;
}
#xo-reply-rescue * { box-sizing:border-box; }
#xo-reply-rescue a {
color:#0645ad;
text-decoration:underline;
}
#xo-reply-rescue a.action {
display:block;
margin:7px 0 4px;
font-weight:bold;
}
#xo-reply-rescue button {
cursor:pointer;
padding:3px 6px;
font:12px Arial,sans-serif;
}
#xo-reply-rescue .small {
font:11px/1.4 Arial,sans-serif;
color:#444;
margin-top:6px;
}
#xo-reply-rescue .warn { color:#872b00; }
#xo-reply-rescue .controls {
display:flex;
flex-wrap:wrap;
gap:6px;
margin-top:7px;
}
#xo-reply-rescue textarea {
width:100%;
height:110px;
font:11px monospace;
margin-top:6px;
}
.xo-tree-rescue-link {
margin-left:8px !important;
color:#066428 !important;
font:normal 11px Arial,sans-serif !important;
text-decoration:underline !important;
}
tr.xo-tree-rescue-selected > td:first-child {
background:#ffffd9;
}
@media(max-width:500px) {
#xo-reply-rescue {
max-width:calc(100vw - 16px);
right:8px;
bottom:8px;
}
}
`;
document.head.append(styles);
const panel = document.createElement('aside');
panel.id = 'xo-reply-rescue';
const head = make(panel, 'div');
head.style.cssText =
'display:flex;align-items:center;justify-content:space-between;gap:10px';
make(head, 'strong', 'XO reply rescue');
const minimize = make(head, 'button', 'Hide');
minimize.type = 'button';
const content = make(panel, 'div');
const status = make(
content, 'div', 'Scanning thread…', 'margin-top:7px;'
);
const target = make(
content, 'div', '', 'margin-top:7px;'
);
const reply = make(content, 'a', '', 'display:none;');
reply.className = 'action';
const newMessage = make(
content, 'a', 'Poast new message in thread', 'display:none;'
);
newMessage.className = 'action';
const info = make(
content,
'div',
'Click a message title in the tree to select it. Green [reply] links are added when its message ID can be read.',
''
);
info.className = 'small';
const controls = make(content, 'div');
controls.className = 'controls';
const rescan = make(controls, 'button', 'Rescan');
rescan.type = 'button';
const debug = make(controls, 'button', 'Show diagnostics');
debug.type = 'button';
const diag = make(content, 'textarea');
diag.readOnly = true;
diag.style.display = 'none';
document.body.append(panel);
minimize.addEventListener('click', () => {
panelOpen = !panelOpen;
content.style.display = panelOpen ? '' : 'none';
minimize.textContent = panelOpen ? 'Hide' : 'Show';
});
function select(entry, idInfo) {
for (const r of rows)
r.row.classList.remove('xo-tree-rescue-selected');
selected = { ...entry, idInfo: idInfo || null };
entry.row.classList.add('xo-tree-rescue-selected');
update();
}
function update() {
const native = nativeReply();
const identified = rows.filter(r => r.idInfo).length;
status.textContent =
`Detected ${rows.length} tree posts; ${identified} reply target${identified === 1 ? '' : 's'} identified.`;
if (selected) {
const { idInfo, title, author } = selected;
target.textContent =
`Selected: ${title.slice(0, 100)}${author ? ` (${author})` : ''}`;
if (idInfo && validId(idInfo.id)) {
const u = replyUrl(idInfo.id);
reply.href = u || '#';
reply.textContent = `Reply to message ${idInfo.id}`;
reply.style.display = u ? 'block' : 'none';
info.textContent = idInfo.certain
? `Message ID from ${idInfo.source}. Opens XO's own posting form.`
: 'Message ID inferred from the click handler. Confirm it is the correct target before posting.';
info.className = idInfo.certain ? 'small' : 'small warn';
} else {
reply.style.display = 'none';
reply.removeAttribute('href');
info.textContent =
'This title did not expose a reliable message ID. Use Show diagnostics to inspect the clicked link.';
info.className = 'small warn';
}
} else {
target.textContent = 'Select a poast in the tree.';
if (native) {
reply.href = native.url;
reply.textContent = `Existing Reply link (message ${native.id})`;
reply.style.display = 'block';
info.textContent =
'Existing Reply link targets the page-selected post, which may be the root. Click a post first to choose a specific target.';
} else {
reply.style.display = 'none';
}
}
const newUrl = newMessageUrl();
newMessage.style.display = newUrl ? 'block' : 'none';
if (newUrl) newMessage.href = newUrl;
}
function rescanTree() {
for (const a of document.querySelectorAll('.xo-tree-rescue-link'))
a.remove();
rows = collectRows();
initialNative ||= nativeReply();
rows.forEach((entry, index) => {
let idInfo = extractId(entry.anchor, entry.row);
if (!idInfo && index === 0 && initialNative) {
idInfo = {
id: initialNative.id,
source: 'initial native reply link (root)',
certain: true
};
}
entry.idInfo = idInfo;
tagged.set(entry.anchor, entry);
if (!idInfo) return;
const link = document.createElement('a');
link.className = 'xo-tree-rescue-link';
link.textContent = '[reply]';
link.href = replyUrl(idInfo.id) || '#';
link.title =
`Reply to message ${idInfo.id}${idInfo.certain ? '' : ' (inferred)'}`;
link.addEventListener('click', event => {
event.stopPropagation();
if (!idInfo.certain &&
!confirm(
`Reply to inferred message ID ${idInfo.id}?\n\nVerify the target before submitting.`
)) {
event.preventDefault();
}
});
entry.anchor.insertAdjacentElement('afterend', link);
});
selected = null;
update();
}
document.addEventListener('click', e => {
const a = e.target.closest?.('a');
if (!a ||
a.classList.contains('xo-tree-rescue-link') ||
a.closest('#xo-reply-rescue')) return;
const entry = tagged.get(a);
if (!entry) return;
const oldId = nativeReply()?.id || '';
select(entry, entry.idInfo);
for (const delay of [80, 300, 1000]) {
setTimeout(() => {
if (selected?.anchor !== a) return;
const now = nativeReply();
if (now &&
now.id !== oldId &&
now.id !== initialNative?.id) {
entry.idInfo = {
id: now.id,
source: 'XO native Reply link',
certain: true
};
selected.idInfo = entry.idInfo;
update();
}
}, delay);
}
}, true);
reply.addEventListener('click', e => {
if (selected?.idInfo && !selected.idInfo.certain) {
if (!confirm(
`Open XO's reply form for inferred message ${selected.idInfo.id}?\n\nVerify the target before posting.`
)) e.preventDefault();
}
});
rescan.addEventListener('click', rescanTree);
debug.addEventListener('click', () => {
const a = selected?.anchor;
const row = selected?.row;
diag.style.display =
diag.style.display === 'none' ? 'block' : 'none';
debug.textContent =
diag.style.display === 'none'
? 'Show diagnostics'
: 'Hide diagnostics';
const attributes = el => el
? [...el.attributes].reduce((out, p) => {
if (!/^(?:href|onclick|id|name|data-[\w-]+)$/i.test(p.name))
return out;
if (/token|session|csrf|secret|auth/i.test(p.name))
return out;
out[p.name] = p.value
.slice(0, 500)
.replace(
/((?:csrf|token|sid|session|auth|secret|api_key|key)\s*[=:]\s*['"]?)[^&#'"\s;]+/gi,
'$1[REDACTED]'
);
return out;
}, {})
: null;
diag.value = JSON.stringify({
page: location.origin + location.pathname +
`?thread_id=${threadId}&forum_id=${forumId}`,
rowCount: rows.length,
identifiedCount: rows.filter(r => r.idInfo).length,
initialNativeId: initialNative?.id || null,
currentNativeId: nativeReply()?.id || null,
selected: selected && {
title: selected.title,
author: selected.author,
parsed: selected.idInfo,
anchor: attributes(a),
cell: attributes(a?.parentElement),
row: attributes(row)
},
examples: rows.slice(0, 3).map(r => ({
title: r.title,
parsed: r.idInfo,
anchor: attributes(r.anchor)
}))
}, null, 2);
if (diag.style.display !== 'none') diag.select();
});
rescanTree();
})();
(http://www.autoadmit.com/thread.php?thread_id=6000486&forum_id=2#50202061)