186 lines
5.6 KiB
JavaScript
186 lines
5.6 KiB
JavaScript
// ATProto API client
|
|
import { ATProtoError, logError } from '../utils/errorHandler.js'
|
|
|
|
const ENDPOINTS = {
|
|
describeRepo: 'com.atproto.repo.describeRepo',
|
|
getProfile: 'app.bsky.actor.getProfile',
|
|
listRecords: 'com.atproto.repo.listRecords',
|
|
putRecord: 'com.atproto.repo.putRecord'
|
|
}
|
|
|
|
async function request(url, options = {}) {
|
|
try {
|
|
const controller = new AbortController()
|
|
const timeoutId = setTimeout(() => controller.abort(), 15000) // 15秒タイムアウト
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
signal: controller.signal
|
|
})
|
|
|
|
clearTimeout(timeoutId)
|
|
|
|
if (!response.ok) {
|
|
throw new ATProtoError(
|
|
`Request failed: ${response.statusText}`,
|
|
response.status,
|
|
{ url, method: options.method || 'GET' }
|
|
)
|
|
}
|
|
|
|
return await response.json()
|
|
} catch (error) {
|
|
if (error.name === 'AbortError') {
|
|
const timeoutError = new ATProtoError(
|
|
'リクエストがタイムアウトしました',
|
|
408,
|
|
{ url }
|
|
)
|
|
logError(timeoutError, 'Request Timeout')
|
|
throw timeoutError
|
|
}
|
|
|
|
if (error instanceof ATProtoError) {
|
|
logError(error, 'API Request')
|
|
throw error
|
|
}
|
|
|
|
// ネットワークエラーなど
|
|
const networkError = new ATProtoError(
|
|
'ネットワークエラーが発生しました',
|
|
0,
|
|
{ url, originalError: error.message }
|
|
)
|
|
logError(networkError, 'Network Error')
|
|
throw networkError
|
|
}
|
|
}
|
|
|
|
export const atproto = {
|
|
async getDid(pds, handle) {
|
|
const res = await request(`https://${pds}/xrpc/${ENDPOINTS.describeRepo}?repo=${handle}`)
|
|
return res.did
|
|
},
|
|
|
|
async getProfile(bsky, actor) {
|
|
// Skip test DIDs
|
|
if (actor && actor.includes('test-')) {
|
|
console.log('Skipping profile fetch for test DID:', actor)
|
|
return {
|
|
did: actor,
|
|
handle: 'test.user',
|
|
displayName: 'Test User',
|
|
avatar: null
|
|
}
|
|
}
|
|
|
|
// Check if endpoint supports getProfile
|
|
let apiEndpoint = bsky
|
|
|
|
// Allow public.api.bsky.app and bsky.syu.is, redirect other PDS endpoints
|
|
if (!bsky.includes('public.api.bsky.app') && !bsky.includes('bsky.syu.is')) {
|
|
// If it's a PDS endpoint that doesn't support getProfile, redirect to public API
|
|
console.warn(`getProfile called with PDS endpoint ${bsky}, redirecting to public API`)
|
|
apiEndpoint = 'https://public.api.bsky.app'
|
|
}
|
|
|
|
return await request(`${apiEndpoint}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`)
|
|
},
|
|
|
|
async getRecords(pds, repo, collection, limit = 10) {
|
|
const res = await request(`${pds}/xrpc/${ENDPOINTS.listRecords}?repo=${repo}&collection=${collection}&limit=${limit}`)
|
|
return res.records || []
|
|
},
|
|
|
|
async searchPlc(plc, did) {
|
|
try {
|
|
const data = await request(`${plc}/${did}`)
|
|
return {
|
|
success: true,
|
|
endpoint: data?.service?.[0]?.serviceEndpoint || null,
|
|
handle: data?.alsoKnownAs?.[0]?.replace('at://', '') || null
|
|
}
|
|
} catch {
|
|
return { success: false, endpoint: null, handle: null }
|
|
}
|
|
},
|
|
|
|
async putRecord(pds, record, agent) {
|
|
if (!agent) {
|
|
throw new Error('Agent required for putRecord')
|
|
}
|
|
|
|
// Use Agent's putRecord method instead of direct fetch
|
|
return await agent.com.atproto.repo.putRecord(record)
|
|
}
|
|
}
|
|
|
|
import { dataCache } from '../utils/cache.js'
|
|
|
|
// Collection specific methods
|
|
export const collections = {
|
|
async getBase(pds, repo, collection, limit = 10) {
|
|
const cacheKey = dataCache.generateKey('base', pds, repo, collection, limit)
|
|
const cached = dataCache.get(cacheKey)
|
|
if (cached) return cached
|
|
|
|
const data = await atproto.getRecords(pds, repo, collection, limit)
|
|
dataCache.set(cacheKey, data)
|
|
return data
|
|
},
|
|
|
|
async getLang(pds, repo, collection, limit = 10) {
|
|
const cacheKey = dataCache.generateKey('lang', pds, repo, collection, limit)
|
|
const cached = dataCache.get(cacheKey)
|
|
if (cached) return cached
|
|
|
|
const data = await atproto.getRecords(pds, repo, `${collection}.chat.lang`, limit)
|
|
dataCache.set(cacheKey, data)
|
|
return data
|
|
},
|
|
|
|
async getComment(pds, repo, collection, limit = 10) {
|
|
const cacheKey = dataCache.generateKey('comment', pds, repo, collection, limit)
|
|
const cached = dataCache.get(cacheKey)
|
|
if (cached) return cached
|
|
|
|
const data = await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit)
|
|
dataCache.set(cacheKey, data)
|
|
return data
|
|
},
|
|
|
|
async getChat(pds, repo, collection, limit = 10) {
|
|
const cacheKey = dataCache.generateKey('chat', pds, repo, collection, limit)
|
|
const cached = dataCache.get(cacheKey)
|
|
if (cached) return cached
|
|
|
|
const data = await atproto.getRecords(pds, repo, `${collection}.chat`, limit)
|
|
dataCache.set(cacheKey, data)
|
|
return data
|
|
},
|
|
|
|
async getUserList(pds, repo, collection, limit = 100) {
|
|
const cacheKey = dataCache.generateKey('userlist', pds, repo, collection, limit)
|
|
const cached = dataCache.get(cacheKey)
|
|
if (cached) return cached
|
|
|
|
const data = await atproto.getRecords(pds, repo, `${collection}.user`, limit)
|
|
dataCache.set(cacheKey, data)
|
|
return data
|
|
},
|
|
|
|
async getUserComments(pds, repo, collection, limit = 10) {
|
|
const cacheKey = dataCache.generateKey('usercomments', pds, repo, collection, limit)
|
|
const cached = dataCache.get(cacheKey)
|
|
if (cached) return cached
|
|
|
|
const data = await atproto.getRecords(pds, repo, collection, limit)
|
|
dataCache.set(cacheKey, data)
|
|
return data
|
|
},
|
|
|
|
// 投稿後にキャッシュを無効化
|
|
invalidateCache(collection) {
|
|
dataCache.invalidatePattern(collection)
|
|
}
|
|
} |