fix hugo callback
This commit is contained in:
		@@ -53,7 +53,9 @@
 | 
				
			|||||||
      "WebFetch(domain:atproto.com)",
 | 
					      "WebFetch(domain:atproto.com)",
 | 
				
			||||||
      "WebFetch(domain:syu.is)",
 | 
					      "WebFetch(domain:syu.is)",
 | 
				
			||||||
      "Bash(sed:*)",
 | 
					      "Bash(sed:*)",
 | 
				
			||||||
      "Bash(./scpt/run.zsh:*)"
 | 
					      "Bash(./scpt/run.zsh:*)",
 | 
				
			||||||
 | 
					      "Bash(RUST_LOG=debug cargo run -- stream status)",
 | 
				
			||||||
 | 
					      "Bash(RUST_LOG=debug cargo run -- stream test-api)"
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "deny": []
 | 
					    "deny": []
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -253,6 +253,24 @@ function setupAskAIEventListeners() {
 | 
				
			|||||||
        handleAIResponse(event.detail);
 | 
					        handleAIResponse(event.detail);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Listen for OAuth callback completion from iframe
 | 
				
			||||||
 | 
					    window.addEventListener('message', function(event) {
 | 
				
			||||||
 | 
					        if (event.data.type === 'oauth_success') {
 | 
				
			||||||
 | 
					            console.log('Received OAuth success message:', event.data);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Close any OAuth popups/iframes
 | 
				
			||||||
 | 
					            const oauthFrame = document.getElementById('oauth-frame');
 | 
				
			||||||
 | 
					            if (oauthFrame) {
 | 
				
			||||||
 | 
					                oauthFrame.remove();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Reload the page to refresh OAuth app state
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                window.location.reload();
 | 
				
			||||||
 | 
					            }, 500);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // Track IME composition state
 | 
					    // Track IME composition state
 | 
				
			||||||
    let isComposing = false;
 | 
					    let isComposing = false;
 | 
				
			||||||
    const aiQuestionInput = document.getElementById('aiQuestion');
 | 
					    const aiQuestionInput = document.getElementById('aiQuestion');
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										147
									
								
								my-blog/static/oauth/assets/comment-atproto-BQKPMV57.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								my-blog/static/oauth/assets/comment-atproto-BQKPMV57.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								my-blog/static/oauth/assets/comment-atproto-BUFiApUA.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								my-blog/static/oauth/assets/comment-atproto-BUFiApUA.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3
									
								
								my-blog/static/oauth/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								my-blog/static/oauth/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<!-- OAuth Comment System - Load globally for session management -->
 | 
				
			||||||
 | 
					<script type="module" crossorigin src="/assets/comment-atproto-BQKPMV57.js"></script>
 | 
				
			||||||
 | 
					<link rel="stylesheet" crossorigin href="/assets/comment-atproto-BUFiApUA.css">
 | 
				
			||||||
@@ -44,7 +44,7 @@ body {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.oauth-header-content {
 | 
					.oauth-header-content {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: flex-start;
 | 
					  justify-content: center;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  max-width: 800px;
 | 
					  max-width: 800px;
 | 
				
			||||||
  margin: 0 auto;
 | 
					  margin: 0 auto;
 | 
				
			||||||
@@ -52,6 +52,10 @@ body {
 | 
				
			|||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.oauth-header-content:has(.oauth-user-profile) {
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.oauth-app-title {
 | 
					.oauth-app-title {
 | 
				
			||||||
  font-size: 20px;
 | 
					  font-size: 20px;
 | 
				
			||||||
  font-weight: 800;
 | 
					  font-weight: 800;
 | 
				
			||||||
@@ -62,7 +66,80 @@ body {
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  gap: 8px;
 | 
					  gap: 8px;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  width: 100%;
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* When user is logged in, actions take normal space */
 | 
				
			||||||
 | 
					.oauth-header-content:has(.oauth-user-profile) .oauth-header-actions {
 | 
				
			||||||
 | 
					  flex: 0 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* OAuth User Profile in Header */
 | 
				
			||||||
 | 
					.oauth-user-profile {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 12px;
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-avatar-section {
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-avatar-section .profile-avatar {
 | 
				
			||||||
 | 
					  width: 48px;
 | 
				
			||||||
 | 
					  height: 48px;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  object-fit: cover;
 | 
				
			||||||
 | 
					  border: 2px solid var(--border);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-avatar-fallback {
 | 
				
			||||||
 | 
					  width: 48px;
 | 
				
			||||||
 | 
					  height: 48px;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  background: var(--background-secondary);
 | 
				
			||||||
 | 
					  border: 2px solid var(--border);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  font-size: 18px;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  color: var(--text-secondary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-info {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  min-width: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-display-name {
 | 
				
			||||||
 | 
					  font-size: 18px;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  color: var(--text);
 | 
				
			||||||
 | 
					  margin-bottom: 2px;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-handle {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  color: var(--text-secondary);
 | 
				
			||||||
 | 
					  margin-bottom: 2px;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-did {
 | 
				
			||||||
 | 
					  font-size: 11px;
 | 
				
			||||||
 | 
					  color: var(--text-secondary);
 | 
				
			||||||
 | 
					  font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  opacity: 0.7;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Buttons */
 | 
					/* Buttons */
 | 
				
			||||||
@@ -128,6 +205,7 @@ body {
 | 
				
			|||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
  gap: 0;
 | 
					  gap: 0;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  max-width: 400px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.auth-section.search-bar-layout .handle-input {
 | 
					.auth-section.search-bar-layout .handle-input {
 | 
				
			||||||
@@ -465,7 +543,7 @@ body {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.user-message .message-content {
 | 
					.user-message .message-content {
 | 
				
			||||||
  color: white;
 | 
					  color: #000;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.question-form {
 | 
					.question-form {
 | 
				
			||||||
@@ -647,6 +725,41 @@ body {
 | 
				
			|||||||
  .chat-container {
 | 
					  .chat-container {
 | 
				
			||||||
    height: 300px;
 | 
					    height: 300px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  /* OAuth User Profile Mobile */
 | 
				
			||||||
 | 
					  .oauth-user-profile {
 | 
				
			||||||
 | 
					    gap: 8px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .profile-avatar-section .profile-avatar,
 | 
				
			||||||
 | 
					  .profile-avatar-fallback {
 | 
				
			||||||
 | 
					    width: 36px;
 | 
				
			||||||
 | 
					    height: 36px;
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .profile-display-name {
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .profile-handle {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .profile-did {
 | 
				
			||||||
 | 
					    font-size: 9px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .oauth-header-content {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 12px;
 | 
				
			||||||
 | 
					    align-items: flex-start;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .oauth-header-actions {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Avatar Styles */
 | 
					/* Avatar Styles */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -69,8 +69,32 @@ export default function App() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  if (isLoading) {
 | 
					  if (isLoading) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={{ padding: '20px', textAlign: 'center' }}>
 | 
					      <div style={{ 
 | 
				
			||||||
        <p>読み込み中...</p>
 | 
					        display: 'flex', 
 | 
				
			||||||
 | 
					        flexDirection: 'column',
 | 
				
			||||||
 | 
					        alignItems: 'center', 
 | 
				
			||||||
 | 
					        justifyContent: 'center',
 | 
				
			||||||
 | 
					        minHeight: '200px',
 | 
				
			||||||
 | 
					        padding: '40px',
 | 
				
			||||||
 | 
					        textAlign: 'center' 
 | 
				
			||||||
 | 
					      }}>
 | 
				
			||||||
 | 
					        <div style={{
 | 
				
			||||||
 | 
					          width: '40px',
 | 
				
			||||||
 | 
					          height: '40px',
 | 
				
			||||||
 | 
					          border: '4px solid #f3f3f3',
 | 
				
			||||||
 | 
					          borderTop: '4px solid #667eea',
 | 
				
			||||||
 | 
					          borderRadius: '50%',
 | 
				
			||||||
 | 
					          animation: 'spin 1s linear infinite',
 | 
				
			||||||
 | 
					          marginBottom: '16px'
 | 
				
			||||||
 | 
					        }} />
 | 
				
			||||||
 | 
					        <p style={{ color: '#666', margin: 0 }}>読み込み中...</p>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <style jsx>{`
 | 
				
			||||||
 | 
					          @keyframes spin {
 | 
				
			||||||
 | 
					            0% { transform: rotate(0deg); }
 | 
				
			||||||
 | 
					            100% { transform: rotate(360deg); }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        `}</style>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -115,6 +139,34 @@ export default function App() {
 | 
				
			|||||||
    <div className="app">
 | 
					    <div className="app">
 | 
				
			||||||
      <header className="oauth-app-header">
 | 
					      <header className="oauth-app-header">
 | 
				
			||||||
        <div className="oauth-header-content">
 | 
					        <div className="oauth-header-content">
 | 
				
			||||||
 | 
					          {user && (
 | 
				
			||||||
 | 
					            <div className="oauth-user-profile">
 | 
				
			||||||
 | 
					              <div className="profile-avatar-section">
 | 
				
			||||||
 | 
					                {user.avatar ? (
 | 
				
			||||||
 | 
					                  <img 
 | 
				
			||||||
 | 
					                    src={user.avatar} 
 | 
				
			||||||
 | 
					                    alt={user.displayName || user.handle} 
 | 
				
			||||||
 | 
					                    className="profile-avatar"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <div className="profile-avatar-fallback">
 | 
				
			||||||
 | 
					                    {(user.displayName || user.handle || '?').charAt(0).toUpperCase()}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div className="profile-info">
 | 
				
			||||||
 | 
					                <div className="profile-display-name">
 | 
				
			||||||
 | 
					                  {user.displayName || user.handle}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div className="profile-handle">
 | 
				
			||||||
 | 
					                  @{user.handle}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div className="profile-did">
 | 
				
			||||||
 | 
					                  {user.did}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
          <div className="oauth-header-actions">
 | 
					          <div className="oauth-header-actions">
 | 
				
			||||||
            <AuthButton 
 | 
					            <AuthButton 
 | 
				
			||||||
              user={user} 
 | 
					              user={user} 
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,6 +63,17 @@ export const atproto = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getProfile(bsky, actor) {
 | 
					  async getProfile(bsky, actor) {
 | 
				
			||||||
 | 
					    // Skip test DIDs
 | 
				
			||||||
 | 
					    if (actor && actor.includes('test-')) {
 | 
				
			||||||
 | 
					      logger.log('Skipping profile fetch for test DID:', actor)
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        did: actor,
 | 
				
			||||||
 | 
					        handle: 'test.user',
 | 
				
			||||||
 | 
					        displayName: 'Test User',
 | 
				
			||||||
 | 
					        avatar: null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    return await request(`${bsky}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`)
 | 
					    return await request(`${bsky}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`)
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,54 +1,36 @@
 | 
				
			|||||||
import React, { useEffect, useState } from 'react'
 | 
					import React from 'react'
 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function OAuthCallback({ onAuthSuccess }) {
 | 
					 | 
				
			||||||
  const [status, setStatus] = useState('OAuth認証処理中...')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    handleCallback()
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleCallback = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      // BrowserOAuthClientが自動的にコールバックを処理します
 | 
					 | 
				
			||||||
      // URLのパラメータを確認して成功を通知
 | 
					 | 
				
			||||||
      const urlParams = new URLSearchParams(window.location.search)
 | 
					 | 
				
			||||||
      const code = urlParams.get('code')
 | 
					 | 
				
			||||||
      const error = urlParams.get('error')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (error) {
 | 
					 | 
				
			||||||
        throw new Error(`OAuth error: ${error}`)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (code) {
 | 
					 | 
				
			||||||
        setStatus('認証成功!元のページに戻ります...')
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Get the referring page or use root
 | 
					 | 
				
			||||||
        const referrer = document.referrer || window.location.origin
 | 
					 | 
				
			||||||
        const returnUrl = referrer.includes('/oauth/callback') ? window.location.origin : referrer
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // 少し待ってから元のページにリダイレクト
 | 
					 | 
				
			||||||
        setTimeout(() => {
 | 
					 | 
				
			||||||
          window.location.href = returnUrl
 | 
					 | 
				
			||||||
        }, 1500)
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        setStatus('認証情報が見つかりません')
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      console.error('Callback error:', error)
 | 
					 | 
				
			||||||
      setStatus('認証エラー: ' + error.message)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function OAuthCallback() {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div style={{ padding: '20px', textAlign: 'center' }}>
 | 
					    <div style={{ 
 | 
				
			||||||
      <h2>OAuth認証</h2>
 | 
					      display: 'flex', 
 | 
				
			||||||
      <p>{status}</p>
 | 
					      flexDirection: 'column',
 | 
				
			||||||
      {status.includes('エラー') && (
 | 
					      alignItems: 'center', 
 | 
				
			||||||
        <button onClick={() => window.location.href = '/'}>
 | 
					      justifyContent: 'center',
 | 
				
			||||||
          メインページに戻る
 | 
					      minHeight: '50vh',
 | 
				
			||||||
        </button>
 | 
					      padding: '40px',
 | 
				
			||||||
      )}
 | 
					      textAlign: 'center' 
 | 
				
			||||||
 | 
					    }}>
 | 
				
			||||||
 | 
					      <div style={{
 | 
				
			||||||
 | 
					        width: '50px',
 | 
				
			||||||
 | 
					        height: '50px',
 | 
				
			||||||
 | 
					        border: '4px solid #f3f3f3',
 | 
				
			||||||
 | 
					        borderTop: '4px solid #667eea',
 | 
				
			||||||
 | 
					        borderRadius: '50%',
 | 
				
			||||||
 | 
					        animation: 'spin 1s linear infinite',
 | 
				
			||||||
 | 
					        marginBottom: '20px'
 | 
				
			||||||
 | 
					      }} />
 | 
				
			||||||
 | 
					      <h2 style={{ color: '#333', marginBottom: '12px' }}>OAuth認証処理中...</h2>
 | 
				
			||||||
 | 
					      <p style={{ color: '#666', fontSize: '14px' }}>
 | 
				
			||||||
 | 
					        認証が完了しましたら自動で元のページに戻ります
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <style jsx>{`
 | 
				
			||||||
 | 
					        @keyframes spin {
 | 
				
			||||||
 | 
					          0% { transform: rotate(0deg); }
 | 
				
			||||||
 | 
					          100% { transform: rotate(360deg); }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      `}</style>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -9,8 +9,12 @@ export function useAskAI(adminData, userProfile, agent) {
 | 
				
			|||||||
  const [error, setError] = useState(null)
 | 
					  const [error, setError] = useState(null)
 | 
				
			||||||
  const [chatHistory, setChatHistory] = useState([])
 | 
					  const [chatHistory, setChatHistory] = useState([])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ask-AIサーバーのURL(環境変数から取得、フォールバック付き)
 | 
					  // AI設定を環境変数から取得
 | 
				
			||||||
  const askAIUrl = import.meta.env.VITE_ASK_AI_URL || 'http://localhost:3000/ask'
 | 
					  const aiConfig = {
 | 
				
			||||||
 | 
					    host: import.meta.env.VITE_AI_HOST || 'https://ollama.syui.ai',
 | 
				
			||||||
 | 
					    model: import.meta.env.VITE_AI_MODEL || 'gemma3:1b',
 | 
				
			||||||
 | 
					    systemPrompt: import.meta.env.VITE_AI_SYSTEM_PROMPT || 'あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const askQuestion = async (question) => {
 | 
					  const askQuestion = async (question) => {
 | 
				
			||||||
    if (!question.trim()) return
 | 
					    if (!question.trim()) return
 | 
				
			||||||
@@ -19,28 +23,47 @@ export function useAskAI(adminData, userProfile, agent) {
 | 
				
			|||||||
    setError(null)
 | 
					    setError(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      logger.log('Sending question to ask-AI:', question)
 | 
					      logger.log('Sending question to Ollama:', question)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // ask-AIサーバーにリクエスト送信
 | 
					      // Ollamaに直接リクエスト送信(oauth_oldと同じ方式)
 | 
				
			||||||
      const response = await fetch(askAIUrl, {
 | 
					      const prompt = `${aiConfig.systemPrompt}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Question: ${question}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Answer:`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Add timeout to fetch request
 | 
				
			||||||
 | 
					      const controller = new AbortController()
 | 
				
			||||||
 | 
					      const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 second timeout
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      const response = await fetch(`${aiConfig.host}/api/generate`, {
 | 
				
			||||||
        method: 'POST',
 | 
					        method: 'POST',
 | 
				
			||||||
        headers: {
 | 
					        headers: {
 | 
				
			||||||
          'Content-Type': 'application/json',
 | 
					          'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					          'Origin': 'https://syui.ai',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        body: JSON.stringify({
 | 
					        body: JSON.stringify({
 | 
				
			||||||
          question: question.trim(),
 | 
					          model: aiConfig.model,
 | 
				
			||||||
          context: {
 | 
					          prompt: prompt,
 | 
				
			||||||
            url: window.location.href,
 | 
					          stream: false,
 | 
				
			||||||
            timestamp: new Date().toISOString()
 | 
					          options: {
 | 
				
			||||||
 | 
					            temperature: 0.9,
 | 
				
			||||||
 | 
					            top_p: 0.9,
 | 
				
			||||||
 | 
					            num_predict: 200, // Longer responses for better answers
 | 
				
			||||||
 | 
					            repeat_penalty: 1.1,
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        })
 | 
					        }),
 | 
				
			||||||
 | 
					        signal: controller.signal
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      clearTimeout(timeoutId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!response.ok) {
 | 
					      if (!response.ok) {
 | 
				
			||||||
        throw new Error(`ask-AI server error: ${response.status}`)
 | 
					        throw new Error(`Ollama API error: ${response.status}`)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const aiResponse = await response.json()
 | 
					      const data = await response.json()
 | 
				
			||||||
 | 
					      const aiResponse = { answer: data.response || 'エラーが発生しました' }
 | 
				
			||||||
      logger.log('Received AI response:', aiResponse)
 | 
					      logger.log('Received AI response:', aiResponse)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // AI回答をチャット履歴に追加
 | 
					      // AI回答をチャット履歴に追加
 | 
				
			||||||
@@ -81,7 +104,17 @@ export function useAskAI(adminData, userProfile, agent) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      logError(err, 'useAskAI.askQuestion')
 | 
					      logError(err, 'useAskAI.askQuestion')
 | 
				
			||||||
      setError(getErrorMessage(err))
 | 
					      
 | 
				
			||||||
 | 
					      let errorMessage = 'AI応答の生成に失敗しました'
 | 
				
			||||||
 | 
					      if (err.name === 'AbortError') {
 | 
				
			||||||
 | 
					        errorMessage = 'AI応答がタイムアウトしました(30秒)'
 | 
				
			||||||
 | 
					      } else if (err.message.includes('Ollama API error')) {
 | 
				
			||||||
 | 
					        errorMessage = `Ollama API エラー: ${err.message}`
 | 
				
			||||||
 | 
					      } else if (err.message.includes('Failed to fetch')) {
 | 
				
			||||||
 | 
					        errorMessage = 'AI サーバーに接続できませんでした'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      setError(errorMessage)
 | 
				
			||||||
      throw err
 | 
					      throw err
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setLoading(false)
 | 
					      setLoading(false)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,32 @@ export function useAuth() {
 | 
				
			|||||||
      if (authResult) {
 | 
					      if (authResult) {
 | 
				
			||||||
        setUser(authResult.user)
 | 
					        setUser(authResult.user)
 | 
				
			||||||
        setAgent(authResult.agent)
 | 
					        setAgent(authResult.agent)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // If we're on callback page and authentication succeeded, notify parent
 | 
				
			||||||
 | 
					        if (window.location.pathname === '/oauth/callback') {
 | 
				
			||||||
 | 
					          console.log('OAuth callback completed, notifying parent window')
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // Get referrer or use stored return URL
 | 
				
			||||||
 | 
					          const returnUrl = sessionStorage.getItem('oauth_return_url') || 
 | 
				
			||||||
 | 
					                          document.referrer || 
 | 
				
			||||||
 | 
					                          window.location.origin
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          sessionStorage.removeItem('oauth_return_url')
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // Notify parent window if in iframe, otherwise redirect directly
 | 
				
			||||||
 | 
					          if (window.parent !== window) {
 | 
				
			||||||
 | 
					            window.parent.postMessage({
 | 
				
			||||||
 | 
					              type: 'oauth_success',
 | 
				
			||||||
 | 
					              returnUrl: returnUrl,
 | 
				
			||||||
 | 
					              user: authResult.user
 | 
				
			||||||
 | 
					            }, '*')
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            // Direct redirect
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					              window.location.href = returnUrl
 | 
				
			||||||
 | 
					            }, 1000)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('Auth initialization failed:', error)
 | 
					      console.error('Auth initialization failed:', error)
 | 
				
			||||||
@@ -27,6 +53,11 @@ export function useAuth() {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const login = async (handle) => {
 | 
					  const login = async (handle) => {
 | 
				
			||||||
 | 
					    // Store current page URL for post-auth redirect
 | 
				
			||||||
 | 
					    if (window.location.pathname !== '/oauth/callback') {
 | 
				
			||||||
 | 
					      sessionStorage.setItem('oauth_return_url', window.location.href)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    await oauthService.login(handle)
 | 
					    await oauthService.login(handle)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,6 +65,8 @@ export class OAuthService {
 | 
				
			|||||||
  async processSession(session) {
 | 
					  async processSession(session) {
 | 
				
			||||||
    const did = session.sub || session.did
 | 
					    const did = session.sub || session.did
 | 
				
			||||||
    let handle = session.handle || 'unknown'
 | 
					    let handle = session.handle || 'unknown'
 | 
				
			||||||
 | 
					    let displayName = null
 | 
				
			||||||
 | 
					    let avatar = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Create Agent directly with session (per official docs)
 | 
					    // Create Agent directly with session (per official docs)
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@@ -77,21 +79,43 @@ export class OAuthService {
 | 
				
			|||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.sessionInfo = { did, handle }
 | 
					    // Get profile information using authenticated agent
 | 
				
			||||||
 | 
					    // Skip test DIDs
 | 
				
			||||||
    // Resolve handle if missing
 | 
					    if (this.agent && did && !did.includes('test-')) {
 | 
				
			||||||
    if (handle === 'unknown' && this.agent) {
 | 
					 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        await new Promise(resolve => setTimeout(resolve, 300))
 | 
					        await new Promise(resolve => setTimeout(resolve, 300))
 | 
				
			||||||
        const profile = await this.agent.getProfile({ actor: did })
 | 
					        const profile = await this.agent.getProfile({ actor: did })
 | 
				
			||||||
        handle = profile.data.handle
 | 
					        handle = profile.data.handle || handle
 | 
				
			||||||
        this.sessionInfo.handle = handle
 | 
					        displayName = profile.data.displayName || null
 | 
				
			||||||
 | 
					        avatar = profile.data.avatar || null
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        console.log('Profile fetched from session:', {
 | 
				
			||||||
 | 
					          did,
 | 
				
			||||||
 | 
					          handle,
 | 
				
			||||||
 | 
					          displayName,
 | 
				
			||||||
 | 
					          avatar: avatar ? 'present' : 'none'
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.log('Failed to resolve handle:', error)
 | 
					        console.log('Failed to get profile from session:', error)
 | 
				
			||||||
 | 
					        // Keep the basic info we have
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (did && did.includes('test-')) {
 | 
				
			||||||
 | 
					      console.log('Skipping profile fetch for test DID:', did)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return { did, handle }
 | 
					    this.sessionInfo = { 
 | 
				
			||||||
 | 
					      did, 
 | 
				
			||||||
 | 
					      handle, 
 | 
				
			||||||
 | 
					      displayName, 
 | 
				
			||||||
 | 
					      avatar 
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { 
 | 
				
			||||||
 | 
					      did, 
 | 
				
			||||||
 | 
					      handle, 
 | 
				
			||||||
 | 
					      displayName, 
 | 
				
			||||||
 | 
					      avatar 
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async login(handle) {
 | 
					  async login(handle) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,6 +33,12 @@ async function getDid(handle) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// DIDからプロフィール情報を取得
 | 
					// DIDからプロフィール情報を取得
 | 
				
			||||||
async function getProfile(did, handle) {
 | 
					async function getProfile(did, handle) {
 | 
				
			||||||
 | 
					  // Skip test DIDs
 | 
				
			||||||
 | 
					  if (did && did.includes('test-')) {
 | 
				
			||||||
 | 
					    logger.log('Skipping profile fetch for test DID:', did)
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Determine which public API to use based on handle
 | 
					    // Determine which public API to use based on handle
 | 
				
			||||||
    const pds = await getPdsFromHandle(handle)
 | 
					    const pds = await getPdsFromHandle(handle)
 | 
				
			||||||
@@ -81,6 +87,11 @@ async function fetchFreshAvatar(handle, did) {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    // プロフィール取得
 | 
					    // プロフィール取得
 | 
				
			||||||
    const profile = await getProfile(actualDid, handle)
 | 
					    const profile = await getProfile(actualDid, handle)
 | 
				
			||||||
 | 
					    if (!profile) {
 | 
				
			||||||
 | 
					      // Test DID or profile fetch failed
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    const avatarUrl = profile.avatar || null
 | 
					    const avatarUrl = profile.avatar || null
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // キャッシュに保存
 | 
					    // キャッシュに保存
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -315,6 +315,8 @@ struct JetstreamMessage {
 | 
				
			|||||||
struct JetstreamCommit {
 | 
					struct JetstreamCommit {
 | 
				
			||||||
    operation: Option<String>,
 | 
					    operation: Option<String>,
 | 
				
			||||||
    uri: Option<String>,
 | 
					    uri: Option<String>,
 | 
				
			||||||
 | 
					    record: Option<Value>,
 | 
				
			||||||
 | 
					    collection: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
					#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
				
			||||||
@@ -333,7 +335,6 @@ struct UserListRecord {
 | 
				
			|||||||
    created_at: String,
 | 
					    created_at: String,
 | 
				
			||||||
    #[serde(rename = "updatedBy")]
 | 
					    #[serde(rename = "updatedBy")]
 | 
				
			||||||
    updated_by: UserInfo,
 | 
					    updated_by: UserInfo,
 | 
				
			||||||
    metadata: Option<Value>,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Serialize, Deserialize)]
 | 
					#[derive(Debug, Serialize, Deserialize)]
 | 
				
			||||||
@@ -423,10 +424,7 @@ pub async fn init_user_list(project_dir: Option<PathBuf>, handles: Option<String
 | 
				
			|||||||
    // Create the initial user list
 | 
					    // Create the initial user list
 | 
				
			||||||
    println!("{}", format!("📝 Creating user list with {} users...", users.len()).cyan());
 | 
					    println!("{}", format!("📝 Creating user list with {} users...", users.len()).cyan());
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    match post_user_list(&mut config, &users, json!({
 | 
					    match post_user_list(&mut config, &users).await {
 | 
				
			||||||
        "reason": "initial_setup",
 | 
					 | 
				
			||||||
        "created_by": "ailog_stream_init"
 | 
					 | 
				
			||||||
    })).await {
 | 
					 | 
				
			||||||
        Ok(_) => println!("{}", "✅ User list created successfully!".green()),
 | 
					        Ok(_) => println!("{}", "✅ User list created successfully!".green()),
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            println!("{}", format!("❌ Failed to create user list: {}", e).red());
 | 
					            println!("{}", format!("❌ Failed to create user list: {}", e).red());
 | 
				
			||||||
@@ -606,13 +604,15 @@ async fn run_monitor(config: &mut AuthConfig) -> Result<()> {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    let (mut write, mut read) = ws_stream.split();
 | 
					    let (mut write, mut read) = ws_stream.split();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Subscribe to collections
 | 
					    // Subscribe to collections using Jetstream 2.0 format
 | 
				
			||||||
    let subscribe_msg = json!({
 | 
					    let subscribe_msg = json!({
 | 
				
			||||||
        "wantedCollections": config.jetstream.collections
 | 
					        "wantedCollections": config.jetstream.collections,
 | 
				
			||||||
 | 
					        "wantedDids": [config.admin.did]
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    write.send(Message::Text(subscribe_msg.to_string())).await?;
 | 
					    write.send(Message::Text(subscribe_msg.to_string())).await?;
 | 
				
			||||||
    println!("{}", "📨 Subscribed to collections".blue());
 | 
					    println!("{}", format!("📨 Subscribed to collections: {:?} for DID: {}", 
 | 
				
			||||||
 | 
					             config.jetstream.collections, config.admin.did).blue());
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Start periodic polling task
 | 
					    // Start periodic polling task
 | 
				
			||||||
    let config_clone = config.clone();
 | 
					    let config_clone = config.clone();
 | 
				
			||||||
@@ -625,15 +625,27 @@ async fn run_monitor(config: &mut AuthConfig) -> Result<()> {
 | 
				
			|||||||
        while let Some(msg) = read.next().await {
 | 
					        while let Some(msg) = read.next().await {
 | 
				
			||||||
            match msg? {
 | 
					            match msg? {
 | 
				
			||||||
                Message::Text(text) => {
 | 
					                Message::Text(text) => {
 | 
				
			||||||
                    // Filter out standard Bluesky collections for cleaner output
 | 
					                    // Check if this is a commit message with our collection
 | 
				
			||||||
 | 
					                    let is_custom_collection = text.contains("ai.syui.log") || 
 | 
				
			||||||
 | 
					                                             text.contains(&config.admin.did);
 | 
				
			||||||
                    let should_debug = std::env::var("AILOG_DEBUG").is_ok();
 | 
					                    let should_debug = std::env::var("AILOG_DEBUG").is_ok();
 | 
				
			||||||
                    let is_standard_collection = text.contains("app.bsky.feed.") || 
 | 
					                    let is_standard_collection = text.contains("app.bsky.feed.") || 
 | 
				
			||||||
                                               text.contains("app.bsky.actor.") ||
 | 
					                                               text.contains("app.bsky.actor.") ||
 | 
				
			||||||
                                               text.contains("app.bsky.graph.");
 | 
					                                               text.contains("app.bsky.graph.") ||
 | 
				
			||||||
 | 
					                                               text.contains("blue.flashes.") ||
 | 
				
			||||||
 | 
					                                               text.contains("\"kind\":\"identity\"") ||
 | 
				
			||||||
 | 
					                                               text.contains("\"kind\":\"account\"");
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    // Only show debug for custom collections or when explicitly requested
 | 
					                    // Always show custom collection messages
 | 
				
			||||||
 | 
					                    if is_custom_collection {
 | 
				
			||||||
 | 
					                        println!("{}", format!("🎯 Custom collection message: {}", text).green().bold());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Only show debug for non-standard collections or when explicitly requested
 | 
				
			||||||
                    if should_debug && (!is_standard_collection || std::env::var("AILOG_DEBUG_ALL").is_ok()) {
 | 
					                    if should_debug && (!is_standard_collection || std::env::var("AILOG_DEBUG_ALL").is_ok()) {
 | 
				
			||||||
                        println!("{}", format!("🔍 Received: {}", text).blue());
 | 
					                        if !is_custom_collection { // Avoid double printing
 | 
				
			||||||
 | 
					                            println!("{}", format!("🔍 Received: {}", text).blue());
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    if let Err(e) = handle_message(&text, config).await {
 | 
					                    if let Err(e) = handle_message(&text, config).await {
 | 
				
			||||||
@@ -671,7 +683,17 @@ async fn run_monitor(config: &mut AuthConfig) -> Result<()> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn handle_message(text: &str, config: &mut AuthConfig) -> Result<()> {
 | 
					async fn handle_message(text: &str, config: &mut AuthConfig) -> Result<()> {
 | 
				
			||||||
    let message: JetstreamMessage = serde_json::from_str(text)?;
 | 
					    // println!("🔧 handle_message called with text length: {}", text.len());
 | 
				
			||||||
 | 
					    let message: JetstreamMessage = match serde_json::from_str(text) {
 | 
				
			||||||
 | 
					        Ok(msg) => {
 | 
				
			||||||
 | 
					            // println!("✅ JSON parsed successfully");
 | 
				
			||||||
 | 
					            msg
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            println!("❌ JSON parse error: {}", e);
 | 
				
			||||||
 | 
					            return Err(e.into());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Debug: Check all received collections (but filter standard ones)
 | 
					    // Debug: Check all received collections (but filter standard ones)
 | 
				
			||||||
    if let Some(collection) = &message.collection {
 | 
					    if let Some(collection) = &message.collection {
 | 
				
			||||||
@@ -688,35 +710,60 @@ async fn handle_message(text: &str, config: &mut AuthConfig) -> Result<()> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Check if this is a comment creation
 | 
					    // Check if this is a comment creation
 | 
				
			||||||
    if let (Some(collection), Some(commit), Some(did)) = 
 | 
					    if let (Some(commit), Some(did)) = (&message.commit, &message.did) {
 | 
				
			||||||
        (&message.collection, &message.commit, &message.did) {
 | 
					        if let Some(collection) = &commit.collection {
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if collection == &config.collections.comment() && commit.operation.as_deref() == Some("create") {
 | 
					        // Monitor both ai.syui.log and ai.syui.log.chat.comment collections
 | 
				
			||||||
 | 
					        let is_main_collection = collection == &config.collections.comment();
 | 
				
			||||||
 | 
					        let is_chat_comment_collection = collection == "ai.syui.log.chat.comment";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if collection == "ai.syui.log" || collection == "ai.syui.log.chat.comment" {
 | 
				
			||||||
 | 
					            println!("   🔍 Debug: collection='{}', expected='{}', is_main={}, is_chat={}", 
 | 
				
			||||||
 | 
					                    collection, config.collections.comment(), is_main_collection, is_chat_comment_collection);
 | 
				
			||||||
 | 
					            println!("   🔍 Debug: operation={:?}", commit.operation);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (is_main_collection || is_chat_comment_collection) && commit.operation.as_deref() == Some("create") {
 | 
				
			||||||
            let unknown_uri = "unknown".to_string();
 | 
					            let unknown_uri = "unknown".to_string();
 | 
				
			||||||
            let uri = commit.uri.as_ref().unwrap_or(&unknown_uri);
 | 
					            let uri = commit.uri.as_ref().unwrap_or(&unknown_uri);
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            println!("{}", "🆕 New comment detected!".green().bold());
 | 
					            let collection_type = if is_main_collection { 
 | 
				
			||||||
 | 
					                "main collection (ai.syui.log)" 
 | 
				
			||||||
 | 
					            } else { 
 | 
				
			||||||
 | 
					                "chat comment (ai.syui.log.chat.comment)" 
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            println!("{}", format!("🆕 New comment detected from {}!", collection_type).green().bold());
 | 
				
			||||||
            println!("   📝 URI: {}", uri);
 | 
					            println!("   📝 URI: {}", uri);
 | 
				
			||||||
            println!("   👤 Author DID: {}", did);
 | 
					            println!("   👤 Author DID: {}", did);
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // Resolve handle
 | 
					            // Extract author info from the jetstream record
 | 
				
			||||||
            let ai_config = load_ai_config_from_project().unwrap_or_default();
 | 
					            if let Some(record) = &commit.record {
 | 
				
			||||||
            match resolve_handle(did, &ai_config.network).await {
 | 
					                if let Some(author) = record.get("author") {
 | 
				
			||||||
                Ok(handle) => {
 | 
					                    if let (Some(author_did), Some(author_handle)) = (
 | 
				
			||||||
                    println!("   🏷️  Handle: {}", handle.cyan());
 | 
					                        author.get("did").and_then(|v| v.as_str()),
 | 
				
			||||||
                    
 | 
					                        author.get("handle").and_then(|v| v.as_str())
 | 
				
			||||||
                    // Update user list
 | 
					                    ) {
 | 
				
			||||||
                    if let Err(e) = update_user_list(config, did, &handle).await {
 | 
					                        println!("   🏷️  Handle from record: {}", author_handle.cyan());
 | 
				
			||||||
                        println!("{}", format!("   ⚠️  Failed to update user list: {}", e).yellow());
 | 
					                        
 | 
				
			||||||
 | 
					                        // Update user list with jetstream author info
 | 
				
			||||||
 | 
					                        if let Err(e) = update_user_list(config, author_did, author_handle).await {
 | 
				
			||||||
 | 
					                            println!("{}", format!("   ⚠️  Failed to update user list: {}", e).yellow());
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            println!("{}", "   ✅ User list updated successfully".green());
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        println!("{}", "   ⚠️  Missing author DID or handle in record".yellow());
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    println!("{}", "   ⚠️  No author info in record".yellow());
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Err(e) => {
 | 
					            } else {
 | 
				
			||||||
                    println!("{}", format!("   ⚠️  Failed to resolve handle: {}", e).yellow());
 | 
					                println!("{}", "   ⚠️  No record data in commit".yellow());
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            println!();
 | 
					            println!();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
@@ -862,11 +909,7 @@ async fn update_user_list(config: &mut AuthConfig, did: &str, handle: &str) -> R
 | 
				
			|||||||
    updated_users.push(new_user);
 | 
					    updated_users.push(new_user);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Post updated user list
 | 
					    // Post updated user list
 | 
				
			||||||
    post_user_list(config, &updated_users, json!({
 | 
					    post_user_list(config, &updated_users).await?;
 | 
				
			||||||
        "reason": "auto_add_commenter",
 | 
					 | 
				
			||||||
        "trigger_did": did,
 | 
					 | 
				
			||||||
        "trigger_handle": handle
 | 
					 | 
				
			||||||
    })).await?;
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    println!("{}", "   ✅ User list updated successfully".green());
 | 
					    println!("{}", "   ✅ User list updated successfully".green());
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -930,7 +973,7 @@ async fn get_current_user_list(config: &mut AuthConfig) -> Result<Vec<UserRecord
 | 
				
			|||||||
    Ok(user_list)
 | 
					    Ok(user_list)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn post_user_list(config: &mut AuthConfig, users: &[UserRecord], metadata: Value) -> Result<()> {
 | 
					async fn post_user_list(config: &mut AuthConfig, users: &[UserRecord]) -> Result<()> {
 | 
				
			||||||
    let client = reqwest::Client::new();
 | 
					    let client = reqwest::Client::new();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    let now = chrono::Utc::now();
 | 
					    let now = chrono::Utc::now();
 | 
				
			||||||
@@ -948,7 +991,6 @@ async fn post_user_list(config: &mut AuthConfig, users: &[UserRecord], metadata:
 | 
				
			|||||||
            did: config.admin.did.clone(),
 | 
					            did: config.admin.did.clone(),
 | 
				
			||||||
            handle: config.admin.handle.clone(),
 | 
					            handle: config.admin.handle.clone(),
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        metadata: Some(metadata.clone()),
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    let url = format!("{}/xrpc/com.atproto.repo.putRecord", config.admin.pds);
 | 
					    let url = format!("{}/xrpc/com.atproto.repo.putRecord", config.admin.pds);
 | 
				
			||||||
@@ -975,7 +1017,7 @@ async fn post_user_list(config: &mut AuthConfig, users: &[UserRecord], metadata:
 | 
				
			|||||||
            if let Ok(_) = super::auth::load_config_with_refresh().await {
 | 
					            if let Ok(_) = super::auth::load_config_with_refresh().await {
 | 
				
			||||||
                let refreshed_config = super::auth::load_config()?;
 | 
					                let refreshed_config = super::auth::load_config()?;
 | 
				
			||||||
                *config = refreshed_config;
 | 
					                *config = refreshed_config;
 | 
				
			||||||
                return Box::pin(post_user_list(config, users, metadata)).await;
 | 
					                return Box::pin(post_user_list(config, users)).await;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let error_text = response.text().await?;
 | 
					        let error_text = response.text().await?;
 | 
				
			||||||
@@ -1113,17 +1155,35 @@ async fn poll_comments_periodically(mut config: AuthConfig) -> Result<()> {
 | 
				
			|||||||
    let mut known_comments = HashSet::new();
 | 
					    let mut known_comments = HashSet::new();
 | 
				
			||||||
    let mut interval = interval(Duration::from_secs(30)); // Poll every 30 seconds
 | 
					    let mut interval = interval(Duration::from_secs(30)); // Poll every 30 seconds
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Initial population of known comments
 | 
					    // Initial population of known comments - only add comments older than 5 minutes to allow recent ones to be processed
 | 
				
			||||||
    if let Ok(comments) = get_recent_comments(&mut config).await {
 | 
					    if let Ok(comments) = get_recent_comments(&mut config).await {
 | 
				
			||||||
        for comment in &comments {
 | 
					        for comment in &comments {
 | 
				
			||||||
            if let Some(uri) = comment.get("uri").and_then(|v| v.as_str()) {
 | 
					            if let Some(uri) = comment.get("uri").and_then(|v| v.as_str()) {
 | 
				
			||||||
                known_comments.insert(uri.to_string());
 | 
					                // Check if this comment is old enough to be considered "existing"
 | 
				
			||||||
                if std::env::var("AILOG_DEBUG").is_ok() {
 | 
					                let is_old_comment = if let Some(value) = comment.get("value") {
 | 
				
			||||||
                    println!("{}", format!("🔍 Existing comment: {}", uri).blue());
 | 
					                    if let Some(created_at) = value.get("createdAt").and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                        !is_recent_comment(created_at)
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        true // If no timestamp, consider it old
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    true
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if is_old_comment {
 | 
				
			||||||
 | 
					                    known_comments.insert(uri.to_string());
 | 
				
			||||||
 | 
					                    if std::env::var("AILOG_DEBUG").is_ok() {
 | 
				
			||||||
 | 
					                        println!("{}", format!("🔍 Existing comment: {}", uri).blue());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    if std::env::var("AILOG_DEBUG").is_ok() {
 | 
				
			||||||
 | 
					                        println!("{}", format!("🆕 Recent comment will be processed: {}", uri).green());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        println!("{}", format!("📝 Found {} existing comments", known_comments.len()).blue());
 | 
					        println!("{}", format!("📝 Found {} existing comments, {} recent comments will be processed", 
 | 
				
			||||||
 | 
					                 known_comments.len(), comments.len() - known_comments.len()).blue());
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Debug: Show full response for first comment
 | 
					        // Debug: Show full response for first comment
 | 
				
			||||||
        if std::env::var("AILOG_DEBUG").is_ok() && !comments.is_empty() {
 | 
					        if std::env::var("AILOG_DEBUG").is_ok() && !comments.is_empty() {
 | 
				
			||||||
@@ -1161,24 +1221,23 @@ async fn poll_comments_periodically(mut config: AuthConfig) -> Result<()> {
 | 
				
			|||||||
                                    println!("{}", "🆕 New comment detected via polling!".green().bold());
 | 
					                                    println!("{}", "🆕 New comment detected via polling!".green().bold());
 | 
				
			||||||
                                    println!("   📝 URI: {}", uri);
 | 
					                                    println!("   📝 URI: {}", uri);
 | 
				
			||||||
                                    
 | 
					                                    
 | 
				
			||||||
                                    // Extract author DID from URI
 | 
					                                    // Extract author DID and handle from comment value
 | 
				
			||||||
                                    if let Some(did) = extract_did_from_uri(uri) {
 | 
					                                    if let Some(author) = value.get("author") {
 | 
				
			||||||
                                        println!("   👤 Author DID: {}", did);
 | 
					                                        if let (Some(author_did), Some(author_handle)) = (
 | 
				
			||||||
                                        
 | 
					                                            author.get("did").and_then(|v| v.as_str()),
 | 
				
			||||||
                                        // Resolve handle and update user list
 | 
					                                            author.get("handle").and_then(|v| v.as_str())
 | 
				
			||||||
                                        let ai_config = load_ai_config_from_project().unwrap_or_default();
 | 
					                                        ) {
 | 
				
			||||||
                                        match resolve_handle(&did, &ai_config.network).await {
 | 
					                                            println!("   👤 Author DID: {}", author_did);
 | 
				
			||||||
                                            Ok(handle) => {
 | 
					                                            println!("   🏷️  Handle: {}", author_handle.cyan());
 | 
				
			||||||
                                                println!("   🏷️  Handle: {}", handle.cyan());
 | 
					                                            
 | 
				
			||||||
                                                
 | 
					                                            if let Err(e) = update_user_list(&mut config, author_did, author_handle).await {
 | 
				
			||||||
                                                if let Err(e) = update_user_list(&mut config, &did, &handle).await {
 | 
					                                                println!("{}", format!("   ⚠️  Failed to update user list: {}", e).yellow());
 | 
				
			||||||
                                                    println!("{}", format!("   ⚠️  Failed to update user list: {}", e).yellow());
 | 
					 | 
				
			||||||
                                                }
 | 
					 | 
				
			||||||
                                            }
 | 
					 | 
				
			||||||
                                            Err(e) => {
 | 
					 | 
				
			||||||
                                                println!("{}", format!("   ⚠️  Failed to resolve handle: {}", e).yellow());
 | 
					 | 
				
			||||||
                                            }
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        } else {
 | 
				
			||||||
 | 
					                                            println!("{}", "   ⚠️  Comment missing author DID or handle".yellow());
 | 
				
			||||||
                                        }
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    } else {
 | 
				
			||||||
 | 
					                                        println!("{}", "   ⚠️  Comment missing author information".yellow());
 | 
				
			||||||
                                    }
 | 
					                                    }
 | 
				
			||||||
                                    
 | 
					                                    
 | 
				
			||||||
                                    println!();
 | 
					                                    println!();
 | 
				
			||||||
@@ -1248,8 +1307,8 @@ fn is_recent_comment(created_at: &str) -> bool {
 | 
				
			|||||||
        let comment_utc = comment_time.with_timezone(&Utc);
 | 
					        let comment_utc = comment_time.with_timezone(&Utc);
 | 
				
			||||||
        let diff = now.signed_duration_since(comment_utc);
 | 
					        let diff = now.signed_duration_since(comment_utc);
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Consider comments from the last 5 minutes as "recent"
 | 
					        // Consider comments from the last 15 minutes as "recent" (more generous for testing)
 | 
				
			||||||
        diff <= Duration::minutes(5) && diff >= Duration::zero()
 | 
					        diff <= Duration::minutes(15) && diff >= Duration::zero()
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        false
 | 
					        false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -1327,6 +1386,197 @@ async fn resolve_handle_to_did(handle: &str, network_config: &NetworkConfig) ->
 | 
				
			|||||||
    Ok(did.to_string())
 | 
					    Ok(did.to_string())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn test_polling_cycle() -> Result<()> {
 | 
				
			||||||
 | 
					    println!("{}", "🧪 Testing complete polling cycle logic...".cyan().bold());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let mut config = load_config_with_refresh().await?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("👤 Testing as: {}", config.admin.handle.green());
 | 
				
			||||||
 | 
					    println!("🌐 PDS: {}", config.admin.pds);
 | 
				
			||||||
 | 
					    println!("🆔 DID: {}", config.admin.did);
 | 
				
			||||||
 | 
					    println!();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Simulate the polling logic exactly as it runs in the stream
 | 
				
			||||||
 | 
					    let mut known_comments = HashSet::new();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Initial population (skip recent comments)
 | 
				
			||||||
 | 
					    println!("{}", "📊 Step 1: Initial population of known comments".cyan());
 | 
				
			||||||
 | 
					    if let Ok(comments) = get_recent_comments(&mut config).await {
 | 
				
			||||||
 | 
					        for comment in &comments {
 | 
				
			||||||
 | 
					            if let Some(uri) = comment.get("uri").and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                let is_old_comment = if let Some(value) = comment.get("value") {
 | 
				
			||||||
 | 
					                    if let Some(created_at) = value.get("createdAt").and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                        !is_recent_comment(created_at)
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        true
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    true
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if is_old_comment {
 | 
				
			||||||
 | 
					                    known_comments.insert(uri.to_string());
 | 
				
			||||||
 | 
					                    println!("   🔍 Added to known: {}", uri);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    println!("   🆕 Skipped (recent): {}", uri);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        println!("{}", format!("📝 Found {} existing comments, {} recent comments will be processed", 
 | 
				
			||||||
 | 
					                 known_comments.len(), comments.len() - known_comments.len()).blue());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Simulate first polling cycle
 | 
				
			||||||
 | 
					    println!("{}", "\n📊 Step 2: First polling cycle".cyan());
 | 
				
			||||||
 | 
					    if let Ok(comments) = get_recent_comments(&mut config).await {
 | 
				
			||||||
 | 
					        for comment in comments {
 | 
				
			||||||
 | 
					            if let (Some(uri), Some(value)) = (
 | 
				
			||||||
 | 
					                comment.get("uri").and_then(|v| v.as_str()),
 | 
				
			||||||
 | 
					                comment.get("value")
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                if !known_comments.contains(uri) {
 | 
				
			||||||
 | 
					                    known_comments.insert(uri.to_string());
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if let Some(created_at) = value.get("createdAt").and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                        if is_recent_comment(created_at) {
 | 
				
			||||||
 | 
					                            println!("{}", "🆕 New comment detected via polling!".green().bold());
 | 
				
			||||||
 | 
					                            println!("   📝 URI: {}", uri);
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Extract author DID and handle from comment value
 | 
				
			||||||
 | 
					                            if let Some(author) = value.get("author") {
 | 
				
			||||||
 | 
					                                if let (Some(author_did), Some(author_handle)) = (
 | 
				
			||||||
 | 
					                                    author.get("did").and_then(|v| v.as_str()),
 | 
				
			||||||
 | 
					                                    author.get("handle").and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					                                ) {
 | 
				
			||||||
 | 
					                                    println!("   👤 Author DID: {}", author_did);
 | 
				
			||||||
 | 
					                                    println!("   🏷️  Handle: {}", author_handle.cyan());
 | 
				
			||||||
 | 
					                                    
 | 
				
			||||||
 | 
					                                    println!("   🧪 Calling update_user_list...");
 | 
				
			||||||
 | 
					                                    match update_user_list(&mut config, author_did, author_handle).await {
 | 
				
			||||||
 | 
					                                        Ok(_) => {
 | 
				
			||||||
 | 
					                                            println!("   ✅ User list updated successfully!");
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                        Err(e) => {
 | 
				
			||||||
 | 
					                                            println!("{}", format!("   ❌ Failed to update user list: {}", e).red());
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    println!("{}", "   ⚠️  Comment missing author DID or handle".yellow());
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                println!("{}", "   ⚠️  Comment missing author information".yellow());
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            println!();
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            println!("   ⏭️  Not recent: {}", uri);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    println!("   ⏭️  Already known: {}", uri);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn test_recent_detection() -> Result<()> {
 | 
				
			||||||
 | 
					    println!("{}", "🧪 Testing recent comment detection logic...".cyan().bold());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let mut config = load_config_with_refresh().await?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("👤 Testing as: {}", config.admin.handle.green());
 | 
				
			||||||
 | 
					    println!("🌐 PDS: {}", config.admin.pds);
 | 
				
			||||||
 | 
					    println!("🆔 DID: {}", config.admin.did);
 | 
				
			||||||
 | 
					    println!();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Get recent comments and test the detection logic
 | 
				
			||||||
 | 
					    match get_recent_comments(&mut config).await {
 | 
				
			||||||
 | 
					        Ok(comments) => {
 | 
				
			||||||
 | 
					            println!("{}", format!("📊 Retrieved {} comments from API", comments.len()).cyan());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for (i, comment) in comments.iter().enumerate() {
 | 
				
			||||||
 | 
					                if let (Some(uri), Some(value)) = (
 | 
				
			||||||
 | 
					                    comment.get("uri").and_then(|v| v.as_str()),
 | 
				
			||||||
 | 
					                    comment.get("value")
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                    println!("   {}. URI: {}", i + 1, uri);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if let Some(created_at) = value.get("createdAt").and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                        let is_recent = is_recent_comment(created_at);
 | 
				
			||||||
 | 
					                        println!("      Created: {} - Recent: {}", created_at, 
 | 
				
			||||||
 | 
					                                if is_recent { "✅ YES".green() } else { "❌ NO".red() });
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        if is_recent {
 | 
				
			||||||
 | 
					                            if let Some(did) = extract_did_from_uri(uri) {
 | 
				
			||||||
 | 
					                                println!("      🎯 Would process: DID {} from recent comment", did.cyan());
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if let Some(author) = value.get("author") {
 | 
				
			||||||
 | 
					                        if let Some(author_did) = author.get("did").and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                            if let Some(handle) = author.get("handle").and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                                println!("      👤 Author: {} ({})", handle, author_did);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    println!();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            println!("{}", format!("❌ Failed to get comments: {}", e).red());
 | 
				
			||||||
 | 
					            return Err(e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn test_user_update() -> Result<()> {
 | 
				
			||||||
 | 
					    println!("{}", "🧪 Testing user list update functionality...".cyan().bold());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let mut config = load_config_with_refresh().await?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("👤 Testing as: {}", config.admin.handle.green());
 | 
				
			||||||
 | 
					    println!("🌐 PDS: {}", config.admin.pds);
 | 
				
			||||||
 | 
					    println!("🆔 DID: {}", config.admin.did);
 | 
				
			||||||
 | 
					    println!();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Get existing user list
 | 
				
			||||||
 | 
					    let current_users = get_current_user_list(&mut config).await?;
 | 
				
			||||||
 | 
					    println!("{}", format!("📋 Current user list has {} users", current_users.len()).cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for user in ¤t_users {
 | 
				
			||||||
 | 
					        println!("   👤 {} ({}) - {}", user.handle, user.did, user.pds);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Test adding a dummy user (simulate a new commenter)
 | 
				
			||||||
 | 
					    let test_did = "did:plc:test-user-update-12345";
 | 
				
			||||||
 | 
					    let test_handle = "test.user.bsky.social";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("{}", format!("🧪 Simulating new user: {} ({})", test_handle, test_did).yellow());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    match update_user_list(&mut config, test_did, test_handle).await {
 | 
				
			||||||
 | 
					        Ok(_) => {
 | 
				
			||||||
 | 
					            println!("{}", "✅ User list update test successful!".green());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Verify the update
 | 
				
			||||||
 | 
					            let updated_users = get_current_user_list(&mut config).await?;
 | 
				
			||||||
 | 
					            println!("{}", format!("📋 Updated user list now has {} users", updated_users.len()).cyan());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            println!("{}", format!("❌ User list update test failed: {}", e).red());
 | 
				
			||||||
 | 
					            return Err(e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn test_api() -> Result<()> {
 | 
					pub async fn test_api() -> Result<()> {
 | 
				
			||||||
    println!("{}", "🧪 Testing API access to comments collection...".cyan().bold());
 | 
					    println!("{}", "🧪 Testing API access to comments collection...".cyan().bold());
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -1356,8 +1606,9 @@ pub async fn test_api() -> Result<()> {
 | 
				
			|||||||
                            println!("      Created: {}", created_at);
 | 
					                            println!("      Created: {}", created_at);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        if let Some(text) = value.get("text").and_then(|v| v.as_str()) {
 | 
					                        if let Some(text) = value.get("text").and_then(|v| v.as_str()) {
 | 
				
			||||||
                            let preview = if text.len() > 50 {
 | 
					                            let preview = if text.chars().count() > 50 {
 | 
				
			||||||
                                format!("{}...", &text[..50])
 | 
					                                let truncated: String = text.chars().take(50).collect();
 | 
				
			||||||
 | 
					                                format!("{}...", truncated)
 | 
				
			||||||
                            } else {
 | 
					                            } else {
 | 
				
			||||||
                                text.to_string()
 | 
					                                text.to_string()
 | 
				
			||||||
                            };
 | 
					                            };
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -152,6 +152,12 @@ enum StreamCommands {
 | 
				
			|||||||
    Status,
 | 
					    Status,
 | 
				
			||||||
    /// Test API access to comments collection
 | 
					    /// Test API access to comments collection
 | 
				
			||||||
    Test,
 | 
					    Test,
 | 
				
			||||||
 | 
					    /// Test user list update functionality
 | 
				
			||||||
 | 
					    TestUserUpdate,
 | 
				
			||||||
 | 
					    /// Test recent comment detection logic
 | 
				
			||||||
 | 
					    TestRecentDetection,
 | 
				
			||||||
 | 
					    /// Test complete polling cycle logic
 | 
				
			||||||
 | 
					    TestPollingCycle,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Subcommand)]
 | 
					#[derive(Subcommand)]
 | 
				
			||||||
@@ -235,6 +241,15 @@ async fn main() -> Result<()> {
 | 
				
			|||||||
                StreamCommands::Test => {
 | 
					                StreamCommands::Test => {
 | 
				
			||||||
                    commands::stream::test_api().await?;
 | 
					                    commands::stream::test_api().await?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                StreamCommands::TestUserUpdate => {
 | 
				
			||||||
 | 
					                    commands::stream::test_user_update().await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                StreamCommands::TestRecentDetection => {
 | 
				
			||||||
 | 
					                    commands::stream::test_recent_detection().await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                StreamCommands::TestPollingCycle => {
 | 
				
			||||||
 | 
					                    commands::stream::test_polling_cycle().await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Commands::Oauth { command } => {
 | 
					        Commands::Oauth { command } => {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user