From 746b1ccfd3b2a3f9df4f2ed8acd131dd4386cce1 Mon Sep 17 00:00:00 2001 From: syui Date: Fri, 16 Jan 2026 18:04:33 +0900 Subject: [PATCH] fix plc --- .../.well-known/lexicon/ai.syui.log.post.json | 36 +++++++++- .../.well-known/lexicon/ai/syui/log/post.json | 68 +++++++++++++++++++ scripts/put-lexicon.sh | 43 ++++++++++++ src/components/atbrowser.ts | 23 +++++-- src/lib/api.ts | 27 ++++++++ 5 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 public/.well-known/lexicon/ai/syui/log/post.json create mode 100755 scripts/put-lexicon.sh diff --git a/public/.well-known/lexicon/ai.syui.log.post.json b/public/.well-known/lexicon/ai.syui.log.post.json index bb7f93b..ad2f85b 100644 --- a/public/.well-known/lexicon/ai.syui.log.post.json +++ b/public/.well-known/lexicon/ai.syui.log.post.json @@ -20,15 +20,49 @@ "type": "string", "maxLength": 1000000, "maxGraphemes": 100000, - "description": "The content of the post." + "description": "The content of the post (markdown)." }, "createdAt": { "type": "string", "format": "datetime", "description": "Client-declared timestamp when this post was originally created." + }, + "lang": { + "type": "string", + "maxLength": 10, + "description": "Language code of the original content (e.g., 'ja', 'en')." + }, + "translations": { + "type": "ref", + "ref": "#translationMap", + "description": "Translations of the post in other languages." } } } + }, + "translationMap": { + "type": "object", + "description": "Map of language codes to translations.", + "properties": { + "en": { "type": "ref", "ref": "#translation" }, + "ja": { "type": "ref", "ref": "#translation" } + } + }, + "translation": { + "type": "object", + "description": "A translation of a post.", + "properties": { + "title": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "content": { + "type": "string", + "maxLength": 1000000, + "maxGraphemes": 100000 + } + } } } } diff --git a/public/.well-known/lexicon/ai/syui/log/post.json b/public/.well-known/lexicon/ai/syui/log/post.json new file mode 100644 index 0000000..ad2f85b --- /dev/null +++ b/public/.well-known/lexicon/ai/syui/log/post.json @@ -0,0 +1,68 @@ +{ + "lexicon": 1, + "id": "ai.syui.log.post", + "defs": { + "main": { + "type": "record", + "description": "Record containing a blog post.", + "key": "tid", + "record": { + "type": "object", + "required": ["title", "content", "createdAt"], + "properties": { + "title": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300, + "description": "The title of the post." + }, + "content": { + "type": "string", + "maxLength": 1000000, + "maxGraphemes": 100000, + "description": "The content of the post (markdown)." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this post was originally created." + }, + "lang": { + "type": "string", + "maxLength": 10, + "description": "Language code of the original content (e.g., 'ja', 'en')." + }, + "translations": { + "type": "ref", + "ref": "#translationMap", + "description": "Translations of the post in other languages." + } + } + } + }, + "translationMap": { + "type": "object", + "description": "Map of language codes to translations.", + "properties": { + "en": { "type": "ref", "ref": "#translation" }, + "ja": { "type": "ref", "ref": "#translation" } + } + }, + "translation": { + "type": "object", + "description": "A translation of a post.", + "properties": { + "title": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "content": { + "type": "string", + "maxLength": 1000000, + "maxGraphemes": 100000 + } + } + } + } +} diff --git a/scripts/put-lexicon.sh b/scripts/put-lexicon.sh new file mode 100755 index 0000000..fefea95 --- /dev/null +++ b/scripts/put-lexicon.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Usage: TOKEN=xxx ./scripts/put-lexicon.sh + +if [ -z "$TOKEN" ]; then + echo "Error: TOKEN environment variable is required" + echo "Usage: TOKEN=xxx ./scripts/put-lexicon.sh" + exit 1 +fi + +curl -X POST "https://bsky.social/xrpc/com.atproto.repo.putRecord" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "repo": "did:plc:uqzpqmrjnptsxezjx4xuh2mn", + "collection": "com.atproto.lexicon.schema", + "rkey": "ai.syui.log.post", + "record": { + "$type": "com.atproto.lexicon.schema", + "lexicon": 1, + "id": "ai.syui.log.post", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "Record containing a blog post.", + "record": { + "type": "object", + "required": ["title", "content", "createdAt"], + "properties": { + "title": { "type": "string", "maxLength": 3000, "maxGraphemes": 300 }, + "content": { "type": "string", "maxLength": 1000000, "maxGraphemes": 100000 }, + "createdAt": { "type": "string", "format": "datetime" }, + "lang": { "type": "string", "maxLength": 10 }, + "translations": { "type": "unknown" } + } + } + } + } + } + }' + +echo "" diff --git a/src/components/atbrowser.ts b/src/components/atbrowser.ts index 7a27eeb..5a08dd6 100644 --- a/src/components/atbrowser.ts +++ b/src/components/atbrowser.ts @@ -1,6 +1,17 @@ -import { describeRepo, listRecordsRaw, getRecordRaw, fetchLexicon, resolveHandle, getServiceInfo, resolvePds, getPlc } from '../lib/api.js' +import { describeRepo, listRecordsRaw, getRecordRaw, fetchLexicon, resolveHandle, getServiceInfo, resolvePds, getPlcForPds } from '../lib/api.js' import { deleteRecord } from '../lib/auth.js' import { escapeHtml } from '../lib/utils.js' +import type { Networks } from '../types.js' + +// Cache networks config +let networksConfig: Networks | null = null + +async function loadNetworks(): Promise { + if (networksConfig) return networksConfig + const res = await fetch('/networks.json') + networksConfig = await res.json() + return networksConfig! +} function extractRkey(uri: string): string { const parts = uri.split('/') @@ -8,13 +19,15 @@ function extractRkey(uri: string): string { } async function renderServices(did: string, handle: string): Promise { - const [collections, pds] = await Promise.all([ + const [collections, pds, networks] = await Promise.all([ describeRepo(did), - resolvePds(did) + resolvePds(did), + loadNetworks() ]) - // Server info section - const plcUrl = `${getPlc()}/${did}/log` + // Server info section - use PLC based on PDS + const plc = getPlcForPds(pds, networks) + const plcUrl = `${plc}/${did}/log` const serverHtml = `

Server

diff --git a/src/lib/api.ts b/src/lib/api.ts index 809e1f5..0eefe4f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -14,6 +14,33 @@ export function getPlc(): string { return networkConfig?.plc || 'https://plc.directory' } +// Get PLC URL based on PDS endpoint +export function getPlcForPds(pds: string, networks: Record): string { + // Check if PDS matches any network + for (const [_key, config] of Object.entries(networks)) { + // Match by domain (e.g., "https://syu.is" or "https://bsky.syu.is") + try { + const pdsHost = new URL(pds).hostname + const bskyHost = new URL(config.bsky).hostname + // Check if PDS host matches network's bsky host + if (pdsHost === bskyHost || pdsHost.endsWith('.' + bskyHost)) { + return config.plc + } + // Also check web host if available + if (config.web) { + const webHost = new URL(config.web).hostname + if (pdsHost === webHost || pdsHost.endsWith('.' + webHost)) { + return config.plc + } + } + } catch { + continue + } + } + // Default to plc.directory + return 'https://plc.directory' +} + function getBsky(): string { return networkConfig?.bsky || 'https://public.api.bsky.app' }