update
This commit is contained in:
		
							
								
								
									
										11
									
								
								.github/workflows/cloudflare-pages.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/cloudflare-pages.yml
									
									
									
									
										vendored
									
									
								
							| @@ -40,6 +40,17 @@ jobs: | ||||
|           rm -rf my-blog/static/assets | ||||
|           cp -rf ${{ env.OAUTH_DIR }}/dist/* my-blog/static/ | ||||
|           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 | ||||
|         uses: actions/cache@v4 | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "ailog" | ||||
| version = "0.2.9" | ||||
| version = "0.3.0" | ||||
| edition = "2021" | ||||
| authors = ["syui"] | ||||
| 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> | ||||
|      | ||||
|     {% include "oauth-assets.html" %} | ||||
|     {% include "at-browser-assets.html" %} | ||||
| </body> | ||||
| </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", | ||||
|   "version": "0.2.9", | ||||
|   "version": "0.3.0", | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "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 | ||||
| 	oauth=$d/oauth | ||||
| 	myblog=$d/my-blog | ||||
| 	pds=$d/pds | ||||
| 	port=4173 | ||||
| 	#source $oauth/.env.production | ||||
| 	case $OSTYPE in | ||||
| @@ -43,6 +44,21 @@ function _oauth_build() { | ||||
| 	#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() { | ||||
| 	cargo build --release | ||||
| @@ -65,6 +81,12 @@ case "${1:-serve}" in | ||||
| 	oauth|o) | ||||
| 		_oauth_build | ||||
| 		;; | ||||
| 	pds|p) | ||||
| 		_pds_build | ||||
| 		;; | ||||
| 	pds-server|ps) | ||||
| 		_pds_server | ||||
| 		;; | ||||
| 	n) | ||||
| 		oauth=$d/oauth_old | ||||
| 		_oauth_build | ||||
|   | ||||
| @@ -328,7 +328,7 @@ async fn serve_file(path: &str) -> Result<(&'static str, Vec<u8>, &'static str)> | ||||
|     // Remove query parameters from 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") | ||||
|     } else { | ||||
|         PathBuf::from("public").join(clean_path.trim_start_matches('/')) | ||||
| @@ -337,9 +337,42 @@ async fn serve_file(path: &str) -> Result<(&'static str, Vec<u8>, &'static str)> | ||||
|     println!("Serving file: {}", file_path.display()); | ||||
|  | ||||
|     // Check if file exists and get metadata | ||||
|     let metadata = tokio::fs::metadata(&file_path).await?; | ||||
|     if !metadata.is_file() { | ||||
|         return Err(anyhow::anyhow!("Not a file: {}", file_path.display())); | ||||
|     let metadata = tokio::fs::metadata(&file_path).await; | ||||
|      | ||||
|     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())); | ||||
|         } | ||||
|         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()) { | ||||
|   | ||||
| @@ -86,6 +86,9 @@ impl Generator { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Generate PDS page | ||||
|         self.generate_pds_page().await?; | ||||
|  | ||||
|         println!("{} {} posts", "Generated".cyan(), posts.len()); | ||||
|  | ||||
|         Ok(()) | ||||
| @@ -491,6 +494,30 @@ impl Generator { | ||||
|         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 { | ||||
|         // Remove HTML tags and extract plain text | ||||
|         let mut text = String::new(); | ||||
| @@ -536,6 +563,7 @@ pub struct Post { | ||||
|     pub extra: Option<serde_json::Value>, | ||||
| } | ||||
|  | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize)] | ||||
| pub struct Translation { | ||||
|     pub lang: String, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user