mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(directories): support multisub public keys
This commit is contained in:
+56
-26
@@ -2,14 +2,15 @@
|
|||||||
// Updates the vendored fallback in src/data/ so production builds ship a fresh snapshot.
|
// 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.
|
// Never fails the build — if the fetch fails (offline, rate-limited, etc.), the existing file is kept.
|
||||||
|
|
||||||
import { writeFileSync, readFileSync } from 'fs';
|
import { existsSync, writeFileSync, readFileSync } from 'fs';
|
||||||
import { join, dirname } from 'path';
|
import { isAbsolute, join, dirname, resolve } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
const GITHUB_URL = 'https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories.json';
|
const GITHUB_URL = 'https://raw.githubusercontent.com/bitsocialnet/lists/master/5chan-directories.json';
|
||||||
|
const DIRECTORIES_SOURCE_PATH = process.env.DIRECTORIES_SOURCE_PATH;
|
||||||
const OUTPUT_PATH = join(__dirname, '..', 'src', 'data', '5chan-directories.json');
|
const OUTPUT_PATH = join(__dirname, '..', 'src', 'data', '5chan-directories.json');
|
||||||
const TIMEOUT_MS = 5000;
|
const TIMEOUT_MS = 5000;
|
||||||
const DEFAULT_METADATA = {
|
const DEFAULT_METADATA = {
|
||||||
@@ -21,6 +22,8 @@ const DEFAULT_METADATA = {
|
|||||||
|
|
||||||
const isRecord = (value) => typeof value === 'object' && value !== null;
|
const isRecord = (value) => typeof value === 'object' && value !== null;
|
||||||
|
|
||||||
|
const getStringValue = (...values) => values.find((value) => typeof value === 'string' && value.length > 0);
|
||||||
|
|
||||||
const normalizeFeatures = (value) => {
|
const normalizeFeatures = (value) => {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -36,21 +39,33 @@ const normalizeFeatures = (value) => {
|
|||||||
return Object.keys(normalizedFeatures).length > 0 ? normalizedFeatures : undefined;
|
return Object.keys(normalizedFeatures).length > 0 ? normalizedFeatures : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toCanonicalCommunity = ({ address, title, nsfw, directoryCode, features }) => {
|
const deriveNsfw = (value) => {
|
||||||
if (typeof address !== 'string') {
|
const features = value.features;
|
||||||
|
const safeForWork = typeof features?.safeForWork === 'boolean' ? features.safeForWork : undefined;
|
||||||
|
if (safeForWork !== undefined) {
|
||||||
|
return !safeForWork;
|
||||||
|
}
|
||||||
|
const featuresNsfw = typeof features?.nsfw === 'boolean' ? features.nsfw : undefined;
|
||||||
|
const topLevelNsfw = typeof value.nsfw === 'boolean' ? value.nsfw : undefined;
|
||||||
|
return topLevelNsfw ?? featuresNsfw;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toCanonicalCommunity = ({ address, communityAddress, name, publicKey, title, nsfw, directoryCode, features }) => {
|
||||||
|
const normalizedName = getStringValue(name, address, communityAddress);
|
||||||
|
if (!normalizedName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedFeatures = normalizeFeatures(features);
|
const normalizedFeatures = normalizeFeatures(features);
|
||||||
const topLevelNsfw = typeof nsfw === 'boolean' ? nsfw : undefined;
|
const normalizedNsfw = deriveNsfw({ nsfw, features: normalizedFeatures });
|
||||||
const featuresNsfw = typeof normalizedFeatures?.nsfw === 'boolean' ? normalizedFeatures.nsfw : undefined;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
address,
|
name: normalizedName,
|
||||||
|
...(typeof publicKey === 'string' ? { publicKey } : {}),
|
||||||
...(typeof title === 'string' ? { title } : {}),
|
...(typeof title === 'string' ? { title } : {}),
|
||||||
...(typeof directoryCode === 'string' ? { directoryCode } : {}),
|
...(typeof directoryCode === 'string' ? { directoryCode } : {}),
|
||||||
...(normalizedFeatures ? { features: normalizedFeatures } : {}),
|
...(normalizedFeatures ? { features: normalizedFeatures } : {}),
|
||||||
...((topLevelNsfw ?? featuresNsfw) !== undefined ? { nsfw: topLevelNsfw ?? featuresNsfw } : {}),
|
...(normalizedNsfw !== undefined ? { nsfw: normalizedNsfw } : {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,10 +74,11 @@ const dedupeCommunities = (entries) => {
|
|||||||
const normalized = [];
|
const normalized = [];
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (seenAddresses.has(entry.address)) {
|
const dedupeKey = entry.publicKey || entry.name;
|
||||||
|
if (seenAddresses.has(dedupeKey)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
seenAddresses.add(entry.address);
|
seenAddresses.add(dedupeKey);
|
||||||
normalized.push(entry);
|
normalized.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +97,10 @@ const adaptV2Directories = (value) => {
|
|||||||
}
|
}
|
||||||
const features = isRecord(directory.features) ? directory.features : null;
|
const features = isRecord(directory.features) ? directory.features : null;
|
||||||
return toCanonicalCommunity({
|
return toCanonicalCommunity({
|
||||||
address: directory.communityAddress,
|
communityAddress: directory.communityAddress,
|
||||||
|
name: directory.name,
|
||||||
|
publicKey: directory.publicKey,
|
||||||
title: directory.title,
|
title: directory.title,
|
||||||
nsfw: features?.nsfw,
|
|
||||||
directoryCode: directory.directoryCode,
|
directoryCode: directory.directoryCode,
|
||||||
features,
|
features,
|
||||||
});
|
});
|
||||||
@@ -105,6 +122,8 @@ const adaptV1Communities = (value) => {
|
|||||||
}
|
}
|
||||||
return toCanonicalCommunity({
|
return toCanonicalCommunity({
|
||||||
address: community.address,
|
address: community.address,
|
||||||
|
name: community.name,
|
||||||
|
publicKey: community.publicKey,
|
||||||
title: community.title,
|
title: community.title,
|
||||||
nsfw: community.nsfw,
|
nsfw: community.nsfw,
|
||||||
directoryCode: community.directoryCode,
|
directoryCode: community.directoryCode,
|
||||||
@@ -138,6 +157,30 @@ const normalizeDirectoriesData = (value, fallbackMetadata = DEFAULT_METADATA) =>
|
|||||||
|
|
||||||
const getErrorMessage = (error) => (error instanceof Error ? error.message : String(error));
|
const getErrorMessage = (error) => (error instanceof Error ? error.message : String(error));
|
||||||
|
|
||||||
|
const loadDirectoriesSource = async () => {
|
||||||
|
if (DIRECTORIES_SOURCE_PATH) {
|
||||||
|
const resolvedSourcePath = isAbsolute(DIRECTORIES_SOURCE_PATH) ? DIRECTORIES_SOURCE_PATH : resolve(process.cwd(), DIRECTORIES_SOURCE_PATH);
|
||||||
|
console.log(`ℹ️ Syncing vendored directories from local file: ${resolvedSourcePath}`);
|
||||||
|
if (!existsSync(resolvedSourcePath)) {
|
||||||
|
throw new Error(`Local directories source not found: ${resolvedSourcePath}`);
|
||||||
|
}
|
||||||
|
return JSON.parse(readFileSync(resolvedSourcePath, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`ℹ️ Syncing vendored directories from URL: ${GITHUB_URL}`);
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const response = await fetch(GITHUB_URL, { signal: controller.signal });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const sync = async () => {
|
const sync = async () => {
|
||||||
try {
|
try {
|
||||||
let existing = '';
|
let existing = '';
|
||||||
@@ -158,20 +201,7 @@ const sync = async () => {
|
|||||||
// file doesn't exist yet or is invalid JSON
|
// file doesn't exist yet or is invalid JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const data = normalizeDirectoriesData(await loadDirectoriesSource(), fallbackMetadata);
|
||||||
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
response = await fetch(GITHUB_URL, { signal: controller.signal });
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response || !response.ok) {
|
|
||||||
throw new Error(`HTTP ${response?.status ?? 'unknown'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = normalizeDirectoriesData(await response.json(), fallbackMetadata);
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error('Invalid directories payload');
|
throw new Error('Invalid directories payload');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
"title": "/all/ - All 5chan Directories",
|
"title": "/all/ - All 5chan Directories",
|
||||||
"description": "Each 5chan directory is assigned to a board handpicked by the team, until DAO curation is implemented.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories.json",
|
"description": "Each 5chan directory is assigned to a board handpicked by the team, until DAO curation is implemented.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories.json",
|
||||||
"createdAt": 1762179811,
|
"createdAt": 1762179811,
|
||||||
"updatedAt": 1771140295,
|
"updatedAt": 1776065598,
|
||||||
"communities": [
|
"communities": [
|
||||||
{
|
{
|
||||||
"address": "anime-and-manga.bso",
|
"name": "anime-and-manga.bso",
|
||||||
|
"publicKey": "12D3KooWMDt3RMVMHrm2pEEyfraBtHP86dmkim7EVAapDj3Mchdy",
|
||||||
"title": "/a/ - Anime & Manga",
|
"title": "/a/ - Anime & Manga",
|
||||||
"directoryCode": "a",
|
"directoryCode": "a",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -21,7 +22,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "food-and-cooking.bso",
|
"name": "food-and-cooking.bso",
|
||||||
|
"publicKey": "12D3KooWFfANpSeemwvqqGsXNPHDuyDMCK6wE8cxGTuNw7xoWUp7",
|
||||||
"title": "/ck/ - Food & Cooking",
|
"title": "/ck/ - Food & Cooking",
|
||||||
"directoryCode": "ck",
|
"directoryCode": "ck",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -37,7 +39,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "politically-incorrect.bso",
|
"name": "politically-incorrect.bso",
|
||||||
|
"publicKey": "12D3KooWMVob74DQoTLGZ4B8kWgPwDfbtiWqdJE4DrGtf2rmVw36",
|
||||||
"title": "/pol/ - Politically Incorrect",
|
"title": "/pol/ - Politically Incorrect",
|
||||||
"directoryCode": "pol",
|
"directoryCode": "pol",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -53,7 +56,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "business-and-finance.bso",
|
"name": "business-and-finance.bso",
|
||||||
|
"publicKey": "12D3KooWNMybS8JqELi38ZBX897PrjWbCrGoMKfw3bgoqzC2n1Dh",
|
||||||
"title": "/biz/ - Business & Finance",
|
"title": "/biz/ - Business & Finance",
|
||||||
"directoryCode": "biz",
|
"directoryCode": "biz",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -69,7 +73,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "science-and-math.bso",
|
"name": "science-and-math.bso",
|
||||||
|
"publicKey": "12D3KooWEtHqwtEpgHsXS8VSFcGDt2DhzeqaLm9tTCRFucXaWp62",
|
||||||
"title": "/sci/ - Science & Math",
|
"title": "/sci/ - Science & Math",
|
||||||
"directoryCode": "sci",
|
"directoryCode": "sci",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -85,7 +90,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "technology-posting.bso",
|
"name": "technology-posting.bso",
|
||||||
|
"publicKey": "12D3KooWFnLrUYHpvqki7gbL4w9JzdxjpQPKE2JBDEd23Ly6X82X",
|
||||||
"title": "/g/ - Technology",
|
"title": "/g/ - Technology",
|
||||||
"directoryCode": "g",
|
"directoryCode": "g",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -101,7 +107,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "videogame-generals.bso",
|
"name": "videogame-generals.bso",
|
||||||
|
"publicKey": "12D3KooWMvAcJFV45CS3hSZcx7EFhwhQrXfXhVHXQJFPfKeakWfq",
|
||||||
"title": "/vg/ - Video Game Generals",
|
"title": "/vg/ - Video Game Generals",
|
||||||
"directoryCode": "vg",
|
"directoryCode": "vg",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -117,7 +124,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "fitness-posting.bso",
|
"name": "fitness-posting.bso",
|
||||||
|
"publicKey": "12D3KooWHcqcJJmjAiBJA3uyhSW6ep4HFadUfteDrzU1Pn9ALkkw",
|
||||||
"title": "/fit/ - Fitness",
|
"title": "/fit/ - Fitness",
|
||||||
"directoryCode": "fit",
|
"directoryCode": "fit",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -133,7 +141,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "advice-posting.bso",
|
"name": "advice-posting.bso",
|
||||||
|
"publicKey": "12D3KooWH5UFNs9yfwJVsSUhEW5bYrtF8KmzfeJzwg84qWMoFJVY",
|
||||||
"title": "/adv/ - Advice",
|
"title": "/adv/ - Advice",
|
||||||
"directoryCode": "adv",
|
"directoryCode": "adv",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -149,7 +158,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "worksafe-gif.bso",
|
"name": "worksafe-gif.bso",
|
||||||
|
"publicKey": "12D3KooWAGHV9th2FN48KPvHMJwxen8h5mSM39pkaZWvvzcWMPr9",
|
||||||
"title": "/wsg/ - Worksafe GIF",
|
"title": "/wsg/ - Worksafe GIF",
|
||||||
"directoryCode": "wsg",
|
"directoryCode": "wsg",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -165,7 +175,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "do-it-yourself.bso",
|
"name": "do-it-yourself.bso",
|
||||||
|
"publicKey": "12D3KooWKKbiaFu7x2EwueN6wxw3DJRQsvCv8KVRGbTTYpsGZXVa",
|
||||||
"title": "/diy/ - Do It Yourself",
|
"title": "/diy/ - Do It Yourself",
|
||||||
"directoryCode": "diy",
|
"directoryCode": "diy",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -181,7 +192,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "outdoors-posting.bso",
|
"name": "outdoors-posting.bso",
|
||||||
|
"publicKey": "12D3KooWQ2nPGxQxuSMY92kM3zKu1rBekJsXxXb5uNkHLDfSSxTm",
|
||||||
"title": "/out/ - Outdoors",
|
"title": "/out/ - Outdoors",
|
||||||
"directoryCode": "out",
|
"directoryCode": "out",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -197,7 +209,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "artwork-critique.bso",
|
"name": "artwork-critique.bso",
|
||||||
|
"publicKey": "12D3KooWQZ5HkAspHUPGEVoSaAZgqachZGtxCXxAXYBDTreR4uWJ",
|
||||||
"title": "/ic/ - Artwork/Critique",
|
"title": "/ic/ - Artwork/Critique",
|
||||||
"directoryCode": "ic",
|
"directoryCode": "ic",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -213,7 +226,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "music-posting.bso",
|
"name": "music-posting.bso",
|
||||||
|
"publicKey": "12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy",
|
||||||
"title": "/mu/ - Music",
|
"title": "/mu/ - Music",
|
||||||
"directoryCode": "mu",
|
"directoryCode": "mu",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -229,7 +243,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "international-sfw.bso",
|
"name": "international-sfw.bso",
|
||||||
|
"publicKey": "12D3KooWMSUYcergS2BwbRzFynR5hQURc962W9iR81eTJKz89Q6n",
|
||||||
"title": "/int/ - International",
|
"title": "/int/ - International",
|
||||||
"directoryCode": "int",
|
"directoryCode": "int",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -245,7 +260,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "literature-posting.bso",
|
"name": "literature-posting.bso",
|
||||||
|
"publicKey": "12D3KooWKXimxiZWtgF3LoTdaW3btHhDzJN7P3qrsgHwRkow186x",
|
||||||
"title": "/lit/ - Literature",
|
"title": "/lit/ - Literature",
|
||||||
"directoryCode": "lit",
|
"directoryCode": "lit",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -261,7 +277,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "television-and-film.bso",
|
"name": "television-and-film.bso",
|
||||||
|
"publicKey": "12D3KooWGSRg4crzV5vXEya1ghXD7UBTqimqXTQvNUqy9NdzZNyd",
|
||||||
"title": "/tv/ - Television & Film",
|
"title": "/tv/ - Television & Film",
|
||||||
"directoryCode": "tv",
|
"directoryCode": "tv",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -277,7 +294,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "very-important-posts.bso",
|
"name": "very-important-posts.bso",
|
||||||
|
"publicKey": "12D3KooWNMnBJ5phkBWrcjfdnzdqPwCARmXwGWeZDKNJvCVPR8GT",
|
||||||
"title": "/vip/ - Very Important Posts",
|
"title": "/vip/ - Very Important Posts",
|
||||||
"directoryCode": "vip",
|
"directoryCode": "vip",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -293,7 +311,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "adult-gif.bso",
|
"name": "adult-gif.bso",
|
||||||
|
"publicKey": "12D3KooWJP8v5VUzKUfxr38BbhQPb5yidF3WapGQ6CjLctBV6QC1",
|
||||||
"title": "/gif/ - Adult GIF",
|
"title": "/gif/ - Adult GIF",
|
||||||
"directoryCode": "gif",
|
"directoryCode": "gif",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -309,7 +328,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "international-nsfw.bso",
|
"name": "international-nsfw.bso",
|
||||||
|
"publicKey": "12D3KooWPuYtTzrq8gFnv6yM1r4egy1rd4cxLr1nsXTjfdYkfwcN",
|
||||||
"title": "/bant/ - International/Random",
|
"title": "/bant/ - International/Random",
|
||||||
"directoryCode": "bant",
|
"directoryCode": "bant",
|
||||||
"features": {
|
"features": {
|
||||||
@@ -325,7 +345,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "random-nsfw.bso",
|
"name": "random-nsfw.bso",
|
||||||
|
"publicKey": "12D3KooWJkJYkuVbJQhapcVGpo8oNmsXsKmFRrFqc1SHezZW9fX8",
|
||||||
"title": "/b/ - Random",
|
"title": "/b/ - Random",
|
||||||
"directoryCode": "b",
|
"directoryCode": "b",
|
||||||
"features": {
|
"features": {
|
||||||
|
|||||||
@@ -117,7 +117,12 @@ describe('use-directories', () => {
|
|||||||
|
|
||||||
it('normalizes aliases and finds matching directories by exact or alias address', () => {
|
it('normalizes aliases and finds matching directories by exact or alias address', () => {
|
||||||
const communities = [
|
const communities = [
|
||||||
{ address: 'music-posting.bso', title: '/mu/ - Music' },
|
{
|
||||||
|
address: 'music-posting.bso',
|
||||||
|
name: 'music-posting.bso',
|
||||||
|
publicKey: '12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy',
|
||||||
|
title: '/mu/ - Music',
|
||||||
|
},
|
||||||
{ address: 'business.eth', title: '/biz/ - Business & Finance' },
|
{ address: 'business.eth', title: '/biz/ - Business & Finance' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -127,6 +132,7 @@ describe('use-directories', () => {
|
|||||||
|
|
||||||
expect(findDirectoryByAddress(communities, 'music-posting.bso')?.address).toBe('music-posting.bso');
|
expect(findDirectoryByAddress(communities, 'music-posting.bso')?.address).toBe('music-posting.bso');
|
||||||
expect(findDirectoryByAddress(communities, 'music-posting.eth')?.address).toBe('music-posting.bso');
|
expect(findDirectoryByAddress(communities, 'music-posting.eth')?.address).toBe('music-posting.bso');
|
||||||
|
expect(findDirectoryByAddress(communities, '12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy')?.address).toBe('music-posting.bso');
|
||||||
expect(findDirectoryByAddress(communities, undefined)).toBeUndefined();
|
expect(findDirectoryByAddress(communities, undefined)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,19 +155,22 @@ describe('use-directories', () => {
|
|||||||
updatedAt: 4,
|
updatedAt: 4,
|
||||||
directories: [
|
directories: [
|
||||||
{
|
{
|
||||||
communityAddress: 'music-posting.bso',
|
name: 'music-posting.bso',
|
||||||
|
publicKey: 'music-public-key',
|
||||||
title: '/mu/ - Music',
|
title: '/mu/ - Music',
|
||||||
directoryCode: 'mu',
|
directoryCode: 'mu',
|
||||||
features: { safeForWork: true, postsPerPage: 25, nested: { ignore: true } },
|
features: { safeForWork: true, postsPerPage: 25, nested: { ignore: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
communityAddress: 'flash.bso',
|
name: 'flash.bso',
|
||||||
|
publicKey: 'flash-public-key',
|
||||||
title: '/f/ - Flash',
|
title: '/f/ - Flash',
|
||||||
directoryCode: 'f',
|
directoryCode: 'f',
|
||||||
features: { nsfw: true, postsPerPage: 10 },
|
features: { nsfw: true, postsPerPage: 10 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
communityAddress: 'flash.bso',
|
name: 'flash.bso',
|
||||||
|
publicKey: 'flash-public-key',
|
||||||
title: '/f/ - Duplicate Flash',
|
title: '/f/ - Duplicate Flash',
|
||||||
directoryCode: 'f',
|
directoryCode: 'f',
|
||||||
},
|
},
|
||||||
@@ -196,6 +205,8 @@ describe('use-directories', () => {
|
|||||||
expect(latestSnapshot?.directories).toEqual([
|
expect(latestSnapshot?.directories).toEqual([
|
||||||
{
|
{
|
||||||
address: 'music-posting.bso',
|
address: 'music-posting.bso',
|
||||||
|
name: 'music-posting.bso',
|
||||||
|
publicKey: 'music-public-key',
|
||||||
title: '/mu/ - Music',
|
title: '/mu/ - Music',
|
||||||
directoryCode: 'mu',
|
directoryCode: 'mu',
|
||||||
features: { safeForWork: true, postsPerPage: 25 },
|
features: { safeForWork: true, postsPerPage: 25 },
|
||||||
@@ -203,6 +214,8 @@ describe('use-directories', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
address: 'flash.bso',
|
address: 'flash.bso',
|
||||||
|
name: 'flash.bso',
|
||||||
|
publicKey: 'flash-public-key',
|
||||||
title: '/f/ - Flash',
|
title: '/f/ - Flash',
|
||||||
directoryCode: 'f',
|
directoryCode: 'f',
|
||||||
features: { nsfw: true, postsPerPage: 10 },
|
features: { nsfw: true, postsPerPage: 10 },
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface DirectoryFeatures {
|
|||||||
export interface DirectoryCommunity {
|
export interface DirectoryCommunity {
|
||||||
title?: string;
|
title?: string;
|
||||||
address: string;
|
address: string;
|
||||||
|
name?: string;
|
||||||
|
publicKey?: string;
|
||||||
nsfw?: boolean;
|
nsfw?: boolean;
|
||||||
directoryCode?: string;
|
directoryCode?: string;
|
||||||
features?: DirectoryFeatures;
|
features?: DirectoryFeatures;
|
||||||
@@ -62,6 +64,8 @@ export const __resetDirectoriesModuleStateForTests = () => {
|
|||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;
|
||||||
|
|
||||||
|
const getStringValue = (...values: unknown[]): string | undefined => values.find((value): value is string => typeof value === 'string' && value.length > 0);
|
||||||
|
|
||||||
const normalizeFeatures = (value: unknown): DirectoryFeatures | undefined => {
|
const normalizeFeatures = (value: unknown): DirectoryFeatures | undefined => {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -88,8 +92,22 @@ const deriveNsfw = (value: { nsfw?: unknown; features?: { nsfw?: boolean; safeFo
|
|||||||
return topLevelNsfw ?? featuresNsfw;
|
return topLevelNsfw ?? featuresNsfw;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toCanonicalCommunity = (value: { address: unknown; title?: unknown; nsfw?: unknown; directoryCode?: unknown; features?: unknown }): DirectoryCommunity | null => {
|
const getDirectoryIdentifiers = (community: DirectoryCommunity): string[] => [
|
||||||
if (typeof value.address !== 'string') {
|
...new Set([community.address, community.name, community.publicKey].filter((value): value is string => typeof value === 'string' && value.length > 0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
const toCanonicalCommunity = (value: {
|
||||||
|
address?: unknown;
|
||||||
|
communityAddress?: unknown;
|
||||||
|
name?: unknown;
|
||||||
|
publicKey?: unknown;
|
||||||
|
title?: unknown;
|
||||||
|
nsfw?: unknown;
|
||||||
|
directoryCode?: unknown;
|
||||||
|
features?: unknown;
|
||||||
|
}): DirectoryCommunity | null => {
|
||||||
|
const name = getStringValue(value.name, value.address, value.communityAddress);
|
||||||
|
if (!name) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +115,9 @@ const toCanonicalCommunity = (value: { address: unknown; title?: unknown; nsfw?:
|
|||||||
const nsfw = deriveNsfw({ ...value, features });
|
const nsfw = deriveNsfw({ ...value, features });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
address: value.address,
|
address: name,
|
||||||
|
name,
|
||||||
|
...(typeof value.publicKey === 'string' ? { publicKey: value.publicKey } : {}),
|
||||||
...(typeof value.title === 'string' ? { title: value.title } : {}),
|
...(typeof value.title === 'string' ? { title: value.title } : {}),
|
||||||
...(typeof value.directoryCode === 'string' ? { directoryCode: value.directoryCode } : {}),
|
...(typeof value.directoryCode === 'string' ? { directoryCode: value.directoryCode } : {}),
|
||||||
...(features ? { features } : {}),
|
...(features ? { features } : {}),
|
||||||
@@ -110,10 +130,11 @@ const dedupeCommunities = (entries: DirectoryCommunity[]): DirectoryCommunity[]
|
|||||||
const normalizedEntries: DirectoryCommunity[] = [];
|
const normalizedEntries: DirectoryCommunity[] = [];
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (seenAddresses.has(entry.address)) {
|
const dedupeKey = entry.publicKey ?? entry.address;
|
||||||
|
if (seenAddresses.has(dedupeKey)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
seenAddresses.add(entry.address);
|
seenAddresses.add(dedupeKey);
|
||||||
normalizedEntries.push(entry);
|
normalizedEntries.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +153,9 @@ const adaptV2Directories = (value: Record<string, unknown>): DirectoryCommunity[
|
|||||||
}
|
}
|
||||||
const features = isRecord(directory.features) ? directory.features : null;
|
const features = isRecord(directory.features) ? directory.features : null;
|
||||||
return toCanonicalCommunity({
|
return toCanonicalCommunity({
|
||||||
address: directory.communityAddress,
|
communityAddress: directory.communityAddress,
|
||||||
|
name: directory.name,
|
||||||
|
publicKey: directory.publicKey,
|
||||||
title: directory.title,
|
title: directory.title,
|
||||||
directoryCode: directory.directoryCode,
|
directoryCode: directory.directoryCode,
|
||||||
features,
|
features,
|
||||||
@@ -163,8 +186,13 @@ export const findDirectoryByAddress = (directories: DirectoryCommunity[], addres
|
|||||||
return exactMatch;
|
return exactMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exactIdentifierMatch = directories.find((community) => getDirectoryIdentifiers(community).includes(address));
|
||||||
|
if (exactIdentifierMatch) {
|
||||||
|
return exactIdentifierMatch;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedAddress = normalizeBoardAddress(address);
|
const normalizedAddress = normalizeBoardAddress(address);
|
||||||
return directories.find((community) => normalizeBoardAddress(community.address) === normalizedAddress);
|
return directories.find((community) => getDirectoryIdentifiers(community).some((identifier) => normalizeBoardAddress(identifier) === normalizedAddress));
|
||||||
};
|
};
|
||||||
|
|
||||||
const adaptV1Communities = (value: Record<string, unknown>): DirectoryCommunity[] => {
|
const adaptV1Communities = (value: Record<string, unknown>): DirectoryCommunity[] => {
|
||||||
@@ -179,6 +207,8 @@ const adaptV1Communities = (value: Record<string, unknown>): DirectoryCommunity[
|
|||||||
}
|
}
|
||||||
return toCanonicalCommunity({
|
return toCanonicalCommunity({
|
||||||
address: community.address,
|
address: community.address,
|
||||||
|
name: community.name,
|
||||||
|
publicKey: community.publicKey,
|
||||||
title: community.title,
|
title: community.title,
|
||||||
nsfw: community.nsfw,
|
nsfw: community.nsfw,
|
||||||
directoryCode: community.directoryCode,
|
directoryCode: community.directoryCode,
|
||||||
@@ -190,6 +220,24 @@ const adaptV1Communities = (value: Record<string, unknown>): DirectoryCommunity[
|
|||||||
return dedupeCommunities(communities);
|
return dedupeCommunities(communities);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDirectoriesMetadata = (value: unknown): DirectoriesMetadata => {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
createdAt: 0,
|
||||||
|
updatedAt: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: typeof value.title === 'string' ? value.title : '',
|
||||||
|
description: typeof value.description === 'string' ? value.description : '',
|
||||||
|
createdAt: typeof value.createdAt === 'number' ? value.createdAt : 0,
|
||||||
|
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeDirectoriesData = (value: unknown): DirectoriesData | null => {
|
const normalizeDirectoriesData = (value: unknown): DirectoriesData | null => {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -202,30 +250,26 @@ const normalizeDirectoriesData = (value: unknown): DirectoriesData | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = directoriesData as DirectoriesData;
|
const fallbackMetadata = getDirectoriesMetadata(directoriesData as unknown);
|
||||||
return {
|
return {
|
||||||
title: typeof value.title === 'string' ? value.title : (raw.title ?? ''),
|
title: typeof value.title === 'string' ? value.title : fallbackMetadata.title,
|
||||||
description: typeof value.description === 'string' ? value.description : (raw.description ?? ''),
|
description: typeof value.description === 'string' ? value.description : fallbackMetadata.description,
|
||||||
createdAt: typeof value.createdAt === 'number' ? value.createdAt : (raw.createdAt ?? 0),
|
createdAt: typeof value.createdAt === 'number' ? value.createdAt : fallbackMetadata.createdAt,
|
||||||
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : (raw.updatedAt ?? 0),
|
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : fallbackMetadata.updatedAt,
|
||||||
communities,
|
communities,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let fallbackDirectoriesData: DirectoriesData | null = null;
|
let fallbackDirectoriesData: DirectoriesData | null = null;
|
||||||
|
|
||||||
const getFallbackDirectoriesData = (): DirectoriesData => {
|
export const getFallbackDirectoriesData = (): DirectoriesData => {
|
||||||
if (fallbackDirectoriesData) return fallbackDirectoriesData;
|
if (fallbackDirectoriesData) return fallbackDirectoriesData;
|
||||||
const normalized = normalizeDirectoriesData(directoriesData as unknown);
|
const normalized = normalizeDirectoriesData(directoriesData as unknown);
|
||||||
fallbackDirectoriesData =
|
const metadata = getDirectoriesMetadata(directoriesData as unknown);
|
||||||
normalized ??
|
fallbackDirectoriesData = normalized ?? {
|
||||||
({
|
...metadata,
|
||||||
title: (directoriesData as DirectoriesData).title ?? '',
|
communities: [],
|
||||||
description: (directoriesData as DirectoriesData).description ?? '',
|
};
|
||||||
createdAt: (directoriesData as DirectoriesData).createdAt ?? 0,
|
|
||||||
updatedAt: (directoriesData as DirectoriesData).updatedAt ?? 0,
|
|
||||||
communities: (directoriesData as DirectoriesData).communities ?? [],
|
|
||||||
} as DirectoriesData);
|
|
||||||
return fallbackDirectoriesData;
|
return fallbackDirectoriesData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ import {
|
|||||||
|
|
||||||
const communities = [
|
const communities = [
|
||||||
{ address: 'business.eth', title: '/biz/ - Business & Finance' },
|
{ address: 'business.eth', title: '/biz/ - Business & Finance' },
|
||||||
{ address: 'music-posting.bso', title: '/mu/ - Music' },
|
{
|
||||||
|
address: 'music-posting.bso',
|
||||||
|
name: 'music-posting.bso',
|
||||||
|
publicKey: '12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy',
|
||||||
|
title: '/mu/ - Music',
|
||||||
|
},
|
||||||
{ address: 'random.eth', directoryCode: 'b', title: 'Random' },
|
{ address: 'random.eth', directoryCode: 'b', title: 'Random' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -36,6 +41,7 @@ describe('directory mapping helpers', () => {
|
|||||||
it('maps addresses to canonical board paths and back', () => {
|
it('maps addresses to canonical board paths and back', () => {
|
||||||
expect(getBoardPath('business.eth', communities)).toBe('biz');
|
expect(getBoardPath('business.eth', communities)).toBe('biz');
|
||||||
expect(getBoardPath('music-posting.eth', communities)).toBe('mu');
|
expect(getBoardPath('music-posting.eth', communities)).toBe('mu');
|
||||||
|
expect(getBoardPath('12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy', communities)).toBe('mu');
|
||||||
expect(getBoardPath('unknown.example', communities)).toBe('unknown.example');
|
expect(getBoardPath('unknown.example', communities)).toBe('unknown.example');
|
||||||
|
|
||||||
expect(getSubplebbitAddress('biz', communities)).toBe('business.eth');
|
expect(getSubplebbitAddress('biz', communities)).toBe('business.eth');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChallengeVerification } from '@bitsocialnet/bitsocial-react-hooks';
|
import { ChallengeVerification } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import directoriesData from '../../data/5chan-directories.json';
|
import { getFallbackDirectoriesData } from '../../hooks/use-directories';
|
||||||
import { getBoardPath } from './route-utils';
|
import { getBoardPath } from './route-utils';
|
||||||
|
|
||||||
const resolveBoardIdentifier = (communityAddress: unknown): string => {
|
const resolveBoardIdentifier = (communityAddress: unknown): string => {
|
||||||
@@ -7,7 +7,7 @@ const resolveBoardIdentifier = (communityAddress: unknown): string => {
|
|||||||
return 'unknown board';
|
return 'unknown board';
|
||||||
}
|
}
|
||||||
|
|
||||||
const boardPath = getBoardPath(communityAddress, directoriesData.communities);
|
const boardPath = getBoardPath(communityAddress, getFallbackDirectoriesData().communities);
|
||||||
return boardPath === communityAddress ? communityAddress : `/${boardPath}/`;
|
return boardPath === communityAddress ? communityAddress : `/${boardPath}/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ let cachedAddressToDirectoryMap: Map<string, string> | null = null;
|
|||||||
|
|
||||||
const getDirectoryCode = (community: DirectoryCommunity): string | null => community.directoryCode ?? extractDirectoryFromTitle(community.title ?? '');
|
const getDirectoryCode = (community: DirectoryCommunity): string | null => community.directoryCode ?? extractDirectoryFromTitle(community.title ?? '');
|
||||||
|
|
||||||
|
const getCommunityLookupAddresses = (community: DirectoryCommunity): string[] => [
|
||||||
|
...new Set([community.address, community.name, community.publicKey].filter((value): value is string => typeof value === 'string' && value.length > 0)),
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a map from directory codes to community addresses
|
* Create a map from directory codes to community addresses
|
||||||
* Uses caching to avoid recreating the map when communities array hasn't changed
|
* Uses caching to avoid recreating the map when communities array hasn't changed
|
||||||
@@ -58,7 +62,9 @@ const getAddressToDirectoryMap = (communities: DirectoryCommunity[]): Map<string
|
|||||||
if (!community.address) continue;
|
if (!community.address) continue;
|
||||||
const directory = getDirectoryCode(community);
|
const directory = getDirectoryCode(community);
|
||||||
if (directory) {
|
if (directory) {
|
||||||
map.set(community.address, directory);
|
for (const address of getCommunityLookupAddresses(community)) {
|
||||||
|
map.set(address, directory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user