- anvil-hosting.pages.dev → hosting.anvil.it.com Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
211 lines
6.1 KiB
TypeScript
211 lines
6.1 KiB
TypeScript
import { Env, TelegramUpdate } from './types';
|
|
import { validateWebhookRequest, checkRateLimit } from './security';
|
|
import { sendMessage, sendMessageWithKeyboard, setWebhook, getWebhookInfo, sendChatAction } from './telegram';
|
|
import {
|
|
addToBuffer,
|
|
processAndSummarize,
|
|
generateAIResponse,
|
|
} from './summary-service';
|
|
import { handleCommand } from './commands';
|
|
|
|
// 사용자 조회/생성
|
|
async function getOrCreateUser(
|
|
db: D1Database,
|
|
telegramId: string,
|
|
firstName: string,
|
|
username?: string
|
|
): Promise<number> {
|
|
const existing = await db
|
|
.prepare('SELECT id FROM users WHERE telegram_id = ?')
|
|
.bind(telegramId)
|
|
.first<{ id: number }>();
|
|
|
|
if (existing) {
|
|
// 마지막 활동 시간 업데이트
|
|
await db
|
|
.prepare('UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
|
.bind(existing.id)
|
|
.run();
|
|
return existing.id;
|
|
}
|
|
|
|
// 새 사용자 생성
|
|
const result = await db
|
|
.prepare('INSERT INTO users (telegram_id, first_name, username) VALUES (?, ?, ?)')
|
|
.bind(telegramId, firstName, username || null)
|
|
.run();
|
|
|
|
return result.meta.last_row_id as number;
|
|
}
|
|
|
|
// 메시지 처리
|
|
async function handleMessage(
|
|
env: Env,
|
|
update: TelegramUpdate
|
|
): Promise<void> {
|
|
if (!update.message?.text) return;
|
|
|
|
const { message } = update;
|
|
const chatId = message.chat.id;
|
|
const chatIdStr = chatId.toString();
|
|
const text = message.text;
|
|
const telegramUserId = message.from.id.toString();
|
|
|
|
// Rate Limiting 체크
|
|
if (!checkRateLimit(telegramUserId)) {
|
|
await sendMessage(
|
|
env.BOT_TOKEN,
|
|
chatId,
|
|
'⚠️ 너무 많은 요청입니다. 잠시 후 다시 시도해주세요.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 사용자 처리
|
|
const userId = await getOrCreateUser(
|
|
env.DB,
|
|
telegramUserId,
|
|
message.from.first_name,
|
|
message.from.username
|
|
);
|
|
|
|
let responseText: string;
|
|
|
|
// 명령어 처리
|
|
if (text.startsWith('/')) {
|
|
const [command, ...argParts] = text.split(' ');
|
|
const args = argParts.join(' ');
|
|
responseText = await handleCommand(env, userId, chatIdStr, command, args);
|
|
|
|
// /start 명령어는 미니앱 버튼과 함께 전송
|
|
if (command === '/start') {
|
|
await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, responseText, [
|
|
[{ text: '🌐 서비스 보기', web_app: { url: 'https://hosting.anvil.it.com' } }],
|
|
[{ text: '💬 문의하기', url: 'https://t.me/AnvilForgeBot' }],
|
|
]);
|
|
return;
|
|
}
|
|
} else {
|
|
// 타이핑 표시
|
|
await sendChatAction(env.BOT_TOKEN, chatId, 'typing');
|
|
|
|
// 1. 사용자 메시지 버퍼에 추가
|
|
await addToBuffer(env.DB, userId, chatIdStr, 'user', text);
|
|
|
|
try {
|
|
// 2. AI 응답 생성
|
|
responseText = await generateAIResponse(env, userId, chatIdStr, text);
|
|
|
|
// 3. 봇 응답 버퍼에 추가
|
|
await addToBuffer(env.DB, userId, chatIdStr, 'bot', responseText);
|
|
|
|
// 4. 임계값 도달시 프로필 업데이트
|
|
const { summarized } = await processAndSummarize(env, userId, chatIdStr);
|
|
|
|
if (summarized) {
|
|
responseText += '\n\n<i>👤 프로필이 업데이트되었습니다.</i>';
|
|
}
|
|
} catch (error) {
|
|
console.error('AI Response error:', error);
|
|
responseText = `⚠️ AI 응답 생성 중 오류가 발생했습니다.\n\n<code>${String(error)}</code>`;
|
|
}
|
|
}
|
|
|
|
await sendMessage(env.BOT_TOKEN, chatId, responseText);
|
|
}
|
|
|
|
export default {
|
|
// HTTP 요청 핸들러
|
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
const url = new URL(request.url);
|
|
|
|
// Webhook 설정 엔드포인트
|
|
if (url.pathname === '/setup-webhook') {
|
|
if (!env.BOT_TOKEN) {
|
|
return Response.json({ error: 'BOT_TOKEN not configured' }, { status: 500 });
|
|
}
|
|
if (!env.WEBHOOK_SECRET) {
|
|
return Response.json({ error: 'WEBHOOK_SECRET not configured' }, { status: 500 });
|
|
}
|
|
|
|
const webhookUrl = `${url.origin}/webhook`;
|
|
const result = await setWebhook(env.BOT_TOKEN, webhookUrl, env.WEBHOOK_SECRET);
|
|
return Response.json(result);
|
|
}
|
|
|
|
// Webhook 정보 조회
|
|
if (url.pathname === '/webhook-info') {
|
|
if (!env.BOT_TOKEN) {
|
|
return Response.json({ error: 'BOT_TOKEN not configured' }, { status: 500 });
|
|
}
|
|
const result = await getWebhookInfo(env.BOT_TOKEN);
|
|
return Response.json(result);
|
|
}
|
|
|
|
// 헬스 체크
|
|
if (url.pathname === '/health') {
|
|
try {
|
|
const userCount = await env.DB
|
|
.prepare('SELECT COUNT(*) as cnt FROM users')
|
|
.first<{ cnt: number }>();
|
|
|
|
const summaryCount = await env.DB
|
|
.prepare('SELECT COUNT(*) as cnt FROM summaries')
|
|
.first<{ cnt: number }>();
|
|
|
|
return Response.json({
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
stats: {
|
|
users: userCount?.cnt || 0,
|
|
summaries: summaryCount?.cnt || 0,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
return Response.json({
|
|
status: 'error',
|
|
error: String(error),
|
|
}, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// Telegram Webhook 처리
|
|
if (url.pathname === '/webhook') {
|
|
// 보안 검증
|
|
const validation = await validateWebhookRequest(request, env);
|
|
|
|
if (!validation.valid) {
|
|
console.error('Webhook validation failed:', validation.error);
|
|
return new Response(validation.error, { status: 401 });
|
|
}
|
|
|
|
try {
|
|
await handleMessage(env, validation.update!);
|
|
return new Response('OK');
|
|
} catch (error) {
|
|
console.error('Message handling error:', error);
|
|
return new Response('Error', { status: 500 });
|
|
}
|
|
}
|
|
|
|
// 루트 경로
|
|
if (url.pathname === '/') {
|
|
return new Response(`
|
|
Telegram Rolling Summary Bot
|
|
|
|
Endpoints:
|
|
GET /health - Health check
|
|
GET /webhook-info - Webhook status
|
|
GET /setup-webhook - Configure webhook
|
|
POST /webhook - Telegram webhook (authenticated)
|
|
|
|
Documentation: https://github.com/your-repo
|
|
`.trim(), {
|
|
headers: { 'Content-Type': 'text/plain' },
|
|
});
|
|
}
|
|
|
|
return new Response('Not Found', { status: 404 });
|
|
},
|
|
};
|