21258edd95
Hop-State: A_06FP5KEDBTB4A9ZT7QB498G Hop-Proposal: R_06FP5KDWMCKVVMQZT4MVZ28 Hop-Task: T_06FP5DZ7T0G45FG93PT90B8 Hop-Attempt: AT_06FP5DZ7T0PKQW99V27JCV8
223 lines
6.5 KiB
TypeScript
223 lines
6.5 KiB
TypeScript
/**
|
|
* Bot API Routes
|
|
*
|
|
* POST /api/bots - Create a new bot
|
|
* GET /api/bots - List user's bots
|
|
*
|
|
* Requirements: 1.1, 1.3
|
|
*/
|
|
|
|
import { NextResponse } from 'next/server';
|
|
import { requireAuth } from '@/lib/auth';
|
|
import { z } from 'zod';
|
|
import {
|
|
createBot,
|
|
getBotsByUser,
|
|
BotLimitExceededError,
|
|
BotHandleTakenError,
|
|
BotValidationError,
|
|
} from '@/lib/bots/botManager';
|
|
import { generateAndUploadAvatarToUserStorage } from '@/lib/storage/s3';
|
|
import { createStorageSession, getStorageSession } from '@/lib/storage/session';
|
|
|
|
// Schema for creating a bot
|
|
const createBotSchema = z.object({
|
|
name: z.string().min(1).max(100),
|
|
handle: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_]+$/, 'Handle must be alphanumeric and underscores only'),
|
|
bio: z.string().max(500).optional(),
|
|
avatarUrl: z.string().url().optional(),
|
|
headerUrl: z.string().url().optional(),
|
|
personality: z.object({
|
|
systemPrompt: z.string().min(1).max(10000),
|
|
temperature: z.number().min(0).max(2),
|
|
maxTokens: z.number().int().min(1).max(100000),
|
|
responseStyle: z.string().optional(),
|
|
}),
|
|
llmProvider: z.enum(['openrouter', 'openai', 'anthropic', 'custom']),
|
|
llmModel: z.string().min(1),
|
|
llmEndpoint: z.string().url().optional(),
|
|
llmApiKey: z.string().min(1),
|
|
schedule: z.object({
|
|
type: z.enum(['interval', 'times', 'cron']),
|
|
intervalMinutes: z.number().int().min(5).optional(),
|
|
times: z.array(z.string().regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/)).optional(),
|
|
cronExpression: z.string().optional(),
|
|
timezone: z.string().optional(),
|
|
}).optional(),
|
|
autonomousMode: z.boolean().optional(),
|
|
ownerPassword: z.string().optional(),
|
|
});
|
|
|
|
/**
|
|
* POST /api/bots - Create a new bot
|
|
*
|
|
* Requires authentication.
|
|
* Creates a new bot linked to the authenticated user's account.
|
|
*
|
|
* Validates: Requirements 1.1, 1.2
|
|
*/
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const user = await requireAuth();
|
|
const body = await request.json();
|
|
const data = createBotSchema.parse(body);
|
|
|
|
const storageSession =
|
|
(await getStorageSession(user.id)) ||
|
|
(data.ownerPassword ? await createStorageSession(user, data.ownerPassword) : null);
|
|
|
|
// Generate bot avatar using owner's S3 storage if no avatar URL
|
|
let botAvatarUrl: string | null | undefined = data.avatarUrl;
|
|
if (!botAvatarUrl && storageSession) {
|
|
try {
|
|
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
|
const botHandle = `${data.handle.toLowerCase()}@${nodeDomain}`;
|
|
|
|
botAvatarUrl = await generateAndUploadAvatarToUserStorage(
|
|
botHandle,
|
|
storageSession.endpoint || undefined,
|
|
storageSession.publicBaseUrl || undefined,
|
|
storageSession.region || 'us-east-1',
|
|
storageSession.bucket,
|
|
storageSession.accessKeyId,
|
|
storageSession.secretAccessKey
|
|
);
|
|
} catch (err) {
|
|
console.error('[Bot API] Failed to generate bot avatar:', err);
|
|
// Continue without avatar - user can set it later
|
|
}
|
|
}
|
|
|
|
const bot = await createBot(user.id, {
|
|
name: data.name,
|
|
handle: data.handle,
|
|
bio: data.bio,
|
|
avatarUrl: botAvatarUrl ?? undefined,
|
|
headerUrl: data.headerUrl,
|
|
personality: data.personality,
|
|
llmProvider: data.llmProvider,
|
|
llmModel: data.llmModel,
|
|
llmEndpoint: data.llmEndpoint,
|
|
llmApiKey: data.llmApiKey,
|
|
schedule: data.schedule,
|
|
autonomousMode: data.autonomousMode,
|
|
});
|
|
|
|
// Return bot without sensitive data
|
|
return NextResponse.json({
|
|
success: true,
|
|
bot: {
|
|
id: bot.id,
|
|
userId: bot.userId,
|
|
name: bot.name,
|
|
handle: bot.handle,
|
|
bio: bot.bio,
|
|
avatarUrl: bot.avatarUrl,
|
|
personalityConfig: bot.personalityConfig,
|
|
llmProvider: bot.llmProvider,
|
|
llmModel: bot.llmModel,
|
|
llmEndpoint: bot.llmEndpoint,
|
|
scheduleConfig: bot.scheduleConfig,
|
|
autonomousMode: bot.autonomousMode,
|
|
isActive: bot.isActive,
|
|
isSuspended: bot.isSuspended,
|
|
lastPostAt: bot.lastPostAt,
|
|
createdAt: bot.createdAt,
|
|
updatedAt: bot.updatedAt,
|
|
},
|
|
}, { status: 201 });
|
|
} catch (error) {
|
|
console.error('Create bot error:', error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid input', details: error.issues },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (error instanceof Error && error.message === 'Authentication required') {
|
|
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
|
}
|
|
|
|
if (error instanceof BotLimitExceededError) {
|
|
return NextResponse.json(
|
|
{ error: error.message, code: error.code },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
if (error instanceof BotHandleTakenError) {
|
|
return NextResponse.json(
|
|
{ error: error.message, code: error.code },
|
|
{ status: 409 }
|
|
);
|
|
}
|
|
|
|
if (error instanceof BotValidationError) {
|
|
return NextResponse.json(
|
|
{ error: error.message, code: error.code },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{ error: 'Failed to create bot' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/bots - List user's bots
|
|
*
|
|
* Requires authentication.
|
|
* Returns all bots belonging to the authenticated user.
|
|
*
|
|
* Validates: Requirements 1.3
|
|
*/
|
|
export async function GET() {
|
|
try {
|
|
const user = await requireAuth();
|
|
const userBots = await getBotsByUser(user.id);
|
|
|
|
// Return bots without sensitive data
|
|
const sanitizedBots = userBots.map(bot => ({
|
|
id: bot.id,
|
|
userId: bot.userId,
|
|
name: bot.name,
|
|
handle: bot.handle,
|
|
bio: bot.bio,
|
|
avatarUrl: bot.avatarUrl,
|
|
personalityConfig: bot.personalityConfig,
|
|
llmProvider: bot.llmProvider,
|
|
llmModel: bot.llmModel,
|
|
llmEndpoint: bot.llmEndpoint,
|
|
scheduleConfig: bot.scheduleConfig,
|
|
autonomousMode: bot.autonomousMode,
|
|
isActive: bot.isActive,
|
|
isSuspended: bot.isSuspended,
|
|
suspensionReason: bot.suspensionReason,
|
|
lastPostAt: bot.lastPostAt,
|
|
createdAt: bot.createdAt,
|
|
updatedAt: bot.updatedAt,
|
|
}));
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
bots: sanitizedBots,
|
|
});
|
|
} catch (error) {
|
|
console.error('List bots error:', error);
|
|
|
|
if (error instanceof Error && error.message === 'Authentication required') {
|
|
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{ error: 'Failed to list bots' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|