From b442e05191780f80b52f7da80e02719ccf43fe0d Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Sun, 31 May 2026 11:39:40 +0700 Subject: [PATCH] feat(rules page): rebuild directory rules page with vendored lists (#1147) * feat(rules page): rebuild directory rules page with vendored lists Mirror per-directory JSON from lists into src/data/5chan-directories, rework /rules layout to match 4chan (sidebar nav, category boxes, P2P load), and keep spoiler markup visible in rule text via parseSpoilers. * fix(rules): keep directory defaults cache atomic * fix(rules): derive defaults from shared directory refresh * fix(rules): address directory refresh edge cases --- scripts/smoke-web-app.js | 2 +- scripts/sync-directories.js | 403 ++---- .../markdown/__tests__/markdown.test.tsx | 15 +- src/components/markdown/markdown.tsx | 52 +- .../5chan-directories/5chan-3-directory.json | 13 + .../5chan-directories/5chan-a-directory.json | 13 + .../5chan-adv-directory.json | 13 + .../5chan-directories/5chan-an-directory.json | 13 + .../5chan-directories/5chan-b-directory.json | 13 + .../5chan-bant-directory.json | 13 + .../5chan-biz-directory.json | 19 + .../5chan-directories/5chan-c-directory.json | 13 + .../5chan-directories/5chan-ck-directory.json | 13 + .../5chan-directories/5chan-co-directory.json | 13 + .../5chan-directories-defaults.json | 1244 +++++++++++++++++ .../5chan-diy-directory.json | 13 + .../5chan-directories/5chan-f-directory.json | 16 + .../5chan-directories/5chan-fa-directory.json | 13 + .../5chan-fit-directory.json | 13 + .../5chan-directories/5chan-g-directory.json | 13 + .../5chan-directories/5chan-gd-directory.json | 13 + .../5chan-gif-directory.json | 13 + .../5chan-his-directory.json | 13 + .../5chan-directories/5chan-i-directory.json | 13 + .../5chan-directories/5chan-ic-directory.json | 13 + .../5chan-int-directory.json | 13 + .../5chan-directories/5chan-jp-directory.json | 13 + .../5chan-directories/5chan-k-directory.json | 13 + .../5chan-lit-directory.json | 13 + .../5chan-directories/5chan-m-directory.json | 13 + .../5chan-mlp-directory.json | 13 + .../5chan-directories/5chan-mu-directory.json | 13 + .../5chan-directories/5chan-n-directory.json | 13 + .../5chan-news-directory.json | 13 + .../5chan-directories/5chan-o-directory.json | 13 + .../5chan-out-directory.json | 13 + .../5chan-directories/5chan-p-directory.json | 13 + .../5chan-directories/5chan-po-directory.json | 13 + .../5chan-pol-directory.json | 19 + .../5chan-directories/5chan-pw-directory.json | 13 + .../5chan-qst-directory.json | 13 + .../5chan-r9k-directory.json | 13 + .../5chan-s5s-directory.json | 13 + .../5chan-sci-directory.json | 13 + .../5chan-soc-directory.json | 13 + .../5chan-directories/5chan-sp-directory.json | 13 + .../5chan-directories/5chan-t-directory.json | 13 + .../5chan-directories/5chan-tg-directory.json | 13 + .../5chan-toy-directory.json | 13 + .../5chan-trv-directory.json | 13 + .../5chan-directories/5chan-tv-directory.json | 13 + .../5chan-directories/5chan-v-directory.json | 13 + .../5chan-directories/5chan-vg-directory.json | 13 + .../5chan-vip-directory.json | 13 + .../5chan-directories/5chan-vm-directory.json | 13 + .../5chan-vmg-directory.json | 13 + .../5chan-directories/5chan-vp-directory.json | 13 + .../5chan-directories/5chan-vr-directory.json | 13 + .../5chan-vrpg-directory.json | 13 + .../5chan-vst-directory.json | 13 + .../5chan-directories/5chan-vt-directory.json | 13 + .../5chan-directories/5chan-w-directory.json | 13 + .../5chan-directories/5chan-wg-directory.json | 13 + .../5chan-wsg-directory.json | 13 + .../5chan-wsr-directory.json | 13 + .../5chan-directories/5chan-x-directory.json | 13 + .../5chan-directories/5chan-xs-directory.json | 13 + src/data/5chan-directory-lists.json | 857 ------------ src/data/vendored-directory-lists.ts | 68 + src/hooks/__tests__/use-directories.test.tsx | 69 +- src/hooks/use-directories.ts | 51 +- src/lib/utils/directory-list-lookup-utils.ts | 2 +- src/views/rules/__tests__/rules.test.tsx | 171 ++- src/views/rules/rules.module.css | 165 ++- src/views/rules/rules.tsx | 266 +++- 75 files changed, 2853 insertions(+), 1333 deletions(-) create mode 100644 src/data/5chan-directories/5chan-3-directory.json create mode 100644 src/data/5chan-directories/5chan-a-directory.json create mode 100644 src/data/5chan-directories/5chan-adv-directory.json create mode 100644 src/data/5chan-directories/5chan-an-directory.json create mode 100644 src/data/5chan-directories/5chan-b-directory.json create mode 100644 src/data/5chan-directories/5chan-bant-directory.json create mode 100644 src/data/5chan-directories/5chan-biz-directory.json create mode 100644 src/data/5chan-directories/5chan-c-directory.json create mode 100644 src/data/5chan-directories/5chan-ck-directory.json create mode 100644 src/data/5chan-directories/5chan-co-directory.json create mode 100644 src/data/5chan-directories/5chan-directories-defaults.json create mode 100644 src/data/5chan-directories/5chan-diy-directory.json create mode 100644 src/data/5chan-directories/5chan-f-directory.json create mode 100644 src/data/5chan-directories/5chan-fa-directory.json create mode 100644 src/data/5chan-directories/5chan-fit-directory.json create mode 100644 src/data/5chan-directories/5chan-g-directory.json create mode 100644 src/data/5chan-directories/5chan-gd-directory.json create mode 100644 src/data/5chan-directories/5chan-gif-directory.json create mode 100644 src/data/5chan-directories/5chan-his-directory.json create mode 100644 src/data/5chan-directories/5chan-i-directory.json create mode 100644 src/data/5chan-directories/5chan-ic-directory.json create mode 100644 src/data/5chan-directories/5chan-int-directory.json create mode 100644 src/data/5chan-directories/5chan-jp-directory.json create mode 100644 src/data/5chan-directories/5chan-k-directory.json create mode 100644 src/data/5chan-directories/5chan-lit-directory.json create mode 100644 src/data/5chan-directories/5chan-m-directory.json create mode 100644 src/data/5chan-directories/5chan-mlp-directory.json create mode 100644 src/data/5chan-directories/5chan-mu-directory.json create mode 100644 src/data/5chan-directories/5chan-n-directory.json create mode 100644 src/data/5chan-directories/5chan-news-directory.json create mode 100644 src/data/5chan-directories/5chan-o-directory.json create mode 100644 src/data/5chan-directories/5chan-out-directory.json create mode 100644 src/data/5chan-directories/5chan-p-directory.json create mode 100644 src/data/5chan-directories/5chan-po-directory.json create mode 100644 src/data/5chan-directories/5chan-pol-directory.json create mode 100644 src/data/5chan-directories/5chan-pw-directory.json create mode 100644 src/data/5chan-directories/5chan-qst-directory.json create mode 100644 src/data/5chan-directories/5chan-r9k-directory.json create mode 100644 src/data/5chan-directories/5chan-s5s-directory.json create mode 100644 src/data/5chan-directories/5chan-sci-directory.json create mode 100644 src/data/5chan-directories/5chan-soc-directory.json create mode 100644 src/data/5chan-directories/5chan-sp-directory.json create mode 100644 src/data/5chan-directories/5chan-t-directory.json create mode 100644 src/data/5chan-directories/5chan-tg-directory.json create mode 100644 src/data/5chan-directories/5chan-toy-directory.json create mode 100644 src/data/5chan-directories/5chan-trv-directory.json create mode 100644 src/data/5chan-directories/5chan-tv-directory.json create mode 100644 src/data/5chan-directories/5chan-v-directory.json create mode 100644 src/data/5chan-directories/5chan-vg-directory.json create mode 100644 src/data/5chan-directories/5chan-vip-directory.json create mode 100644 src/data/5chan-directories/5chan-vm-directory.json create mode 100644 src/data/5chan-directories/5chan-vmg-directory.json create mode 100644 src/data/5chan-directories/5chan-vp-directory.json create mode 100644 src/data/5chan-directories/5chan-vr-directory.json create mode 100644 src/data/5chan-directories/5chan-vrpg-directory.json create mode 100644 src/data/5chan-directories/5chan-vst-directory.json create mode 100644 src/data/5chan-directories/5chan-vt-directory.json create mode 100644 src/data/5chan-directories/5chan-w-directory.json create mode 100644 src/data/5chan-directories/5chan-wg-directory.json create mode 100644 src/data/5chan-directories/5chan-wsg-directory.json create mode 100644 src/data/5chan-directories/5chan-wsr-directory.json create mode 100644 src/data/5chan-directories/5chan-x-directory.json create mode 100644 src/data/5chan-directories/5chan-xs-directory.json delete mode 100644 src/data/5chan-directory-lists.json create mode 100644 src/data/vendored-directory-lists.ts diff --git a/scripts/smoke-web-app.js b/scripts/smoke-web-app.js index f3bbd174..e761b3f4 100644 --- a/scripts/smoke-web-app.js +++ b/scripts/smoke-web-app.js @@ -133,7 +133,7 @@ const runScenario = async (browser, scenario) => { await page.waitForURL((url) => url.hash === '#/rules', { timeout: PAGE_TIMEOUT_MS }); await waitForTitle(page, 'Rules - 5chan'); await waitForVisibleText(page, /^Rules$/i); - await waitForVisibleText(page, /Load rules from a board/i); + await waitForVisibleText(page, /Load rules P2P from any board/i); assertNoPageErrors(pageErrors, scenario.name, 'Rules route'); await page.goto(buildRouteUrl('/all/settings'), { waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT_MS }); diff --git a/scripts/sync-directories.js b/scripts/sync-directories.js index b0e9e27e..377cb724 100644 --- a/scripts/sync-directories.js +++ b/scripts/sync-directories.js @@ -1,8 +1,10 @@ -// Best-effort sync of per-directory 5chan lists from GitHub. -// Updates the vendored fallback in src/data/ so production builds ship a fresh snapshot. -// Never fails the build: if the fetch fails (offline, rate-limited, etc.), the existing file is kept. +// Best-effort mirror of the 5chan directories folder from GitHub. +// Keeps src/data/5chan-directories/ a byte-for-byte copy of +// https://github.com/bitsocialnet/lists/tree/master/5chan-directories so the app has an +// offline fallback (loaded via src/data/vendored-directory-lists.ts) when GitHub is down. +// Never fails the build: if the fetch fails (offline, rate-limited, etc.), existing files are kept. -import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'; +import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs'; import { isAbsolute, join, dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; @@ -12,256 +14,21 @@ const __dirname = dirname(__filename); const GITHUB_CONTENTS_URL = 'https://api.github.com/repos/bitsocialnet/lists/contents/5chan-directories?ref=master'; const GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories'; const DIRECTORIES_SOURCE_PATH = process.env.DIRECTORIES_SOURCE_PATH; -const OUTPUT_PATH = join(__dirname, '..', 'src', 'data', '5chan-directory-lists.json'); +const OUTPUT_DIR = join(__dirname, '..', 'src', 'data', '5chan-directories'); const TIMEOUT_MS = 5000; -const DEFAULT_METADATA = { - title: '5chan directories', - description: 'Directory assignments built from per-directory candidate lists in https://github.com/bitsocialnet/lists/tree/master/5chan-directories', - createdAt: 0, - updatedAt: 0, -}; - -const DEFAULTS_FILE_NAME = '5chan-directories-defaults.json'; -const DIRECTORY_LIST_FILE_RE = /^5chan-.+-directory\.json$/; -const FLASH_DIRECTORY_RULES = [ - 'The tagging of uploaded files is mandatory. Improperly tagged items may be removed without notice. Abuse of the tagging system may result in temporary ban.', -]; -const DIRECTORY_CODE_ORDER = [ - 'a', - 'f', - 'co', - 'ck', - 'pol', - 'biz', - 'sci', - 'g', - 'v', - 'vg', - 'vr', - 'fit', - 'sp', - 'tg', - 'adv', - 'wsg', - 'diy', - 'out', - 'i', - 'ic', - 'mu', - 'int', - 'lit', - 'his', - 'tv', - 't', - 'x', - 'vip', - 'gif', - 'bant', - 'b', - 'an', -]; -const DIRECTORY_CODE_ORDER_INDEX = new Map(DIRECTORY_CODE_ORDER.map((code, index) => [code, index])); - -const BOOTSTRAP_DIRECTORY_LISTS = [ - { - directoryCode: 'f', - title: '/f/ - Flash', - description: - 'Boards competing to host the /f/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-f-directory.json', - features: { - pseudonymityMode: 'per-reply', - safeForWork: false, - hasFlags: false, - postFlairs: true, - requirePostFlairs: true, - requirePostLink: true, - requirePostLinkIsMedia: false, - bumpLimit: 300, - noSpoilers: true, - noSpoilerReplies: true, - postsPerPage: 50, - }, - rules: FLASH_DIRECTORY_RULES, - createdAt: 1780123897, - updatedAt: 1780123897, - boards: [ - { - address: 'flash-posting.bso', - publicKey: '12D3KooWPFckNTD8YHVJrjpa9hRYvuomvM9VsvQQkJqjWRtpLv1F', - owner: 'plebeius.bso', - addedAt: 1780123897, - }, - ], - }, -]; +const isJsonFile = (fileName) => typeof fileName === 'string' && fileName.endsWith('.json'); const isRecord = (value) => typeof value === 'object' && value !== null; -const isDirectoryListFile = (fileName) => DIRECTORY_LIST_FILE_RE.test(fileName); -const toNumber = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined); -const toString = (value) => (typeof value === 'string' && value.length > 0 ? value : undefined); - -const normalizeRules = (value) => { - if (!Array.isArray(value)) { - return undefined; - } - - const rules = value.filter((rule) => typeof rule === 'string' && rule.length > 0); - return rules.length > 0 ? rules : undefined; -}; - -const normalizeFeatures = (value) => { - if (!isRecord(value)) { - return undefined; - } - - const normalizedFeatures = Object.entries(value).reduce((acc, [key, featureValue]) => { - if (typeof featureValue === 'string' || typeof featureValue === 'boolean' || typeof featureValue === 'number') { - acc[key] = featureValue; - } - return acc; - }, {}); - - return Object.keys(normalizedFeatures).length > 0 ? normalizedFeatures : undefined; -}; - -const deriveNsfw = ({ nsfw, features }) => { - const safeForWork = typeof features?.safeForWork === 'boolean' ? features.safeForWork : undefined; - if (safeForWork !== undefined) { - return !safeForWork; - } - const featuresNsfw = typeof features?.nsfw === 'boolean' ? features.nsfw : undefined; - return typeof nsfw === 'boolean' ? nsfw : featuresNsfw; -}; - -const normalizeBoard = (raw) => { - if (!isRecord(raw)) return null; - const address = toString(raw.address) || toString(raw.name); - if (!address) return null; - const features = normalizeFeatures(raw.features); - const nsfw = deriveNsfw({ nsfw: raw.nsfw, features }); - const score = toNumber(raw.score); - - return { - address, - ...(toString(raw.publicKey) ? { publicKey: toString(raw.publicKey) } : {}), - ...(toString(raw.owner) ? { owner: toString(raw.owner) } : {}), - ...(score !== undefined ? { score } : {}), - ...(toNumber(raw.addedAt) !== undefined ? { addedAt: toNumber(raw.addedAt) } : {}), - ...(features ? { features } : {}), - ...(nsfw !== undefined ? { nsfw } : {}), - }; -}; - -const normalizeDirectoryDefaultsEntry = (code, raw) => { - if (!isRecord(raw)) { - return { directoryCode: code }; - } - - const features = normalizeFeatures(raw.features); - const rules = normalizeRules(raw.rules); - return { - directoryCode: toString(raw.directoryCode) || code, - ...(toString(raw.title) ? { title: toString(raw.title) } : {}), - ...(features ? { features } : {}), - ...(rules ? { rules } : {}), - }; -}; - -const normalizeDirectoryDefaultsData = (raw) => { - const directoriesRaw = isRecord(raw) && isRecord(raw.directories) ? raw.directories : {}; - const directories = Object.fromEntries(Object.entries(directoriesRaw).map(([code, value]) => [code, normalizeDirectoryDefaultsEntry(code, value)])); - return { - ...(isRecord(raw) && toString(raw.title) ? { title: toString(raw.title) } : {}), - ...(isRecord(raw) && toString(raw.description) ? { description: toString(raw.description) } : {}), - ...(isRecord(raw) && toNumber(raw.createdAt) !== undefined ? { createdAt: toNumber(raw.createdAt) } : {}), - ...(isRecord(raw) && toNumber(raw.updatedAt) !== undefined ? { updatedAt: toNumber(raw.updatedAt) } : {}), - directories, - }; -}; - -const normalizeDirectoryList = (raw, fallbackCode, defaults) => { - if (!isRecord(raw)) return null; - const boardsRaw = Array.isArray(raw.boards) ? raw.boards : Array.isArray(raw.communities) ? raw.communities : null; - if (!boardsRaw) return null; - - const boards = boardsRaw.map(normalizeBoard).filter(Boolean); - if (boards.length === 0) return null; - const rawCode = toString(raw.directoryCode); - const defaultEntry = defaults?.directories?.[rawCode || fallbackCode] || defaults?.directories?.[fallbackCode]; - const features = normalizeFeatures(defaultEntry?.features) || normalizeFeatures(raw.features); - const rules = normalizeRules(raw.rules); - - return { - directoryCode: toString(defaultEntry?.directoryCode) || rawCode || fallbackCode, - ...(toString(defaultEntry?.title) ? { title: toString(defaultEntry.title) } : toString(raw.title) ? { title: toString(raw.title) } : {}), - ...(toString(raw.description) ? { description: toString(raw.description) } : {}), - ...(features ? { features } : {}), - ...(rules ? { rules } : {}), - ...(toNumber(raw.createdAt) !== undefined ? { createdAt: toNumber(raw.createdAt) } : {}), - ...(toNumber(raw.updatedAt) !== undefined ? { updatedAt: toNumber(raw.updatedAt) } : {}), - boards, - }; -}; - -const getCodeFromFileName = (fileName) => fileName.replace(/^5chan-/, '').replace(/-directory\.json$/, ''); - -const sortDirectoryLists = (lists) => - [...lists].sort((a, b) => { - const aIndex = DIRECTORY_CODE_ORDER_INDEX.get(a.directoryCode) ?? Number.MAX_SAFE_INTEGER; - const bIndex = DIRECTORY_CODE_ORDER_INDEX.get(b.directoryCode) ?? Number.MAX_SAFE_INTEGER; - if (aIndex !== bIndex) { - return aIndex - bIndex; - } - return a.directoryCode.localeCompare(b.directoryCode); - }); - -const addBootstrapDirectoryLists = (directories) => { - const codes = new Set(directories.map((directory) => directory.directoryCode)); - return [...directories, ...BOOTSTRAP_DIRECTORY_LISTS.filter((directory) => !codes.has(directory.directoryCode))]; -}; - -const toDirectoryListsData = (directories, fallbackMetadata = DEFAULT_METADATA) => { - const directoryLists = addBootstrapDirectoryLists(directories); - const timestamps = [fallbackMetadata.createdAt, fallbackMetadata.updatedAt, ...directoryLists.flatMap((list) => [list.createdAt, list.updatedAt])].filter( - (value) => typeof value === 'number', - ); - return { - title: fallbackMetadata.title || DEFAULT_METADATA.title, - description: fallbackMetadata.description || DEFAULT_METADATA.description, - createdAt: timestamps.length > 0 ? Math.min(...timestamps) : fallbackMetadata.createdAt, - updatedAt: timestamps.length > 0 ? Math.max(...timestamps) : fallbackMetadata.updatedAt, - directories: sortDirectoryLists(directoryLists), - }; -}; - -const normalizeDirectoryListsData = (value, fallbackMetadata = DEFAULT_METADATA, defaults) => { - if (!isRecord(value) || !Array.isArray(value.directories)) { - return null; - } - - const directories = value.directories - .map((directory, index) => { - if (!isRecord(directory)) { - return null; - } - const fallbackCode = toString(directory.directoryCode) || Object.keys(defaults?.directories || {})[index]; - return fallbackCode ? normalizeDirectoryList(directory, fallbackCode, defaults) : null; - }) - .filter(Boolean); - - const metadata = { - title: toString(value.title) || fallbackMetadata.title, - description: toString(value.description) || fallbackMetadata.description, - createdAt: toNumber(value.createdAt) ?? fallbackMetadata.createdAt, - updatedAt: toNumber(value.updatedAt) ?? fallbackMetadata.updatedAt, - }; - - return directories.length > 0 ? toDirectoryListsData(directories, metadata) : null; -}; - const getErrorMessage = (error) => (error instanceof Error ? error.message : String(error)); +const getSourceLabel = () => { + if (!DIRECTORIES_SOURCE_PATH) { + return `GitHub folder: ${GITHUB_CONTENTS_URL}`; + } + const resolvedSourcePath = isAbsolute(DIRECTORIES_SOURCE_PATH) ? DIRECTORIES_SOURCE_PATH : resolve(process.cwd(), DIRECTORIES_SOURCE_PATH); + return `local directory: ${resolvedSourcePath}`; +}; -const fetchJson = async (url) => { +const fetchWithTimeout = async (url, asJson) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS); try { @@ -269,109 +36,103 @@ const fetchJson = async (url) => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } - return await response.json(); + return asJson ? response.json() : response.text(); } finally { clearTimeout(timeout); } }; +// Load the { fileName -> verbatim text } map from a local mirror directory. const loadFromLocalDirectory = (directoryPath) => { - console.log(`ℹ️ Syncing vendored directories from local directory: ${directoryPath}`); - const defaultsPath = join(directoryPath, DEFAULTS_FILE_NAME); - const defaults = existsSync(defaultsPath) ? normalizeDirectoryDefaultsData(JSON.parse(readFileSync(defaultsPath, 'utf8'))) : normalizeDirectoryDefaultsData({}); - const directories = readdirSync(directoryPath) - .filter(isDirectoryListFile) - .map((fileName) => { - const raw = JSON.parse(readFileSync(join(directoryPath, fileName), 'utf8')); - return normalizeDirectoryList(raw, getCodeFromFileName(fileName), defaults); - }) - .filter(Boolean); - - return toDirectoryListsData(directories, { - title: DEFAULT_METADATA.title, - description: DEFAULT_METADATA.description, - createdAt: defaults.createdAt ?? DEFAULT_METADATA.createdAt, - updatedAt: defaults.updatedAt ?? DEFAULT_METADATA.updatedAt, - }); + console.log(`ℹ️ Mirroring directories from local directory: ${directoryPath}`); + const files = {}; + for (const fileName of readdirSync(directoryPath).filter(isJsonFile)) { + files[fileName] = readFileSync(join(directoryPath, fileName), 'utf8'); + } + return files; }; -const loadDirectoriesSource = async () => { - if (DIRECTORIES_SOURCE_PATH) { - const resolvedSourcePath = isAbsolute(DIRECTORIES_SOURCE_PATH) ? DIRECTORIES_SOURCE_PATH : resolve(process.cwd(), DIRECTORIES_SOURCE_PATH); - if (!existsSync(resolvedSourcePath)) { - throw new Error(`Local directories source not found: ${resolvedSourcePath}`); - } - - if (statSync(resolvedSourcePath).isDirectory()) { - return loadFromLocalDirectory(resolvedSourcePath); - } - - console.log(`ℹ️ Syncing vendored directories from local file: ${resolvedSourcePath}`); - return JSON.parse(readFileSync(resolvedSourcePath, 'utf8')); - } - - console.log(`ℹ️ Syncing vendored directories from GitHub folder: ${GITHUB_CONTENTS_URL}`); - const contents = await fetchJson(GITHUB_CONTENTS_URL); +// Load the { fileName -> verbatim text } map from the GitHub folder. +const loadFromGitHub = async () => { + console.log(`ℹ️ Mirroring directories from GitHub folder: ${GITHUB_CONTENTS_URL}`); + const contents = await fetchWithTimeout(GITHUB_CONTENTS_URL, true); if (!Array.isArray(contents)) { throw new Error('Invalid GitHub directory listing'); } const fileNames = contents - .map((entry) => (isRecord(entry) ? entry.name : undefined)) - .filter((name) => typeof name === 'string' && isDirectoryListFile(name)) + .filter((entry) => isRecord(entry) && entry.type === 'file' && isJsonFile(entry.name)) + .map((entry) => entry.name) .sort(); - const defaults = normalizeDirectoryDefaultsData(await fetchJson(`${GITHUB_RAW_BASE_URL}/${DEFAULTS_FILE_NAME}`)); - const directories = await Promise.all( + + const files = {}; + await Promise.all( fileNames.map(async (fileName) => { - const raw = await fetchJson(`${GITHUB_RAW_BASE_URL}/${fileName}`); - return normalizeDirectoryList(raw, getCodeFromFileName(fileName), defaults); + files[fileName] = await fetchWithTimeout(`${GITHUB_RAW_BASE_URL}/${fileName}`, false); }), ); + return files; +}; - return toDirectoryListsData(directories.filter(Boolean), { - title: DEFAULT_METADATA.title, - description: DEFAULT_METADATA.description, - createdAt: defaults.createdAt ?? DEFAULT_METADATA.createdAt, - updatedAt: defaults.updatedAt ?? DEFAULT_METADATA.updatedAt, - }); +const loadSourceFiles = async () => { + if (DIRECTORIES_SOURCE_PATH) { + const resolvedSourcePath = isAbsolute(DIRECTORIES_SOURCE_PATH) ? DIRECTORIES_SOURCE_PATH : resolve(process.cwd(), DIRECTORIES_SOURCE_PATH); + if (!existsSync(resolvedSourcePath) || !statSync(resolvedSourcePath).isDirectory()) { + throw new Error(`Local directories source folder not found: ${resolvedSourcePath}`); + } + return loadFromLocalDirectory(resolvedSourcePath); + } + return loadFromGitHub(); }; const sync = async () => { try { - let existing = ''; - let fallbackMetadata = DEFAULT_METADATA; - try { - existing = readFileSync(OUTPUT_PATH, 'utf8'); - const parsedExisting = JSON.parse(existing); - const normalizedExisting = normalizeDirectoryListsData(parsedExisting); - if (normalizedExisting) { - fallbackMetadata = { - title: normalizedExisting.title, - description: normalizedExisting.description, - createdAt: normalizedExisting.createdAt, - updatedAt: normalizedExisting.updatedAt, - }; + const files = await loadSourceFiles(); + const fileNames = Object.keys(files); + if (fileNames.length === 0) { + throw new Error('No directory files found in source'); + } + + // Validate every file parses as JSON before touching disk, so a transient HTML error page + // (or a truncated download) can never overwrite a good vendored mirror. + for (const [fileName, text] of Object.entries(files)) { + try { + JSON.parse(text); + } catch { + throw new Error(`Invalid JSON for ${fileName}`); } - } catch { - // File does not exist yet or is invalid JSON. } - const data = normalizeDirectoryListsData(await loadDirectoriesSource(), fallbackMetadata); - if (!data) { - throw new Error('Invalid directory lists payload'); + mkdirSync(OUTPUT_DIR, { recursive: true }); + + let written = 0; + for (const [fileName, text] of Object.entries(files)) { + const outputPath = join(OUTPUT_DIR, fileName); + // Write verbatim; the upstream files are the source of truth, mirror them byte-for-byte. + const existing = existsSync(outputPath) ? readFileSync(outputPath, 'utf8') : null; + if (existing !== text) { + writeFileSync(outputPath, text, 'utf8'); + written += 1; + } } - const formatted = `${JSON.stringify(data, null, 2)}\n`; + // Prune local json files that no longer exist upstream so the mirror stays exact. + const sourceNames = new Set(fileNames); + let removed = 0; + for (const fileName of readdirSync(OUTPUT_DIR).filter(isJsonFile)) { + if (!sourceNames.has(fileName)) { + rmSync(join(OUTPUT_DIR, fileName)); + removed += 1; + } + } - if (formatted === existing) { - console.log('✅ Vendored directory lists already up to date'); + if (written === 0 && removed === 0) { + console.log(`✅ Vendored directories already up to date (${fileNames.length} files)`); return; } - - writeFileSync(OUTPUT_PATH, formatted, 'utf8'); - console.log(`✅ Synced vendored directory lists (${data.directories.length} directories)`); + console.log(`✅ Mirrored directories (${fileNames.length} files, ${written} updated, ${removed} removed)`); } catch (e) { - console.warn(`⚠️ Could not sync directory lists from GitHub (keeping existing file): ${getErrorMessage(e)}`); + console.warn(`⚠️ Could not mirror directories from ${getSourceLabel()} (keeping existing files): ${getErrorMessage(e)}`); } }; diff --git a/src/components/markdown/__tests__/markdown.test.tsx b/src/components/markdown/__tests__/markdown.test.tsx index e118dd3c..ae88860d 100644 --- a/src/components/markdown/__tests__/markdown.test.tsx +++ b/src/components/markdown/__tests__/markdown.test.tsx @@ -179,7 +179,10 @@ vi.mock('../external-number-quote-link', () => ({ let container: HTMLDivElement; let root: Root; -const renderMarkdown = async (props: { content: string; postCid?: string; communityAddress?: string; title?: string }, initialEntry = '/mu/thread/post-1') => { +const renderMarkdown = async ( + props: { content: string; postCid?: string; communityAddress?: string; title?: string; parseSpoilers?: boolean }, + initialEntry = '/mu/thread/post-1', +) => { await act(async () => { root.render(createElement(MemoryRouter, { initialEntries: [initialEntry] }, createElement(Markdown, props))); }); @@ -230,6 +233,16 @@ describe('Markdown', () => { expect(links.find((link) => link.getAttribute('href') === '/fit')?.textContent).toBe('>>>/fit/'); }); + it('keeps spoiler markup visible when parseSpoilers is false', async () => { + await renderMarkdown({ + content: 'enclose it like so: [spoiler]spoiled text[/spoiler].', + parseSpoilers: false, + }); + + expect(container.querySelector('.spoilertext')).toBeNull(); + expect(container.textContent).toContain('[spoiler]spoiled text[/spoiler]'); + }); + it('renders greentext for any leading marker run while preserving quote links', async () => { await renderMarkdown({ content: '>green line\n>>test\n>>>>>>>test\n>>42\n>>>/fit/', diff --git a/src/components/markdown/markdown.tsx b/src/components/markdown/markdown.tsx index 35704d24..6a2dfd27 100644 --- a/src/components/markdown/markdown.tsx +++ b/src/components/markdown/markdown.tsx @@ -171,6 +171,11 @@ const COMBINED_REGEX = new RegExp( 'g', ); +const COMBINED_REGEX_WITHOUT_SPOILER = new RegExp( + `(${CROSSBOARD_NUMBER_QUOTE_TOKEN_REGEX.source})|(${CROSSBOARD_REGEX.source})|(${QUOTE_LINK_REGEX.source})|(${URL_REGEX.source})`, + 'g', +); + const makeTokenKey = (prefix: string, type: Token['type'], start: number, end: number): string => `${prefix}${type}:${start}:${end}`; const isGreentextLine = (line: string): boolean => { @@ -236,11 +241,11 @@ function getCrossboardRoute(fullPattern: string): string | null { return `/${pathPart}`; } -function tokenize(text: string, keyPrefix = ''): Token[] { +function tokenize(text: string, keyPrefix = '', parseSpoilers = true): Token[] { const tokens: Token[] = []; let lastIndex = 0; - const regex = new RegExp(COMBINED_REGEX.source, 'g'); + const regex = new RegExp((parseSpoilers ? COMBINED_REGEX : COMBINED_REGEX_WITHOUT_SPOILER).source, 'g'); let match: RegExpExecArray | null; while ((match = regex.exec(text)) !== null) { @@ -256,13 +261,13 @@ function tokenize(text: string, keyPrefix = ''): Token[] { }); } - if (match[1] !== undefined) { + if (parseSpoilers && match[1] !== undefined) { const innerContent = match[2]; const key = makeTokenKey(keyPrefix, 'spoiler', matchStart, matchEnd); - tokens.push({ key, type: 'spoiler', tokens: tokenize(innerContent, `${key}/`) }); - } else if (match[3] !== undefined) { - const boardIdentifier = match[4]; - const number = parseInt(match[5], 10); + tokens.push({ key, type: 'spoiler', tokens: tokenize(innerContent, `${key}/`, parseSpoilers) }); + } else if (match[parseSpoilers ? 3 : 1] !== undefined) { + const boardIdentifier = match[parseSpoilers ? 4 : 2]; + const number = parseInt(match[parseSpoilers ? 5 : 3], 10); if (boardIdentifier && !Number.isNaN(number)) { tokens.push({ key: makeTokenKey(keyPrefix, 'crossBoardNumberQuoteLink', matchStart, matchEnd), @@ -277,8 +282,8 @@ function tokenize(text: string, keyPrefix = ''): Token[] { } else { tokens.push({ key: makeTokenKey(keyPrefix, 'text', matchStart, matchEnd), type: 'text', value: fullMatch }); } - } else if (match[6] !== undefined) { - const pathPart = match[7]; + } else if (match[parseSpoilers ? 6 : 4] !== undefined) { + const pathPart = match[parseSpoilers ? 7 : 5]; const fullPattern = `>>>/${pathPart}`; const route = getCrossboardRoute(fullPattern); if (route) { @@ -291,10 +296,10 @@ function tokenize(text: string, keyPrefix = ''): Token[] { } else { tokens.push({ key: makeTokenKey(keyPrefix, 'text', matchStart, matchEnd), type: 'text', value: fullMatch }); } - } else if (match[8] !== undefined) { - const number = parseInt(match[9], 10); + } else if (match[parseSpoilers ? 8 : 6] !== undefined) { + const number = parseInt(match[parseSpoilers ? 9 : 7], 10); tokens.push({ key: makeTokenKey(keyPrefix, 'quoteLink', matchStart, matchEnd), type: 'quoteLink', number }); - } else if (match[10] !== undefined) { + } else if (match[parseSpoilers ? 10 : 8] !== undefined) { const { href, trailingText } = splitUrlTrailingText(fullMatch); const linkEnd = trailingText ? matchEnd - trailingText.length : matchEnd; tokens.push({ key: makeTokenKey(keyPrefix, 'url', matchStart, linkEnd), type: 'url', href }); @@ -322,6 +327,7 @@ interface RenderContext { postCid?: string; communityAddress?: string; enableQstBbcode: boolean; + parseSpoilers: boolean; } interface MarkdownProps { @@ -329,6 +335,8 @@ interface MarkdownProps { title?: string; postCid?: string; communityAddress?: string; + /** When false, [spoiler] tags stay visible (e.g. rules text teaching the syntax). Default true. */ + parseSpoilers?: boolean; } const NumberQuoteLink = ({ number, threadPostCid, communityAddress }: { number: number; threadPostCid?: string; communityAddress?: string }) => { @@ -575,7 +583,13 @@ const renderLineContent = (line: string, context: RenderContext): React.ReactNod const matchEnd = matchStart + fullMatch.length; if (matchStart > lastIndex) { - elements.push(); + elements.push( + , + ); } if (nextMatch.type === 'dice') { @@ -586,7 +600,7 @@ const renderLineContent = (line: string, context: RenderContext): React.ReactNod if (fortune) { elements.push(); } else { - elements.push(); + elements.push(); } } @@ -594,13 +608,15 @@ const renderLineContent = (line: string, context: RenderContext): React.ReactNod } if (lastIndex < line.length) { - elements.push(); + elements.push( + , + ); } return elements; }; -const Markdown = ({ content, title, postCid, communityAddress }: MarkdownProps) => { +const Markdown = ({ content, title, postCid, communityAddress, parseSpoilers = true }: MarkdownProps) => { const location = useLocation(); const params = useParams(); const isInCatalogView = isCatalogView(location.pathname, params); @@ -612,7 +628,7 @@ const Markdown = ({ content, title, postCid, communityAddress }: MarkdownProps) const elements: React.ReactNode[] = []; let lineOffset = 0; - const context = { isInCatalogView, postCid, communityAddress, enableQstBbcode }; + const context = { isInCatalogView, postCid, communityAddress, enableQstBbcode, parseSpoilers }; lines.forEach((line, lineIndex) => { const lineKey = `line-${lineOffset}`; @@ -640,7 +656,7 @@ const Markdown = ({ content, title, postCid, communityAddress }: MarkdownProps) }); return elements; - }, [content, isInCatalogView, postCid, communityAddress, enableQstBbcode]); + }, [content, isInCatalogView, postCid, communityAddress, enableQstBbcode, parseSpoilers]); return ( diff --git a/src/data/5chan-directories/5chan-3-directory.json b/src/data/5chan-directories/5chan-3-directory.json new file mode 100644 index 00000000..edf70a96 --- /dev/null +++ b/src/data/5chan-directories/5chan-3-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /3/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-3-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "3dcg-posting.bso", + "publicKey": "12D3KooWBRCcwxWTx23tyK7Uhqs9ViE3MnSgvWSLip2ikrWaD3c2", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-a-directory.json b/src/data/5chan-directories/5chan-a-directory.json new file mode 100644 index 00000000..fbb09548 --- /dev/null +++ b/src/data/5chan-directories/5chan-a-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /a/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-a-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "anime-and-manga.bso", + "publicKey": "12D3KooWMDt3RMVMHrm2pEEyfraBtHP86dmkim7EVAapDj3Mchdy", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-adv-directory.json b/src/data/5chan-directories/5chan-adv-directory.json new file mode 100644 index 00000000..daa474b8 --- /dev/null +++ b/src/data/5chan-directories/5chan-adv-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /adv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-adv-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "advice-posting.bso", + "publicKey": "12D3KooWH5UFNs9yfwJVsSUhEW5bYrtF8KmzfeJzwg84qWMoFJVY", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-an-directory.json b/src/data/5chan-directories/5chan-an-directory.json new file mode 100644 index 00000000..1ea3cde0 --- /dev/null +++ b/src/data/5chan-directories/5chan-an-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /an/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-an-directory.json", + "createdAt": 1779262963, + "updatedAt": 1779262963, + "boards": [ + { + "address": "animals-and-nature.bso", + "publicKey": "12D3KooWSpKszPM2c17KBgbnoRrkWPCHJosGGFg3bKnzarYhHeSc", + "addedAt": 1779262963, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-b-directory.json b/src/data/5chan-directories/5chan-b-directory.json new file mode 100644 index 00000000..81ed2cc2 --- /dev/null +++ b/src/data/5chan-directories/5chan-b-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /b/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-b-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "random-nsfw.bso", + "publicKey": "12D3KooWJkJYkuVbJQhapcVGpo8oNmsXsKmFRrFqc1SHezZW9fX8", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-bant-directory.json b/src/data/5chan-directories/5chan-bant-directory.json new file mode 100644 index 00000000..4535c7aa --- /dev/null +++ b/src/data/5chan-directories/5chan-bant-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /bant/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-bant-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "international-nsfw.bso", + "publicKey": "12D3KooWPuYtTzrq8gFnv6yM1r4egy1rd4cxLr1nsXTjfdYkfwcN", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-biz-directory.json b/src/data/5chan-directories/5chan-biz-directory.json new file mode 100644 index 00000000..9b58a850 --- /dev/null +++ b/src/data/5chan-directories/5chan-biz-directory.json @@ -0,0 +1,19 @@ +{ + "description": "Boards competing to host the /biz/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-biz-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779212075, + "boards": [ + { + "address": "business-and-finance.bso", + "publicKey": "12D3KooWNMybS8JqELi38ZBX897PrjWbCrGoMKfw3bgoqzC2n1Dh", + "addedAt": 1779182014, + "owner": "rinse12.bso" + }, + { + "address": "bizraelis.bso", + "publicKey": "12D3KooWR7nTdKZqZ1twGWMfVsXYDGp1XAKUrnYznKP651jFrizE", + "addedAt": 1779212075, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-c-directory.json b/src/data/5chan-directories/5chan-c-directory.json new file mode 100644 index 00000000..94205077 --- /dev/null +++ b/src/data/5chan-directories/5chan-c-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /c/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-c-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "anime-cute.bso", + "publicKey": "12D3KooWMKVj7dzag2fcYP24kgmxDz2DSvE7yGzBW9fJEYA94aMC", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-ck-directory.json b/src/data/5chan-directories/5chan-ck-directory.json new file mode 100644 index 00000000..20b01152 --- /dev/null +++ b/src/data/5chan-directories/5chan-ck-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /ck/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-ck-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "food-and-cooking.bso", + "publicKey": "12D3KooWFfANpSeemwvqqGsXNPHDuyDMCK6wE8cxGTuNw7xoWUp7", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-co-directory.json b/src/data/5chan-directories/5chan-co-directory.json new file mode 100644 index 00000000..d823c164 --- /dev/null +++ b/src/data/5chan-directories/5chan-co-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /co/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-co-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "comics-and-cartoons.bso", + "publicKey": "12D3KooWRLmyg671KVYb1xekZ8eTQSMfRraB3DmQZPsKbCdMsEuc", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-directories-defaults.json b/src/data/5chan-directories/5chan-directories-defaults.json new file mode 100644 index 00000000..9ce98907 --- /dev/null +++ b/src/data/5chan-directories/5chan-directories-defaults.json @@ -0,0 +1,1244 @@ +{ + "title": "5chan Directory Defaults", + "description": "Expected 5chan UX defaults by directory code. These defaults apply to the directory, not to individual candidate boards.", + "createdAt": 1779262179, + "updatedAt": 1780129468, + "directories": { + "3": { + "directoryCode": "3", + "title": "/3/ - 3DCG", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Images and discussion should relate to 3D modeling and imagery.", + "Remember: This is a worksafe board! No adult content is allowed." + ] + }, + "a": { + "directoryCode": "a", + "title": "/a/ - Anime & Manga", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All images and resulting discussion should pertain to anime or manga.", + "The use of spoiler tags is highly recommended. To spoilerize text, enclose it like so: [spoiler]spoiled text[/spoiler]. In addition, images may be spoilerized by checking the \"[Spoiler Image?]\" box on the submission form.", + "Purposeful spoiling of a series may result in post deletion and temporary ban.", + "The discussion of \"live action\" television shows is permitted so long as they are distinctly rooted in, or based off of an anime or manga series.", + "Japanese visual novels should be posted in /jp/, and Western on /vg/.", + "Threads should be created with substantial OP text containing a meaningful topic of discussion. Please refrain from posting catch-phrase or template threads." + ] + }, + "f": { + "directoryCode": "f", + "title": "/f/ - Flash", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": false, + "hasFlags": false, + "postFlairs": true, + "requirePostFlairs": true, + "requirePostLink": true, + "requirePostLinkIsMedia": false, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 50 + }, + "rules": [ + "The tagging of uploaded files is mandatory. Improperly tagged items may be removed without notice. Abuse of the tagging system may result in temporary ban." + ] + }, + "adv": { + "directoryCode": "adv", + "title": "/adv/ - Advice", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All advice threads welcome. Offering or requesting is allowed.", + "This is the destination for all questions regarding specific personal problems.", + "All threads are expected to be constructive in nature; BAWWWing and venting is discouraged.", + "No hookup or camwhore threads! Take that to /soc/." + ] + }, + "an": { + "directoryCode": "an", + "title": "/an/ - Animals & Nature", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All images of animals and nature are welcome.", + "Posting images depicting animal cruelty is strictly forbidden." + ] + }, + "b": { + "directoryCode": "b", + "title": "/b/ - Random", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": false, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "ZOMG NONE!!!1*", + "Note: \"ZOMG NONE!!!1\" applies to moderators as well." + ] + }, + "bant": { + "directoryCode": "bant", + "title": "/bant/ - International/Random", + "features": { + "pseudonymityMode": "per-post", + "safeForWork": false, + "hasFlags": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Porn dump threads are not permitted. Please use the appropriate boards for porn." + ] + }, + "biz": { + "directoryCode": "biz", + "title": "/biz/ - Business & Finance", + "features": { + "pseudonymityMode": "per-post", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 20 + }, + "rules": [ + "All discussion should pertain to business, finance, and economics related topics. This includes financial markets, securities, currencies (including cryptocurrencies), and commodities—as well as topics relating to starting and running a business.", + "All political discussion belongs in /pol/. Conspiracy theories belong in /x/.", + "Absolutely no advertising, soliciting, or promotion of any kind. You may not discuss projects you have an interest in without making that disclosure. We will remove anything that remotely breaks this rule.", + "Begging/asking (including tipping) for cryptocurrencies or asking for money/capital is also strictly forbidden." + ] + }, + "c": { + "directoryCode": "c", + "title": "/c/ - Anime/Cute", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Images should be cute (\"moe\") in nature.", + "Ecchi belongs on its respective board. Males belong on the cute/male board.", + "Heterosexual couples are allowed." + ] + }, + "ck": { + "directoryCode": "ck", + "title": "/ck/ - Food & Cooking", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Images and discussion should relate to food and cooking.", + "Recipes are welcome! Feel free to talk about your favorite dishes and discuss past cooking experiences." + ] + }, + "co": { + "directoryCode": "co", + "title": "/co/ - Comics & Cartoons", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "Images and discussion should pertain to media of Western origin.", + "Spoiler tag use is enforced. Don't know how to use spoiler tags? Read [this](/faq#spoiler)." + ] + }, + "diy": { + "directoryCode": "diy", + "title": "/diy/ - Do It Yourself", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "MAKERS GONNA MAKE!", + "Do not post or request instructions for how to make weapons or any device that might bring harm to a person. Think \"Instructables\" and \"Make Magazine\" not \"Anarchist's Cookbook.\"" + ] + }, + "fa": { + "directoryCode": "fa", + "title": "/fa/ - Fashion", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Images and discussion should pertain to fashion and apparel." + ] + }, + "fit": { + "directoryCode": "fit", + "title": "/fit/ - Fitness", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Images and discussion related to dieting, exercise, healthy living, and workout plans is welcome here.", + "Relationship, dating advice, and \"mental health\" threads belong on /adv/." + ] + }, + "g": { + "directoryCode": "g", + "title": "/g/ - Technology", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Images depicting the technology of past, present, and future are all welcome here.", + "Tech support threads should be posted on /wsr/.", + "Trolling is not permitted. Do not instigate or participate in flamewars.", + "You may use [code] tags to highlight syntax and preserve whitespace. To use them, just wrap your code snippets [code]like this[/code]." + ] + }, + "gd": { + "directoryCode": "gd", + "title": "/gd/ - Graphic Design", + "features": { + "noSpoilers": true, + "noSpoilerReplies": true, + "safeForWork": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "pseudonymityMode": "per-reply", + "hasFlags": false, + "bumpLimit": 300, + "postsPerPage": 15 + }, + "rules": [ + "This board is for the discussion of graphic design software and computer-aided graphic design techniques (ie. \"Photoshopping\").", + "Requests for Photoshopping or any other work should go on /wsr/." + ] + }, + "gif": { + "directoryCode": "gif", + "title": "/gif/ - Adult GIF", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": false, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All GIFs should be animated-no static images." + ] + }, + "his": { + "directoryCode": "his", + "title": "/his/ - History & Humanities", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "1. All images and discussion should pertain to the humanities: history, philosophy, religion, law, classical artwork, archeology, anthropology, ancient languages, etc.", + "2. Discussions about books, poetry, and other forms of literature should be posted on /lit/.", + "3. Discussions about politics and current events should be posted on /pol/.", + "4. Please do not start threads about events taking place less than 25 years ago." + ] + }, + "i": { + "directoryCode": "i", + "title": "/i/ - Oekaki", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": false, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Oekakis can be random in nature, but please, no \"junk\" drawings. Quality over quantity!" + ] + }, + "ic": { + "directoryCode": "ic", + "title": "/ic/ - Artwork/Critique", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All images and discussion should pertain to the critique of visual artwork.", + "User-created artwork is submitted for critique, and visual art is discussed. Do not claim authorship of works you did not create.", + "Only constructive criticism will be accepted. Rude or offensive comments will result in a ban.", + "Requests for free work of any sort are forbidden.", + "Don't bring community vendettas onto this board. Singling out individual artists for the purposes of trolling them will not be tolerated.", + "All discussion of artists should pertain directly to their work, and not their personal lives." + ] + }, + "int": { + "directoryCode": "int", + "title": "/int/ - International", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "This board is for the discussion of foreign culture and language. Please be respectful.", + "Posting in languages other than English is allowed-and encouraged!", + "All politics and current events belong on /pol/." + ] + }, + "jp": { + "directoryCode": "jp", + "title": "/jp/ - Otaku Culture", + "features": { + "noSpoilers": false, + "noSpoilerReplies": false, + "safeForWork": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "pseudonymityMode": "per-reply", + "hasFlags": false, + "bumpLimit": 300, + "postsPerPage": 15 + }, + "rules": [ + "All images and discussion should pertain to light and visual novels, figures and other otaku paraphernalia, Touhou Project, Vocaloid, doujin works and music, and diverse niche Japanese interests (kigurumi, idols, mahjong, tea).", + "This is the appropriate board for the discussion of Japanese visual novels and light novels. Western visual novels should be posted to /vg/, and translated visual novels are fine on either board." + ] + }, + "k": { + "directoryCode": "k", + "title": "/k/ - Weapons", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All weaponry is welcome. Military vehicles/knives/other weapons are included, this board isn't just for firearms!" + ] + }, + "lit": { + "directoryCode": "lit", + "title": "/lit/ - Literature", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All literature discussion is welcome, however fan-fic is not allowed." + ] + }, + "m": { + "directoryCode": "m", + "title": "/m/ - Mecha", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All mecha and mecha-related (ex: pilots, core fighter, mecha girl, hobby model) images are allowed." + ] + }, + "mlp": { + "directoryCode": "mlp", + "title": "/mlp/ - Pony", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "This is the destination for all cartoon/anime pony related content on 4chan.", + "This is a work safe board. No pornographic images or other not safe for work content is allowed (this includes clop).", + "Topics must be show-related. When discussing people, they must be associated with the show and not the fandom.", + "No roleplay.", + "Ponies only—no anthro." + ] + }, + "mu": { + "directoryCode": "mu", + "title": "/mu/ - Music", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Discuss music, artists, and instruments here. All images should relate to the topic at hand.", + "Trolling, instigating, or participating in a flamewar will result in a ban.", + "Please try to keep duplicate topics to a minimum by always searching for an old or active thread before posting a new one.", + "Do not post a continuation of a thread until the bump limit is reached." + ] + }, + "n": { + "directoryCode": "n", + "title": "/n/ - Transportation", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All sorts of transportation pictures and discussion welcome. Trains, planes, ships, bicycles, flying elevators—post 'em all!", + "Please keep automobiles on the auto board.", + "Military vehicles should be posted on the weapons board." + ] + }, + "news": { + "directoryCode": "news", + "title": "/news/ - Current News", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": false, + "requirePostLinkIsMedia": false, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All topics and discussion should be about current news articles. The OP must contain a valid URL of an article from a credible news site (i.e., a newspaper, news magazine, or TV channel). Blog and editorial articles are not acceptable.", + "News articles should be current; no articles older than 48 hours should be posted.", + "Blatant trolling and racism is not permitted.", + "Note that you can also discuss news articles on /pol/; /news/ is specifically for current news articles." + ] + }, + "o": { + "directoryCode": "o", + "title": "/o/ - Auto", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All motor vehicle pictures are allowed.", + "Photos of models and \"race queens\" are permitted, so long as they are sufficiently clothed." + ] + }, + "out": { + "directoryCode": "out", + "title": "/out/ - Outdoors", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "This board is for discussions about hiking, camping, geocaching, orienteering, farming, gardening, etc.", + "GO OUTSIDE AND TOSS A BALL OR SOMETHING." + ] + }, + "p": { + "directoryCode": "p", + "title": "/p/ - Photography", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Only upload images that you, the photographer, have taken.", + "Post only photos that show at least trace amounts of thoughtful composition. Do not upload random snapshots.", + "Even though EXIF data is made available when encoded, please post as much relevant technical information as possible, such as: camera, kit, lens, etc.", + "Include a short description with your photograph, such as when and where it was photographed and under what circumstances.", + "As with the art critique board, only constructive criticism will be tolerated." + ] + }, + "po": { + "directoryCode": "po", + "title": "/po/ - Papercraft & Origami", + "features": { + "noSpoilers": true, + "noSpoilerReplies": true, + "safeForWork": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "pseudonymityMode": "per-reply", + "hasFlags": false, + "bumpLimit": 300, + "postsPerPage": 15 + }, + "rules": [ + "Upload your papercraft and origami images here!", + "Requesting is permitted." + ] + }, + "pol": { + "directoryCode": "pol", + "title": "/pol/ - Politically Incorrect", + "features": { + "pseudonymityMode": "per-post", + "safeForWork": false, + "hasFlags": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 20 + }, + "rules": [ + "Debate and discussion related to politics and current events is welcome.", + "You are free to speak your mind, but *do not attack other users.* You may challenge one another, but *keep it civil!*", + "Posting pornography is not permitted. This is a politics board, not a porn board." + ] + }, + "pw": { + "directoryCode": "pw", + "title": "/pw/ - Professional Wrestling", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All images and discussion should pertain to professional wrestling." + ] + }, + "qst": { + "directoryCode": "qst", + "title": "/qst/ - Quests", + "features": { + "noSpoilers": false, + "noSpoilerReplies": false, + "safeForWork": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "pseudonymityMode": "per-reply", + "hasFlags": false, + "bumpLimit": 300, + "postsPerPage": 15 + }, + "rules": [ + "This board is for author-driven collaborative storytelling (i.e. \"quests\"). All threads posted here should be part of a new or ongoing quest.", + "The author controls the story of the quest. They can take suggestions from other posters, or not, at their sole discretion. If things don't go your way don't complain!", + "All threads on this board should be created by the associated quest author. Please refrain from making meta-threads.", + "Erotic roleplay is not permitted. Frequent explicit descriptions of sexual encounters by a quest author may result in mod intervention." + ] + }, + "r9k": { + "directoryCode": "r9k", + "title": "/r9k/ - ROBOT9002", + "features": { + "noSpoilers": false, + "noSpoilerReplies": false, + "safeForWork": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "pseudonymityMode": "per-reply", + "hasFlags": false, + "bumpLimit": 300, + "postsPerPage": 15 + }, + "rules": [ + "A place for hanging out and posting greentext stories.", + "Advice threads belong on /adv/, and \"rate me,\" meetup, and camwhore threads on /soc/.", + "Purposefully evading the ROBOT9002 filter is not permitted." + ] + }, + "s5s": { + "directoryCode": "s5s", + "title": "/s5s/ - Shit 5chan Says", + "features": { + "noSpoilers": false, + "noSpoilerReplies": false, + "safeForWork": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "pseudonymityMode": "per-reply", + "hasFlags": false, + "bumpLimit": 300, + "postsPerPage": 15 + }, + "rules": [ + "You must check your #fortune in order to post on this board.", + "No porn dump threads. Keep that shit on /b/.", + "Moderators must check their #fortune as well." + ] + }, + "sci": { + "directoryCode": "sci", + "title": "/sci/ - Science & Math", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All science and math related topics welcome.", + "Homework threads will be deleted, and the poster banned.", + "No \"religion vs. science\" threads." + ] + }, + "soc": { + "directoryCode": "soc", + "title": "/soc/ - Cams & Meetups", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": false, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All images and discussion should be related to camming and socializing with other users (\"rate me,\" meetup, report in, etc). Photos should be of yourself (timestamps encouraged!)—random porn dumps are not allowed.", + "No whining (aka \"BAWWW\") or \"life\" threads. This is not your personal blog.", + "Tinychat and Amazon wishlist/begging threads are not allowed.", + "Soliciting or offering payments for photos, webcam shows, etc is not allowed.", + "Do not stalk or harass any users. Be civil to camwhores.", + "Requesting contact info outside of contact and meetup threads is not allowed. Offering your contact info unsolicited is also disallowed. Responding to a request for contact info is allowed.", + "Asking a user to write on a sign is not allowed, unless said user is actively taking requests. Timestamp requests are permitted.", + "Please try to keep duplicate topics to a minimum by always searching for an old or active thread before posting a new one. Do not post a continuation of a thread until the bump limit is reached.", + "Respect the gender of a thread. If the OP specifies women, only women should reply with images (and vice versa for men). tl;dr: Men, don't post pictures of yourself or your nether regions in female threads." + ] + }, + "sp": { + "directoryCode": "sp", + "title": "/sp/ - Sports", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All sports welcome.", + "In the interest of keeping this board general, please combine threads related to similar topics (example: specific teams, games, players, etc.).", + "Discussion of \"eSports\" (i.e., video games) belongs on /v/. General threads about eSports belong on /vg/." + ] + }, + "t": { + "directoryCode": "t", + "title": "/t/ - Torrents", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": false, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "No illegal stuff. Warez is allowed." + ] + }, + "tg": { + "directoryCode": "tg", + "title": "/tg/ - Traditional Games", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "Board games, paper games, war games, card games, etc. go here!" + ] + }, + "toy": { + "directoryCode": "toy", + "title": "/toy/ - Toys", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Toys, toys, toys!", + "Absolutely no Japanese figurines. Action figures are permitted.", + "No \"hot glue\" fetish images." + ] + }, + "trv": { + "directoryCode": "trv", + "title": "/trv/ - Travel", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Post images and information about various locales, places you've traveled, or hope to visit in the future.", + "Feel free to ask questions about other cultures, and post in native languages.", + "This board is for all the countries of the world. The more diverse, the better!", + "Discussions of prostitution or sex tourism are not permitted." + ] + }, + "tv": { + "directoryCode": "tv", + "title": "/tv/ - Television & Film", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "Content should pertain to television shows, movies, actors/actresses, film equipment, etc.", + "All actress and actor discussion should pertain directly to their roles and careers. Off-topic discussion will be deleted.", + "All images of actresses should be accompanied by relevant discussion.", + "Please make use of the spoiler image checkbox and spoiler tags. Purposeful spoiling of a series or movie is not allowed." + ] + }, + "v": { + "directoryCode": "v", + "title": "/v/ - Video Games", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All posts should pertain to video games, their consoles, and video game culture. Threads should remain on topic and stay in theme with the board. Don't post off-topic garbage.", + "Flaming/flagrant \"fanboyism\"/etc. will not be tolerated.", + "Threads should not devolve into flamewars. Instigating or encouraging such activity will not be tolerated.", + "Please refrain from reposting. If possible, try to skim the board for threads pertaining to your topics/info that may have already been posted.", + "Purposeful spoiling of a game's plot or other details may result in post deletion and temporary ban. Use spoiler tags where appropriate.", + "\"Generals\"—long-term, recurring threads about a specific game—should be posted in >>>/vg/. This is not to say threads about specific games cannot be created on /v/, just that long-term, recurring threads belong in /vg/." + ] + }, + "vg": { + "directoryCode": "vg", + "title": "/vg/ - Video Game Generals", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 20 + }, + "rules": [ + "This board is for the posting of video game \"general\" threads, which are long-term, recurring threads about a specific topic.", + "Western visual novels should be posted here, and Japanese visual novels on /jp/. Translated visual novels are allowed on either board.", + "Purposeful spoiling of a game's plot or other details may result in post deletion and temporary ban. Use spoiler tags where appropriate." + ] + }, + "vip": { + "directoryCode": "vip", + "title": "/vip/ - Very Important Posts", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Only [5chan pass](/pass) and/or [BSO](https://bso.network) holders shall post in here." + ] + }, + "vm": { + "directoryCode": "vm", + "title": "/vm/ - Video Games/Multiplayer", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All topics should pertain to multiplayer games, i.e., games that are played with more than one person, either online or locally, competitive or co-op, on console, handheld, or PC. If a game includes a single player component, discussion should be primarily based around the multiplayer component.", + "Flagrant fanboy threads are not allowed, nor are threads for the purpose of instigating a flamewar. All mobile platforms are welcome on this board; device wars are not.", + "Purposeful spoiling of a game's plot or other details may result in post deletion and temporary ban. Use spoiler tags where appropriate." + ] + }, + "vmg": { + "directoryCode": "vmg", + "title": "/vmg/ - Video Games/Mobile", + "features": { + "noSpoilers": false, + "noSpoilerReplies": false, + "safeForWork": true, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "pseudonymityMode": "per-reply", + "hasFlags": false, + "bumpLimit": 300, + "postsPerPage": 15 + }, + "rules": [ + "All topics should pertain to mobile games, i.e., games that are played on Android, iOS, or other phones or tablets. All mobile platforms are welcome. Threads about console or PC video games should not be posted here.", + "Flagrant fanboy threads are not allowed, nor are threads for the purpose of instigating a flamewar. All mobile platforms are welcome on this board; device wars are not.", + "Purposeful spoiling of a game's plot or other details may result in post deletion and temporary ban. Use spoiler tags where appropriate." + ] + }, + "vp": { + "directoryCode": "vp", + "title": "/vp/ - Pokémon", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "For Pokémon related discussion.", + "No non-work safe or \"furry\" material should be posted on this board.", + "Post all Pokégirl hentai to /h/.", + "Do not incite genwars. Keep discussions civil and don't let them devolve into flaming.", + "GOTTA CATCH 'EM ALL. This will be severely punished and strictly enforced.", + "Purposeful spoiling of a game's plot or other details may result in post deletion and temporary ban. Use spoiler tags where appropriate." + ] + }, + "vr": { + "directoryCode": "vr", + "title": "/vr/ - Retro Games", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "This board is for the discussion of classic, or \"retro\" games, including consoles, computer games, arcade games (including pinball) and any other forms of video games. Retro gaming means platforms launched in 2001 and earlier, and official game titles for those platforms released no later than December 2007 (homebrew console games made after this date will be permitted).", + "Please do not instigate or participate in flamewars. Personal attacks will not be tolerated." + ] + }, + "vrpg": { + "directoryCode": "vrpg", + "title": "/vrpg/ - Video Games/RPG", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All topics should pertain to role-playing video games, including both western and JRPG styled RPGs, turn-based and action RPGs, and MMOs. All platforms are welcome. Threads about video games that do not belong to the role-playing genre should not be posted here.", + "Flagrant fanboy threads are not allowed, nor are threads for the purpose of instigating a flamewar. All platforms and consoles are welcome on this board; console wars are not.", + "Purposeful spoiling of a game's plot or other details may result in post deletion and temporary ban. Use spoiler tags where appropriate." + ] + }, + "vst": { + "directoryCode": "vst", + "title": "/vst/ - Video Games/Strategy", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All topics should pertain to strategy video games, including both turn-based and real-time strategy games, turn-based tactics and real-time tactics games, single-player or multiplayer (including MOBAs). All platforms are welcome. Threads about video games that do not belong to the strategy genre should not be posted here.", + "Flagrant fanboy threads are not allowed, nor are threads for the purpose of instigating a flamewar. All platforms and consoles are welcome on this board; console wars are not.", + "Purposeful spoiling of a game's plot or other details may result in post deletion and temporary ban. Use spoiler tags where appropriate." + ] + }, + "vt": { + "directoryCode": "vt", + "title": "/vt/ - Virtual YouTubers", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": false, + "noSpoilerReplies": false, + "postsPerPage": 15 + }, + "rules": [ + "All images and discussion should pertain to virtual YouTubers (\"VTubers\").", + "All VTuber discussion should pertain directly to their streams and content. Off-topic and 'IRL' discussion will be deleted.", + "Singling out individual VTubers for the purposes of trolling them will not be tolerated. Do not stalk or harass any VTubers." + ] + }, + "w": { + "directoryCode": "w", + "title": "/w/ - Anime/Wallpapers", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "Wallpapers should be distinctly anime or J-pop related. They can be real world pictures, just make sure they're noticeably tied to Japanese culture.", + "Image quality is a must. A minimum resolution of 480x600 pixels is enforced on this board." + ] + }, + "wg": { + "directoryCode": "wg", + "title": "/wg/ - Wallpapers/General", + "features": { + "noSpoilers": true, + "noSpoilerReplies": true, + "safeForWork": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "pseudonymityMode": "per-reply", + "hasFlags": false, + "bumpLimit": 300, + "postsPerPage": 15 + }, + "rules": [ + "Wallpapers may depict anything within reason. Artistic nudes are allowed, but please do not post hardcore or other pornographic content.", + "Image quality is a must. A minimum resolution of 480x600 pixels is enforced on this board." + ] + }, + "wsg": { + "directoryCode": "wsg", + "title": "/wsg/ - Worksafe GIF", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "All worksafe GIFs welcome.", + "All GIFs should be animated-no static images." + ] + }, + "wsr": { + "directoryCode": "wsr", + "title": "/wsr/ - Worksafe Requests", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "This board is for asking for help in finding an image or the source of an image, or more of a certain kind of image.", + "This board is for asking for photoshop requests.", + "This board is for asking for recommendations for a new anime or TV series to watch, or a new manga or comic series to read.", + "This board is for asking for tech support or help with your homework.", + "This board is for any other kind of work-safe request.", + "Once you have posted your request, please check the catalog for requests that you can help others with.", + "All threads and images should pertain to work-safe material. For adult content please use /r/ - Requests and for help with personal matters please use /adv/ - Advice.", + "Do not post or request personal information (\"dox\") or calls to invasion (\"raids\").", + "Personal army requests of any form are strictly forbidden." + ] + }, + "x": { + "directoryCode": "x", + "title": "/x/ - Paranormal", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "For all your creepy images and stories." + ] + }, + "xs": { + "directoryCode": "xs", + "title": "/xs/ - Extreme Sports", + "features": { + "pseudonymityMode": "per-reply", + "safeForWork": true, + "hasFlags": false, + "requirePostLink": true, + "requirePostLinkIsMedia": true, + "bumpLimit": 300, + "noSpoilers": true, + "noSpoilerReplies": true, + "postsPerPage": 15 + }, + "rules": [ + "This board is for the discussion of extreme sports and similar physical activities. This includes topics such as skydiving, surfing, skateboarding, climbing, bungee-jumping, parkour, BMX, airsoft, paintball, etc.", + "Discussions about professional sports teams, leagues, and athletes should be posted to /sp/. Discussions about eSports should be posted to /vg/. Discussions about professional wrestling should be posted to /pw/." + ] + } + } +} diff --git a/src/data/5chan-directories/5chan-diy-directory.json b/src/data/5chan-directories/5chan-diy-directory.json new file mode 100644 index 00000000..16f72346 --- /dev/null +++ b/src/data/5chan-directories/5chan-diy-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /diy/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-diy-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "do-it-yourself.bso", + "publicKey": "12D3KooWKKbiaFu7x2EwueN6wxw3DJRQsvCv8KVRGbTTYpsGZXVa", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-f-directory.json b/src/data/5chan-directories/5chan-f-directory.json new file mode 100644 index 00000000..d5916101 --- /dev/null +++ b/src/data/5chan-directories/5chan-f-directory.json @@ -0,0 +1,16 @@ +{ + "description": "Boards competing to host the /f/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-f-directory.json", + "createdAt": 1780123897, + "updatedAt": 1780123897, + "rules": [ + "The tagging of uploaded files is mandatory. Improperly tagged items may be removed without notice. Abuse of the tagging system may result in temporary ban." + ], + "boards": [ + { + "address": "flash-posting.bso", + "publicKey": "12D3KooWPFckNTD8YHVJrjpa9hRYvuomvM9VsvQQkJqjWRtpLv1F", + "addedAt": 1780123897, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-fa-directory.json b/src/data/5chan-directories/5chan-fa-directory.json new file mode 100644 index 00000000..3f301214 --- /dev/null +++ b/src/data/5chan-directories/5chan-fa-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /fa/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-fa-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "fashion-posting.bso", + "publicKey": "12D3KooWMLd7K4MKPY4YJrpwV2vrjFN357x93bHs2g4gSetTLJ6t", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-fit-directory.json b/src/data/5chan-directories/5chan-fit-directory.json new file mode 100644 index 00000000..5f911ef1 --- /dev/null +++ b/src/data/5chan-directories/5chan-fit-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /fit/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-fit-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "fitness-posting.bso", + "publicKey": "12D3KooWHcqcJJmjAiBJA3uyhSW6ep4HFadUfteDrzU1Pn9ALkkw", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-g-directory.json b/src/data/5chan-directories/5chan-g-directory.json new file mode 100644 index 00000000..0d1c414c --- /dev/null +++ b/src/data/5chan-directories/5chan-g-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /g/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-g-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "technology-posting.bso", + "publicKey": "12D3KooWFnLrUYHpvqki7gbL4w9JzdxjpQPKE2JBDEd23Ly6X82X", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-gd-directory.json b/src/data/5chan-directories/5chan-gd-directory.json new file mode 100644 index 00000000..bd6656b6 --- /dev/null +++ b/src/data/5chan-directories/5chan-gd-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /gd/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-gd-directory.json", + "createdAt": 1779517639, + "updatedAt": 1779517639, + "boards": [ + { + "address": "graphic-design.bso", + "publicKey": "12D3KooWSrH1Kp6TvaLxGS3vtoqqFdy3gRMkTKfrYGPaDAcJRJAd", + "addedAt": 1779517639, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-gif-directory.json b/src/data/5chan-directories/5chan-gif-directory.json new file mode 100644 index 00000000..90ae068d --- /dev/null +++ b/src/data/5chan-directories/5chan-gif-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /gif/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-gif-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "adult-gif.bso", + "publicKey": "12D3KooWJP8v5VUzKUfxr38BbhQPb5yidF3WapGQ6CjLctBV6QC1", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-his-directory.json b/src/data/5chan-directories/5chan-his-directory.json new file mode 100644 index 00000000..2adde1df --- /dev/null +++ b/src/data/5chan-directories/5chan-his-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /his/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-his-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "history-posting.bso", + "publicKey": "12D3KooWN7bzigSgU4rxayrPCjvq2ETJMN9Zv3cyazpRYh36QWiN", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-i-directory.json b/src/data/5chan-directories/5chan-i-directory.json new file mode 100644 index 00000000..752540b5 --- /dev/null +++ b/src/data/5chan-directories/5chan-i-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /i/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-i-directory.json", + "createdAt": 1780054375, + "updatedAt": 1780054375, + "boards": [ + { + "address": "oekaki-posting.bso", + "publicKey": "12D3KooWNAqMhmHi3hPSbcH27Q7cqBCuN2Mu9HNzs6fB8YX14k4o", + "addedAt": 1780054375, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-ic-directory.json b/src/data/5chan-directories/5chan-ic-directory.json new file mode 100644 index 00000000..e538ba63 --- /dev/null +++ b/src/data/5chan-directories/5chan-ic-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /ic/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-ic-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "artwork-critique.bso", + "publicKey": "12D3KooWQZ5HkAspHUPGEVoSaAZgqachZGtxCXxAXYBDTreR4uWJ", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-int-directory.json b/src/data/5chan-directories/5chan-int-directory.json new file mode 100644 index 00000000..62e88d2e --- /dev/null +++ b/src/data/5chan-directories/5chan-int-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /int/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-int-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "international-sfw.bso", + "publicKey": "12D3KooWMSUYcergS2BwbRzFynR5hQURc962W9iR81eTJKz89Q6n", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-jp-directory.json b/src/data/5chan-directories/5chan-jp-directory.json new file mode 100644 index 00000000..dbc72d77 --- /dev/null +++ b/src/data/5chan-directories/5chan-jp-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /jp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-jp-directory.json", + "createdAt": 1779517639, + "updatedAt": 1779517639, + "boards": [ + { + "address": "otaku-culture.bso", + "publicKey": "12D3KooWFtPbTBQ6Lit5wH2PHCvQQqj3RB5v2QHPtT1cnGeKjBaW", + "addedAt": 1779517639, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-k-directory.json b/src/data/5chan-directories/5chan-k-directory.json new file mode 100644 index 00000000..3daab521 --- /dev/null +++ b/src/data/5chan-directories/5chan-k-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /k/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-k-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "weapons-posting.bso", + "publicKey": "12D3KooWNRSdq3aLHHCvm4zyFQfASgYzy5LGXRXnKC4uj6aUXzHq", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-lit-directory.json b/src/data/5chan-directories/5chan-lit-directory.json new file mode 100644 index 00000000..39d54a45 --- /dev/null +++ b/src/data/5chan-directories/5chan-lit-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /lit/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-lit-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "literature-posting.bso", + "publicKey": "12D3KooWKXimxiZWtgF3LoTdaW3btHhDzJN7P3qrsgHwRkow186x", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-m-directory.json b/src/data/5chan-directories/5chan-m-directory.json new file mode 100644 index 00000000..46a2f0c5 --- /dev/null +++ b/src/data/5chan-directories/5chan-m-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /m/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-m-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "mecha-posting.bso", + "publicKey": "12D3KooWH7TCLGzDmvptLmnoxbLK2EjSuysMfj1ScN1ekFuba1Fu", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-mlp-directory.json b/src/data/5chan-directories/5chan-mlp-directory.json new file mode 100644 index 00000000..8b609957 --- /dev/null +++ b/src/data/5chan-directories/5chan-mlp-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /mlp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-mlp-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "pony-posting.bso", + "publicKey": "12D3KooWEViZKRnJjqKDXGadQtz1547wgLTFPAaFYPBZ5NVwJ3jJ", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-mu-directory.json b/src/data/5chan-directories/5chan-mu-directory.json new file mode 100644 index 00000000..ca1b6ff9 --- /dev/null +++ b/src/data/5chan-directories/5chan-mu-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /mu/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-mu-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "music-posting.bso", + "publicKey": "12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-n-directory.json b/src/data/5chan-directories/5chan-n-directory.json new file mode 100644 index 00000000..c0fd887f --- /dev/null +++ b/src/data/5chan-directories/5chan-n-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /n/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-n-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "transportation-posting.bso", + "publicKey": "12D3KooWBLfdUT4sbVjzsQBz6EFPGWJC5byjnG44i5HHBKk7XKc3", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-news-directory.json b/src/data/5chan-directories/5chan-news-directory.json new file mode 100644 index 00000000..9b901112 --- /dev/null +++ b/src/data/5chan-directories/5chan-news-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /news/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-news-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "current-news.bso", + "publicKey": "12D3KooWSdDpdxUajbj19SHhUxcNS7wG9QbQqYvj29mCwokqnFNS", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-o-directory.json b/src/data/5chan-directories/5chan-o-directory.json new file mode 100644 index 00000000..96bbdc7e --- /dev/null +++ b/src/data/5chan-directories/5chan-o-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /o/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-o-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "auto-posting.bso", + "publicKey": "12D3KooWBD4agW9pc73hBNXtXWTpZCukcFvigmkM8Z2MDKR72CCk", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-out-directory.json b/src/data/5chan-directories/5chan-out-directory.json new file mode 100644 index 00000000..e106b1ea --- /dev/null +++ b/src/data/5chan-directories/5chan-out-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /out/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-out-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "outdoors-posting.bso", + "publicKey": "12D3KooWQ2nPGxQxuSMY92kM3zKu1rBekJsXxXb5uNkHLDfSSxTm", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-p-directory.json b/src/data/5chan-directories/5chan-p-directory.json new file mode 100644 index 00000000..f8b1a11e --- /dev/null +++ b/src/data/5chan-directories/5chan-p-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /p/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-p-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "photography-posting.bso", + "publicKey": "12D3KooWK7qqtH32qAeZ5nUZGHBEsiaGsXGhMU9AWDKYzKCcit7w", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-po-directory.json b/src/data/5chan-directories/5chan-po-directory.json new file mode 100644 index 00000000..b485934c --- /dev/null +++ b/src/data/5chan-directories/5chan-po-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /po/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-po-directory.json", + "createdAt": 1779517639, + "updatedAt": 1779517639, + "boards": [ + { + "address": "papercraft-and-origami.bso", + "publicKey": "12D3KooWGfhD9o9T4QRYX2RQgwXRmEmWXmaw4bAGyDHioz98g3ro", + "addedAt": 1779517639, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-pol-directory.json b/src/data/5chan-directories/5chan-pol-directory.json new file mode 100644 index 00000000..393e422b --- /dev/null +++ b/src/data/5chan-directories/5chan-pol-directory.json @@ -0,0 +1,19 @@ +{ + "description": "Boards competing to host the /pol/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-pol-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779212075, + "boards": [ + { + "address": "politically-incorrect.bso", + "publicKey": "12D3KooWMVob74DQoTLGZ4B8kWgPwDfbtiWqdJE4DrGtf2rmVw36", + "addedAt": 1779182014, + "owner": "rinse12.bso" + }, + { + "address": "nothing-is-beyond-our-reach.bso", + "publicKey": "12D3KooWDpiDmJCGKbXB3RJFfATmRMzmwTkjimBvf8xKZmUx2L1P", + "addedAt": 1779212075, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-pw-directory.json b/src/data/5chan-directories/5chan-pw-directory.json new file mode 100644 index 00000000..1438652b --- /dev/null +++ b/src/data/5chan-directories/5chan-pw-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /pw/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-pw-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "professional-wrestling.bso", + "publicKey": "12D3KooWBMHPHSJhwKivg6KhbGScgau8xBBNqVQKZk24NZQo7beU", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-qst-directory.json b/src/data/5chan-directories/5chan-qst-directory.json new file mode 100644 index 00000000..3edf03e4 --- /dev/null +++ b/src/data/5chan-directories/5chan-qst-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /qst/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-qst-directory.json", + "createdAt": 1779517639, + "updatedAt": 1779517639, + "boards": [ + { + "address": "quests-posting.bso", + "publicKey": "12D3KooWBmh43DUBbqXAeJVxxu9qgWgMqn91UBECgP4aTuxS5uK2", + "addedAt": 1779517639, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-r9k-directory.json b/src/data/5chan-directories/5chan-r9k-directory.json new file mode 100644 index 00000000..f2f5b0a6 --- /dev/null +++ b/src/data/5chan-directories/5chan-r9k-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /r9k/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-r9k-directory.json", + "createdAt": 1779523735, + "updatedAt": 1779525738, + "boards": [ + { + "address": "robot9002.bso", + "publicKey": "12D3KooWPXkEw8GSZcM8N97fkWJjZGV7hLccZSrf2PqHUKrp4xJQ", + "addedAt": 1779525738, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-s5s-directory.json b/src/data/5chan-directories/5chan-s5s-directory.json new file mode 100644 index 00000000..6cc1e389 --- /dev/null +++ b/src/data/5chan-directories/5chan-s5s-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /s5s/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-s5s-directory.json", + "createdAt": 1779517639, + "updatedAt": 1779517639, + "boards": [ + { + "address": "shit-5chan-says.bso", + "publicKey": "12D3KooWR4vHjSsTHs3MU436avk4eW1XRqY9zKfVcvvZXkLdzJo6", + "addedAt": 1779517639, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-sci-directory.json b/src/data/5chan-directories/5chan-sci-directory.json new file mode 100644 index 00000000..6671b0dc --- /dev/null +++ b/src/data/5chan-directories/5chan-sci-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /sci/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-sci-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "science-and-math.bso", + "publicKey": "12D3KooWEtHqwtEpgHsXS8VSFcGDt2DhzeqaLm9tTCRFucXaWp62", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-soc-directory.json b/src/data/5chan-directories/5chan-soc-directory.json new file mode 100644 index 00000000..575d6392 --- /dev/null +++ b/src/data/5chan-directories/5chan-soc-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /soc/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-soc-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "cams-and-meetups.bso", + "publicKey": "12D3KooWCGnYfbrm8UZpv8YxViXgJJx9hha486LCAFFXSEYNtj1B", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-sp-directory.json b/src/data/5chan-directories/5chan-sp-directory.json new file mode 100644 index 00000000..6229977b --- /dev/null +++ b/src/data/5chan-directories/5chan-sp-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /sp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-sp-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "sports-posting.bso", + "publicKey": "12D3KooWGJA6zN3Q63FtSgwNhtfA26Skdzdxz5X7A9PFfE4FBMGE", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-t-directory.json b/src/data/5chan-directories/5chan-t-directory.json new file mode 100644 index 00000000..5438b0de --- /dev/null +++ b/src/data/5chan-directories/5chan-t-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /t/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-t-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "torrents-posting.bso", + "publicKey": "12D3KooWFwieYtPDEd3UBK5qG6HFZ3Qu8cDd4B4RcY2HJ9CBkfaZ", + "addedAt": 1779182014, + "owner": "rinse12.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-tg-directory.json b/src/data/5chan-directories/5chan-tg-directory.json new file mode 100644 index 00000000..eb2f6d63 --- /dev/null +++ b/src/data/5chan-directories/5chan-tg-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /tg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-tg-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "traditional-games.bso", + "publicKey": "12D3KooWK6G3EHUCkfcgX6kPDihXJAdpucrZvn4WjPwf7fF484nt", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-toy-directory.json b/src/data/5chan-directories/5chan-toy-directory.json new file mode 100644 index 00000000..d101e679 --- /dev/null +++ b/src/data/5chan-directories/5chan-toy-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /toy/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-toy-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "toys-posting.bso", + "publicKey": "12D3KooWQrRW8JHu1HagE9E3QUB7hAnSbeYHLkz5mTbSivhkoPtJ", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-trv-directory.json b/src/data/5chan-directories/5chan-trv-directory.json new file mode 100644 index 00000000..222ef064 --- /dev/null +++ b/src/data/5chan-directories/5chan-trv-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /trv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-trv-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "travel-posting.bso", + "publicKey": "12D3KooWP9kG9qYxHoyxfDSWiNpeuGfuxwbDdfu6vtRwvVY9uU96", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-tv-directory.json b/src/data/5chan-directories/5chan-tv-directory.json new file mode 100644 index 00000000..02c75ff5 --- /dev/null +++ b/src/data/5chan-directories/5chan-tv-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /tv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-tv-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "television-and-film.bso", + "publicKey": "12D3KooWGSRg4crzV5vXEya1ghXD7UBTqimqXTQvNUqy9NdzZNyd", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-v-directory.json b/src/data/5chan-directories/5chan-v-directory.json new file mode 100644 index 00000000..25f4cf53 --- /dev/null +++ b/src/data/5chan-directories/5chan-v-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /v/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-v-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "videogames-posting.bso", + "publicKey": "12D3KooWRAbo3HU5ThCAkJGQ5q7MTEJfhAX6Hu7xHyPtxHGi1Fgh", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vg-directory.json b/src/data/5chan-directories/5chan-vg-directory.json new file mode 100644 index 00000000..6e63d9e1 --- /dev/null +++ b/src/data/5chan-directories/5chan-vg-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vg-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "videogame-generals.bso", + "publicKey": "12D3KooWMvAcJFV45CS3hSZcx7EFhwhQrXfXhVHXQJFPfKeakWfq", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vip-directory.json b/src/data/5chan-directories/5chan-vip-directory.json new file mode 100644 index 00000000..16f8cd2a --- /dev/null +++ b/src/data/5chan-directories/5chan-vip-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vip/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vip-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "very-important-posts.bso", + "publicKey": "12D3KooWNMnBJ5phkBWrcjfdnzdqPwCARmXwGWeZDKNJvCVPR8GT", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vm-directory.json b/src/data/5chan-directories/5chan-vm-directory.json new file mode 100644 index 00000000..380f658d --- /dev/null +++ b/src/data/5chan-directories/5chan-vm-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vm/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vm-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "videogames-multiplayer.bso", + "publicKey": "12D3KooWMW3B4R3C7z5M6zDo9Q9t3fCMuUwHbnPWARsDSqiUYybU", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vmg-directory.json b/src/data/5chan-directories/5chan-vmg-directory.json new file mode 100644 index 00000000..c5810f45 --- /dev/null +++ b/src/data/5chan-directories/5chan-vmg-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vmg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vmg-directory.json", + "createdAt": 1779517639, + "updatedAt": 1779517639, + "boards": [ + { + "address": "videogames-mobile.bso", + "publicKey": "12D3KooWLSaxYpQb4Wx7KxiJaXQD9SgzmBD67W41CN6gNLTxASVg", + "addedAt": 1779517639, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vp-directory.json b/src/data/5chan-directories/5chan-vp-directory.json new file mode 100644 index 00000000..7e5a8272 --- /dev/null +++ b/src/data/5chan-directories/5chan-vp-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vp-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "pokemon-posting.bso", + "publicKey": "12D3KooWFhrazFTHLh6sSA9qL2wTf4piBar2HXtirKNTfTwSTjN3", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vr-directory.json b/src/data/5chan-directories/5chan-vr-directory.json new file mode 100644 index 00000000..20431528 --- /dev/null +++ b/src/data/5chan-directories/5chan-vr-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vr/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vr-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "retro-games.bso", + "publicKey": "12D3KooWJfwp2WRwU3gsaNNPjZ1iHgxc1QFySr361cQarkhLp1fY", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vrpg-directory.json b/src/data/5chan-directories/5chan-vrpg-directory.json new file mode 100644 index 00000000..22e9bb84 --- /dev/null +++ b/src/data/5chan-directories/5chan-vrpg-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vrpg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vrpg-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "videogames-rpg.bso", + "publicKey": "12D3KooWJ4HZgaL3tPSRp5LfnFmwasxdQenPuDpPBYHHEaUMJDiF", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vst-directory.json b/src/data/5chan-directories/5chan-vst-directory.json new file mode 100644 index 00000000..ea874ef6 --- /dev/null +++ b/src/data/5chan-directories/5chan-vst-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vst/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vst-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "videogames-strategy.bso", + "publicKey": "12D3KooWBNAetYuTx5Zosf6jRVJ2GpMx3jzEQufDcP4DRTXzAgwp", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-vt-directory.json b/src/data/5chan-directories/5chan-vt-directory.json new file mode 100644 index 00000000..b6bda82a --- /dev/null +++ b/src/data/5chan-directories/5chan-vt-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /vt/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vt-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "virtual-youtubers.bso", + "publicKey": "12D3KooWKiziqgcvVLMDThXQC4cPi5SJmFSQFoc6jv1kSRU1XXxV", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-w-directory.json b/src/data/5chan-directories/5chan-w-directory.json new file mode 100644 index 00000000..1e5c357a --- /dev/null +++ b/src/data/5chan-directories/5chan-w-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /w/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-w-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "anime-wallpapers.bso", + "publicKey": "12D3KooWPpdMSMLnz5qZ6NR3jTjqmqdpbMT3tDM5RwzWSPYtd9AV", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-wg-directory.json b/src/data/5chan-directories/5chan-wg-directory.json new file mode 100644 index 00000000..2123b6a1 --- /dev/null +++ b/src/data/5chan-directories/5chan-wg-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /wg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-wg-directory.json", + "createdAt": 1779517639, + "updatedAt": 1779517639, + "boards": [ + { + "address": "wallpapers-general.bso", + "publicKey": "12D3KooWJCXw8a5CKgH5248qXMDQdG5AFxP4ZvbsAmFCGfuykE3U", + "addedAt": 1779517639, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-wsg-directory.json b/src/data/5chan-directories/5chan-wsg-directory.json new file mode 100644 index 00000000..100b7e14 --- /dev/null +++ b/src/data/5chan-directories/5chan-wsg-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /wsg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-wsg-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "worksafe-gif.bso", + "publicKey": "12D3KooWAGHV9th2FN48KPvHMJwxen8h5mSM39pkaZWvvzcWMPr9", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-wsr-directory.json b/src/data/5chan-directories/5chan-wsr-directory.json new file mode 100644 index 00000000..1b0b0d18 --- /dev/null +++ b/src/data/5chan-directories/5chan-wsr-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /wsr/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-wsr-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "worksafe-requests.bso", + "publicKey": "12D3KooWKuKGqAAKQT7GY1cnXqiXcbeVPJULXPwTByfSy4Ci7eBg", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-x-directory.json b/src/data/5chan-directories/5chan-x-directory.json new file mode 100644 index 00000000..676eb885 --- /dev/null +++ b/src/data/5chan-directories/5chan-x-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /x/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-x-directory.json", + "createdAt": 1779182014, + "updatedAt": 1779182014, + "boards": [ + { + "address": "paranormal-posting.bso", + "publicKey": "12D3KooWFZn7r5gYKbq5YQMr1FYQ5eUwoZr41Tjd8KmTgQ4dxTTP", + "addedAt": 1779182014, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directories/5chan-xs-directory.json b/src/data/5chan-directories/5chan-xs-directory.json new file mode 100644 index 00000000..95bd76a7 --- /dev/null +++ b/src/data/5chan-directories/5chan-xs-directory.json @@ -0,0 +1,13 @@ +{ + "description": "Boards competing to host the /xs/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-xs-directory.json", + "createdAt": 1779357555, + "updatedAt": 1779357555, + "boards": [ + { + "address": "extreme-sports.bso", + "publicKey": "12D3KooWBwnXwtridc4b3VNnhK2KyNHsm52wDrGPYYGLNf8f34bm", + "addedAt": 1779357555, + "owner": "plebeius.bso" + } + ] +} diff --git a/src/data/5chan-directory-lists.json b/src/data/5chan-directory-lists.json deleted file mode 100644 index ad2c0ac9..00000000 --- a/src/data/5chan-directory-lists.json +++ /dev/null @@ -1,857 +0,0 @@ -{ - "title": "5chan directories", - "description": "Directory assignments built from per-directory candidate lists in https://github.com/bitsocialnet/lists/tree/master/5chan-directories", - "createdAt": 1779182014, - "updatedAt": 1780123897, - "directories": [ - { - "directoryCode": "a", - "title": "/a/ - Anime & Manga", - "description": "Boards competing to host the /a/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-a-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": false, - "noSpoilerReplies": false, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "anime-and-manga.bso", - "publicKey": "12D3KooWMDt3RMVMHrm2pEEyfraBtHP86dmkim7EVAapDj3Mchdy", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "f", - "title": "/f/ - Flash", - "description": "Boards competing to host the /f/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-f-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": false, - "hasFlags": false, - "postFlairs": true, - "requirePostFlairs": true, - "requirePostLink": true, - "requirePostLinkIsMedia": false, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 50 - }, - "rules": [ - "The tagging of uploaded files is mandatory. Improperly tagged items may be removed without notice. Abuse of the tagging system may result in temporary ban." - ], - "createdAt": 1780123897, - "updatedAt": 1780123897, - "boards": [ - { - "address": "flash-posting.bso", - "publicKey": "12D3KooWPFckNTD8YHVJrjpa9hRYvuomvM9VsvQQkJqjWRtpLv1F", - "owner": "plebeius.bso", - "addedAt": 1780123897 - } - ] - }, - { - "directoryCode": "co", - "title": "/co/ - Comics & Cartoons", - "description": "Boards competing to host the /co/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-co-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": false, - "noSpoilerReplies": false, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "comics-and-cartoons.bso", - "publicKey": "12D3KooWRLmyg671KVYb1xekZ8eTQSMfRraB3DmQZPsKbCdMsEuc", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "ck", - "title": "/ck/ - Food & Cooking", - "description": "Boards competing to host the /ck/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-ck-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "food-and-cooking.bso", - "publicKey": "12D3KooWFfANpSeemwvqqGsXNPHDuyDMCK6wE8cxGTuNw7xoWUp7", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "pol", - "title": "/pol/ - Politically Incorrect", - "description": "Boards competing to host the /pol/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-pol-directory.json", - "features": { - "pseudonymityMode": "per-post", - "safeForWork": false, - "hasFlags": true, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 20 - }, - "createdAt": 1779182014, - "updatedAt": 1779212075, - "boards": [ - { - "address": "politically-incorrect.bso", - "publicKey": "12D3KooWMVob74DQoTLGZ4B8kWgPwDfbtiWqdJE4DrGtf2rmVw36", - "owner": "rinse12.bso", - "addedAt": 1779182014 - }, - { - "address": "nothing-is-beyond-our-reach.bso", - "publicKey": "12D3KooWDpiDmJCGKbXB3RJFfATmRMzmwTkjimBvf8xKZmUx2L1P", - "owner": "plebeius.bso", - "addedAt": 1779212075 - } - ] - }, - { - "directoryCode": "biz", - "title": "/biz/ - Business & Finance", - "description": "Boards competing to host the /biz/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-biz-directory.json", - "features": { - "pseudonymityMode": "per-post", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 20 - }, - "createdAt": 1779182014, - "updatedAt": 1779212075, - "boards": [ - { - "address": "business-and-finance.bso", - "publicKey": "12D3KooWNMybS8JqELi38ZBX897PrjWbCrGoMKfw3bgoqzC2n1Dh", - "owner": "rinse12.bso", - "addedAt": 1779182014 - }, - { - "address": "bizraelis.bso", - "publicKey": "12D3KooWR7nTdKZqZ1twGWMfVsXYDGp1XAKUrnYznKP651jFrizE", - "owner": "plebeius.bso", - "addedAt": 1779212075 - } - ] - }, - { - "directoryCode": "sci", - "title": "/sci/ - Science & Math", - "description": "Boards competing to host the /sci/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-sci-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "science-and-math.bso", - "publicKey": "12D3KooWEtHqwtEpgHsXS8VSFcGDt2DhzeqaLm9tTCRFucXaWp62", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "g", - "title": "/g/ - Technology", - "description": "Boards competing to host the /g/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-g-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "technology-posting.bso", - "publicKey": "12D3KooWFnLrUYHpvqki7gbL4w9JzdxjpQPKE2JBDEd23Ly6X82X", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "v", - "title": "/v/ - Video Games", - "description": "Boards competing to host the /v/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-v-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": false, - "noSpoilerReplies": false, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "videogames-posting.bso", - "publicKey": "12D3KooWRAbo3HU5ThCAkJGQ5q7MTEJfhAX6Hu7xHyPtxHGi1Fgh", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "vg", - "title": "/vg/ - Video Game Generals", - "description": "Boards competing to host the /vg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vg-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": false, - "noSpoilerReplies": false, - "postsPerPage": 20 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "videogame-generals.bso", - "publicKey": "12D3KooWMvAcJFV45CS3hSZcx7EFhwhQrXfXhVHXQJFPfKeakWfq", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "vr", - "title": "/vr/ - Retro Games", - "description": "Boards competing to host the /vr/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vr-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": false, - "noSpoilerReplies": false, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "retro-games.bso", - "publicKey": "12D3KooWJfwp2WRwU3gsaNNPjZ1iHgxc1QFySr361cQarkhLp1fY", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "fit", - "title": "/fit/ - Fitness", - "description": "Boards competing to host the /fit/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-fit-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "fitness-posting.bso", - "publicKey": "12D3KooWHcqcJJmjAiBJA3uyhSW6ep4HFadUfteDrzU1Pn9ALkkw", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "sp", - "title": "/sp/ - Sports", - "description": "Boards competing to host the /sp/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-sp-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": true, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "sports-posting.bso", - "publicKey": "12D3KooWGJA6zN3Q63FtSgwNhtfA26Skdzdxz5X7A9PFfE4FBMGE", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "tg", - "title": "/tg/ - Traditional Games", - "description": "Boards competing to host the /tg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-tg-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": false, - "noSpoilerReplies": false, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "traditional-games.bso", - "publicKey": "12D3KooWK6G3EHUCkfcgX6kPDihXJAdpucrZvn4WjPwf7fF484nt", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "adv", - "title": "/adv/ - Advice", - "description": "Boards competing to host the /adv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-adv-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "advice-posting.bso", - "publicKey": "12D3KooWH5UFNs9yfwJVsSUhEW5bYrtF8KmzfeJzwg84qWMoFJVY", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "wsg", - "title": "/wsg/ - Worksafe GIF", - "description": "Boards competing to host the /wsg/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-wsg-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "worksafe-gif.bso", - "publicKey": "12D3KooWAGHV9th2FN48KPvHMJwxen8h5mSM39pkaZWvvzcWMPr9", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "diy", - "title": "/diy/ - Do It Yourself", - "description": "Boards competing to host the /diy/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-diy-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "do-it-yourself.bso", - "publicKey": "12D3KooWKKbiaFu7x2EwueN6wxw3DJRQsvCv8KVRGbTTYpsGZXVa", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "out", - "title": "/out/ - Outdoors", - "description": "Boards competing to host the /out/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-out-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "outdoors-posting.bso", - "publicKey": "12D3KooWQ2nPGxQxuSMY92kM3zKu1rBekJsXxXb5uNkHLDfSSxTm", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "i", - "title": "/i/ - Oekaki", - "description": "Boards competing to host the /i/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-i-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": false, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1780054375, - "updatedAt": 1780054375, - "boards": [ - { - "address": "oekaki-posting.bso", - "publicKey": "12D3KooWNAqMhmHi3hPSbcH27Q7cqBCuN2Mu9HNzs6fB8YX14k4o", - "owner": "plebeius.bso", - "addedAt": 1780054375 - } - ] - }, - { - "directoryCode": "ic", - "title": "/ic/ - Artwork/Critique", - "description": "Boards competing to host the /ic/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-ic-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "artwork-critique.bso", - "publicKey": "12D3KooWQZ5HkAspHUPGEVoSaAZgqachZGtxCXxAXYBDTreR4uWJ", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "mu", - "title": "/mu/ - Music", - "description": "Boards competing to host the /mu/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-mu-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "music-posting.bso", - "publicKey": "12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "int", - "title": "/int/ - International", - "description": "Boards competing to host the /int/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-int-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": true, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "international-sfw.bso", - "publicKey": "12D3KooWMSUYcergS2BwbRzFynR5hQURc962W9iR81eTJKz89Q6n", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "lit", - "title": "/lit/ - Literature", - "description": "Boards competing to host the /lit/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-lit-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": false, - "noSpoilerReplies": false, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "literature-posting.bso", - "publicKey": "12D3KooWKXimxiZWtgF3LoTdaW3btHhDzJN7P3qrsgHwRkow186x", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "his", - "title": "/his/ - History & Humanities", - "description": "Boards competing to host the /his/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-his-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "history-posting.bso", - "publicKey": "12D3KooWN7bzigSgU4rxayrPCjvq2ETJMN9Zv3cyazpRYh36QWiN", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "tv", - "title": "/tv/ - Television & Film", - "description": "Boards competing to host the /tv/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-tv-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": false, - "noSpoilerReplies": false, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "television-and-film.bso", - "publicKey": "12D3KooWGSRg4crzV5vXEya1ghXD7UBTqimqXTQvNUqy9NdzZNyd", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "t", - "title": "/t/ - Torrents", - "description": "Boards competing to host the /t/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-t-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": false, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "torrents-posting.bso", - "publicKey": "12D3KooWFwieYtPDEd3UBK5qG6HFZ3Qu8cDd4B4RcY2HJ9CBkfaZ", - "owner": "rinse12.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "x", - "title": "/x/ - Paranormal", - "description": "Boards competing to host the /x/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-x-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "paranormal-posting.bso", - "publicKey": "12D3KooWFZn7r5gYKbq5YQMr1FYQ5eUwoZr41Tjd8KmTgQ4dxTTP", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "vip", - "title": "/vip/ - Very Important Posts", - "description": "Boards competing to host the /vip/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-vip-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "very-important-posts.bso", - "publicKey": "12D3KooWNMnBJ5phkBWrcjfdnzdqPwCARmXwGWeZDKNJvCVPR8GT", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "gif", - "title": "/gif/ - Adult GIF", - "description": "Boards competing to host the /gif/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-gif-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": false, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "adult-gif.bso", - "publicKey": "12D3KooWJP8v5VUzKUfxr38BbhQPb5yidF3WapGQ6CjLctBV6QC1", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "bant", - "title": "/bant/ - International/Random", - "description": "Boards competing to host the /bant/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-bant-directory.json", - "features": { - "pseudonymityMode": "per-post", - "safeForWork": false, - "hasFlags": true, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "international-nsfw.bso", - "publicKey": "12D3KooWPuYtTzrq8gFnv6yM1r4egy1rd4cxLr1nsXTjfdYkfwcN", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "b", - "title": "/b/ - Random", - "description": "Boards competing to host the /b/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-b-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": false, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779182014, - "updatedAt": 1779182014, - "boards": [ - { - "address": "random-nsfw.bso", - "publicKey": "12D3KooWJkJYkuVbJQhapcVGpo8oNmsXsKmFRrFqc1SHezZW9fX8", - "owner": "plebeius.bso", - "addedAt": 1779182014 - } - ] - }, - { - "directoryCode": "an", - "title": "/an/ - Animals & Nature", - "description": "Boards competing to host the /an/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-an-directory.json", - "features": { - "pseudonymityMode": "per-reply", - "safeForWork": true, - "hasFlags": false, - "requirePostLink": true, - "requirePostLinkIsMedia": true, - "bumpLimit": 300, - "noSpoilers": true, - "noSpoilerReplies": true, - "postsPerPage": 15 - }, - "createdAt": 1779262963, - "updatedAt": 1779262963, - "boards": [ - { - "address": "animals-and-nature.bso", - "publicKey": "12D3KooWSpKszPM2c17KBgbnoRrkWPCHJosGGFg3bKnzarYhHeSc", - "owner": "plebeius.bso", - "addedAt": 1779262963 - } - ] - } - ] -} diff --git a/src/data/vendored-directory-lists.ts b/src/data/vendored-directory-lists.ts new file mode 100644 index 00000000..1316e11a --- /dev/null +++ b/src/data/vendored-directory-lists.ts @@ -0,0 +1,68 @@ +import { normalizeDirectoryDefaultsData, normalizeDirectoryList, sortDirectoryLists, type DirectoryList } from '../lib/utils/directory-list-utils'; + +/** + * Offline fallback for the directory data. + * + * `./5chan-directories/` is a byte-for-byte mirror of + * https://github.com/bitsocialnet/lists/tree/master/5chan-directories (kept fresh by + * `yarn sync:directories`). The raw per-directory files only carry candidate boards; their + * code/title/features come from the filename and the shared defaults file, exactly like the + * GitHub fetch path. This module assembles the same merged shape the app consumes so the + * directory list and rules keep working when GitHub is unreachable. + */ +const rawModules = import.meta.glob<{ default: unknown }>('./5chan-directories/*.json', { eager: true }); + +// Matched against the file's basename (not the full path) so the leading "5chan-directories/" folder +// segment cannot be greedily captured as part of the directory code. +const DIRECTORY_FILE_RE = /^5chan-(.+)-directory\.json$/; +const DEFAULTS_FILE_NAME = '5chan-directories-defaults.json'; + +// 5chan-side metadata for the assembled list (the raw mirror has none of its own). +const VENDORED_METADATA = { + title: '5chan directories', + description: 'Directory assignments built from per-directory candidate lists in https://github.com/bitsocialnet/lists/tree/master/5chan-directories', +}; + +export interface VendoredDirectoryLists { + title: string; + description: string; + createdAt: number; + updatedAt: number; + directories: DirectoryList[]; +} + +const directoryEntries: Array<{ code: string; raw: unknown }> = []; +let defaultsRaw: unknown = null; + +for (const [path, module] of Object.entries(rawModules)) { + const raw = module.default; + const fileName = path.split('/').pop() ?? ''; + if (fileName === DEFAULTS_FILE_NAME) { + defaultsRaw = raw; + continue; + } + const match = fileName.match(DIRECTORY_FILE_RE); + if (match) { + directoryEntries.push({ code: match[1], raw }); + } +} + +const defaults = normalizeDirectoryDefaultsData(defaultsRaw); + +const directories = sortDirectoryLists( + directoryEntries.map(({ code, raw }) => normalizeDirectoryList(raw, code, defaults)).filter((list): list is DirectoryList => list !== null), +); + +const timestamps = [defaults.createdAt, defaults.updatedAt, ...directories.flatMap((list) => [list.createdAt, list.updatedAt])].filter( + (value): value is number => typeof value === 'number', +); + +export const vendoredDirectoryLists: VendoredDirectoryLists = { + ...VENDORED_METADATA, + createdAt: timestamps.length > 0 ? Math.min(...timestamps) : 0, + updatedAt: timestamps.length > 0 ? Math.max(...timestamps) : 0, + directories, +}; + +// Raw defaults JSON, consumed by the directory-defaults fallback (it normalizes on its own). +export const vendoredDirectoryDefaults: unknown = defaultsRaw; diff --git a/src/hooks/__tests__/use-directories.test.tsx b/src/hooks/__tests__/use-directories.test.tsx index 2e3972eb..f165f70c 100644 --- a/src/hooks/__tests__/use-directories.test.tsx +++ b/src/hooks/__tests__/use-directories.test.tsx @@ -9,15 +9,18 @@ import { useDirectories, useDirectoriesMetadata, useDirectoriesState, + useDirectoryDefaults, useDirectoryAddresses, useDirectoryByAddress, type DirectoriesData, + type DirectoryDefaultsData, } from '../use-directories'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const act = (React as { act?: (cb: () => void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; const LOCALSTORAGE_KEY = '5chan-directories-cache'; +const LOCALSTORAGE_DEFAULTS_KEY = '5chan-directory-defaults-cache'; const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp'; type Snapshot = { @@ -25,6 +28,7 @@ type Snapshot = { state: ReturnType; addresses: ReturnType; directory: ReturnType; + directoryDefaults: DirectoryDefaultsData; metadata: ReturnType; }; @@ -47,6 +51,7 @@ const HookHarness = ({ address = 'music-posting.eth' }: { address?: string }) => const state = useDirectoriesState(); const addresses = useDirectoryAddresses(); const directory = useDirectoryByAddress(address); + const directoryDefaults = useDirectoryDefaults(); const metadata = useDirectoriesMetadata(); React.useLayoutEffect(() => { @@ -55,9 +60,10 @@ const HookHarness = ({ address = 'music-posting.eth' }: { address?: string }) => state, addresses, directory, + directoryDefaults, metadata, }; - }, [addresses, directory, directories, metadata, state]); + }, [addresses, directory, directories, directoryDefaults, metadata, state]); return null; }; @@ -273,6 +279,9 @@ describe('use-directories', () => { const persisted = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY) ?? '{}'); expect(persisted.title).toBe('5chan directories'); expect(persisted.communities.length).toBeGreaterThan(1); + + const persistedDefaults = JSON.parse(localStorage.getItem(LOCALSTORAGE_DEFAULTS_KEY) ?? '{}'); + expect(persistedDefaults.description).toBe('remote defaults'); }); it('does not refetch GitHub directories for each later hook mount after a successful refresh', async () => { @@ -342,6 +351,64 @@ describe('use-directories', () => { expect(warnSpy.mock.calls.some((call: ConsoleWarnCall) => String(call[0]).includes('Failed to fetch directory list "biz"'))).toBe(true); }); + it('does not expose refreshed defaults before the matching directory payload commits', async () => { + const pendingDefaults = createDeferred>(); + const pendingFetches = new Map>>(); + fetchMock.mockImplementation((url: unknown) => { + if (isDefaultsUrl(url)) { + return pendingDefaults.promise; + } + const code = getDirectoryCodeFromUrl(url); + const deferred = createDeferred>(); + pendingFetches.set(code, deferred); + return deferred.promise; + }); + + renderHarness(); + await flushEffects(); + + pendingDefaults.resolve(createFetchResponse(createRemoteDefaults())); + await flushEffects(); + + renderHarness('business-and-finance.bso'); + await flushEffects(); + + expect(latestSnapshot?.directoryDefaults.description).not.toBe('remote defaults'); + + pendingFetches.forEach((pendingFetch, code) => { + pendingFetch.resolve(createFetchResponse(createRemoteDirectoryList(code))); + }); + await flushEffects(8); + + expect(latestSnapshot?.directoryDefaults.description).toBe('remote defaults'); + }); + + it('hydrates cached directory defaults with cached communities when GitHub refresh fails', async () => { + const cachedDefaults = createRemoteDefaults(REMOTE_DIRECTORY_CODES, { + mu: { title: '/mu/ - Cached Remote Music' }, + }); + const cachedData: DirectoriesData = { + title: 'Cached directories', + description: 'cached description', + createdAt: 1, + updatedAt: 2, + communities: [{ address: 'music-posting.bso', title: '/mu/ - Cached Music', directoryCode: 'mu', nsfw: false }], + }; + + localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(cachedData)); + localStorage.setItem(LOCALSTORAGE_DEFAULTS_KEY, JSON.stringify(cachedDefaults)); + localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, String(Date.now())); + fetchMock.mockRejectedValueOnce(new Error('network down')); + + renderHarness('music-posting.bso'); + await flushEffects(8); + + expect(latestSnapshot?.directories.map((community) => community.address)).toEqual(['music-posting.bso']); + expect(latestSnapshot?.directoryDefaults.description).toBe('remote defaults'); + expect(latestSnapshot?.directoryDefaults.directories.mu.title).toBe('/mu/ - Cached Remote Music'); + expect(warnSpy.mock.calls.some((call: ConsoleWarnCall) => String(call[0]).includes('Failed to fetch directories'))).toBe(true); + }); + it('clears invalid recent cache entries and falls back to vendored data when GitHub refresh fails', async () => { localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify({ title: 'broken cache' })); localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, String(Date.now())); diff --git a/src/hooks/use-directories.ts b/src/hooks/use-directories.ts index 9f8e33dd..54f836f6 100644 --- a/src/hooks/use-directories.ts +++ b/src/hooks/use-directories.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import directoryListsData from '../data/5chan-directory-lists.json'; +import { vendoredDirectoryLists as directoryListsData, vendoredDirectoryDefaults as directoryDefaultsData } from '../data/vendored-directory-lists'; import { directoryListToCommunity, isRecord, @@ -15,7 +15,7 @@ import { } from '../lib/utils/directory-list-utils'; import { normalizeBoardAddress } from '../lib/utils/directory-list-lookup-utils'; -export type { DirectoriesData, DirectoryCommunity } from '../lib/utils/directory-list-utils'; +export type { DirectoriesData, DirectoryCommunity, DirectoryDefaultsData } from '../lib/utils/directory-list-utils'; export { normalizeBoardAddress }; interface DirectoriesMetadata { @@ -34,6 +34,7 @@ interface DirectoriesState { const GITHUB_URL_TEMPLATE = 'https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories/5chan-{code}-directory.json'; const GITHUB_DEFAULTS_URL = 'https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories/5chan-directories-defaults.json'; const LOCALSTORAGE_KEY = '5chan-directories-cache'; +const LOCALSTORAGE_DEFAULTS_KEY = '5chan-directory-defaults-cache'; const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp'; const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour const FETCH_RETRY_DELAY_MS = 60 * 1000; // 1 minute @@ -44,6 +45,7 @@ let cacheMetadata: DirectoriesMetadata | null = null; let inFlightGitHubFetch: Promise | null = null; let lastSuccessfulGitHubFetchAt: number | null = null; let lastGitHubFetchAttemptAt: number | null = null; +let cacheDefaults: DirectoryDefaultsData | null = null; // Exposed for deterministic unit tests around module-level cache state. export const __resetDirectoriesModuleStateForTests = () => { cacheCommunities = null; @@ -51,7 +53,9 @@ export const __resetDirectoriesModuleStateForTests = () => { inFlightGitHubFetch = null; lastSuccessfulGitHubFetchAt = null; lastGitHubFetchAttemptAt = null; + cacheDefaults = null; fallbackDirectoriesData = null; + fallbackDirectoryDefaults = null; }; const getDirectoryIdentifiers = (community: DirectoryCommunity): string[] => [ @@ -212,6 +216,13 @@ const normalizeDirectoriesData = (value: unknown): DirectoriesData | null => { }; let fallbackDirectoriesData: DirectoriesData | null = null; +let fallbackDirectoryDefaults: DirectoryDefaultsData | null = null; + +export const getFallbackDirectoryDefaults = (): DirectoryDefaultsData => { + if (fallbackDirectoryDefaults) return fallbackDirectoryDefaults; + fallbackDirectoryDefaults = normalizeDirectoryDefaultsData(directoryDefaultsData as unknown); + return fallbackDirectoryDefaults; +}; export const getFallbackDirectoriesData = (): DirectoriesData => { if (fallbackDirectoriesData) return fallbackDirectoriesData; @@ -224,6 +235,26 @@ export const getFallbackDirectoriesData = (): DirectoriesData => { return fallbackDirectoriesData; }; +const getDirectoryDefaultsFromLocalStorage = (): DirectoryDefaultsData | null => { + try { + const cached = localStorage.getItem(LOCALSTORAGE_DEFAULTS_KEY); + if (!cached) { + return null; + } + + const normalized = normalizeDirectoryDefaultsData(JSON.parse(cached)); + if (Object.keys(normalized.directories).length > 0) { + return normalized; + } + console.warn('Invalid directory defaults cache format, clearing stale cache'); + localStorage.removeItem(LOCALSTORAGE_DEFAULTS_KEY); + } catch (e) { + console.warn('Failed to read directory defaults from localStorage:', e); + localStorage.removeItem(LOCALSTORAGE_DEFAULTS_KEY); + } + return null; +}; + const getFromLocalStorage = (): DirectoriesData | null => { try { const cached = localStorage.getItem(LOCALSTORAGE_KEY); @@ -234,10 +265,12 @@ const getFromLocalStorage = (): DirectoriesData | null => { const parsed = JSON.parse(cached); const normalized = normalizeDirectoriesData(parsed); if (normalized) { + cacheDefaults ??= getDirectoryDefaultsFromLocalStorage(); return normalized; } console.warn('Invalid directories cache format, clearing stale cache'); localStorage.removeItem(LOCALSTORAGE_KEY); + localStorage.removeItem(LOCALSTORAGE_DEFAULTS_KEY); localStorage.removeItem(LOCALSTORAGE_TIMESTAMP_KEY); } } @@ -247,9 +280,12 @@ const getFromLocalStorage = (): DirectoriesData | null => { return null; }; -const saveToLocalStorage = (data: DirectoriesData) => { +const saveToLocalStorage = (data: DirectoriesData, defaults?: DirectoryDefaultsData) => { try { localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(data)); + if (defaults) { + localStorage.setItem(LOCALSTORAGE_DEFAULTS_KEY, JSON.stringify(defaults)); + } localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, Date.now().toString()); } catch (e) { console.warn('Failed to save to localStorage:', e); @@ -356,8 +392,9 @@ const fetchDirectoriesFromGitHub = async (): Promise => { throw new Error('Invalid directories payload'); } hydrateModuleCaches(data); + cacheDefaults = defaults; lastSuccessfulGitHubFetchAt = Date.now(); - saveToLocalStorage(data); + saveToLocalStorage(data, defaults); return data; }; @@ -463,6 +500,12 @@ export const useDirectories = () => { return cacheCommunities || state.communities || getFallbackDirectoriesData().communities; }; +export const useDirectoryDefaults = (): DirectoryDefaultsData => { + // Subscribe to the shared directory refresh so defaults update only when the matching directory payload commits. + useDirectories(); + return cacheDefaults ?? getFallbackDirectoryDefaults(); +}; + export const useDirectoriesState = () => { // Use vendored data as fallback to prevent theme flash on first load const [state, setState] = useState({ diff --git a/src/lib/utils/directory-list-lookup-utils.ts b/src/lib/utils/directory-list-lookup-utils.ts index 500264aa..2999b949 100644 --- a/src/lib/utils/directory-list-lookup-utils.ts +++ b/src/lib/utils/directory-list-lookup-utils.ts @@ -1,4 +1,4 @@ -import directoryListsData from '../../data/5chan-directory-lists.json'; +import { vendoredDirectoryLists as directoryListsData } from '../../data/vendored-directory-lists'; import { normalizeDirectoryList, type DirectoryList, type DirectoryListBoard } from './directory-list-utils'; const DIRECTORY_ALIAS_SUFFIXES = ['.bso', '.eth'] as const; diff --git a/src/views/rules/__tests__/rules.test.tsx b/src/views/rules/__tests__/rules.test.tsx index 8cb2e92b..70eb84f3 100644 --- a/src/views/rules/__tests__/rules.test.tsx +++ b/src/views/rules/__tests__/rules.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { createElement } from 'react'; import { createRoot, type Root } from 'react-dom/client'; +import { MemoryRouter } from 'react-router-dom'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import Rules from '../rules'; @@ -13,8 +14,15 @@ const testState = vi.hoisted(() => ({ directories: [ { address: 'anime-posting.eth', title: '/a/ - Anime & Manga' }, { address: 'random-posting.eth', title: '/b/ - Random' }, - ] as Array<{ address: string; title?: string }>, - navigateMock: vi.fn(), + { address: 'flash-posting.eth', title: '/f/ - Flash' }, + ] as Array<{ address: string; title?: string; directoryCode?: string }>, + directoryDefaults: { + directories: { + a: { directoryCode: 'a', title: '/a/ - Anime & Manga', rules: ['All anime discussion welcome.'] }, + b: { directoryCode: 'b', title: '/b/ - Random', rules: ['Be excellent to each other.'] }, + f: { directoryCode: 'f', title: '/f/ - Flash', features: { postFlairs: true }, rules: ['Tag your uploads.'] }, + }, + } as { directories: Record }> }, })); vi.mock('react-i18next', () => ({ @@ -27,7 +35,6 @@ vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, - useNavigate: () => testState.navigateMock, useParams: () => ({ boardIdentifier: testState.boardIdentifier, }), @@ -49,6 +56,7 @@ vi.mock('../../../hooks/use-directories', async () => { return { ...actual, useDirectories: () => testState.directories, + useDirectoryDefaults: () => testState.directoryDefaults, }; }); @@ -71,10 +79,31 @@ vi.mock('lodash/debounce', () => ({ let container: HTMLDivElement; let root: Root; +let scrollIntoViewMock: ReturnType; const renderRules = async () => { await act(async () => { - root.render(createElement(Rules)); + root.render(createElement(MemoryRouter, null, createElement(Rules))); + }); +}; + +// Set a controlled input's value via the native setter so React's value tracker still fires onChange. +const setInputValue = (input: HTMLInputElement, value: string) => { + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; + setter?.call(input, value); + input.dispatchEvent(new Event('input', { bubbles: true })); +}; + +const submitBoardAddress = async (address: string) => { + const input = container.querySelector('input[type="text"]') as HTMLInputElement; + expect(input).toBeTruthy(); + const form = input.closest('form') as HTMLFormElement; + + await act(async () => { + setInputValue(input, address); + }); + await act(async () => { + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); }); }; @@ -86,8 +115,18 @@ describe('Rules', () => { testState.directories = [ { address: 'anime-posting.eth', title: '/a/ - Anime & Manga' }, { address: 'random-posting.eth', title: '/b/ - Random' }, + { address: 'flash-posting.eth', title: '/f/ - Flash' }, ]; + testState.directoryDefaults = { + directories: { + a: { directoryCode: 'a', title: '/a/ - Anime & Manga', rules: ['All anime discussion welcome.'] }, + b: { directoryCode: 'b', title: '/b/ - Random', rules: ['Be excellent to each other.'] }, + f: { directoryCode: 'f', title: '/f/ - Flash', features: { postFlairs: true }, rules: ['Tag your uploads.'] }, + }, + }; window.scrollTo = vi.fn(); + scrollIntoViewMock = vi.fn(); + Element.prototype.scrollIntoView = scrollIntoViewMock as unknown as typeof Element.prototype.scrollIntoView; container = document.createElement('div'); document.body.appendChild(container); @@ -99,54 +138,140 @@ describe('Rules', () => { container.remove(); }); - it('keeps custom-address routes out of the default board select', async () => { - testState.boardIdentifier = 'custom-board.eth'; + it('renders a quick-jump nav link and a rules section for every directory', async () => { + await renderRules(); + + // Quick-jump nav links use the directory name (like 4chan's board list) and point at the per-directory route. + expect(container.querySelector('a[href="/rules/a"]')?.textContent).toBe('Anime & Manga'); + expect(container.querySelector('a[href="/rules/b"]')?.textContent).toBe('Random'); + + // One anchored rules section per directory. + expect(container.querySelector('#a')).toBeTruthy(); + expect(container.querySelector('#b')).toBeTruthy(); + expect(container.textContent).toContain('/a/ - Anime & Manga'); + expect(container.textContent).toContain('/b/ - Random'); + }); + + it('renders directory rules from the directories JSON without loading any board over P2P', async () => { + // communities (the P2P source) is empty, yet the rules still render because they come from the defaults JSON. + await renderRules(); + + expect(container.textContent).toContain('All anime discussion welcome.'); + expect(container.textContent).toContain('Be excellent to each other.'); + // The directory rules are not framed as a P2P "Rules for:" board fetch. + expect(container.textContent).not.toContain('Rules for:'); + }); + + it('insta-scrolls to a directory section when deep-linked via /rules/:code', async () => { + testState.boardIdentifier = 'a'; + + await renderRules(); + + expect(scrollIntoViewMock).toHaveBeenCalled(); + }); + + it('loads a board over P2P when an address is submitted in the loader', async () => { testState.communities = { 'custom-board.eth': { - rules: ['No custom options in the select.'], + rules: ['No spamming.'], shortAddress: 'custom-board.eth', state: 'succeeded', }, }; await renderRules(); + await submitBoardAddress('custom-board.eth'); - const select = container.querySelector('select'); - expect(select).toBeTruthy(); - expect(select?.value).toBe(''); - expect(Array.from(select?.options ?? []).map((option) => option.value)).toEqual(['', 'anime-posting.eth', 'random-posting.eth']); expect(container.textContent).toContain('Rules for: custom-board.eth'); + expect(container.textContent).toContain('No spamming.'); }); - it('keeps the canonical default board selected for known directories', async () => { - testState.boardIdentifier = 'a'; + it('clears a loaded P2P rules box when navigating to a directory route', async () => { testState.communities = { - 'anime-posting.eth': { - rules: ['Stay on topic.'], + 'custom-board.eth': { + rules: ['No spamming.'], + shortAddress: 'custom-board.eth', state: 'succeeded', }, }; await renderRules(); + await submitBoardAddress('custom-board.eth'); + expect(container.textContent).toContain('Rules for: custom-board.eth'); - const select = container.querySelector('select'); - expect(select).toBeTruthy(); - expect(select?.value).toBe('anime-posting.eth'); - expect(Array.from(select?.options ?? []).map((option) => option.value)).toEqual(['', 'anime-posting.eth', 'random-posting.eth']); - expect(container.textContent).toContain('Rules for: /a/ - Anime & Manga'); + testState.boardIdentifier = 'a'; + await renderRules(); + + expect(container.textContent).not.toContain('Rules for: custom-board.eth'); + expect(scrollIntoViewMock).toHaveBeenCalled(); }); - it('shows a friendly loading state string while board rules are downloading', async () => { - testState.boardIdentifier = 'a'; + it('shows a friendly loading state string while a board over P2P is downloading', async () => { testState.communities = { - 'anime-posting.eth': { + 'custom-board.eth': { state: 'fetching-ipns', }, }; await renderRules(); + await submitBoardAddress('custom-board.eth'); expect(container.textContent).toContain('Downloading board from peers'); expect(container.textContent).not.toContain('loading...'); }); + + it('groups directories into Image Boards and Upload Boards with an h3 per directory', async () => { + await renderRules(); + + expect(container.textContent).toContain('Image Boards'); + expect(container.textContent).toContain('Upload Boards'); + + const h3Titles = Array.from(container.querySelectorAll('h3')).map((h3) => h3.textContent); + expect(h3Titles).toContain('/a/ - Anime & Manga'); + expect(h3Titles).toContain('/b/ - Random'); + expect(h3Titles).toContain('/f/ - Flash'); + expect(container.textContent).toContain('Tag your uploads.'); + }); + + it('does not scroll bare /rules back to the top when directories refresh', async () => { + await renderRules(); + expect(window.scrollTo).toHaveBeenCalled(); + + vi.mocked(window.scrollTo).mockClear(); + testState.directories = [...testState.directories, { address: 'travel-posting.eth', title: '/trv/ - Travel', directoryCode: 'trv' }]; + testState.directoryDefaults = { + directories: { + ...testState.directoryDefaults.directories, + trv: { directoryCode: 'trv', title: '/trv/ - Travel', rules: ['Stay on topic.'] }, + }, + }; + + await renderRules(); + + expect(window.scrollTo).not.toHaveBeenCalled(); + }); + + it('toggles the loader action to Clear, which removes the loaded rules and empties the input', async () => { + testState.communities = { + 'custom-board.eth': { + rules: ['No spamming.'], + shortAddress: 'custom-board.eth', + state: 'succeeded', + }, + }; + + await renderRules(); + await submitBoardAddress('custom-board.eth'); + expect(container.textContent).toContain('Rules for: custom-board.eth'); + + const clearButton = Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Clear'); + expect(clearButton).toBeTruthy(); + + await act(async () => { + clearButton?.click(); + }); + + expect(container.textContent).not.toContain('Rules for: custom-board.eth'); + expect((container.querySelector('input[type="text"]') as HTMLInputElement).value).toBe(''); + }); }); diff --git a/src/views/rules/rules.module.css b/src/views/rules/rules.module.css index efa97d9a..dbed8e24 100644 --- a/src/views/rules/rules.module.css +++ b/src/views/rules/rules.module.css @@ -66,22 +66,6 @@ box-sizing: border-box; } -.boardSelect { - padding: 5px 10px; - font-size: 14px; - border: 1px solid #aaa; - border-radius: 0; - background: #fff; - cursor: pointer; - min-width: 200px; - box-sizing: border-box; -} - -.orSeparator { - color: #666; - font-style: italic; -} - .customAddressForm { display: flex; align-items: center; @@ -103,13 +87,103 @@ outline: none; } +/* Match homepage search "Go" button (home.module.css .searchButton). */ .goButton { - padding: 5px 10px; + padding: 3px 5px; font-size: 14px; + display: inline-block; + text-transform: capitalize; + white-space: nowrap; + flex-shrink: 0; cursor: pointer; box-sizing: border-box; } +.columns { + display: flex; + align-items: flex-start; + gap: 0.5em; +} + +.leftColumn { + flex: 0 0 29.9%; + width: 29.9%; + min-width: 0; +} + +.rightColumn { + flex: 1 1 auto; + min-width: 0; +} + +/* + * Green sidebar (4chan .left-box) — rules.2.css + global .boxcontent. + * Same 93% font, 1.5em line-height, and .5em padding as other boxes (.boxContent). + */ +.selectorBox .boxContent > .directoryNav > ul { + color: #060; +} + +.selectorBox .directoryNav ul { + margin: 1em; + padding-left: 0; + list-style: disc outside; +} + +/* 4chan: div ul { margin-left: 2em } — top list under boxcontent */ +.selectorBox .directoryNav > ul { + margin-left: 2em; +} + +/* 4chan: li ul { margin-left: 1em; margin-top: 0 } */ +.selectorBox .directoryNav li ul { + margin-left: 1em; + margin-top: 0; +} + +/* 4chan: li li ul { margin-bottom: .5em } */ +.selectorBox .directoryNav li li ul { + margin-bottom: 0.5em; +} + +.selectorBox .directoryNav li { + margin: 0; + line-height: inherit; +} + +.selectorBox .directoryNav a { + color: #00e; + text-decoration: underline; + overflow-wrap: anywhere; +} + +.selectorBox .directoryNavHeader { + appearance: none; + background: transparent; + border: 0; + padding: 0; + font: inherit; + font-weight: 700; + color: #00e; + text-decoration: underline; + cursor: pointer; +} + +.directoryTitle { + font-size: 100%; + font-weight: 700; + color: #006; + margin: 0 0 5px; +} + +.directoryDivider { + border: 0; + height: 1px; + /* Match the rules box border color (#006, the rulesBox currentColor), like 4chan's
. */ + background: #006; + margin: 0 0 1em; +} + .rulesBox { background: #eff; color: #006; @@ -120,14 +194,15 @@ color: #fff; } +/* 4chan's .right-box ol overrides only top + left of the base 1em margin (bottom/right stay 1em). */ .box ol { - margin: 1em; - margin-left: 2em; + margin: 0.5em 1em 1em 2.5em; } +/* No per-item spacing: line height drives the gap. 1.5 matches 4chan's rule list exactly. */ .box ol li { list-style: decimal outside; - margin-bottom: 0.5em; + line-height: 1.5; } @media (max-width: 640px) { @@ -142,28 +217,56 @@ align-items: stretch; } - .boardSelect, .addressInput { - width: 100%; + flex: 1 1 auto; + width: auto; min-width: 0; box-sizing: border-box; } - .orSeparator { - text-align: center; - } - + /* Keep the input and button on one row on mobile instead of stacking them. */ .customAddressForm { - flex-direction: column; width: 100%; box-sizing: border-box; } .goButton { - box-sizing: border-box; - margin-top: 5px; - padding: 5px 10px; - cursor: pointer; + flex: 0 0 auto; + } + + .columns { + flex-direction: column; + } + + .leftColumn { + flex: none; + width: 100%; + } + + /* Mobile: tighter list margins + left inset (font size stays same as desktop .boxContent) */ + .leftColumn .selectorBox .boxContent { + line-height: 130%; + padding: 0.25em 0.5em 0 10px; + } + + .leftColumn .selectorBox .directoryNav ul { + margin: 0; + padding-left: 1.2em; + list-style: disc outside; + } + + .leftColumn .selectorBox .directoryNav > ul { + margin-left: 0; + } + + .leftColumn .selectorBox .directoryNav li ul { + margin-left: 0; + margin-top: 0; + padding-left: 1em; + } + + .leftColumn .selectorBox .directoryNav li { + padding: 5px 10px 5px 0; } } diff --git a/src/views/rules/rules.tsx b/src/views/rules/rules.tsx index 206e4c4b..72d87a45 100644 --- a/src/views/rules/rules.tsx +++ b/src/views/rules/rules.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState, FormEvent } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { Fragment, useEffect, useRef, useState, FormEvent } from 'react'; +import { Link, useParams } from 'react-router-dom'; import { useCommunity } from '@bitsocial/bitsocial-react-hooks'; import { Footer, HomeLogo } from '../home'; -import { useDirectories, DirectoryCommunity, findDirectoryByAddress } from '../../hooks/use-directories'; +import { useDirectories, useDirectoryDefaults, DirectoryCommunity, DirectoryDefaultsData } from '../../hooks/use-directories'; import { useCommunityIdentifier } from '../../hooks/use-community-identifiers'; -import { getCommunityAddress, getBoardPath } from '../../lib/utils/route-utils'; +import { getCommunityAddress, getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils'; import Markdown from '../../components/markdown'; import LoadingEllipsis from '../../components/loading-ellipsis'; import useStateString from '../../hooks/use-state-string'; @@ -12,6 +12,12 @@ import styles from './rules.module.css'; import { useTranslation } from 'react-i18next'; import lowerCase from 'lodash/lowerCase'; +interface CategoryGroup { + key: string; + label: string; + communities: DirectoryCommunity[]; +} + const getBoardShortCode = (title?: string): string => { if (!title) return ''; const match = title.match(/^\/([^/]+)\//); @@ -24,31 +30,128 @@ const getBoardName = (title?: string): string => { return match ? match[1] : title; }; -const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress: string; directories: DirectoryCommunity[] }) => { +const getDirectoryCode = (community: DirectoryCommunity): string => community.directoryCode ?? getBoardShortCode(community.title); + +const getDirectoryDisplayTitle = (community: DirectoryCommunity): string => { + const shortCode = getDirectoryCode(community); + const boardName = getBoardName(community.title); + if (shortCode && boardName) { + return `/${shortCode}/ - ${boardName}`; + } + return community.title ?? community.address; +}; + +const getDirectoryRules = (defaults: DirectoryDefaultsData, code: string): string[] => (code ? (defaults.directories[code]?.rules ?? []) : []); + +// Upload boards (e.g. /f/ - Flash) require post flairs/tagging on uploads; everything else is an image board, mirroring 4chan's split. +const isUploadDirectory = (defaults: DirectoryDefaultsData, code: string): boolean => !!defaults.directories[code]?.features?.postFlairs; + +// Split the directories into ordered category groups (image boards first, then upload boards), dropping empty ones. +const groupDirectoriesByCategory = (directories: DirectoryCommunity[], defaults: DirectoryDefaultsData): CategoryGroup[] => + [ + { key: 'image', label: 'Image Boards' }, + { key: 'upload', label: 'Upload Boards' }, + ] + .map(({ key, label }) => ({ + key, + label, + communities: directories.filter((community) => (key === 'upload') === isUploadDirectory(defaults, getDirectoryCode(community))), + })) + .filter((group) => group.communities.length > 0); + +// Resolve a /rules/:boardIdentifier segment (directory code or board address) to a directory code. +const resolveDirectoryCode = (identifier: string, directories: DirectoryCommunity[]): string | null => { + if (isDirectoryRoute(identifier, directories)) { + return identifier; + } + const code = getBoardPath(getCommunityAddress(identifier, directories), directories); + return isDirectoryRoute(code, directories) ? code : null; +}; + +// A single directory's rules (h3 title + ordered rules), anchored by code for deep-link scrolling. +const DirectorySection = ({ community, rules }: { community: DirectoryCommunity; rules: string[] }) => { + const code = getDirectoryCode(community); + return ( +
+

{getDirectoryDisplayTitle(community)}

+ {rules.length > 0 ? ( +
    + {rules.map((rule, index) => ( +
  1. + +
  2. + ))} +
+ ) : ( +

+ This directory has no specific rules. +

+ )} +
+ ); +}; + +// Directory rules come straight from the directories JSON (defaults), so a whole category renders at once without a P2P fetch. +const CategoryRulesBox = ({ group, defaults }: { group: CategoryGroup; defaults: DirectoryDefaultsData }) => ( +
+
+

