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:
kappa
2026-01-14 13:00:44 +09:00
commit 1e71e035e7
15 changed files with 2272 additions and 0 deletions

201
src/index.ts Normal file
View 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 });
},
};