update
This commit is contained in:
11
.github/workflows/cloudflare-pages.yml
vendored
11
.github/workflows/cloudflare-pages.yml
vendored
@@ -41,6 +41,17 @@ jobs:
|
|||||||
cp -rf ${{ env.OAUTH_DIR }}/dist/* my-blog/static/
|
cp -rf ${{ env.OAUTH_DIR }}/dist/* my-blog/static/
|
||||||
cp ${{ env.OAUTH_DIR }}/dist/index.html my-blog/templates/oauth-assets.html
|
cp ${{ env.OAUTH_DIR }}/dist/index.html my-blog/templates/oauth-assets.html
|
||||||
|
|
||||||
|
- name: Build PDS app
|
||||||
|
run: |
|
||||||
|
cd pds
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Copy PDS build to static
|
||||||
|
run: |
|
||||||
|
rm -rf my-blog/static/pds
|
||||||
|
cp -rf pds/dist my-blog/static/pds
|
||||||
|
|
||||||
- name: Cache ailog binary
|
- name: Cache ailog binary
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ailog"
|
name = "ailog"
|
||||||
version = "0.2.9"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["syui"]
|
authors = ["syui"]
|
||||||
description = "A static blog generator with AI features"
|
description = "A static blog generator with AI features"
|
||||||
|
1
my-blog/static/pds/assets/index-BH2xHPKq.css
Normal file
1
my-blog/static/pds/assets/index-BH2xHPKq.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
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:#fff;padding:30px;border-radius:10px;box-shadow:0 2px 10px #0000001a}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 .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:700}.back-link:hover{text-decoration:underline}.at-uri-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.at-uri-modal-content{background-color:#fff;border-radius:8px;max-width:800px;max-height:600px;width:90%;height:80%;overflow:auto;position:relative;box-shadow:0 4px 6px #0000001a}.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}[data-at-uri]{color:#1976d2;cursor:pointer;text-decoration:underline}[data-at-uri]:hover{color:#1565c0}
|
57
my-blog/static/pds/assets/index-nqqvPufZ.js
Normal file
57
my-blog/static/pds/assets/index-nqqvPufZ.js
Normal file
File diff suppressed because one or more lines are too long
13
my-blog/static/pds/index.html
Normal file
13
my-blog/static/pds/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!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>
|
||||||
|
<script type="module" crossorigin src="/pds/assets/index-nqqvPufZ.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/pds/assets/index-BH2xHPKq.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
61
my-blog/templates/at-browser-assets.html
Normal file
61
my-blog/templates/at-browser-assets.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<!-- AT Browser Integration - Temporarily disabled to fix site display -->
|
||||||
|
<!--
|
||||||
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||||
|
<script src="/assets/pds-browser.umd.js"></script>
|
||||||
|
<script>
|
||||||
|
// AT Browser integration - needs debugging
|
||||||
|
console.log('AT Browser integration temporarily disabled');
|
||||||
|
</script>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -131,5 +131,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% include "oauth-assets.html" %}
|
{% include "oauth-assets.html" %}
|
||||||
|
{% include "at-browser-assets.html" %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
774
my-blog/templates/pds.html
Normal file
774
my-blog/templates/pds.html
Normal file
@@ -0,0 +1,774 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}AT URI Browser - {{ config.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pds-container">
|
||||||
|
<div class="pds-header">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Handle Input Form -->
|
||||||
|
<div class="pds-search-section">
|
||||||
|
<form class="pds-search-form" onsubmit="searchUser(); return false;">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" id="handleInput" placeholder="ai.syui.ai" value="at://ai.syui.ai" />
|
||||||
|
<button type="submit" id="searchButton">
|
||||||
|
<i class="fab fa-bluesky"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current User DID -->
|
||||||
|
<div id="userDidSection" class="user-did-section" style="display: none;">
|
||||||
|
<div class="pds-display">
|
||||||
|
<strong>PDS:</strong> <span id="userPdsText"></span>
|
||||||
|
</div>
|
||||||
|
<div class="handle-display">
|
||||||
|
<strong>Handle:</strong> <span id="userHandleText"></span>
|
||||||
|
</div>
|
||||||
|
<div class="did-display">
|
||||||
|
<span id="userDidText"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collection List -->
|
||||||
|
<div id="collectionsSection" class="collections-section" style="display: none;">
|
||||||
|
<div class="collections-header">
|
||||||
|
<button id="collectionsToggle" class="collections-toggle" onclick="toggleCollections()">[+] Collections</button>
|
||||||
|
</div>
|
||||||
|
<div id="collectionsList" class="collections-list" style="display: none;">
|
||||||
|
<!-- Collections will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AT URI Records -->
|
||||||
|
<div id="recordsSection" class="records-section" style="display: none;">
|
||||||
|
<div id="recordsList" class="records-list">
|
||||||
|
<!-- Records will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AT URI Modal -->
|
||||||
|
<div id="atUriModal" class="at-uri-modal-overlay" style="display: none;" onclick="closeAtUriModal(event)">
|
||||||
|
<div class="at-uri-modal-content">
|
||||||
|
<button class="at-uri-modal-close" onclick="closeAtUriModal()">×</button>
|
||||||
|
<div id="atUriContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pds-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-search-section {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-search-form {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #1976d2;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
min-width: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group button:hover {
|
||||||
|
background: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details h3 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-did-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.did-display {
|
||||||
|
padding: 10px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
word-break: break-all;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-display {
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f0f9f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-display strong {
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-display span {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pds-display {
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #e8f4f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-display strong {
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-display span {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-section,
|
||||||
|
.records-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-section h3,
|
||||||
|
.records-section h3 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-list,
|
||||||
|
.records-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-uri-link {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
color: #1976d2;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-uri-link:hover {
|
||||||
|
background: #e8f4f8;
|
||||||
|
border-color: #1976d2;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-info {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b3e5fc;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1976d2;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-info {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f9f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b3e5b3;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #2e7d32;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-toggle {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-toggle:hover {
|
||||||
|
background: #e8f4f8;
|
||||||
|
border-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pds-test-section,
|
||||||
|
.pds-about-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-test-section h2,
|
||||||
|
.pds-about-section h2 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #1976d2;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-uris {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-uri {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-uri:hover {
|
||||||
|
background: #e8f4f8;
|
||||||
|
border-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-about-section ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pds-about-section li {
|
||||||
|
padding: 5px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* AT URI 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading states */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #d32f2f;
|
||||||
|
background: #ffeaea;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pds-search-form {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// AT Protocol API functions
|
||||||
|
const AT_PROTOCOL_CONFIG = {
|
||||||
|
primary: {
|
||||||
|
pds: 'https://syu.is',
|
||||||
|
plc: 'https://plc.syu.is',
|
||||||
|
bsky: 'https://bsky.syu.is',
|
||||||
|
web: 'https://web.syu.is'
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
pds: 'https://bsky.social',
|
||||||
|
plc: 'https://plc.directory',
|
||||||
|
bsky: 'https://public.api.bsky.app',
|
||||||
|
web: 'https://bsky.app'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search user function
|
||||||
|
async function searchUser() {
|
||||||
|
const handleInput = document.getElementById('handleInput');
|
||||||
|
const userInfo = document.getElementById('userInfo');
|
||||||
|
const collectionsList = document.getElementById('collectionsList');
|
||||||
|
const recordsList = document.getElementById('recordsList');
|
||||||
|
const searchButton = document.getElementById('searchButton');
|
||||||
|
|
||||||
|
const input = handleInput.value.trim();
|
||||||
|
if (!input) {
|
||||||
|
alert('Handle nameまたはAT URIを入力してください');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchButton.disabled = true;
|
||||||
|
searchButton.innerHTML = '<i class="fab fa-bluesky"></i>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clear previous results
|
||||||
|
document.getElementById('userDidSection').style.display = 'none';
|
||||||
|
document.getElementById('collectionsSection').style.display = 'none';
|
||||||
|
document.getElementById('recordsSection').style.display = 'none';
|
||||||
|
collectionsList.innerHTML = '';
|
||||||
|
recordsList.innerHTML = '';
|
||||||
|
|
||||||
|
// Check if input is AT URI
|
||||||
|
if (input.startsWith('at://')) {
|
||||||
|
// Parse AT URI to check if it's a full record or just a handle/collection
|
||||||
|
const uriParts = input.replace('at://', '').split('/').filter(part => part.length > 0);
|
||||||
|
|
||||||
|
if (uriParts.length >= 3) {
|
||||||
|
// Full AT URI with rkey - show in modal
|
||||||
|
showAtUriModal(input);
|
||||||
|
return;
|
||||||
|
} else if (uriParts.length === 1) {
|
||||||
|
// Just handle in AT URI format (at://handle) - treat as regular handle
|
||||||
|
const handle = uriParts[0];
|
||||||
|
const userProfile = await resolveUserProfile(handle);
|
||||||
|
|
||||||
|
if (userProfile.success) {
|
||||||
|
displayUserDid(userProfile.data);
|
||||||
|
await loadUserCollections(handle, userProfile.data.did);
|
||||||
|
} else {
|
||||||
|
alert('ユーザーが見つかりません: ' + userProfile.error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (uriParts.length === 2) {
|
||||||
|
// Collection level AT URI - load collection records
|
||||||
|
const [repo, collection] = uriParts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First resolve the repo to get handle if it's a DID
|
||||||
|
let handle = repo;
|
||||||
|
if (repo.startsWith('did:')) {
|
||||||
|
// Try to resolve DID to handle - for now just use the DID
|
||||||
|
handle = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCollectionRecords(handle, collection, repo);
|
||||||
|
} catch (error) {
|
||||||
|
alert('コレクションの読み込みに失敗しました: ' + error.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular handle search
|
||||||
|
const userProfile = await resolveUserProfile(input);
|
||||||
|
|
||||||
|
if (userProfile.success) {
|
||||||
|
displayUserDid(userProfile.data);
|
||||||
|
await loadUserCollections(input, userProfile.data.did);
|
||||||
|
} else {
|
||||||
|
alert('ユーザーが見つかりません: ' + userProfile.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('エラーが発生しました: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
searchButton.disabled = false;
|
||||||
|
searchButton.innerHTML = '<i class="fab fa-bluesky"></i>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve user profile
|
||||||
|
async function resolveUserProfile(handle) {
|
||||||
|
try {
|
||||||
|
let response = null;
|
||||||
|
|
||||||
|
// Try syu.is first
|
||||||
|
try {
|
||||||
|
response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to resolve from syu.is:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If syu.is fails, try bsky.social
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to resolve handle');
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoData = await response.json();
|
||||||
|
|
||||||
|
// Get profile data
|
||||||
|
const profileResponse = await fetch(`${AT_PROTOCOL_CONFIG.fallback.bsky}/xrpc/app.bsky.actor.getProfile?actor=${repoData.did}`);
|
||||||
|
const profileData = await profileResponse.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
did: repoData.did,
|
||||||
|
handle: profileData.handle,
|
||||||
|
displayName: profileData.displayName,
|
||||||
|
avatar: profileData.avatar,
|
||||||
|
description: profileData.description,
|
||||||
|
pds: repoData.didDoc.service.find(s => s.type === 'AtprotoPersonalDataServer')?.serviceEndpoint
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display user DID
|
||||||
|
function displayUserDid(profile) {
|
||||||
|
document.getElementById('userPdsText').textContent = profile.pds || 'Unknown';
|
||||||
|
document.getElementById('userHandleText').textContent = profile.handle;
|
||||||
|
document.getElementById('userDidText').textContent = profile.did;
|
||||||
|
document.getElementById('userDidSection').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user collections
|
||||||
|
async function loadUserCollections(handle, did) {
|
||||||
|
const collectionsList = document.getElementById('collectionsList');
|
||||||
|
|
||||||
|
collectionsList.innerHTML = '<div class="loading">コレクションを読み込み中...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to get collections from describeRepo
|
||||||
|
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
|
||||||
|
let usedPds = AT_PROTOCOL_CONFIG.primary.pds;
|
||||||
|
|
||||||
|
// If syu.is fails, try bsky.social
|
||||||
|
if (!response.ok) {
|
||||||
|
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
|
||||||
|
usedPds = AT_PROTOCOL_CONFIG.fallback.pds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to describe repository');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const collections = data.collections || [];
|
||||||
|
|
||||||
|
// Display collections as AT URI links
|
||||||
|
collectionsList.innerHTML = '';
|
||||||
|
if (collections.length === 0) {
|
||||||
|
collectionsList.innerHTML = '<div class="error">コレクションが見つかりませんでした</div>';
|
||||||
|
} else {
|
||||||
|
|
||||||
|
collections.forEach(collection => {
|
||||||
|
const atUri = `at://${did}/${collection}/`;
|
||||||
|
const collectionElement = document.createElement('a');
|
||||||
|
collectionElement.className = 'at-uri-link';
|
||||||
|
collectionElement.href = '#';
|
||||||
|
collectionElement.textContent = atUri;
|
||||||
|
collectionElement.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
loadCollectionRecords(handle, collection, did);
|
||||||
|
// Close collections and update toggle
|
||||||
|
document.getElementById('collectionsList').style.display = 'none';
|
||||||
|
document.getElementById('collectionsToggle').textContent = '[-] Collections';
|
||||||
|
};
|
||||||
|
collectionsList.appendChild(collectionElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('collectionsSection').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
collectionsList.innerHTML = '<div class="error">コレクションの読み込みに失敗しました: ' + error.message + '</div>';
|
||||||
|
document.getElementById('collectionsSection').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load collection records
|
||||||
|
async function loadCollectionRecords(handle, collection, did) {
|
||||||
|
const recordsList = document.getElementById('recordsList');
|
||||||
|
|
||||||
|
recordsList.innerHTML = '<div class="loading">レコードを読み込み中...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try with syu.is first
|
||||||
|
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=${collection}`);
|
||||||
|
let usedPds = AT_PROTOCOL_CONFIG.primary.pds;
|
||||||
|
|
||||||
|
// If that fails, try with bsky.social
|
||||||
|
if (!response.ok) {
|
||||||
|
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=${collection}`);
|
||||||
|
usedPds = AT_PROTOCOL_CONFIG.fallback.pds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load records');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Display records as AT URI links
|
||||||
|
recordsList.innerHTML = '';
|
||||||
|
|
||||||
|
// Add collection info for records
|
||||||
|
const collectionInfo = document.createElement('div');
|
||||||
|
collectionInfo.className = 'collection-info';
|
||||||
|
collectionInfo.innerHTML = `<strong>${collection}</strong>`;
|
||||||
|
recordsList.appendChild(collectionInfo);
|
||||||
|
|
||||||
|
data.records.forEach(record => {
|
||||||
|
const atUri = record.uri;
|
||||||
|
const recordElement = document.createElement('a');
|
||||||
|
recordElement.className = 'at-uri-link';
|
||||||
|
recordElement.href = '#';
|
||||||
|
recordElement.textContent = atUri;
|
||||||
|
recordElement.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showAtUriModal(atUri);
|
||||||
|
};
|
||||||
|
recordsList.appendChild(recordElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('recordsSection').style.display = 'block';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
recordsList.innerHTML = '<div class="error">レコードの読み込みに失敗しました: ' + error.message + '</div>';
|
||||||
|
document.getElementById('recordsSection').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show AT URI modal
|
||||||
|
function showAtUriModal(uri) {
|
||||||
|
const modal = document.getElementById('atUriModal');
|
||||||
|
const content = document.getElementById('atUriContent');
|
||||||
|
|
||||||
|
content.innerHTML = '<div class="loading">レコードを読み込み中...</div>';
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
// Load record data
|
||||||
|
loadAtUriRecord(uri, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load AT URI record
|
||||||
|
async function loadAtUriRecord(uri, contentElement) {
|
||||||
|
try {
|
||||||
|
const parts = uri.replace('at://', '').split('/');
|
||||||
|
const repo = parts[0];
|
||||||
|
const collection = parts[1];
|
||||||
|
const rkey = parts[2];
|
||||||
|
|
||||||
|
// Try with syu.is first
|
||||||
|
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${rkey}`);
|
||||||
|
|
||||||
|
// If that fails, try with bsky.social
|
||||||
|
if (!response.ok) {
|
||||||
|
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${rkey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load record');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
contentElement.innerHTML = `
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<h3>AT URI Record</h3>
|
||||||
|
<div style="font-family: monospace; font-size: 14px; color: #666; margin-bottom: 20px; word-break: break-all;">
|
||||||
|
${uri}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #999; margin-bottom: 20px;">
|
||||||
|
Repo: ${repo} | Collection: ${collection} | RKey: ${rkey}
|
||||||
|
</div>
|
||||||
|
<h4>Record Data</h4>
|
||||||
|
<pre style="background: #f5f5f5; padding: 15px; border-radius: 4px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
contentElement.innerHTML = `
|
||||||
|
<div style="padding: 20px; color: red;">
|
||||||
|
<strong>Error:</strong> ${error.message}
|
||||||
|
<div style="margin-top: 10px; font-size: 12px;">
|
||||||
|
<strong>URI:</strong> ${uri}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close AT URI modal
|
||||||
|
function closeAtUriModal(event) {
|
||||||
|
const modal = document.getElementById('atUriModal');
|
||||||
|
if (event && event.target !== modal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize AT URI click handlers
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Add click handlers to existing AT URIs
|
||||||
|
document.querySelectorAll('.at-uri').forEach(element => {
|
||||||
|
element.addEventListener('click', function() {
|
||||||
|
const uri = this.getAttribute('data-at-uri');
|
||||||
|
showAtUriModal(uri);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ESC key to close modal
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeAtUriModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter key to search
|
||||||
|
document.getElementById('handleInput').addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
searchUser();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle collections visibility
|
||||||
|
function toggleCollections() {
|
||||||
|
const collectionsList = document.getElementById('collectionsList');
|
||||||
|
const toggleButton = document.getElementById('collectionsToggle');
|
||||||
|
|
||||||
|
if (collectionsList.style.display === 'none') {
|
||||||
|
collectionsList.style.display = 'block';
|
||||||
|
toggleButton.textContent = '[-] Collections';
|
||||||
|
} else {
|
||||||
|
collectionsList.style.display = 'none';
|
||||||
|
toggleButton.textContent = '[+] Collections';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ailog-oauth",
|
"name": "ailog-oauth",
|
||||||
"version": "0.2.9",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
22
scpt/run.zsh
22
scpt/run.zsh
@@ -5,6 +5,7 @@ function _env() {
|
|||||||
ailog=$d/target/release/ailog
|
ailog=$d/target/release/ailog
|
||||||
oauth=$d/oauth
|
oauth=$d/oauth
|
||||||
myblog=$d/my-blog
|
myblog=$d/my-blog
|
||||||
|
pds=$d/pds
|
||||||
port=4173
|
port=4173
|
||||||
#source $oauth/.env.production
|
#source $oauth/.env.production
|
||||||
case $OSTYPE in
|
case $OSTYPE in
|
||||||
@@ -43,6 +44,21 @@ function _oauth_build() {
|
|||||||
#npm run preview
|
#npm run preview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _pds_build() {
|
||||||
|
cd $pds
|
||||||
|
nvm use 21
|
||||||
|
npm i
|
||||||
|
npm run build
|
||||||
|
rm -rf $myblog/static/pds
|
||||||
|
cp -rf dist $myblog/static/pds
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pds_server() {
|
||||||
|
cd $pds
|
||||||
|
nvm use 21
|
||||||
|
npm run preview
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function _server_comment() {
|
function _server_comment() {
|
||||||
cargo build --release
|
cargo build --release
|
||||||
@@ -65,6 +81,12 @@ case "${1:-serve}" in
|
|||||||
oauth|o)
|
oauth|o)
|
||||||
_oauth_build
|
_oauth_build
|
||||||
;;
|
;;
|
||||||
|
pds|p)
|
||||||
|
_pds_build
|
||||||
|
;;
|
||||||
|
pds-server|ps)
|
||||||
|
_pds_server
|
||||||
|
;;
|
||||||
n)
|
n)
|
||||||
oauth=$d/oauth_old
|
oauth=$d/oauth_old
|
||||||
_oauth_build
|
_oauth_build
|
||||||
|
@@ -328,7 +328,7 @@ async fn serve_file(path: &str) -> Result<(&'static str, Vec<u8>, &'static str)>
|
|||||||
// Remove query parameters from path
|
// Remove query parameters from path
|
||||||
let clean_path = path.split('?').next().unwrap_or(path);
|
let clean_path = path.split('?').next().unwrap_or(path);
|
||||||
|
|
||||||
let file_path = if clean_path == "/" {
|
let mut file_path = if clean_path == "/" {
|
||||||
PathBuf::from("public/index.html")
|
PathBuf::from("public/index.html")
|
||||||
} else {
|
} else {
|
||||||
PathBuf::from("public").join(clean_path.trim_start_matches('/'))
|
PathBuf::from("public").join(clean_path.trim_start_matches('/'))
|
||||||
@@ -337,10 +337,43 @@ async fn serve_file(path: &str) -> Result<(&'static str, Vec<u8>, &'static str)>
|
|||||||
println!("Serving file: {}", file_path.display());
|
println!("Serving file: {}", file_path.display());
|
||||||
|
|
||||||
// Check if file exists and get metadata
|
// Check if file exists and get metadata
|
||||||
let metadata = tokio::fs::metadata(&file_path).await?;
|
let metadata = tokio::fs::metadata(&file_path).await;
|
||||||
if !metadata.is_file() {
|
|
||||||
|
match metadata {
|
||||||
|
Ok(meta) if meta.is_file() => {
|
||||||
|
// File exists, proceed normally
|
||||||
|
}
|
||||||
|
Ok(meta) if meta.is_dir() => {
|
||||||
|
// Directory exists, try to serve index.html
|
||||||
|
file_path = file_path.join("index.html");
|
||||||
|
println!("Directory found, trying index.html: {}", file_path.display());
|
||||||
|
let index_metadata = tokio::fs::metadata(&file_path).await?;
|
||||||
|
if !index_metadata.is_file() {
|
||||||
|
return Err(anyhow::anyhow!("No index.html in directory: {}", file_path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
return Err(anyhow::anyhow!("Not a file: {}", file_path.display()));
|
return Err(anyhow::anyhow!("Not a file: {}", file_path.display()));
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Try adding index.html to the original path
|
||||||
|
let index_path = PathBuf::from("public")
|
||||||
|
.join(clean_path.trim_start_matches('/'))
|
||||||
|
.join("index.html");
|
||||||
|
|
||||||
|
println!("File not found, trying index.html: {}", index_path.display());
|
||||||
|
let index_metadata = tokio::fs::metadata(&index_path).await;
|
||||||
|
if let Ok(meta) = index_metadata {
|
||||||
|
if meta.is_file() {
|
||||||
|
file_path = index_path;
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Original error: {}", e));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("File not found: {}", file_path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (content_type, cache_control) = match file_path.extension().and_then(|ext| ext.to_str()) {
|
let (content_type, cache_control) = match file_path.extension().and_then(|ext| ext.to_str()) {
|
||||||
Some("html") => ("text/html; charset=utf-8", "no-cache"),
|
Some("html") => ("text/html; charset=utf-8", "no-cache"),
|
||||||
|
@@ -86,6 +86,9 @@ impl Generator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate PDS page
|
||||||
|
self.generate_pds_page().await?;
|
||||||
|
|
||||||
println!("{} {} posts", "Generated".cyan(), posts.len());
|
println!("{} {} posts", "Generated".cyan(), posts.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -491,6 +494,30 @@ impl Generator {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn generate_pds_page(&self) -> Result<()> {
|
||||||
|
let public_dir = self.base_path.join("public");
|
||||||
|
let pds_dir = public_dir.join("pds");
|
||||||
|
fs::create_dir_all(&pds_dir)?;
|
||||||
|
|
||||||
|
// Generate PDS page using the pds.html template
|
||||||
|
let config_with_timestamp = self.create_config_with_timestamp()?;
|
||||||
|
let mut context = tera::Context::new();
|
||||||
|
context.insert("config", &config_with_timestamp);
|
||||||
|
context.insert("site", &self.config.site);
|
||||||
|
context.insert("page", &serde_json::json!({
|
||||||
|
"title": "AT URI Browser",
|
||||||
|
"description": "AT Protocol レコードをブラウズし、分散SNSのコンテンツを探索できます"
|
||||||
|
}));
|
||||||
|
|
||||||
|
let rendered_content = self.template_engine.render("pds.html", &context)?;
|
||||||
|
let output_path = pds_dir.join("index.html");
|
||||||
|
fs::write(output_path, rendered_content)?;
|
||||||
|
|
||||||
|
println!("{} PDS page", "Generated".cyan());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_plain_text(&self, html_content: &str) -> String {
|
fn extract_plain_text(&self, html_content: &str) -> String {
|
||||||
// Remove HTML tags and extract plain text
|
// Remove HTML tags and extract plain text
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
@@ -536,6 +563,7 @@ pub struct Post {
|
|||||||
pub extra: Option<serde_json::Value>,
|
pub extra: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
pub struct Translation {
|
pub struct Translation {
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
Reference in New Issue
Block a user