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