b4a9d82d05
Replace docker/metadata GH Action with a docker-publish.sh driven workflow (supports auto-versioning, extra tags, multi-arch builds and optional builder). Update docs and READMEs to use the updater and new publish flow. Add server-side swarm/node blocklist support and admin nodes API. Improve auth endpoints (me, logout, switch, session accounts) and support account switching. Extend bots API to support custom LLM provider and optional endpoint. Enforce node blocklist on chat send/receive, refactor link preview handling into dedicated preview modules, and enhance notifications and posts like/repost handlers to accept both signed actions and regular auth flows. Misc: remove admin update UI details, add helper libs (media previews, node-blocklist, reposts, notifications, etc.), and minor housekeeping (pyc, workflow tweaks).
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:3000';
|
|
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 }
|
|
);
|
|
}
|
|
}
|