update
This commit is contained in:
12
pds/index.html
Normal file
12
pds/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AT URI Browser - syui.ai</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
27
pds/package.json
Normal file
27
pds/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "pds-browser",
|
||||
"version": "0.3.0",
|
||||
"description": "AT Protocol browser for ai.log",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.0",
|
||||
"@atproto/did": "^0.1.0",
|
||||
"@atproto/lexicon": "^0.4.0",
|
||||
"@atproto/syntax": "^0.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.37",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
128
pds/src/App.css
Normal file
128
pds/src/App.css
Normal file
@@ -0,0 +1,128 @@
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 3px solid #007acc;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #007acc;
|
||||
}
|
||||
|
||||
.test-uris {
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ddd;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.at-uri {
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
background: #f4f4f4;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.at-uri:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #e8f4f8;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.instructions ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
color: #007acc;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* AT Browser Modal Styles */
|
||||
.at-uri-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.at-uri-modal-content {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
max-width: 800px;
|
||||
max-height: 600px;
|
||||
width: 90%;
|
||||
height: 80%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.at-uri-modal-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
/* AT URI Link Styles */
|
||||
[data-at-uri] {
|
||||
color: #1976d2;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
[data-at-uri]:hover {
|
||||
color: #1565c0;
|
||||
}
|
62
pds/src/App.jsx
Normal file
62
pds/src/App.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react'
|
||||
import { AtUriBrowser } from './components/AtUriBrowser.jsx'
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AtUriBrowser>
|
||||
<div className="container">
|
||||
<h1>AT URI Browser</h1>
|
||||
|
||||
<div className="test-section">
|
||||
<h2>テスト用 AT URI</h2>
|
||||
<p>以下のAT URIをクリックすると、モーダルでコンテンツが表示されます。</p>
|
||||
|
||||
<div className="test-uris">
|
||||
<div className="at-uri" data-at-uri="at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.feed.post/3lu5givmkc222">
|
||||
at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.feed.post/3lu5givmkc222
|
||||
</div>
|
||||
<div className="at-uri" data-at-uri="at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self">
|
||||
at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self
|
||||
</div>
|
||||
<div className="at-uri" data-at-uri="at://syui.ai/app.bsky.actor.profile/self">
|
||||
at://syui.ai/app.bsky.actor.profile/self
|
||||
</div>
|
||||
<div className="at-uri" data-at-uri="at://bsky.app/app.bsky.actor.profile/self">
|
||||
at://bsky.app/app.bsky.actor.profile/self
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="instructions">
|
||||
<h3>使用方法:</h3>
|
||||
<ol>
|
||||
<li>上記のAT URIをクリックしてください</li>
|
||||
<li>モーダルがポップアップし、AT Protocolレコードの内容が表示されます</li>
|
||||
<li>モーダルは×ボタンまたはEscキーで閉じることができます</li>
|
||||
<li>モーダルはレスポンシブ対応で、異なる画面サイズに対応します</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="test-section">
|
||||
<h2>AT URI について</h2>
|
||||
<p>AT URIは、AT Protocolで使用される統一リソース識別子です。この形式により、分散ソーシャルネットワーク上のコンテンツを一意に識別できます。</p>
|
||||
<p>このブラウザを使用することで、ブログ投稿やその他のコンテンツに埋め込まれたAT URIを直接探索することが可能です。</p>
|
||||
|
||||
<h3>対応PDS環境</h3>
|
||||
<ul>
|
||||
<li><strong>bsky.social</strong> - メインのBlueskyネットワーク</li>
|
||||
<li><strong>syu.is</strong> - 独立したPDS環境</li>
|
||||
<li><strong>plc.directory</strong> + <strong>plc.syu.is</strong> - DID解決</li>
|
||||
</ul>
|
||||
|
||||
<p><small>注意: 独立したPDS環境では、レコードの同期状況により、一部のコンテンツが利用できない場合があります。</small></p>
|
||||
</div>
|
||||
|
||||
<a href="/" className="back-link">← ブログに戻る</a>
|
||||
</div>
|
||||
</AtUriBrowser>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
75
pds/src/components/AtUriBrowser.jsx
Normal file
75
pds/src/components/AtUriBrowser.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* AT URI Browser Component
|
||||
* Copyright (c) 2025 ai.log
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { AtUriModal } from './AtUriModal.jsx'
|
||||
import { isAtUri } from '../lib/atproto.js'
|
||||
|
||||
export function AtUriBrowser({ children }) {
|
||||
const [modalUri, setModalUri] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleAtUriClick = (e) => {
|
||||
const target = e.target
|
||||
|
||||
// Check if clicked element has at-uri data attribute
|
||||
if (target.dataset.atUri) {
|
||||
e.preventDefault()
|
||||
setModalUri(target.dataset.atUri)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if clicked element contains at-uri text
|
||||
const text = target.textContent
|
||||
if (text && isAtUri(text)) {
|
||||
e.preventDefault()
|
||||
setModalUri(text)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if parent element has at-uri
|
||||
const parent = target.parentElement
|
||||
if (parent && parent.dataset.atUri) {
|
||||
e.preventDefault()
|
||||
setModalUri(parent.dataset.atUri)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleAtUriClick)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleAtUriClick)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleAtUriClick = (uri) => {
|
||||
setModalUri(uri)
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalUri(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<AtUriModal
|
||||
uri={modalUri}
|
||||
onClose={handleCloseModal}
|
||||
onAtUriClick={handleAtUriClick}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Utility function to wrap at-uri text with clickable spans
|
||||
export const wrapAtUris = (text) => {
|
||||
const atUriRegex = /at:\/\/[^\s]+/g
|
||||
return text.replace(atUriRegex, (match) => {
|
||||
return `<span data-at-uri="${match}" style="color: blue; cursor: pointer; text-decoration: underline;">${match}</span>`
|
||||
})
|
||||
}
|
130
pds/src/components/AtUriJson.jsx
Normal file
130
pds/src/components/AtUriJson.jsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Based on frontpage/atproto-browser
|
||||
* Copyright (c) 2025 The Frontpage Authors
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { isDid } from '@atproto/did'
|
||||
import { parseAtUri, isAtUri } from '../lib/atproto.js'
|
||||
|
||||
const JSONString = ({ data, onAtUriClick }) => {
|
||||
const handleClick = (uri) => {
|
||||
if (onAtUriClick) {
|
||||
onAtUriClick(uri)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<pre style={{ color: 'darkgreen', margin: 0, display: 'inline' }}>
|
||||
{isAtUri(data) ? (
|
||||
<>
|
||||
"
|
||||
<span
|
||||
onClick={() => handleClick(data)}
|
||||
style={{
|
||||
color: 'blue',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
>
|
||||
{data}
|
||||
</span>
|
||||
"
|
||||
</>
|
||||
) : isDid(data) ? (
|
||||
<>
|
||||
"
|
||||
<span
|
||||
onClick={() => handleClick(`at://${data}`)}
|
||||
style={{
|
||||
color: 'blue',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
>
|
||||
{data}
|
||||
</span>
|
||||
"
|
||||
</>
|
||||
) : URL.canParse(data) ? (
|
||||
<>
|
||||
"
|
||||
<a href={data} rel="noopener noreferrer ugc" target="_blank">
|
||||
{data}
|
||||
</a>
|
||||
"
|
||||
</>
|
||||
) : (
|
||||
`"${data}"`
|
||||
)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
const JSONValue = ({ data, onAtUriClick }) => {
|
||||
if (data === null) {
|
||||
return <pre style={{ color: 'gray', margin: 0, display: 'inline' }}>null</pre>
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return <JSONString data={data} onAtUriClick={onAtUriClick} />
|
||||
}
|
||||
|
||||
if (typeof data === 'number') {
|
||||
return <pre style={{ color: 'darkorange', margin: 0, display: 'inline' }}>{data}</pre>
|
||||
}
|
||||
|
||||
if (typeof data === 'boolean') {
|
||||
return <pre style={{ color: 'darkred', margin: 0, display: 'inline' }}>{data.toString()}</pre>
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return (
|
||||
<div style={{ paddingLeft: '20px' }}>
|
||||
[
|
||||
{data.map((item, index) => (
|
||||
<div key={index} style={{ paddingLeft: '20px' }}>
|
||||
<JSONValue data={item} onAtUriClick={onAtUriClick} />
|
||||
{index < data.length - 1 && ','}
|
||||
</div>
|
||||
))}
|
||||
]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
return (
|
||||
<div style={{ paddingLeft: '20px' }}>
|
||||
{'{'}
|
||||
{Object.entries(data).map(([key, value], index, entries) => (
|
||||
<div key={key} style={{ paddingLeft: '20px' }}>
|
||||
<span style={{ color: 'darkblue' }}>"{key}"</span>: <JSONValue data={value} onAtUriClick={onAtUriClick} />
|
||||
{index < entries.length - 1 && ','}
|
||||
</div>
|
||||
))}
|
||||
{'}'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <pre style={{ margin: 0, display: 'inline' }}>{String(data)}</pre>
|
||||
}
|
||||
|
||||
export default function AtUriJson({ data, onAtUriClick }) {
|
||||
return (
|
||||
<div style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px',
|
||||
padding: '10px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '400px'
|
||||
}}>
|
||||
<JSONValue data={data} onAtUriClick={onAtUriClick} />
|
||||
</div>
|
||||
)
|
||||
}
|
80
pds/src/components/AtUriModal.jsx
Normal file
80
pds/src/components/AtUriModal.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* AT URI Modal Component
|
||||
* Copyright (c) 2025 ai.log
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import AtUriViewer from './AtUriViewer.jsx'
|
||||
|
||||
export function AtUriModal({ uri, onClose, onAtUriClick }) {
|
||||
useEffect(() => {
|
||||
const handleEscape = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (e) => {
|
||||
if (e.target.classList.contains('at-uri-modal-overlay')) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
}, [onClose])
|
||||
|
||||
if (!uri) return null
|
||||
|
||||
return (
|
||||
<div className="at-uri-modal-overlay" style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '800px',
|
||||
maxHeight: '600px',
|
||||
width: '90%',
|
||||
height: '80%',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
right: '10px',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '20px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 1001,
|
||||
padding: '5px 10px'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<AtUriViewer uri={uri} onAtUriClick={onAtUriClick} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
111
pds/src/components/AtUriViewer.jsx
Normal file
111
pds/src/components/AtUriViewer.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Based on frontpage/atproto-browser
|
||||
* Copyright (c) 2025 The Frontpage Authors
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { parseAtUri, getRecord } from '../lib/atproto.js'
|
||||
import AtUriJson from './AtUriJson.jsx'
|
||||
|
||||
export default function AtUriViewer({ uri, onAtUriClick }) {
|
||||
const [record, setRecord] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const loadRecord = async () => {
|
||||
if (!uri) return
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
console.log('Loading AT URI:', uri)
|
||||
const atUri = parseAtUri(uri)
|
||||
if (!atUri) {
|
||||
throw new Error('Invalid AT URI')
|
||||
}
|
||||
|
||||
console.log('Parsed AT URI:', {
|
||||
hostname: atUri.hostname,
|
||||
collection: atUri.collection,
|
||||
rkey: atUri.rkey
|
||||
})
|
||||
|
||||
const result = await getRecord(atUri.hostname, atUri.collection, atUri.rkey)
|
||||
|
||||
console.log('getRecord result:', result)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
setRecord(result.data)
|
||||
} catch (err) {
|
||||
console.error('AtUriViewer error:', err)
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadRecord()
|
||||
}, [uri])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: '20px', color: 'red' }}>
|
||||
<div><strong>Error:</strong> {error}</div>
|
||||
<div style={{ marginTop: '10px', fontSize: '12px' }}>
|
||||
<strong>URI:</strong> {uri}
|
||||
</div>
|
||||
<div style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
|
||||
デバッグ情報: このAT URIは有効ではないか、レコードが存在しません。
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!record) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div>No record found</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const atUri = parseAtUri(uri)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px' }}>AT URI Record</h3>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all'
|
||||
}}>
|
||||
{uri}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '5px' }}>
|
||||
DID: {atUri.hostname} | Collection: {atUri.collection} | RKey: {atUri.rkey}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ margin: '0 0 10px 0', fontSize: '16px' }}>Record Data</h4>
|
||||
<AtUriJson data={record} onAtUriClick={onAtUriClick} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
33
pds/src/config.js
Normal file
33
pds/src/config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* AT Protocol Configuration for syu.is environment
|
||||
*/
|
||||
|
||||
export const AT_PROTOCOL_CONFIG = {
|
||||
// Primary PDS environment (syu.is)
|
||||
primary: {
|
||||
pds: 'https://syu.is',
|
||||
plc: 'https://plc.syu.is',
|
||||
bsky: 'https://bsky.syu.is',
|
||||
web: 'https://web.syu.is'
|
||||
},
|
||||
|
||||
// Fallback PDS environment (bsky.social)
|
||||
fallback: {
|
||||
pds: 'https://bsky.social',
|
||||
plc: 'https://plc.directory',
|
||||
bsky: 'https://public.api.bsky.app',
|
||||
web: 'https://bsky.app'
|
||||
}
|
||||
}
|
||||
|
||||
export const getPDSConfig = (pds) => {
|
||||
// Map PDS URL to appropriate config
|
||||
if (pds.includes('syu.is')) {
|
||||
return AT_PROTOCOL_CONFIG.primary
|
||||
} else if (pds.includes('bsky.social')) {
|
||||
return AT_PROTOCOL_CONFIG.fallback
|
||||
}
|
||||
|
||||
// Default to primary for unknown PDS
|
||||
return AT_PROTOCOL_CONFIG.primary
|
||||
}
|
9
pds/src/index.js
Normal file
9
pds/src/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Based on frontpage/atproto-browser
|
||||
* Copyright (c) 2025 The Frontpage Authors
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
export { AtUriBrowser } from './components/AtUriBrowser.jsx'
|
||||
export { AtUriModal } from './components/AtUriModal.jsx'
|
||||
export { default as AtUriViewer } from './components/AtUriViewer.jsx'
|
165
pds/src/lib/atproto.js
Normal file
165
pds/src/lib/atproto.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Based on frontpage/atproto-browser
|
||||
* Copyright (c) 2025 The Frontpage Authors
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
import { AtpBaseClient } from '@atproto/api'
|
||||
import { AtUri } from '@atproto/syntax'
|
||||
import { isDid } from '@atproto/did'
|
||||
import { AT_PROTOCOL_CONFIG } from '../config.js'
|
||||
|
||||
// Identity resolution cache
|
||||
const identityCache = new Map()
|
||||
|
||||
// Create AT Protocol client
|
||||
export const createAtpClient = (pds) => {
|
||||
return new AtpBaseClient({
|
||||
service: pds.startsWith('http') ? pds : `https://${pds}`
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve identity (DID/Handle)
|
||||
export const resolveIdentity = async (identifier) => {
|
||||
if (identityCache.has(identifier)) {
|
||||
return identityCache.get(identifier)
|
||||
}
|
||||
|
||||
try {
|
||||
let did = identifier
|
||||
|
||||
// If it's a handle, resolve to DID
|
||||
if (!isDid(identifier)) {
|
||||
// Try syu.is first, then fallback to bsky.social
|
||||
let resolved = false
|
||||
|
||||
try {
|
||||
const client = createAtpClient(AT_PROTOCOL_CONFIG.primary.pds)
|
||||
const response = await client.com.atproto.repo.describeRepo({ repo: identifier })
|
||||
did = response.data.did
|
||||
resolved = true
|
||||
} catch (error) {
|
||||
console.log('Failed to resolve from syu.is:', error)
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
try {
|
||||
const client = createAtpClient(AT_PROTOCOL_CONFIG.fallback.pds)
|
||||
const response = await client.com.atproto.repo.describeRepo({ repo: identifier })
|
||||
did = response.data.did
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to resolve handle: ${identifier}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get DID document to find PDS
|
||||
// Try plc.syu.is first, then fallback to plc.directory
|
||||
let didDoc = null
|
||||
let plcResponse = null
|
||||
|
||||
try {
|
||||
plcResponse = await fetch(`${AT_PROTOCOL_CONFIG.primary.plc}/${did}`)
|
||||
if (plcResponse.ok) {
|
||||
didDoc = await plcResponse.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to resolve from plc.syu.is:', error)
|
||||
}
|
||||
|
||||
// If plc.syu.is fails, try plc.directory
|
||||
if (!didDoc) {
|
||||
try {
|
||||
plcResponse = await fetch(`${AT_PROTOCOL_CONFIG.fallback.plc}/${did}`)
|
||||
if (plcResponse.ok) {
|
||||
didDoc = await plcResponse.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to resolve from plc.directory:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!didDoc) {
|
||||
throw new Error(`Failed to resolve DID document from any PLC server`)
|
||||
}
|
||||
|
||||
// Find PDS service endpoint
|
||||
const pdsService = didDoc.service?.find(service =>
|
||||
service.type === 'AtprotoPersonalDataServer' ||
|
||||
service.id === '#atproto_pds'
|
||||
)
|
||||
|
||||
if (!pdsService) {
|
||||
throw new Error('No PDS service found in DID document')
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
didDocument: didDoc,
|
||||
pdsUrl: pdsService.serviceEndpoint
|
||||
}
|
||||
|
||||
identityCache.set(identifier, result)
|
||||
return result
|
||||
} catch (error) {
|
||||
const result = {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
identityCache.set(identifier, result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Get record from AT Protocol
|
||||
export const getRecord = async (did, collection, rkey) => {
|
||||
try {
|
||||
console.log('getRecord called with:', { did, collection, rkey })
|
||||
|
||||
const identityResult = await resolveIdentity(did)
|
||||
console.log('resolveIdentity result:', identityResult)
|
||||
|
||||
if (!identityResult.success) {
|
||||
return { success: false, error: identityResult.error }
|
||||
}
|
||||
|
||||
const pdsUrl = identityResult.pdsUrl
|
||||
console.log('Using PDS URL:', pdsUrl)
|
||||
|
||||
const client = createAtpClient(pdsUrl)
|
||||
|
||||
const response = await client.com.atproto.repo.getRecord({
|
||||
repo: did,
|
||||
collection,
|
||||
rkey
|
||||
})
|
||||
|
||||
console.log('getRecord response:', response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
pdsUrl
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('getRecord error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse AT URI
|
||||
export const parseAtUri = (uri) => {
|
||||
try {
|
||||
return new AtUri(uri)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Check if string is AT URI
|
||||
export const isAtUri = (str) => {
|
||||
return str.startsWith('at://') && str.split(' ').length === 1
|
||||
}
|
9
pds/src/main.jsx
Normal file
9
pds/src/main.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
10
pds/vite.config.js
Normal file
10
pds/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: '/pds/',
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
}
|
||||
})
|
Reference in New Issue
Block a user