Shared runtime and feature registry for the BooruKit userscript suite.
Este script não deve ser instalado diretamente. Este script é uma biblioteca de outros scripts para incluir com o diretório meta // @require https://update.sleazyfork.org/scripts/589427/1890613/BooruKit%20Core.js
// ==UserScript==
// @name BooruKit Core
// @namespace codeberg.org/0xdev/boorukit
// @version 0.1.0
// @description Shared runtime and feature registry for the BooruKit userscript suite.
// @author 0xdev
// @license LGPL-3.0-or-later
// ==/UserScript==
// THIS FILE IS AUTO-GENERATED — DO NOT MANUALLY EDIT.
// Source: boorukit/boorukit-core.library.js
(() => {
'use strict';
const BOORUKIT_API_VERSION = 1;
const BOORUKIT_RUNTIME_KEY = Symbol.for('codeberg.org/0xdev/boorukit/runtime/v1');
function defaultSettings() {
return {
schemaVersion: 1,
modules: { grabber: true, 'pool-party': true },
grabber: null,
migrations: { bbdSettings: false },
};
}
function validateSettings(value) {
if (!value || typeof value !== 'object')
return defaultSettings();
const raw = value;
const modules = raw.modules;
const migrations = raw.migrations;
const grabber = raw.grabber;
if (raw.schemaVersion !== 1
|| !modules
|| typeof modules.grabber !== 'boolean'
|| typeof modules['pool-party'] !== 'boolean'
|| !migrations
|| typeof migrations.bbdSettings !== 'boolean'
|| (grabber !== null
&& (!grabber
|| typeof grabber.writeTagsTxt !== 'boolean'))) {
return defaultSettings();
}
return {
schemaVersion: 1,
modules: {
grabber: modules.grabber,
'pool-party': modules['pool-party'],
},
grabber: grabber === null
? null
: { writeTagsTxt: grabber.writeTagsTxt },
migrations: { bbdSettings: migrations.bbdSettings },
};
}
function createStorageApi() {
const GM = globalThis.GM;
return {
async read(key, codec) {
const raw = await GM.getValue(key);
return raw === undefined ? null : codec.decode(raw);
},
async write(key, value, codec) {
await GM.setValue(key, codec.encode(value));
},
async delete(key) {
await GM.deleteValue(key);
},
};
}
function sanitizeDiagnostic(error) {
const sensitiveKey = /key|secret|token|password|credential|authorization|cookie|headers?|body|settings/i;
const seen = new WeakSet();
function sanitize(value, key) {
if (key && sensitiveKey.test(key))
return '[redacted]';
if (typeof value === 'string') {
return value
.replace(/https?:\/\/[^\s/@:]+:[^\s/@]+@[^\s]+/gi, '[redacted-url]')
.replace(/\b((?:proxy-)?authorization)\s*:\s*[^\r\n]+/gi, '$1: [redacted]')
.replace(/\b(set-cookie|cookie)\s*:\s*[^\r\n]+/gi, '$1: [redacted]')
.replace(/\bbearer\s+[^\s,;]+/gi, 'Bearer [redacted]')
.replace(/([?&](?:api[_-]?key|access[_-]?token|auth(?:orization)?|secret|password|credential|session)=)[^&#\s]+/gi, '$1[redacted]')
.replace(/\b(api[_-]?key|secret|token|password|credential)\s*[=:]\s*[^\s&,;]+/gi, '$1=[redacted]');
}
if (!value || typeof value !== 'object')
return value;
if (seen.has(value))
return '[circular]';
seen.add(value);
if (value instanceof Error) {
return {
name: value.name,
message: sanitize(value.message),
};
}
if (Array.isArray(value)) {
return value.map((item) => sanitize(item));
}
const sanitized = {};
for (const [entryKey, entryValue] of Object.entries(value)) {
sanitized[entryKey] = sanitize(entryValue, entryKey);
}
return sanitized;
}
try {
return JSON.stringify(sanitize(error));
}
catch {
return JSON.stringify({ name: 'Error', message: 'Diagnostic unavailable' });
}
}
function createTaskCoordinator() {
let cancelled = false;
let activeLabel = null;
const listeners = new Set();
function publish(progress) {
for (const listener of listeners)
listener(progress);
}
const coordinator = {
async run(label, operation) {
if (activeLabel !== null) {
throw new Error('foreground task already running: ' + activeLabel);
}
activeLabel = label;
cancelled = false;
const signal = Object.freeze({
get cancelled() {
return cancelled;
},
throwIfCancelled() {
if (cancelled)
throw new Error('foreground task cancelled: ' + label);
},
});
publish({ label, completed: 0, total: 0 });
try {
return await operation(signal, (completed, total) => {
publish({ label, completed, total });
});
}
finally {
activeLabel = null;
publish(null);
}
},
cancel() {
if (activeLabel !== null)
cancelled = true;
},
subscribe(listener) {
listeners.add(listener);
return () => { listeners.delete(listener); };
},
};
return Object.freeze(coordinator);
}
function createSequentialQueue() {
const queue = {
async run(items, worker, policy) {
const results = [];
for (let index = 0; index < items.length; index += 1) {
if (policy.signal.cancelled)
break;
if (index > 0) {
if (policy.signal.cancelled)
break;
await policy.sleep(policy.throttleMs);
if (policy.signal.cancelled)
break;
}
if (policy.signal.cancelled)
break;
const item = items[index];
for (let attempt = 1; attempt <= policy.attempts; attempt += 1) {
if (policy.signal.cancelled)
break;
policy.onProgress?.(item, 'running', results.length, items.length);
if (policy.signal.cancelled)
break;
try {
const value = await worker(item, policy.signal);
results.push({ item, status: 'fulfilled', value });
policy.onProgress?.(item, 'done', results.length, items.length);
break;
}
catch (caught) {
const error = caught instanceof Error
? caught
: new Error(String(caught));
if (policy.signal.cancelled)
break;
if (attempt < policy.attempts) {
policy.onProgress?.(item, 'retrying', results.length, items.length, error);
if (policy.signal.cancelled)
break;
await policy.sleep(policy.retryDelayMs);
if (policy.signal.cancelled)
break;
continue;
}
results.push({ item, status: 'rejected', reason: error });
policy.onProgress?.(item, 'failed', results.length, items.length, error);
}
}
}
return results;
},
};
return Object.freeze(queue);
}
function createPageContext(href) {
const url = new URL(href);
const hostname = url.hostname.toLowerCase();
const normalizedHostname = hostname.replace(/^www\./, '');
const e621Hosts = new Set(['e621.net', 'e926.net']);
const gelbooruHosts = new Set([
'gelbooru.com',
'hypnohub.net',
'rule34.xxx',
'safebooru.org',
]);
const family = e621Hosts.has(normalizedHostname)
? 'e621'
: gelbooruHosts.has(normalizedHostname)
|| normalizedHostname.endsWith('.booru.org')
? 'gelbooru'
: 'unknown';
let pageKind = 'other';
if (family === 'e621') {
if (url.pathname === '/posts' || url.pathname === '/posts/') {
pageKind = 'gallery';
}
else if (/^\/posts\/\d+\/?$/.test(url.pathname)) {
pageKind = 'post';
}
else if (/^\/pools\/\d+\/?$/.test(url.pathname)) {
pageKind = 'pool';
}
}
else if (family === 'gelbooru') {
const page = url.searchParams.get('page');
const section = url.searchParams.get('s');
if (page === 'post' && section === 'list') {
pageKind = 'gallery';
}
else if (page === 'post' && section === 'view') {
pageKind = 'post';
}
else if (page === 'pool' && section === 'show') {
pageKind = 'pool';
}
}
return Object.freeze({
url,
origin: url.origin,
hostname,
normalizedHostname,
family,
pageKind,
});
}
function createCoreRuntime(effects) {
const descriptors = new Map();
const tasks = createTaskCoordinator();
const queue = createSequentialQueue();
const diagnostics = Object.freeze({
report(source, error) {
effects.diagnosticSink(source, sanitizeDiagnostic(error));
},
});
const settingsCodec = Object.freeze({
decode: validateSettings,
encode: validateSettings,
});
let settingsSnapshot = defaultSettings();
const settings = Object.freeze({
snapshot() {
return validateSettings(settingsSnapshot);
},
async update(transform) {
const current = validateSettings(settingsSnapshot);
const next = validateSettings(transform(current));
await effects.storage.write('boorukit.settings', next, settingsCodec);
settingsSnapshot = next;
return validateSettings(settingsSnapshot);
},
});
let startPromise = null;
async function activate() {
const candidate = descriptors.get('deck');
if (!candidate || candidate.kind !== 'deck') {
diagnostics.report('core', new Error('required Deck module is not registered'));
return;
}
if (!candidate.supports(effects.page))
return;
const stored = await effects.storage.read('boorukit.settings', settingsCodec);
settingsSnapshot = stored === null
? defaultSettings()
: validateSettings(stored);
const deck = await candidate.activate({
page: effects.page,
tasks,
diagnostics,
settings,
});
const moduleLabels = {
grabber: 'Grabber',
'pool-party': 'Pool Party',
};
for (const id of ['grabber', 'pool-party']) {
deck.registerSetting({
id,
label: moduleLabels[id],
checked: settingsSnapshot.modules[id],
reloadRequired: true,
onChange: async (checked) => {
await settings.update((current) => ({
...current,
modules: { ...current.modules, [id]: checked },
}));
},
});
}
for (const descriptor of descriptors.values()) {
if (descriptor.kind !== 'feature')
continue;
if (!settingsSnapshot.modules[descriptor.id])
continue;
try {
if (!descriptor.supports(effects.page))
continue;
await descriptor.activate({
page: effects.page,
tasks,
diagnostics,
settings,
deck,
storage: effects.storage,
queue,
});
}
catch (error) {
const reason = sanitizeDiagnostic(error);
effects.diagnosticSink(descriptor.id, reason);
deck.setFeatureStatus(descriptor.id, 'unavailable', reason);
}
}
}
const api = Object.freeze({
apiVersion: BOORUKIT_API_VERSION,
register(descriptor) {
if (descriptor.apiVersion !== BOORUKIT_API_VERSION) {
throw new Error(descriptor.id + ' requires API ' + String(descriptor.apiVersion)
+ '; Core provides API 1');
}
if (descriptors.has(descriptor.id)) {
throw new Error('duplicate module id: ' + descriptor.id);
}
descriptors.set(descriptor.id, descriptor);
},
start() {
startPromise ??= activate();
return startPromise;
},
});
return { api, descriptors };
}
function createRuntimeHarness() {
const descriptors = new Map();
const api = Object.freeze({
apiVersion: BOORUKIT_API_VERSION,
register(descriptor) {
if (descriptor.apiVersion !== BOORUKIT_API_VERSION) {
throw new Error(descriptor.id + ' requires API ' + String(descriptor.apiVersion)
+ '; Core provides API 1');
}
if (descriptors.has(descriptor.id)) {
throw new Error('duplicate module id: ' + descriptor.id);
}
descriptors.set(descriptor.id, descriptor);
},
start: async () => {
throw new Error('Core runtime is not installed');
},
});
return { api, descriptors };
}
function isRuntimeApi(value) {
if (!value || typeof value !== 'object')
return false;
const candidate = value;
return candidate.apiVersion === BOORUKIT_API_VERSION
&& typeof candidate.register === 'function'
&& typeof candidate.start === 'function';
}
if (typeof window !== 'undefined') {
const runtimes = globalThis;
const existing = runtimes[BOORUKIT_RUNTIME_KEY];
if (existing !== undefined && !isRuntimeApi(existing)) {
throw new Error('BooruKit runtime key contains an incompatible API');
}
if (existing === undefined) {
const runtime = createCoreRuntime({
page: createPageContext(location.href),
storage: createStorageApi(),
diagnosticSink(source, diagnostic) {
console.error('[BooruKit:' + source + '] ' + diagnostic);
},
});
runtimes[BOORUKIT_RUNTIME_KEY] = runtime.api;
}
}
if (typeof window === 'undefined'
&& typeof module !== 'undefined'
&& module.exports) {
module.exports = {
createCoreRuntime,
createPageContext,
createRuntimeHarness,
createSequentialQueue,
createStorageApi,
createTaskCoordinator,
defaultSettings,
sanitizeDiagnostic,
validateSettings,
};
}
})();