Initial commit: Telegram bot with Cloudflare Workers
- OpenAI GPT-4o-mini with Function Calling - Cloudflare D1 for user profiles and message buffer - Sliding window (3 summaries max) for infinite context - Tools: weather, search, time, calculator - Workers AI fallback support - Webhook security with rate limiting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
201
src/index.ts
Normal file
201
src/index.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { Env, TelegramUpdate } from './types';
|
||||
import { validateWebhookRequest, checkRateLimit } from './security';
|
||||
import { sendMessage, 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);
|
||||
} 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 });
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user