Compare commits
13 Commits
v0.2.1
...
292bf39c4e
Author | SHA1 | Date | |
---|---|---|---|
292bf39c4e
|
|||
be8fd38c59
|
|||
abc42330ed
|
|||
2143d5b142
|
|||
549f728b7a
|
|||
849490f5b2
|
|||
c1d3678d96
|
|||
fcab7c7f83
|
|||
51e4a492bc
|
|||
9ed35a6a1e
|
|||
74e1014e77
|
|||
820e47f634
|
|||
4dac4a83e0
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ my-blog/templates/oauth-assets.html
|
||||
cloudflared-config.yml
|
||||
.config
|
||||
oauth-server-example
|
||||
atproto
|
||||
|
Binary file not shown.
@@ -17,7 +17,7 @@ comment_moderation = false
|
||||
ask_ai = true
|
||||
provider = "ollama"
|
||||
model = "gemma3:4b"
|
||||
host = "https://localhost:11434"
|
||||
host = "http://localhost:11434"
|
||||
system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
|
||||
handle = "ai.syui.ai"
|
||||
#num_predict = 200
|
||||
|
@@ -16,5 +16,5 @@ VITE_AI_ENABLED=true
|
||||
VITE_AI_ASK_AI=true
|
||||
VITE_AI_PROVIDER=ollama
|
||||
VITE_AI_MODEL=gemma3:4b
|
||||
VITE_AI_HOST=https://localhost:11434
|
||||
VITE_AI_HOST=http://localhost:11434
|
||||
VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
|
||||
|
@@ -221,19 +221,25 @@ function App() {
|
||||
const oauthResult = await atprotoOAuthService.checkSession();
|
||||
|
||||
if (oauthResult) {
|
||||
console.log('OAuth result found:', oauthResult);
|
||||
|
||||
// Ensure handle is not DID
|
||||
const handle = oauthResult.handle !== oauthResult.did ? oauthResult.handle : oauthResult.handle;
|
||||
console.log('Processed handle:', handle);
|
||||
|
||||
// Check if handle is allowed
|
||||
if (appConfig.allowedHandles.length > 0 && !appConfig.allowedHandles.includes(handle)) {
|
||||
console.log('Handle not allowed:', handle, 'Allowed:', appConfig.allowedHandles);
|
||||
// Handle not in allowed list
|
||||
setError(`Access denied: ${handle} is not authorized for this application.`);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Getting user profile for:', oauthResult.did, handle);
|
||||
// Get user profile including avatar
|
||||
const userProfile = await getUserProfile(oauthResult.did, handle);
|
||||
console.log('User profile received:', userProfile);
|
||||
setUser(userProfile);
|
||||
|
||||
// Load all comments for display (this will be the default view)
|
||||
@@ -550,6 +556,13 @@ function App() {
|
||||
const profileResponse = await fetch(`${apiEndpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`);
|
||||
if (profileResponse.ok) {
|
||||
const profileData = await profileResponse.json();
|
||||
|
||||
// Determine correct web URL based on avatar source
|
||||
let webUrl = config.webUrl; // Default from handle-based detection
|
||||
if (profileData.avatar && profileData.avatar.includes('cdn.bsky.app')) {
|
||||
webUrl = 'https://bsky.app'; // Override to Bluesky if avatar is from Bluesky
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
value: {
|
||||
@@ -559,7 +572,7 @@ function App() {
|
||||
avatar: profileData.avatar,
|
||||
displayName: profileData.displayName || handle,
|
||||
_pdsEndpoint: `https://${pds}`, // Store PDS info for later use
|
||||
_webUrl: config.webUrl, // Store web URL for profile links
|
||||
_webUrl: webUrl, // Store corrected web URL for profile links
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -810,6 +823,14 @@ function App() {
|
||||
const profile = await import('./utils/pds-detection').then(m => m.getProfileForUser(record.value.author.handle));
|
||||
|
||||
if (profile) {
|
||||
// Determine network config based on profile data
|
||||
let webUrl = 'https://bsky.app'; // Default to Bluesky
|
||||
if (profile.avatar && profile.avatar.includes('cdn.bsky.app')) {
|
||||
webUrl = 'https://bsky.app';
|
||||
} else if (profile.avatar && profile.avatar.includes('bsky.syu.is')) {
|
||||
webUrl = 'https://web.syu.is';
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
value: {
|
||||
@@ -818,6 +839,7 @@ function App() {
|
||||
...record.value.author,
|
||||
avatar: profile.avatar,
|
||||
displayName: profile.displayName || record.value.author.handle,
|
||||
_webUrl: webUrl, // Store network config for profile URL generation
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1146,7 +1168,18 @@ function App() {
|
||||
return `${config.webUrl}/profile/${author.did}`;
|
||||
}
|
||||
|
||||
// Get PDS from handle for other users
|
||||
// For other users, detect network based on avatar URL or stored network info
|
||||
if (author.avatar && author.avatar.includes('cdn.bsky.app')) {
|
||||
// User has Bluesky avatar, use Bluesky web interface
|
||||
return `https://bsky.app/profile/${author.did}`;
|
||||
}
|
||||
|
||||
// Check if we have stored network config from profile fetching
|
||||
if (author._webUrl) {
|
||||
return `${author._webUrl}/profile/${author.did}`;
|
||||
}
|
||||
|
||||
// Fallback: Get PDS from handle for other users
|
||||
const pds = detectPdsFromHandle(author.handle);
|
||||
const config = getNetworkConfig(pds);
|
||||
|
||||
@@ -1186,8 +1219,9 @@ function App() {
|
||||
|
||||
// Extract content based on format
|
||||
const contentText = isNewFormat ? value.text : (value.content || value.body || '');
|
||||
// For AI comments, always use the loaded AI profile instead of record.value.author
|
||||
const authorInfo = aiProfile;
|
||||
// Use the author from the record if available, otherwise fall back to AI profile
|
||||
const authorInfo = value.author || aiProfile;
|
||||
|
||||
const postInfo = isNewFormat ? value.post : null;
|
||||
const contentType = value.type || 'unknown';
|
||||
const createdAt = value.createdAt || value.generated_at || '';
|
||||
|
@@ -191,6 +191,7 @@ Answer:`;
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Origin': 'https://syui.ai',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: aiConfig.model,
|
||||
|
@@ -113,7 +113,7 @@ export function getAppConfig(): AppConfig {
|
||||
const aiEnabled = import.meta.env.VITE_AI_ENABLED === 'true';
|
||||
const aiAskAi = import.meta.env.VITE_AI_ASK_AI === 'true';
|
||||
const aiProvider = import.meta.env.VITE_AI_PROVIDER || 'ollama';
|
||||
const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma2:2b';
|
||||
const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma3:4b';
|
||||
const aiHost = import.meta.env.VITE_AI_HOST || 'https://ollama.syui.ai';
|
||||
const aiSystemPrompt = import.meta.env.VITE_AI_SYSTEM_PROMPT || 'You are a helpful AI assistant trained on this blog\'s content.';
|
||||
const atprotoPds = import.meta.env.VITE_ATPROTO_PDS || 'syu.is';
|
||||
|
@@ -41,6 +41,7 @@ class AtprotoOAuthService {
|
||||
this.oauthClient = await BrowserOAuthClient.load({
|
||||
clientId: clientId,
|
||||
handleResolver: 'https://bsky.social', // Default resolver
|
||||
plcDirectoryUrl: 'https://plc.directory', // Default PLC directory
|
||||
});
|
||||
|
||||
|
||||
@@ -92,41 +93,13 @@ class AtprotoOAuthService {
|
||||
}
|
||||
|
||||
private async processSession(session: any): Promise<{ did: string; handle: string }> {
|
||||
|
||||
|
||||
// Log full session structure
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Check if agent has properties we can access
|
||||
if (session.agent) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
const did = session.sub || session.did;
|
||||
let handle = session.handle || 'unknown';
|
||||
|
||||
// Create Agent directly with session (per official docs)
|
||||
try {
|
||||
this.agent = new Agent(session);
|
||||
|
||||
|
||||
// Check if agent has session info after creation
|
||||
|
||||
|
||||
|
||||
if (this.agent.session) {
|
||||
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
// Fallback to dpopFetch method
|
||||
this.agent = new Agent({
|
||||
service: session.server?.serviceEndpoint || 'https://bsky.social',
|
||||
@@ -215,7 +188,6 @@ class AtprotoOAuthService {
|
||||
|
||||
for (const [domain, pdsUrl] of Object.entries(pdsMapping)) {
|
||||
if (handle.endsWith(`.${domain}`)) {
|
||||
// Using PDS for domain match
|
||||
return pdsUrl;
|
||||
}
|
||||
}
|
||||
@@ -231,7 +203,6 @@ class AtprotoOAuthService {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.did) {
|
||||
console.log('[OAuth Debug] Resolved handle via', endpoint, '- using that PDS');
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
@@ -240,20 +211,16 @@ class AtprotoOAuthService {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[OAuth Debug] Handle resolution failed, using default');
|
||||
// Handle resolution failed, using default
|
||||
}
|
||||
|
||||
// Default to bsky.social
|
||||
// Using default bsky.social
|
||||
return 'https://bsky.social';
|
||||
}
|
||||
|
||||
async initiateOAuthFlow(handle?: string): Promise<void> {
|
||||
try {
|
||||
|
||||
|
||||
if (!this.oauthClient) {
|
||||
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
@@ -269,24 +236,20 @@ class AtprotoOAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Detect PDS based on handle
|
||||
const pdsUrl = await this.detectPDSFromHandle(handle);
|
||||
// Starting OAuth flow
|
||||
|
||||
|
||||
// Always re-initialize OAuth client with detected PDS
|
||||
// Re-initializing OAuth client
|
||||
|
||||
// Clear existing client to force fresh initialization
|
||||
this.oauthClient = null;
|
||||
this.initializePromise = null;
|
||||
|
||||
this.oauthClient = await BrowserOAuthClient.load({
|
||||
clientId: this.getClientId(),
|
||||
handleResolver: pdsUrl,
|
||||
});
|
||||
// Re-initialize OAuth client with correct PDS if needed
|
||||
if (pdsUrl !== 'https://bsky.social') {
|
||||
// Determine PLC directory for syu.is only
|
||||
const plcDirectoryUrl = handle.endsWith('.syu.is') || handle.endsWith('.syui.ai') ? 'https://plc.syu.is' : 'https://plc.directory';
|
||||
|
||||
this.oauthClient = await BrowserOAuthClient.load({
|
||||
clientId: this.getClientId(),
|
||||
handleResolver: pdsUrl,
|
||||
plcDirectoryUrl: plcDirectoryUrl,
|
||||
});
|
||||
}
|
||||
|
||||
// OAuth client initialized
|
||||
|
||||
@@ -296,22 +259,8 @@ class AtprotoOAuthService {
|
||||
try {
|
||||
// Starting OAuth authorization
|
||||
|
||||
// Try to authorize with DID instead of handle for syu.is PDS only
|
||||
let authTarget = handle;
|
||||
if (pdsUrl === 'https://syu.is') {
|
||||
try {
|
||||
const resolveResponse = await fetch(`${pdsUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
|
||||
if (resolveResponse.ok) {
|
||||
const resolveData = await resolveResponse.json();
|
||||
authTarget = resolveData.did;
|
||||
// Using DID for syu.is OAuth workaround
|
||||
}
|
||||
} catch (e) {
|
||||
// Could not resolve to DID, using handle
|
||||
}
|
||||
}
|
||||
|
||||
const authUrl = await this.oauthClient.authorize(authTarget, {
|
||||
// Use handle directly since PLC directory is now correctly configured
|
||||
const authUrl = await this.oauthClient.authorize(handle, {
|
||||
scope: 'atproto transition:generic',
|
||||
});
|
||||
|
||||
@@ -379,21 +328,15 @@ class AtprotoOAuthService {
|
||||
|
||||
async checkSession(): Promise<{ did: string; handle: string } | null> {
|
||||
try {
|
||||
|
||||
|
||||
if (!this.oauthClient) {
|
||||
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
if (!this.oauthClient) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const result = await this.oauthClient.init();
|
||||
|
||||
|
||||
if (result?.session) {
|
||||
// Use the common session processing method
|
||||
@@ -458,28 +401,20 @@ class AtprotoOAuthService {
|
||||
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
|
||||
|
||||
// Clear Agent
|
||||
this.agent = null;
|
||||
|
||||
|
||||
// Clear BrowserOAuthClient session
|
||||
if (this.oauthClient) {
|
||||
|
||||
try {
|
||||
// BrowserOAuthClient may have a revoke or signOut method
|
||||
if (typeof (this.oauthClient as any).signOut === 'function') {
|
||||
await (this.oauthClient as any).signOut();
|
||||
|
||||
} else if (typeof (this.oauthClient as any).revoke === 'function') {
|
||||
await (this.oauthClient as any).revoke();
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
} catch (oauthError) {
|
||||
|
||||
// Ignore logout errors
|
||||
}
|
||||
|
||||
// Reset the OAuth client to force re-initialization
|
||||
@@ -491,18 +426,16 @@ class AtprotoOAuthService {
|
||||
localStorage.removeItem('atproto_session');
|
||||
sessionStorage.clear();
|
||||
|
||||
// Clear all localStorage items that might be related to OAuth
|
||||
const keysToRemove: string[] = [];
|
||||
// Clear all OAuth-related storage
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && (key.includes('oauth') || key.includes('atproto') || key.includes('session'))) {
|
||||
keysToRemove.push(key);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => {
|
||||
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
|
||||
// Clear internal session info
|
||||
(this as any)._sessionInfo = null;
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user