835 lines
26 KiB
TypeScript
835 lines
26 KiB
TypeScript
/**
|
|
* User Networking API Routes
|
|
*
|
|
* Handles:
|
|
* - User search by username
|
|
* - Connection management (follow/unfollow)
|
|
* - Edge metadata (labels, notes, colors)
|
|
* - Network graph retrieval
|
|
*/
|
|
|
|
import { IRequest } from 'itty-router';
|
|
import { Environment, UserProfile, UserConnection, ConnectionMetadata, UserNode, GraphEdge, NetworkGraph } from './types';
|
|
|
|
// =============================================================================
|
|
// Helper Functions
|
|
// =============================================================================
|
|
|
|
function generateId(): string {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(data), {
|
|
status,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
function errorResponse(message: string, status = 400): Response {
|
|
return jsonResponse({ error: message }, status);
|
|
}
|
|
|
|
// Extract user ID from request (from auth header or session)
|
|
// For now, we'll use a simple header-based approach
|
|
function getUserIdFromRequest(request: IRequest): string | null {
|
|
// Check for X-User-Id header (set by client after CryptID auth)
|
|
const userId = request.headers.get('X-User-Id');
|
|
return userId;
|
|
}
|
|
|
|
// =============================================================================
|
|
// User Search Routes
|
|
// =============================================================================
|
|
|
|
/**
|
|
* GET /api/networking/users/search?q=query&limit=20
|
|
* Search users by username or display name
|
|
*/
|
|
export async function searchUsers(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const url = new URL(request.url);
|
|
const query = url.searchParams.get('q') || '';
|
|
const limit = Math.min(parseInt(url.searchParams.get('limit') || '20'), 100);
|
|
|
|
if (!query || query.length < 2) {
|
|
return errorResponse('Query must be at least 2 characters');
|
|
}
|
|
|
|
const currentUserId = getUserIdFromRequest(request);
|
|
|
|
try {
|
|
// Search users by username or display name
|
|
const searchPattern = `%${query}%`;
|
|
const users = await db.prepare(`
|
|
SELECT
|
|
u.id,
|
|
u.cryptid_username as username,
|
|
COALESCE(p.display_name, u.cryptid_username) as displayName,
|
|
p.avatar_color as avatarColor,
|
|
p.bio
|
|
FROM users u
|
|
LEFT JOIN user_profiles p ON u.id = p.user_id
|
|
WHERE (
|
|
u.cryptid_username LIKE ?1
|
|
OR p.display_name LIKE ?1
|
|
)
|
|
AND (p.is_searchable = 1 OR p.is_searchable IS NULL)
|
|
LIMIT ?2
|
|
`).bind(searchPattern, limit).all();
|
|
|
|
// If we have a current user, add connection status
|
|
let results = users.results || [];
|
|
|
|
if (currentUserId && results.length > 0) {
|
|
const userIds = results.map((u: any) => u.id);
|
|
|
|
// Get connections from current user
|
|
const myConnections = await db.prepare(`
|
|
SELECT to_user_id FROM user_connections WHERE from_user_id = ?
|
|
`).bind(currentUserId).all();
|
|
const connectedIds = new Set((myConnections.results || []).map((c: any) => c.to_user_id));
|
|
|
|
// Get connections to current user
|
|
const theirConnections = await db.prepare(`
|
|
SELECT from_user_id FROM user_connections WHERE to_user_id = ?
|
|
`).bind(currentUserId).all();
|
|
const connectedBackIds = new Set((theirConnections.results || []).map((c: any) => c.from_user_id));
|
|
|
|
results = results.map((user: any) => ({
|
|
...user,
|
|
isConnected: connectedIds.has(user.id),
|
|
isConnectedBack: connectedBackIds.has(user.id),
|
|
mutualConnections: 0, // TODO: Calculate mutual connections
|
|
}));
|
|
}
|
|
|
|
return jsonResponse(results);
|
|
} catch (error) {
|
|
console.error('User search error:', error);
|
|
return errorResponse('Search failed', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/networking/users/:userId
|
|
* Get a user's public profile
|
|
*/
|
|
export async function getUserProfile(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const { userId } = request.params;
|
|
|
|
try {
|
|
const result = await db.prepare(`
|
|
SELECT
|
|
u.id,
|
|
u.cryptid_username as username,
|
|
COALESCE(p.display_name, u.cryptid_username) as displayName,
|
|
p.avatar_color as avatarColor,
|
|
p.bio
|
|
FROM users u
|
|
LEFT JOIN user_profiles p ON u.id = p.user_id
|
|
WHERE u.id = ?
|
|
`).bind(userId).first();
|
|
|
|
if (!result) {
|
|
return errorResponse('User not found', 404);
|
|
}
|
|
|
|
return jsonResponse(result);
|
|
} catch (error) {
|
|
console.error('Get profile error:', error);
|
|
return errorResponse('Failed to get profile', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/networking/users/me
|
|
* Update current user's profile
|
|
*/
|
|
export async function updateMyProfile(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
try {
|
|
const body = await request.json() as {
|
|
displayName?: string;
|
|
bio?: string;
|
|
avatarColor?: string;
|
|
};
|
|
|
|
// Upsert profile
|
|
await db.prepare(`
|
|
INSERT INTO user_profiles (user_id, display_name, bio, avatar_color, updated_at)
|
|
VALUES (?1, ?2, ?3, ?4, datetime('now'))
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
display_name = COALESCE(?2, display_name),
|
|
bio = COALESCE(?3, bio),
|
|
avatar_color = COALESCE(?4, avatar_color),
|
|
updated_at = datetime('now')
|
|
`).bind(userId, body.displayName || null, body.bio || null, body.avatarColor || null).run();
|
|
|
|
// Return updated profile
|
|
return getUserProfile({ ...request, params: { userId } } as unknown as IRequest, env);
|
|
} catch (error) {
|
|
console.error('Update profile error:', error);
|
|
return errorResponse('Failed to update profile', 500);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Connection Management Routes
|
|
// =============================================================================
|
|
|
|
/**
|
|
* POST /api/networking/connections
|
|
* Create a connection (follow a user)
|
|
* Body: { toUserId: string, trustLevel?: 'connected' | 'trusted' }
|
|
*/
|
|
export async function createConnection(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const fromUserId = getUserIdFromRequest(request);
|
|
if (!fromUserId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
try {
|
|
const body = await request.json() as { toUserId: string; trustLevel?: 'connected' | 'trusted' };
|
|
const { toUserId, trustLevel = 'connected' } = body;
|
|
|
|
if (!toUserId) {
|
|
return errorResponse('toUserId is required');
|
|
}
|
|
|
|
if (fromUserId === toUserId) {
|
|
return errorResponse('Cannot connect to yourself');
|
|
}
|
|
|
|
if (trustLevel !== 'connected' && trustLevel !== 'trusted') {
|
|
return errorResponse('trustLevel must be "connected" or "trusted"');
|
|
}
|
|
|
|
// Check if target user exists
|
|
const targetUser = await db.prepare('SELECT id FROM users WHERE id = ?').bind(toUserId).first();
|
|
if (!targetUser) {
|
|
return errorResponse('User not found', 404);
|
|
}
|
|
|
|
// Check if connection already exists
|
|
const existing = await db.prepare(`
|
|
SELECT id FROM user_connections WHERE from_user_id = ? AND to_user_id = ?
|
|
`).bind(fromUserId, toUserId).first();
|
|
|
|
if (existing) {
|
|
return errorResponse('Already connected');
|
|
}
|
|
|
|
// Create connection
|
|
const connectionId = generateId();
|
|
await db.prepare(`
|
|
INSERT INTO user_connections (id, from_user_id, to_user_id, trust_level)
|
|
VALUES (?, ?, ?, ?)
|
|
`).bind(connectionId, fromUserId, toUserId, trustLevel).run();
|
|
|
|
// Check if mutual and get their trust level
|
|
const reverseConnection = await db.prepare(`
|
|
SELECT id, trust_level FROM user_connections WHERE from_user_id = ? AND to_user_id = ?
|
|
`).bind(toUserId, fromUserId).first() as { id: string; trust_level: string } | null;
|
|
|
|
// Calculate effective trust level (highest of both directions)
|
|
let effectiveTrustLevel = null;
|
|
if (reverseConnection) {
|
|
const theirLevel = reverseConnection.trust_level;
|
|
effectiveTrustLevel = (trustLevel === 'trusted' || theirLevel === 'trusted') ? 'trusted' : 'connected';
|
|
}
|
|
|
|
const connection = {
|
|
id: connectionId,
|
|
fromUserId,
|
|
toUserId,
|
|
trustLevel,
|
|
createdAt: new Date().toISOString(),
|
|
isMutual: !!reverseConnection,
|
|
effectiveTrustLevel,
|
|
};
|
|
|
|
return jsonResponse(connection, 201);
|
|
} catch (error) {
|
|
console.error('Create connection error:', error);
|
|
return errorResponse('Failed to create connection', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/networking/connections/:connectionId/trust
|
|
* Update trust level for a connection
|
|
*/
|
|
export async function updateConnectionTrust(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
const { connectionId } = request.params;
|
|
|
|
try {
|
|
// Verify ownership
|
|
const connection = await db.prepare(`
|
|
SELECT id, from_user_id, to_user_id FROM user_connections WHERE id = ? AND from_user_id = ?
|
|
`).bind(connectionId, userId).first() as { id: string; from_user_id: string; to_user_id: string } | null;
|
|
|
|
if (!connection) {
|
|
return errorResponse('Connection not found or not owned by you', 404);
|
|
}
|
|
|
|
const body = await request.json() as { trustLevel: 'connected' | 'trusted' };
|
|
const { trustLevel } = body;
|
|
|
|
if (trustLevel !== 'connected' && trustLevel !== 'trusted') {
|
|
return errorResponse('trustLevel must be "connected" or "trusted"');
|
|
}
|
|
|
|
// Update trust level
|
|
await db.prepare(`
|
|
UPDATE user_connections SET trust_level = ?, updated_at = datetime('now') WHERE id = ?
|
|
`).bind(trustLevel, connectionId).run();
|
|
|
|
// Check if mutual and get their trust level
|
|
const reverseConnection = await db.prepare(`
|
|
SELECT trust_level FROM user_connections WHERE from_user_id = ? AND to_user_id = ?
|
|
`).bind(connection.to_user_id, connection.from_user_id).first() as { trust_level: string } | null;
|
|
|
|
let effectiveTrustLevel = null;
|
|
if (reverseConnection) {
|
|
const theirLevel = reverseConnection.trust_level;
|
|
effectiveTrustLevel = (trustLevel === 'trusted' || theirLevel === 'trusted') ? 'trusted' : 'connected';
|
|
}
|
|
|
|
return jsonResponse({
|
|
id: connectionId,
|
|
fromUserId: connection.from_user_id,
|
|
toUserId: connection.to_user_id,
|
|
trustLevel,
|
|
isMutual: !!reverseConnection,
|
|
effectiveTrustLevel,
|
|
});
|
|
} catch (error) {
|
|
console.error('Update trust level error:', error);
|
|
return errorResponse('Failed to update trust level', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/networking/connections/:connectionId
|
|
* Remove a connection (unfollow)
|
|
*/
|
|
export async function removeConnection(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
const { connectionId } = request.params;
|
|
|
|
try {
|
|
// Verify ownership
|
|
const connection = await db.prepare(`
|
|
SELECT id FROM user_connections WHERE id = ? AND from_user_id = ?
|
|
`).bind(connectionId, userId).first();
|
|
|
|
if (!connection) {
|
|
return errorResponse('Connection not found or not owned by you', 404);
|
|
}
|
|
|
|
// Delete connection and its metadata
|
|
await db.prepare('DELETE FROM connection_metadata WHERE connection_id = ?').bind(connectionId).run();
|
|
await db.prepare('DELETE FROM user_connections WHERE id = ?').bind(connectionId).run();
|
|
|
|
return new Response(null, { status: 204 });
|
|
} catch (error) {
|
|
console.error('Remove connection error:', error);
|
|
return errorResponse('Failed to remove connection', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/networking/connections
|
|
* Get current user's connections (people they follow)
|
|
*/
|
|
export async function getMyConnections(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
try {
|
|
const connections = await db.prepare(`
|
|
SELECT
|
|
c.id,
|
|
c.from_user_id as fromUserId,
|
|
c.to_user_id as toUserId,
|
|
c.created_at as createdAt,
|
|
m.label,
|
|
m.notes,
|
|
m.color,
|
|
m.strength,
|
|
EXISTS(
|
|
SELECT 1 FROM user_connections r
|
|
WHERE r.from_user_id = c.to_user_id AND r.to_user_id = c.from_user_id
|
|
) as isMutual
|
|
FROM user_connections c
|
|
LEFT JOIN connection_metadata m ON c.id = m.connection_id AND m.user_id = ?
|
|
WHERE c.from_user_id = ?
|
|
`).bind(userId, userId).all();
|
|
|
|
const results = (connections.results || []).map((c: any) => ({
|
|
id: c.id,
|
|
fromUserId: c.fromUserId,
|
|
toUserId: c.toUserId,
|
|
createdAt: c.createdAt,
|
|
isMutual: !!c.isMutual,
|
|
metadata: c.label || c.notes || c.color || c.strength ? {
|
|
label: c.label,
|
|
notes: c.notes,
|
|
color: c.color,
|
|
strength: c.strength || 5,
|
|
} : undefined,
|
|
}));
|
|
|
|
return jsonResponse(results);
|
|
} catch (error) {
|
|
console.error('Get connections error:', error);
|
|
return errorResponse('Failed to get connections', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/networking/connections/followers
|
|
* Get users who follow the current user
|
|
*/
|
|
export async function getMyFollowers(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
try {
|
|
const connections = await db.prepare(`
|
|
SELECT
|
|
c.id,
|
|
c.from_user_id as fromUserId,
|
|
c.to_user_id as toUserId,
|
|
c.created_at as createdAt,
|
|
EXISTS(
|
|
SELECT 1 FROM user_connections r
|
|
WHERE r.from_user_id = c.to_user_id AND r.to_user_id = c.from_user_id
|
|
) as isMutual
|
|
FROM user_connections c
|
|
WHERE c.to_user_id = ?
|
|
`).bind(userId).all();
|
|
|
|
const results = (connections.results || []).map((c: any) => ({
|
|
id: c.id,
|
|
fromUserId: c.fromUserId,
|
|
toUserId: c.toUserId,
|
|
createdAt: c.createdAt,
|
|
isMutual: !!c.isMutual,
|
|
}));
|
|
|
|
return jsonResponse(results);
|
|
} catch (error) {
|
|
console.error('Get followers error:', error);
|
|
return errorResponse('Failed to get followers', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/networking/connections/check/:userId
|
|
* Check if current user is connected to a specific user
|
|
*/
|
|
export async function checkConnection(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const currentUserId = getUserIdFromRequest(request);
|
|
if (!currentUserId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
const { userId } = request.params;
|
|
|
|
try {
|
|
const connection = await db.prepare(`
|
|
SELECT id FROM user_connections WHERE from_user_id = ? AND to_user_id = ?
|
|
`).bind(currentUserId, userId).first();
|
|
|
|
return jsonResponse({ connected: !!connection });
|
|
} catch (error) {
|
|
console.error('Check connection error:', error);
|
|
return errorResponse('Failed to check connection', 500);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Edge Metadata Routes
|
|
// =============================================================================
|
|
|
|
/**
|
|
* PUT /api/networking/connections/:connectionId/metadata
|
|
* Update edge metadata for a connection
|
|
*/
|
|
export async function updateEdgeMetadata(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
const { connectionId } = request.params;
|
|
|
|
try {
|
|
// Verify user is on this connection
|
|
const connection = await db.prepare(`
|
|
SELECT id, from_user_id, to_user_id FROM user_connections WHERE id = ?
|
|
`).bind(connectionId).first() as { id: string; from_user_id: string; to_user_id: string } | null;
|
|
|
|
if (!connection) {
|
|
return errorResponse('Connection not found', 404);
|
|
}
|
|
|
|
if (connection.from_user_id !== userId && connection.to_user_id !== userId) {
|
|
return errorResponse('Not authorized to edit this connection', 403);
|
|
}
|
|
|
|
const body = await request.json() as {
|
|
label?: string;
|
|
notes?: string;
|
|
color?: string;
|
|
strength?: number;
|
|
};
|
|
|
|
// Validate strength
|
|
if (body.strength !== undefined && (body.strength < 1 || body.strength > 10)) {
|
|
return errorResponse('Strength must be between 1 and 10');
|
|
}
|
|
|
|
// Upsert metadata
|
|
const metadataId = generateId();
|
|
await db.prepare(`
|
|
INSERT INTO connection_metadata (id, connection_id, user_id, label, notes, color, strength, updated_at)
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, datetime('now'))
|
|
ON CONFLICT(connection_id, user_id) DO UPDATE SET
|
|
label = COALESCE(?4, label),
|
|
notes = COALESCE(?5, notes),
|
|
color = COALESCE(?6, color),
|
|
strength = COALESCE(?7, strength),
|
|
updated_at = datetime('now')
|
|
`).bind(
|
|
metadataId,
|
|
connectionId,
|
|
userId,
|
|
body.label || null,
|
|
body.notes || null,
|
|
body.color || null,
|
|
body.strength || null
|
|
).run();
|
|
|
|
// Return updated metadata
|
|
const metadata = await db.prepare(`
|
|
SELECT label, notes, color, strength FROM connection_metadata
|
|
WHERE connection_id = ? AND user_id = ?
|
|
`).bind(connectionId, userId).first();
|
|
|
|
return jsonResponse(metadata || { label: null, notes: null, color: null, strength: 5 });
|
|
} catch (error) {
|
|
console.error('Update metadata error:', error);
|
|
return errorResponse('Failed to update metadata', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/networking/connections/:connectionId/metadata
|
|
* Get edge metadata for a connection
|
|
*/
|
|
export async function getEdgeMetadata(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
const { connectionId } = request.params;
|
|
|
|
try {
|
|
// Verify user is on this connection
|
|
const connection = await db.prepare(`
|
|
SELECT id, from_user_id, to_user_id FROM user_connections WHERE id = ?
|
|
`).bind(connectionId).first() as { id: string; from_user_id: string; to_user_id: string } | null;
|
|
|
|
if (!connection) {
|
|
return errorResponse('Connection not found', 404);
|
|
}
|
|
|
|
if (connection.from_user_id !== userId && connection.to_user_id !== userId) {
|
|
return errorResponse('Not authorized to view this connection', 403);
|
|
}
|
|
|
|
const metadata = await db.prepare(`
|
|
SELECT label, notes, color, strength FROM connection_metadata
|
|
WHERE connection_id = ? AND user_id = ?
|
|
`).bind(connectionId, userId).first();
|
|
|
|
return jsonResponse(metadata || { label: null, notes: null, color: null, strength: 5 });
|
|
} catch (error) {
|
|
console.error('Get metadata error:', error);
|
|
return errorResponse('Failed to get metadata', 500);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Network Graph Routes
|
|
// =============================================================================
|
|
|
|
/**
|
|
* GET /api/networking/graph
|
|
* Get the full network graph for current user
|
|
*/
|
|
export async function getNetworkGraph(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
try {
|
|
// Get all users connected to/from current user
|
|
const connections = await db.prepare(`
|
|
SELECT DISTINCT user_id FROM (
|
|
SELECT to_user_id as user_id FROM user_connections WHERE from_user_id = ?
|
|
UNION
|
|
SELECT from_user_id as user_id FROM user_connections WHERE to_user_id = ?
|
|
)
|
|
`).bind(userId, userId).all();
|
|
|
|
const connectedUserIds = (connections.results || []).map((c: any) => c.user_id);
|
|
connectedUserIds.push(userId); // Include self
|
|
|
|
// Get user profiles for all connected users
|
|
const placeholders = connectedUserIds.map(() => '?').join(',');
|
|
const users = await db.prepare(`
|
|
SELECT
|
|
u.id,
|
|
u.cryptid_username as username,
|
|
COALESCE(p.display_name, u.cryptid_username) as displayName,
|
|
p.avatar_color as avatarColor,
|
|
p.bio
|
|
FROM users u
|
|
LEFT JOIN user_profiles p ON u.id = p.user_id
|
|
WHERE u.id IN (${placeholders})
|
|
`).bind(...connectedUserIds).all();
|
|
|
|
// Build nodes
|
|
const nodes: UserNode[] = (users.results || []).map((u: any) => ({
|
|
id: u.id,
|
|
username: u.username,
|
|
displayName: u.displayName,
|
|
avatarColor: u.avatarColor,
|
|
bio: u.bio,
|
|
}));
|
|
|
|
// Get all edges between these users
|
|
const edges = await db.prepare(`
|
|
SELECT
|
|
c.id,
|
|
c.from_user_id as fromUserId,
|
|
c.to_user_id as toUserId,
|
|
c.created_at as createdAt,
|
|
m.label,
|
|
m.notes,
|
|
m.color,
|
|
m.strength,
|
|
EXISTS(
|
|
SELECT 1 FROM user_connections r
|
|
WHERE r.from_user_id = c.to_user_id AND r.to_user_id = c.from_user_id
|
|
) as isMutual
|
|
FROM user_connections c
|
|
LEFT JOIN connection_metadata m ON c.id = m.connection_id AND m.user_id = ?
|
|
WHERE c.from_user_id IN (${placeholders}) AND c.to_user_id IN (${placeholders})
|
|
`).bind(userId, ...connectedUserIds, ...connectedUserIds).all();
|
|
|
|
const graphEdges: GraphEdge[] = (edges.results || []).map((e: any) => ({
|
|
id: e.id,
|
|
source: e.fromUserId,
|
|
target: e.toUserId,
|
|
trustLevel: e.trustLevel || 'connected',
|
|
effectiveTrustLevel: e.isMutual ? (e.trustLevel || 'connected') : null,
|
|
isMutual: !!e.isMutual,
|
|
isVisible: true,
|
|
metadata: (e.fromUserId === userId || e.toUserId === userId) && (e.label || e.notes || e.color || e.strength) ? {
|
|
label: e.label,
|
|
notes: e.notes,
|
|
color: e.color,
|
|
strength: e.strength || 5,
|
|
} : undefined,
|
|
}));
|
|
|
|
// Get list of users current user is connected to
|
|
const myConnections = await db.prepare(`
|
|
SELECT to_user_id FROM user_connections WHERE from_user_id = ?
|
|
`).bind(userId).all();
|
|
|
|
const graph: NetworkGraph = {
|
|
nodes,
|
|
edges: graphEdges,
|
|
myConnections: (myConnections.results || []).map((c: any) => c.to_user_id),
|
|
};
|
|
|
|
return jsonResponse(graph);
|
|
} catch (error) {
|
|
console.error('Get network graph error:', error);
|
|
return errorResponse('Failed to get network graph', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/networking/graph/room
|
|
* Get network graph scoped to room participants
|
|
* Body: { participants: string[] }
|
|
*/
|
|
export async function getRoomNetworkGraph(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const userId = getUserIdFromRequest(request);
|
|
if (!userId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
try {
|
|
const body = await request.json() as { participants: string[] };
|
|
const { participants } = body;
|
|
|
|
if (!participants || !Array.isArray(participants)) {
|
|
return errorResponse('participants array is required');
|
|
}
|
|
|
|
// First get the full network graph
|
|
const graphResponse = await getNetworkGraph(request, env);
|
|
const graph = await graphResponse.json() as NetworkGraph;
|
|
|
|
// Mark which nodes are in the room
|
|
const participantSet = new Set(participants);
|
|
const nodesWithRoomStatus = graph.nodes.map(node => ({
|
|
...node,
|
|
isInRoom: participantSet.has(node.id),
|
|
}));
|
|
|
|
return jsonResponse({
|
|
...graph,
|
|
nodes: nodesWithRoomStatus,
|
|
roomParticipants: participants,
|
|
});
|
|
} catch (error) {
|
|
console.error('Get room network graph error:', error);
|
|
return errorResponse('Failed to get room network graph', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/networking/connections/mutual/:userId
|
|
* Get mutual connections between current user and another user
|
|
*/
|
|
export async function getMutualConnections(request: IRequest, env: Environment): Promise<Response> {
|
|
const db = env.CRYPTID_DB;
|
|
if (!db) {
|
|
return errorResponse('Database not configured', 500);
|
|
}
|
|
|
|
const currentUserId = getUserIdFromRequest(request);
|
|
if (!currentUserId) {
|
|
return errorResponse('Unauthorized', 401);
|
|
}
|
|
|
|
const { userId } = request.params;
|
|
|
|
try {
|
|
// Find users that both current user and target user are connected to
|
|
const mutuals = await db.prepare(`
|
|
SELECT
|
|
u.id,
|
|
u.cryptid_username as username,
|
|
COALESCE(p.display_name, u.cryptid_username) as displayName,
|
|
p.avatar_color as avatarColor,
|
|
p.bio
|
|
FROM users u
|
|
LEFT JOIN user_profiles p ON u.id = p.user_id
|
|
WHERE u.id IN (
|
|
SELECT c1.to_user_id
|
|
FROM user_connections c1
|
|
INNER JOIN user_connections c2 ON c1.to_user_id = c2.to_user_id
|
|
WHERE c1.from_user_id = ? AND c2.from_user_id = ?
|
|
)
|
|
`).bind(currentUserId, userId).all();
|
|
|
|
return jsonResponse(mutuals.results || []);
|
|
} catch (error) {
|
|
console.error('Get mutual connections error:', error);
|
|
return errorResponse('Failed to get mutual connections', 500);
|
|
}
|
|
}
|