add comment
This commit is contained in:
151
src/lib/api.ts
151
src/lib/api.ts
@@ -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
7
src/lib/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
Reference in New Issue
Block a user