{group.label}

+
+
+ {group.communities.map((community, index) => ( + + + {/* Separator below each entry except the last (matches 4chan's
between board rules). */} + {index < group.communities.length - 1 &&
} +
+ ))} +
+
+); + +// Quick-jump nav (left column) grouped by category; clicking a directory insta-scrolls to its rules via /rules/:code. +const DirectoryNav = ({ groups }: { groups: CategoryGroup[] }) => ( +
+
+

Directories

+
+
+ +
+
+); + +// P2P board rules: fetched live from peers for an arbitrary board address. +const BoardRulesDisplay = ({ communityAddress }: { communityAddress: string }) => { const { t } = useTranslation(); const communityIdentifier = useCommunityIdentifier(communityAddress); const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined); - const { rules, state, title, shortAddress } = community || {}; + const { rules, state, shortAddress } = community || {}; const stateString = useStateString(community) || t('downloading_board'); const isLoaded = state === 'succeeded'; - const defaultSub = directories.find((sub) => sub.address === communityAddress); - let displayTitle: string; - if (defaultSub?.title) { - const shortCode = getBoardShortCode(defaultSub.title); - const boardName = getBoardName(defaultSub.title); - displayTitle = `Rules for: /${shortCode}/ - ${boardName}`; - } else if (title) { - const shortCode = getBoardShortCode(title); - const boardName = getBoardName(title); - if (shortCode && boardName && boardName !== title) { - displayTitle = `Rules for: /${shortCode}/ - ${boardName}`; - } else { - displayTitle = `Rules for: ${shortAddress || communityAddress}`; - } - } else { - displayTitle = `Rules for: ${shortAddress || communityAddress}`; - } + const displayTitle = `Rules for: ${shortAddress || communityAddress}`; return (
@@ -64,7 +167,7 @@ const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress
    {rules.map((rule: string, index: number) => (
  1. - +
  2. ))}
