add comment

This commit is contained in:
2026-01-15 23:26:34 +09:00
parent 9980e596ca
commit aa35de397f
16 changed files with 784 additions and 135 deletions

View File

@@ -9,7 +9,7 @@ export function setNetworkConfig(config: NetworkConfig): void {
networkConfig = config
}
function getPlc(): string {
export function getPlc(): string {
return networkConfig?.plc || 'https://plc.directory'
}
@@ -24,30 +24,81 @@ export function getAgent(service: string): AtpAgent {
return agents.get(service)!
}
// Fallback PLC directories
const FALLBACK_PLCS = [
'https://plc.directory',
'https://plc.syu.is',
]
// Fallback endpoints for handle/profile resolution
const FALLBACK_ENDPOINTS = [
'https://public.api.bsky.app',
'https://bsky.syu.is',
]
export async function resolvePds(did: string): Promise<string> {
const res = await fetch(`${getPlc()}/${did}`)
const doc = await res.json()
const service = doc.service?.find((s: any) => s.type === 'AtprotoPersonalDataServer')
return service?.serviceEndpoint || getBsky()
// Try current PLC first, then fallbacks
const plcs = [getPlc(), ...FALLBACK_PLCS.filter(p => p !== getPlc())]
for (const plc of plcs) {
try {
const res = await fetch(`${plc}/${did}`)
if (!res.ok) continue
const doc = await res.json()
const service = doc.service?.find((s: any) => s.type === 'AtprotoPersonalDataServer')
if (service?.serviceEndpoint) {
return service.serviceEndpoint
}
} catch {
continue
}
}
return getBsky()
}
export async function resolveHandle(handle: string): Promise<string> {
const agent = getAgent(getBsky())
const res = await agent.resolveHandle({ handle })
return res.data.did
// Try current network first
try {
const agent = getAgent(getBsky())
const res = await agent.resolveHandle({ handle })
return res.data.did
} catch {
// Try fallback endpoints
for (const endpoint of FALLBACK_ENDPOINTS) {
if (endpoint === getBsky()) continue // Skip if same as current
try {
const agent = getAgent(endpoint)
const res = await agent.resolveHandle({ handle })
return res.data.did
} catch {
continue
}
}
throw new Error(`Could not resolve handle: ${handle}`)
}
}
export async function getProfile(actor: string): Promise<Profile> {
const agent = getAgent(getBsky())
const res = await agent.getProfile({ actor })
return {
did: res.data.did,
handle: res.data.handle,
displayName: res.data.displayName,
description: res.data.description,
avatar: res.data.avatar,
banner: res.data.banner,
// Try current network first
const endpoints = [getBsky(), ...FALLBACK_ENDPOINTS.filter(e => e !== getBsky())]
for (const endpoint of endpoints) {
try {
const agent = getAgent(endpoint)
const res = await agent.getProfile({ actor })
return {
did: res.data.did,
handle: res.data.handle,
displayName: res.data.displayName,
description: res.data.description,
avatar: res.data.avatar,
banner: res.data.banner,
}
} catch {
continue
}
}
throw new Error(`Could not get profile: ${actor}`)
}
export async function listRecords(
@@ -190,6 +241,72 @@ const SERVICE_MAP: Record<string, { domain: string; icon?: string }> = {
'pub.leaflet': { domain: 'leaflet.pub' },
}
// Search Bluesky posts mentioning a URL
export async function searchPostsForUrl(url: string): Promise<any[]> {
// Search ALL endpoints and merge results (different networks have different indexes)
const endpoints = [getBsky(), ...FALLBACK_ENDPOINTS.filter(e => e !== getBsky())]
// Extract search-friendly patterns from URL
// e.g., "https://syui.ai/post/abc123/" -> ["syui.ai/post/abc123", "syui.ai/post"]
const searchQueries: string[] = []
try {
const urlObj = new URL(url)
const pathWithDomain = urlObj.host + urlObj.pathname.replace(/\/$/, '')
searchQueries.push(pathWithDomain) // syui.ai/post/abc123
// Also try shorter path for broader search
const pathParts = urlObj.pathname.split('/').filter(Boolean)
if (pathParts.length >= 1) {
searchQueries.push(urlObj.host + '/' + pathParts[0]) // syui.ai/post
}
} catch {
searchQueries.push(url)
}
// Search all endpoints in parallel and collect results
const allPosts: any[] = []
const seenUris = new Set<string>()
const searchPromises = endpoints.flatMap(endpoint =>
searchQueries.map(async query => {
try {
const res = await fetch(
`${endpoint}/xrpc/app.bsky.feed.searchPosts?q=${encodeURIComponent(query)}&limit=20`
)
if (!res.ok) return []
const data = await res.json()
// Filter posts that actually link to the target URL
return (data.posts || []).filter((post: any) => {
const embedUri = post.record?.embed?.external?.uri
const text = post.record?.text || ''
return embedUri === url || text.includes(url) || embedUri?.includes(url.replace(/\/$/, ''))
})
} catch {
return []
}
})
)
const results = await Promise.all(searchPromises)
// Merge results, removing duplicates by URI
for (const posts of results) {
for (const post of posts) {
if (!seenUris.has(post.uri)) {
seenUris.add(post.uri)
allPosts.push(post)
}
}
}
// Sort by date (newest first)
allPosts.sort((a, b) =>
new Date(b.record?.createdAt || 0).getTime() - new Date(a.record?.createdAt || 0).getTime()
)
return allPosts
}
export function getServiceInfo(collection: string): { name: string; domain: string; favicon: string } | null {
// Try to find matching service prefix
for (const [prefix, info] of Object.entries(SERVICE_MAP)) {

7
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
export function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}