1036 lines
30 KiB
TypeScript
1036 lines
30 KiB
TypeScript
// ==UserScript==
|
|
// @name Refurrer Object Statuses
|
|
// @namespace http://tampermonkey.net/
|
|
// @version 1.0
|
|
// @description Show FA object statuses in Refurrer
|
|
// @author You
|
|
// @match https://www.furaffinity.net/*
|
|
// @match https://twitter.com/*
|
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=furaffinity.net
|
|
// @grant GM_xmlhttpRequest
|
|
// @grant unsafeWindow
|
|
// @connect refurrer.com
|
|
// @connect localhost:3001
|
|
// @connect localhost
|
|
// ==/UserScript==
|
|
// This file is generated from a Typescript file, do not edit the js file directly.
|
|
'use strict';
|
|
declare const unsafeWindow: Window &
|
|
typeof globalThis & {
|
|
submission_data: Record<string, any>;
|
|
};
|
|
|
|
interface GMResponse {
|
|
readyState: number;
|
|
responseHeaders: string;
|
|
responseText: string;
|
|
status: number;
|
|
statusText: string;
|
|
}
|
|
|
|
declare const GM_xmlhttpRequest: (details: {
|
|
url: string;
|
|
method: string;
|
|
headers?: Record<string, string>;
|
|
data?: string;
|
|
onload: (response: GMResponse) => void;
|
|
onabort: (response: GMResponse) => void;
|
|
ontimeout: (response: GMResponse) => void;
|
|
onerror: (response: GMResponse) => void;
|
|
}) => void;
|
|
|
|
type PostObjectState =
|
|
| 'not_seen'
|
|
| 'ok'
|
|
| 'removed'
|
|
| 'scan_error'
|
|
| 'file_error'
|
|
| 'scanned_post'
|
|
| 'have_file';
|
|
type UserObjectState = 'not_seen' | 'ok' | 'account_disabled' | 'error';
|
|
type ScanState = { last_at: string; due_for_scan: boolean };
|
|
|
|
interface ObjectStatusesResponse {
|
|
posts: Record<
|
|
number,
|
|
| {
|
|
state: 'not_seen';
|
|
}
|
|
| {
|
|
state: PostObjectState;
|
|
object_url: string;
|
|
seen_at: string;
|
|
post_scan: ScanState;
|
|
file_scan: ScanState;
|
|
}
|
|
>;
|
|
users: Record<
|
|
string,
|
|
| {
|
|
state: 'not_seen';
|
|
}
|
|
| {
|
|
state: UserObjectState;
|
|
object_url: string;
|
|
created_at: string;
|
|
page_scan: ScanState;
|
|
gallery_scan: ScanState;
|
|
favs_scan: ScanState;
|
|
}
|
|
>;
|
|
}
|
|
|
|
type LiveEntityType = 'post' | 'user' | 'entity_statuses';
|
|
interface LiveEntityStatTable {
|
|
type: LiveEntityType;
|
|
stats: {
|
|
name: string;
|
|
value: string;
|
|
sep?: string;
|
|
}[];
|
|
}
|
|
|
|
async function fa() {
|
|
function isNotNull<T>(val: T | null | undefined): val is T {
|
|
return val != null;
|
|
}
|
|
|
|
// const HOST = 'refurrer.com';
|
|
const HOST = 'refurrer.com';
|
|
const LIGHT_BG_COLOR = '#353b45';
|
|
const DARK_BG_COLOR = '#20242a';
|
|
const LIGHT_BORDER_COLOR = '#69697d';
|
|
|
|
function setupNavbar(): HTMLElement {
|
|
const navbarCssElem = document.createElement('style');
|
|
const navbarHeight = '60px';
|
|
navbarCssElem.innerHTML = `
|
|
li.refurrer-navbar {
|
|
height: 100%;
|
|
display: flex !important;
|
|
}
|
|
|
|
li.refurrer-navbar table {
|
|
height: 100% !important;
|
|
}
|
|
|
|
.refurrer-stats-tables {
|
|
display: flex;
|
|
}
|
|
|
|
.refurrer-entities-dropdown div {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.refurrer-entities-dropdown ul,
|
|
.refurrer-entities-dropdown li,
|
|
.refurrer-entities-dropdown div {
|
|
float: left !important;
|
|
}
|
|
|
|
.refurrer-entities-dropdown ul {
|
|
height: auto !important;
|
|
}
|
|
|
|
.refurrer-entities-dropdown div,
|
|
.refurrer-entities-dropdown li {
|
|
line-height: 1.8em !important;
|
|
user-select: text !important;
|
|
}
|
|
|
|
.refurrer-entities-dropdown li a {
|
|
text-decoration: underline dotted !important;
|
|
}
|
|
|
|
.refurrer-entities-dropdown li a:hover {
|
|
text-decoration: solid !important;
|
|
}
|
|
|
|
li.refurrer-navbar > div,
|
|
.refurrer-stats-tables > table {
|
|
border-right: 1px solid #d7d7d7;
|
|
padding-right: 5px;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
li.refurrer-navbar > div:last-child,
|
|
.refurrer-stats-tables > table:last-child {
|
|
border-right: none;
|
|
padding-right: 0;
|
|
margin-right: 0;
|
|
}
|
|
|
|
.refurrer-entities-dropdown-container {
|
|
position: relative;
|
|
}
|
|
|
|
.refurrer-entities-dropdown {
|
|
position: absolute;
|
|
background: ${LIGHT_BG_COLOR};
|
|
border: 1px solid ${LIGHT_BORDER_COLOR};
|
|
padding: 10px;
|
|
box-shadow: 0px 3px 10px 5px rgb(0 0 0 / 64%);
|
|
top: 60px;
|
|
max-height: calc(100vh - 100px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.refurrer-entities-dropdown .dropdown-header {
|
|
background: ${DARK_BG_COLOR};
|
|
border-bottom: 1px solid ${LIGHT_BORDER_COLOR};
|
|
}
|
|
|
|
// #ddmenu ul {
|
|
// height: ${navbarHeight};
|
|
// }
|
|
// #ddmenu li {
|
|
// line-height: ${navbarHeight};
|
|
// }
|
|
// #searchbox input[type=search] {
|
|
// height: ${navbarHeight};
|
|
// }
|
|
// #ddmenu .menubar-icon-resize {
|
|
// max-height: ${navbarHeight} !important;
|
|
// }
|
|
`;
|
|
document.head.appendChild(navbarCssElem);
|
|
|
|
const navbarStatusNode = document.createElement('li');
|
|
navbarStatusNode.classList.add('refurrer-navbar');
|
|
navbarStatusNode.classList.add('lileft');
|
|
|
|
const faNativeNavbar = document.querySelector('nav');
|
|
if (faNativeNavbar != null) {
|
|
faNativeNavbar.querySelector('ul > div')?.after(navbarStatusNode);
|
|
return navbarStatusNode;
|
|
} else {
|
|
// watch pages, etc don't have a navbar - create one at the top of the page
|
|
const center = document.body.querySelector('div[align=center]');
|
|
center?.prepend(navbarStatusNode);
|
|
return navbarStatusNode;
|
|
}
|
|
}
|
|
|
|
function makeLargeStatusNode(
|
|
opts: {
|
|
type?: string;
|
|
smaller?: boolean;
|
|
style?: Record<string, string>;
|
|
} = {},
|
|
) {
|
|
if (opts.type == null) {
|
|
opts.type = 'span';
|
|
}
|
|
if (opts.smaller == null) {
|
|
opts.smaller = true;
|
|
}
|
|
if (opts.style == null) {
|
|
opts.style = {};
|
|
}
|
|
|
|
const statusNode = document.createElement(opts.type);
|
|
statusNode.style.cssText =
|
|
'margin-left: 5px; color: #b7b7b7!important; display: inline';
|
|
if (opts.smaller) {
|
|
statusNode.style.fontSize = '80%';
|
|
}
|
|
|
|
for (const [property, value] of Object.entries(opts.style)) {
|
|
statusNode.style.setProperty(property, value);
|
|
}
|
|
|
|
statusNode.innerHTML = '(...)';
|
|
return statusNode;
|
|
}
|
|
|
|
function galleryFigureElements(): HTMLElement[] {
|
|
function queryAsHtmlElement(query: string): HTMLElement[] {
|
|
return [...document.querySelectorAll(query)].map(
|
|
(elem) => elem as HTMLElement,
|
|
);
|
|
}
|
|
|
|
return [
|
|
...queryAsHtmlElement('#gallery-search-results figure'),
|
|
...queryAsHtmlElement('#gallery-browse figure'),
|
|
...queryAsHtmlElement('#gallery-favorites figure'),
|
|
...queryAsHtmlElement('#gallery-gallery figure'),
|
|
...queryAsHtmlElement('#gallery-latest-favorites figure'),
|
|
...queryAsHtmlElement('#gallery-latest-submissions figure'),
|
|
...queryAsHtmlElement('section.gallery.messagecenter figure'),
|
|
];
|
|
}
|
|
|
|
function gatherPostElements(): { faId: number; statusNode: HTMLElement }[] {
|
|
// if on a gallery page, links to the list of submissions
|
|
const galleryFigures = galleryFigureElements().map((figure) => {
|
|
const faId = parseInt(figure.id.split('-')[1]);
|
|
const statusNode = document.createElement('p');
|
|
statusNode.innerHTML = '(...)';
|
|
const captionLabel = figure.querySelector('figcaption label p');
|
|
const caption = figure.querySelector('figcaption');
|
|
// gallery pages will include a figcaption
|
|
if (captionLabel) {
|
|
figure.style.height = `calc(${figure.style.height} + 20px)`;
|
|
let b = figure.querySelector('b');
|
|
if (b) {
|
|
b.style.height = 'auto';
|
|
}
|
|
statusNode.style.cssText = 'position:relative;bottom:2px;';
|
|
captionLabel.after(statusNode);
|
|
} else if (caption) {
|
|
statusNode.style.cssText = 'position:relative;bottom:2px;';
|
|
caption.appendChild(statusNode);
|
|
} else {
|
|
const figcaption = document.createElement('div');
|
|
figcaption.style.cssText =
|
|
'display: block !important; position: absolute; bottom: 0px; font-size: 10px; width:100%';
|
|
figcaption.appendChild(statusNode);
|
|
figure.appendChild(figcaption);
|
|
figure.style.height = `calc(${figure.style.height} + 20px)`;
|
|
}
|
|
|
|
return { faId, statusNode };
|
|
});
|
|
|
|
const featuredElem = [
|
|
...document.querySelectorAll('.userpage-featured-title h2'),
|
|
]
|
|
.map((elem) => {
|
|
// skip if it's a dynamically changing preview, `submissionDataElems` handles those
|
|
if (elem.classList.contains('preview_title')) {
|
|
return null;
|
|
}
|
|
const link = elem.querySelector('a');
|
|
if (!link) {
|
|
return null;
|
|
}
|
|
const faId = faPostIdFromViewHref(link.href);
|
|
if (!faId) {
|
|
return null;
|
|
}
|
|
const statusNode = makeLargeStatusNode();
|
|
elem.appendChild(statusNode);
|
|
|
|
return {
|
|
faId,
|
|
statusNode,
|
|
};
|
|
})
|
|
.filter(isNotNull);
|
|
|
|
let submissionDataElems: { faId: number; statusNode: HTMLElement }[] = [];
|
|
if (unsafeWindow.submission_data) {
|
|
submissionDataElems = Object.entries(unsafeWindow.submission_data).map(
|
|
([fa_id, _]) => {
|
|
const statusNode = makeLargeStatusNode();
|
|
return {
|
|
faId: parseInt(fa_id),
|
|
statusNode,
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
// watch for changes to preview user and swap out status node when that happens
|
|
[...document.querySelectorAll('h2.preview_title')].forEach(
|
|
(previewTitleNode) => {
|
|
const swapOutNode = document.createElement('span');
|
|
swapOutNode.classList.add('swapper');
|
|
previewTitleNode.appendChild(swapOutNode);
|
|
const previewTitleLink = previewTitleNode.querySelector('a');
|
|
if (!previewTitleLink) {
|
|
return;
|
|
}
|
|
|
|
const observerCb = () => {
|
|
const previewFaId = faPostIdFromViewHref(previewTitleLink.href);
|
|
if (!previewFaId) {
|
|
return;
|
|
}
|
|
swapOutNode.innerHTML = '';
|
|
const currentSubmissionElem = submissionDataElems.find(
|
|
({ faId }) => faId == previewFaId,
|
|
);
|
|
if (currentSubmissionElem) {
|
|
swapOutNode.appendChild(currentSubmissionElem.statusNode);
|
|
}
|
|
};
|
|
const observer = new MutationObserver(observerCb);
|
|
observer.observe(previewTitleLink, { childList: true, subtree: true });
|
|
observerCb();
|
|
},
|
|
);
|
|
|
|
// /view/<faId>/ page elements
|
|
let primaryViewPageElems: { faId: number; statusNode: HTMLElement }[] = [];
|
|
let faId = faPostIdFromViewHref(window.location.href);
|
|
if (faId) {
|
|
primaryViewPageElems = [
|
|
...document.querySelectorAll('.submission-title h2'),
|
|
].map((elem) => {
|
|
let p = elem.querySelector('p');
|
|
if (p) {
|
|
p.style.display = 'inline';
|
|
}
|
|
const statusNode = makeLargeStatusNode({ type: 'h3' });
|
|
elem.appendChild(statusNode);
|
|
return {
|
|
faId,
|
|
statusNode,
|
|
};
|
|
});
|
|
}
|
|
|
|
return [
|
|
...galleryFigures,
|
|
...submissionDataElems,
|
|
...featuredElem,
|
|
...primaryViewPageElems,
|
|
];
|
|
}
|
|
|
|
function gatherUserElements(): {
|
|
urlName: string;
|
|
statusNode: HTMLElement;
|
|
}[] {
|
|
// if on a gallery / browse page, the creator links from those posts
|
|
const userSubmissionLinks = galleryFigureElements()
|
|
.map((figure) => {
|
|
const userLinkElem = [...figure.querySelectorAll('figcaption a')]
|
|
.map((e) => e as HTMLAnchorElement)
|
|
.map((elem) => ({
|
|
elem: elem,
|
|
urlName: urlNameFromUserHref(elem.href),
|
|
}))
|
|
.filter(({ urlName }) => urlName != null)[0];
|
|
|
|
if (userLinkElem == null || userLinkElem.urlName == null) {
|
|
return null;
|
|
}
|
|
|
|
let u = figure.querySelector('u');
|
|
if (u) {
|
|
u.style.cssText = 'display: block';
|
|
}
|
|
|
|
const statusNode = document.createElement('span');
|
|
statusNode.style.cssText = 'margin-left: 2px; color: #c1c1c1';
|
|
statusNode.innerHTML = '(...)';
|
|
userLinkElem.elem.after(statusNode);
|
|
|
|
return {
|
|
urlName: userLinkElem.urlName,
|
|
statusNode,
|
|
};
|
|
})
|
|
.filter(isNotNull);
|
|
|
|
// if on a /user/ page, the primary username element
|
|
const userPageUrlName = urlNameFromUserHref(window.location.href);
|
|
let userPageMain = userPageUrlName
|
|
? [...document.querySelectorAll('h1 username')].map((elem) => {
|
|
const statusNode = document.createElement('span');
|
|
statusNode.innerHTML = '(...)';
|
|
elem.after(statusNode);
|
|
return {
|
|
urlName: userPageUrlName,
|
|
statusNode,
|
|
};
|
|
})
|
|
: [];
|
|
|
|
// comments made by users on posts or user pages
|
|
let userComments = [...document.querySelectorAll('comment-username a')]
|
|
.map((e) => e as HTMLAnchorElement)
|
|
.map((elem) => {
|
|
const statusNode = makeLargeStatusNode();
|
|
elem.after(statusNode);
|
|
const urlName = urlNameFromUserHref(elem.href);
|
|
if (urlName == null) {
|
|
return null;
|
|
}
|
|
return {
|
|
urlName,
|
|
statusNode,
|
|
};
|
|
})
|
|
.filter(isNotNull);
|
|
|
|
// users mentioned in post descriptions or user bios
|
|
let iconUsernames = [
|
|
...document.querySelectorAll('a.iconusername, a.linkusername'),
|
|
]
|
|
.map((e) => e as HTMLAnchorElement)
|
|
.map((elem) => {
|
|
const statusNode = makeLargeStatusNode();
|
|
elem.after(statusNode);
|
|
const urlName = urlNameFromUserHref(elem.href);
|
|
if (urlName == null) {
|
|
return null;
|
|
}
|
|
return {
|
|
urlName,
|
|
statusNode,
|
|
};
|
|
})
|
|
.filter(isNotNull);
|
|
|
|
// watcher / watching lists on user page
|
|
let watchersAndWatchList = [
|
|
...document.querySelectorAll('a > span.artist_name'),
|
|
]
|
|
.map((elem) => {
|
|
const link = elem.parentNode as HTMLAnchorElement;
|
|
if (!link) {
|
|
return null;
|
|
}
|
|
const statusNode = makeLargeStatusNode();
|
|
link.after(statusNode);
|
|
const urlName = urlNameFromUserHref(link.href);
|
|
if (urlName == null) {
|
|
return null;
|
|
}
|
|
return {
|
|
urlName,
|
|
statusNode,
|
|
};
|
|
})
|
|
.filter(isNotNull);
|
|
|
|
// users mentioned in the "hero" sections on a url page
|
|
let submissionDataElems: { urlName: string; statusNode: HTMLElement }[] =
|
|
[];
|
|
|
|
if (unsafeWindow.submission_data) {
|
|
submissionDataElems = Object.entries(unsafeWindow.submission_data).map(
|
|
([_, { lower }]) => {
|
|
const statusNode = makeLargeStatusNode();
|
|
return {
|
|
urlName: lower,
|
|
statusNode,
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
// watch for changes to preview user and swap out status node when that happens
|
|
const previewUser = document.querySelector(
|
|
'a.preview_user',
|
|
) as HTMLAnchorElement;
|
|
if (previewUser) {
|
|
const swapOutNode = document.createElement('span');
|
|
swapOutNode.classList.add('swapper');
|
|
previewUser.after(swapOutNode);
|
|
|
|
const observerCb = () => {
|
|
const previewUrlName = urlNameFromUserHref(previewUser.href);
|
|
if (!previewUrlName) {
|
|
return;
|
|
}
|
|
swapOutNode.innerHTML = '';
|
|
const currentSubmissionElem = submissionDataElems.find(
|
|
({ urlName }) => urlName == previewUrlName,
|
|
);
|
|
if (currentSubmissionElem) {
|
|
swapOutNode.appendChild(currentSubmissionElem.statusNode);
|
|
}
|
|
};
|
|
const observer = new MutationObserver(observerCb);
|
|
observer.observe(previewUser, { childList: true, subtree: true });
|
|
observerCb();
|
|
}
|
|
|
|
// on a /view/ page, the name of the user
|
|
const submissionContainerUserLinks = [
|
|
...document.querySelectorAll('.submission-id-sub-container a'),
|
|
]
|
|
.map((e) => e as HTMLAnchorElement)
|
|
.map((elem) => {
|
|
const statusNode = makeLargeStatusNode({ smaller: false });
|
|
elem.after(statusNode);
|
|
const urlName = urlNameFromUserHref(elem.href);
|
|
if (urlName == null) {
|
|
return null;
|
|
}
|
|
return {
|
|
urlName,
|
|
statusNode,
|
|
};
|
|
})
|
|
.filter(isNotNull);
|
|
|
|
// on a /watchlist/by/ or /watchlist/to page
|
|
const watchListUserLinks = [
|
|
...document.querySelectorAll('.watch-list-items.watch-row a'),
|
|
]
|
|
.map((e) => e as HTMLAnchorElement)
|
|
.map((elem) => {
|
|
const statusNode = makeLargeStatusNode({
|
|
smaller: false,
|
|
style: {
|
|
display: 'block',
|
|
'margin-bottom': '5px',
|
|
'font-size': '75%',
|
|
},
|
|
});
|
|
elem.parentNode?.appendChild(statusNode);
|
|
const urlName = urlNameFromUserHref(elem.href);
|
|
if (urlName == null) {
|
|
return null;
|
|
}
|
|
return {
|
|
urlName,
|
|
statusNode,
|
|
};
|
|
})
|
|
.filter(isNotNull);
|
|
|
|
return [
|
|
...userSubmissionLinks,
|
|
...userPageMain,
|
|
...userComments,
|
|
...iconUsernames,
|
|
...watchersAndWatchList,
|
|
...submissionDataElems,
|
|
...submissionContainerUserLinks,
|
|
...watchListUserLinks,
|
|
];
|
|
}
|
|
|
|
function urlNameFromUserHref(href: string): string | null {
|
|
const url = new URL(href);
|
|
if (url.host != 'www.furaffinity.net') {
|
|
return null;
|
|
}
|
|
|
|
const userPageRegex = /^\/(user|gallery|scraps|favorites|journals)\/.+/;
|
|
const match = url.pathname.match(userPageRegex);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
return url.pathname.split('/')[2];
|
|
}
|
|
|
|
function faPostIdFromViewHref(href: string): number | null {
|
|
const viewPageRegex = /\/(view|full)\/(\d+)/;
|
|
const match = href.match(viewPageRegex);
|
|
const faId = (match && match[2]) || null;
|
|
if (faId) {
|
|
return parseInt(faId);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function faPostIdFromCurrentPage(): number | null {
|
|
return faPostIdFromViewHref(window.location.href);
|
|
}
|
|
|
|
function faUserUrlNameFromCurrentPage(): string | null {
|
|
const fromViewHref = urlNameFromUserHref(window.location.href);
|
|
if (fromViewHref) {
|
|
return fromViewHref;
|
|
}
|
|
|
|
let userLink = document.querySelector('.submission-id-sub-container a');
|
|
if (userLink && userLink instanceof HTMLAnchorElement) {
|
|
return urlNameFromUserHref(userLink.href);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function renderTable(
|
|
stats: {
|
|
name: string;
|
|
value: string;
|
|
sep?: string;
|
|
nameAlign?: string;
|
|
valueAlign?: string;
|
|
}[],
|
|
styleOpts: Record<string, string> = {},
|
|
): HTMLElement | null {
|
|
if (stats.length == 0) {
|
|
return null;
|
|
}
|
|
|
|
if (styleOpts['border-collapse'] == null) {
|
|
styleOpts['border-collapse'] = 'collapse';
|
|
}
|
|
|
|
let table = document.createElement('table');
|
|
for (const [property, value] of Object.entries(styleOpts)) {
|
|
table.style.setProperty(property, value as string);
|
|
}
|
|
|
|
let tbody = document.createElement('tbody');
|
|
table.appendChild(tbody);
|
|
|
|
stats.forEach(({ name, value, sep, nameAlign, valueAlign }) => {
|
|
if (name == '' && value == '') {
|
|
tbody.innerHTML += `<tr><td>---</td><td>---</td></tr>`;
|
|
} else {
|
|
if (sep == null) {
|
|
sep = ':';
|
|
}
|
|
if (nameAlign == null) {
|
|
nameAlign = 'right';
|
|
}
|
|
if (valueAlign == null) {
|
|
valueAlign = 'right';
|
|
}
|
|
|
|
tbody.innerHTML += `<tr>
|
|
<td style="text-align:${nameAlign};padding-right:5px">${name}${sep}</td>
|
|
<td style="text-align:${valueAlign}">${value}</td>
|
|
</tr>`;
|
|
}
|
|
});
|
|
return table;
|
|
}
|
|
|
|
function optsForNumRows(numRows: number) {
|
|
switch (numRows) {
|
|
case 0:
|
|
case 1:
|
|
case 2:
|
|
case 3:
|
|
return { 'line-height': '1.0em', 'font-size': '1.0em' };
|
|
case 4:
|
|
return { 'line-height': '0.9em', 'font-size': '0.8em' };
|
|
default:
|
|
return { 'line-height': '0.9em', 'font-size': '0.6em' };
|
|
}
|
|
}
|
|
|
|
// gather initial entities on the page
|
|
const userElements = gatherUserElements();
|
|
const urlNames = [...new Set(userElements.map(({ urlName }) => urlName))];
|
|
|
|
const postElements = gatherPostElements();
|
|
const faIds = [...new Set(postElements.map(({ faId }) => faId))];
|
|
|
|
const navbarNode = setupNavbar();
|
|
|
|
function setupNavbarEntitiesOnPageNode() {
|
|
const node = document.createElement('div') as HTMLDivElement;
|
|
node.classList.add('refurrer-entities-dropdown-container');
|
|
|
|
const dropdownElem = document.createElement('div');
|
|
dropdownElem.classList.add('refurrer-entities-dropdown');
|
|
dropdownElem.classList.add('hidden');
|
|
|
|
const pageOverviewStatsTable = renderTable(
|
|
[
|
|
{ name: 'users', value: urlNames.length.toString() },
|
|
{ name: 'posts', value: faIds.length.toString() },
|
|
],
|
|
{ ...optsForNumRows(2), width: 'auto' },
|
|
);
|
|
node.appendChild(pageOverviewStatsTable);
|
|
|
|
if (urlNames.length > 0) {
|
|
const userHeader = document.createElement('div');
|
|
userHeader.textContent = 'Users:';
|
|
dropdownElem.appendChild(userHeader);
|
|
|
|
const userList = document.createElement('ul');
|
|
dropdownElem.appendChild(userList);
|
|
|
|
urlNames.forEach((name) => {
|
|
const li = document.createElement('li');
|
|
li.textContent = name;
|
|
userList.appendChild(li);
|
|
});
|
|
}
|
|
|
|
if (faIds.length > 0) {
|
|
const postHeader = document.createElement('div');
|
|
postHeader.textContent = 'Post IDs:';
|
|
dropdownElem.appendChild(postHeader);
|
|
|
|
const postList = document.createElement('ul');
|
|
dropdownElem.appendChild(postList);
|
|
|
|
faIds.forEach((id) => {
|
|
const li = document.createElement('li');
|
|
li.textContent = id.toString();
|
|
postList.appendChild(li);
|
|
});
|
|
}
|
|
|
|
node.appendChild(dropdownElem);
|
|
|
|
node.addEventListener('mouseenter', () => {
|
|
dropdownElem.classList.remove('hidden');
|
|
});
|
|
|
|
dropdownElem.addEventListener('mouseleave', () => {
|
|
dropdownElem.classList.add('hidden');
|
|
});
|
|
|
|
return [node, dropdownElem];
|
|
}
|
|
|
|
function setupNavbarEntityStatsTablesNode() {
|
|
const node = document.createElement('div') as HTMLDivElement;
|
|
node.classList.add('refurrer-stats-tables');
|
|
node.innerHTML = 'loading...';
|
|
return node;
|
|
}
|
|
|
|
// set up entities on page & dropdown
|
|
const [navbarEntitiesOnPageNode, navbarDropdownNode] =
|
|
setupNavbarEntitiesOnPageNode();
|
|
navbarNode.append(navbarEntitiesOnPageNode);
|
|
|
|
// set up stats tables
|
|
const navbarLiveEntityStatTablesNode = setupNavbarEntityStatsTablesNode();
|
|
navbarNode.append(navbarLiveEntityStatTablesNode);
|
|
|
|
function renderLiveEntityStatTables(
|
|
liveEntityStatTables: LiveEntityStatTable[],
|
|
) {
|
|
const tables = liveEntityStatTables.map(({ type, stats }) => {
|
|
return renderTable(stats, {
|
|
...optsForNumRows(stats.length),
|
|
width: 'auto',
|
|
});
|
|
});
|
|
|
|
navbarLiveEntityStatTablesNode.innerHTML = '';
|
|
tables.forEach((table) => {
|
|
navbarLiveEntityStatTablesNode.appendChild(table);
|
|
});
|
|
}
|
|
|
|
function fetchObjectStatuses(): Promise<ObjectStatusesResponse> {
|
|
return new Promise((resolve, reject) => {
|
|
console.log('fetching object statuses');
|
|
let url = `http://${HOST}/api/fa/object_statuses`;
|
|
let params = new URLSearchParams();
|
|
faIds.forEach((id) => params.append('fa_ids[]', id.toString()));
|
|
urlNames.forEach((name) => params.append('url_names[]', name));
|
|
if (params.toString() != '') {
|
|
url += '?' + params.toString();
|
|
}
|
|
GM_xmlhttpRequest({
|
|
url,
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
onabort: () => {
|
|
reject(new Error('aborted fetching object statuses'));
|
|
},
|
|
ontimeout: () => {
|
|
reject(new Error('timeout fetching object statuses'));
|
|
},
|
|
onerror: (response) => {
|
|
reject(
|
|
new Error(
|
|
'failed to fetch object statuses: ' + response.responseText,
|
|
),
|
|
);
|
|
},
|
|
onload: (response) => {
|
|
console.log('response: ', response);
|
|
if (response.status === 200) {
|
|
const jsonResponse = JSON.parse(
|
|
response.responseText,
|
|
) as ObjectStatusesResponse;
|
|
resolve(jsonResponse);
|
|
} else {
|
|
reject(
|
|
new Error(
|
|
'failed to fetch object statuses: ' + response.responseText,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function handleObjectStatusesResponse(jsonResponse: ObjectStatusesResponse) {
|
|
let numNotSeenPosts = 0;
|
|
let numOkPosts = 0;
|
|
let numScannedPosts = 0;
|
|
let numHaveFile = 0;
|
|
|
|
navbarDropdownNode.innerHTML = '';
|
|
const userHeader = document.createElement('div');
|
|
userHeader.textContent = 'Users:';
|
|
navbarDropdownNode.appendChild(userHeader);
|
|
const userList = document.createElement('ul');
|
|
navbarDropdownNode.appendChild(userList);
|
|
|
|
const postHeader = document.createElement('div');
|
|
postHeader.textContent = 'Post IDs:';
|
|
navbarDropdownNode.appendChild(postHeader);
|
|
const postList = document.createElement('ul');
|
|
navbarDropdownNode.appendChild(postList);
|
|
|
|
function createEntityListElem(
|
|
title: string,
|
|
url: string | null,
|
|
): HTMLElement {
|
|
const li = document.createElement('li');
|
|
if (url == null) {
|
|
li.textContent = title;
|
|
} else {
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.target = '_blank';
|
|
a.textContent = title;
|
|
li.appendChild(a);
|
|
}
|
|
return li;
|
|
}
|
|
|
|
for (const [gotUrlName, userInfo] of Object.entries(jsonResponse.users)) {
|
|
const li = createEntityListElem(
|
|
gotUrlName,
|
|
userInfo.state == 'not_seen' ? null : userInfo.object_url,
|
|
);
|
|
userList.appendChild(li);
|
|
|
|
userElements
|
|
.filter(({ urlName }) => urlName == gotUrlName)
|
|
.forEach(({ statusNode }) => {
|
|
statusNode.innerHTML = userInfo.state;
|
|
});
|
|
}
|
|
|
|
for (const [gotFaId, postInfo] of Object.entries(jsonResponse.posts)) {
|
|
const li = createEntityListElem(
|
|
gotFaId,
|
|
postInfo.state == 'not_seen' ? null : postInfo.object_url,
|
|
);
|
|
postList.appendChild(li);
|
|
|
|
postElements
|
|
.filter(({ faId }) => faId == parseInt(gotFaId))
|
|
.forEach(({ statusNode }) => {
|
|
statusNode.innerHTML = postInfo.state;
|
|
|
|
switch (postInfo.state) {
|
|
case 'not_seen':
|
|
numNotSeenPosts += 1;
|
|
break;
|
|
case 'ok':
|
|
numOkPosts += 1;
|
|
break;
|
|
case 'scanned_post':
|
|
numScannedPosts += 1;
|
|
break;
|
|
case 'have_file':
|
|
numHaveFile += 1;
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
let liveEntities: LiveEntityStatTable[] = [];
|
|
|
|
liveEntities.push({
|
|
type: 'entity_statuses',
|
|
stats: [
|
|
{ name: 'not seen posts', value: numNotSeenPosts.toString() },
|
|
{ name: 'ok posts', value: numOkPosts.toString() },
|
|
{ name: 'scanned posts', value: numScannedPosts.toString() },
|
|
{ name: 'have file', value: numHaveFile.toString() },
|
|
],
|
|
});
|
|
|
|
const thisPageFaId = faPostIdFromCurrentPage();
|
|
const thisPageUrlName = faUserUrlNameFromCurrentPage();
|
|
|
|
const pssCommon = { sep: '', valueAlign: 'left' };
|
|
if (thisPageFaId != null) {
|
|
const postData = jsonResponse.posts[thisPageFaId];
|
|
if (postData.state == 'not_seen') {
|
|
liveEntities.push({
|
|
type: 'post',
|
|
stats: [{ name: 'no entity', value: '', sep: '' }],
|
|
});
|
|
} else {
|
|
liveEntities.push({
|
|
type: 'post',
|
|
stats: [
|
|
{
|
|
name: 'refurrer link',
|
|
value: `<a target="_blank" href="${postData.object_url}" style="text-decoration: underline dotted">${thisPageFaId}</a>`,
|
|
...pssCommon,
|
|
},
|
|
{ name: `seen`, value: postData.seen_at, ...pssCommon },
|
|
{
|
|
name: `scanned`,
|
|
value: postData.post_scan.last_at,
|
|
...pssCommon,
|
|
},
|
|
{
|
|
name: `downloaded`,
|
|
value: postData.file_scan.last_at,
|
|
...pssCommon,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
}
|
|
|
|
if (thisPageUrlName != null) {
|
|
const userData = jsonResponse.users[thisPageUrlName];
|
|
if (userData.state == 'not_seen') {
|
|
liveEntities.push({
|
|
type: 'user',
|
|
stats: [{ name: 'no entity', value: '', sep: '' }],
|
|
});
|
|
} else {
|
|
liveEntities.push({
|
|
type: 'user',
|
|
stats: [
|
|
{
|
|
name: 'refurrer link',
|
|
value: `<a target="_blank" href="${userData.object_url}" style="text-decoration: underline dotted">${thisPageUrlName}</a>`,
|
|
...pssCommon,
|
|
},
|
|
{ name: 'first seen', value: userData.created_at, ...pssCommon },
|
|
{
|
|
name: 'page scan',
|
|
value: userData.page_scan.last_at,
|
|
...pssCommon,
|
|
},
|
|
{
|
|
name: 'gallery scan',
|
|
value: userData.gallery_scan.last_at,
|
|
...pssCommon,
|
|
},
|
|
{
|
|
name: 'favs scan',
|
|
value: userData.favs_scan.last_at,
|
|
...pssCommon,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
}
|
|
|
|
renderLiveEntityStatTables(liveEntities);
|
|
}
|
|
|
|
await fetchObjectStatuses()
|
|
.then(handleObjectStatusesResponse)
|
|
.catch((err) => {
|
|
console.error('error: ', err);
|
|
});
|
|
}
|
|
|
|
(async function () {
|
|
if (window.location.hostname == 'www.furaffinity.net') {
|
|
await fa();
|
|
} else {
|
|
console.log('unhandled domain ', window.location.hostname);
|
|
}
|
|
})();
|