@@ -78,60 +181,33 @@ const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress ); }; -const BoardSelector = ({ - directories, - selectedAddress, - onSelect, -}: { - directories: DirectoryCommunity[]; - selectedAddress: string; - onSelect: (address: string) => void; -}) => { +// Load any board's own rules live from peers (P2P), separate from the static directory rules below. +// Once a board is loaded the action toggles to "Clear", which drops the result box and empties the input. +const LoadBoardRules = ({ onLoad, onClear, isLoaded }: { onLoad: (address: string) => void; onClear: () => void; isLoaded: boolean }) => { + const { t } = useTranslation(); const [customAddress, setCustomAddress] = useState(''); - const selectedDefaultBoard = findDirectoryByAddress(directories, selectedAddress); - const selectedBoardValue = selectedDefaultBoard?.address ?? ''; - const handleSelectChange = (e: React.ChangeEvent) => { - const value = e.target.value; - if (value) { - onSelect(value); - setCustomAddress(''); - } - }; - - const handleCustomSubmit = (e: FormEvent) => { + const handleSubmit = (e: FormEvent) => { e.preventDefault(); const trimmed = customAddress.trim(); if (trimmed) { - onSelect(trimmed); + onLoad(trimmed); } }; - const { t } = useTranslation(); + const handleClear = () => { + setCustomAddress(''); + onClear(); + }; return (
-

Load rules from a board

+

Load rules P2P from any board

- - or -
+ setCustomAddress(e.target.value)} className={styles.addressInput} /> - + {isLoaded ? ( + + ) : ( + + )}
@@ -152,21 +234,44 @@ const BoardSelector = ({ const Rules = () => { const { boardIdentifier } = useParams(); - const navigate = useNavigate(); const directories = useDirectories(); + const directoryDefaults = useDirectoryDefaults(); + const [loadedAddress, setLoadedAddress] = useState(''); + const scrolledForRef = useRef(null); - const selectedAddress = boardIdentifier ? getCommunityAddress(boardIdentifier, directories) : ''; + // Order directories alphabetically by directory code (e.g. /3/, /a/, /aco/...), like 4chan, not by title. + const directoriesWithCode = directories.filter((community) => getDirectoryCode(community)).toSorted((a, b) => getDirectoryCode(a).localeCompare(getDirectoryCode(b))); + const categoryGroups = groupDirectoriesByCategory(directoriesWithCode, directoryDefaults); - const handleBoardSelect = (address: string) => { - const path = getBoardPath(address, directories); - navigate(`/rules/${path}`, { replace: true }); + const handleLoad = (address: string) => { + setLoadedAddress(getCommunityAddress(address, directories)); }; useEffect(() => { - window.scrollTo(0, 0); document.title = 'Rules - 5chan'; }, []); + useEffect(() => { + setLoadedAddress(''); + if (!boardIdentifier) { + scrolledForRef.current = null; + window.scrollTo(0, 0); + } + }, [boardIdentifier]); + + // Deep-link: /rules/:code insta-scrolls to that directory's rules once the matching section is rendered. + useEffect(() => { + if (!boardIdentifier || scrolledForRef.current === boardIdentifier) { + return; + } + const code = resolveDirectoryCode(boardIdentifier, directories); + const element = code ? document.getElementById(code) : null; + if (element) { + element.scrollIntoView(); + scrolledForRef.current = boardIdentifier; + } + }, [boardIdentifier, directories]); + return (
@@ -177,14 +282,25 @@ const Rules = () => {
5chan does not have global rules or moderators. It is a serverless, adminless, static tool for browsing and posting to decentralized imageboards.{' '} - Each board sets its own rules independently, determined by the board owner and board admins, and enforced by the board moderators. + Each directory sets its own rules, listed below and expected of the boards that host it; individual board owners and admins may add their + own.

Please read and respect the rules of whatever board you decide to post to.
- - {selectedAddress && } + setLoadedAddress('')} isLoaded={!!loadedAddress} /> + {loadedAddress && } +
+
+ +
+
+ {categoryGroups.map((group) => ( + + ))} +
+