Files

108 lines
4.1 KiB
Python

import json
import os
import threading
import urllib.request
import urllib.parse
import boto3
# Cache bot token (fetched once at Lambda init)
_bot_token: str | None = None
_token_lock = threading.Lock()
def get_bot_token() -> str:
global _bot_token
if _bot_token is None:
with _token_lock:
if _bot_token is None:
sm = boto3.client('secretsmanager')
_bot_token = sm.get_secret_value(
SecretId=os.environ['TELEGRAM_BOT_TOKEN_SECRET_ARN']
)['SecretString']
return _bot_token
def send_typing(chat_id: str, thread_id: int | None = None) -> None:
"""Fire-and-forget typing action (does not raise on failure)."""
try:
token = get_bot_token()
payload: dict = {'chat_id': chat_id, 'action': 'typing'}
if thread_id is not None:
payload['message_thread_id'] = thread_id
data = json.dumps(payload).encode()
req = urllib.request.Request(
f'https://api.telegram.org/bot{token}/sendChatAction',
data=data,
headers={'Content-Type': 'application/json'},
)
urllib.request.urlopen(req, timeout=3)
except Exception:
pass # typing is best-effort
def handler(event, context):
# ── Validate Telegram webhook secret ──────────────────────────────────
expected_secret = os.environ.get('TELEGRAM_WEBHOOK_SECRET', '')
if expected_secret:
headers = event.get('headers') or {}
received = headers.get('x-telegram-bot-api-secret-token', '')
if received != expected_secret:
return {'statusCode': 403, 'body': 'Forbidden'}
# ── Parse Telegram Update ─────────────────────────────────────────────
try:
body = json.loads(event.get('body', '{}'))
except json.JSONDecodeError:
print(f'[tg-ingest] Bad JSON body')
return {'statusCode': 400, 'body': 'Bad Request'}
print(f'[tg-ingest] Update keys: {list(body.keys())}')
update_id = body.get('update_id')
# Support regular messages and edited messages
message = body.get('message') or body.get('edited_message')
if not message:
print(f'[tg-ingest] No message field, update_type={list(body.keys())}')
return {'statusCode': 200, 'body': 'ok'}
chat_id = str(message.get('chat', {}).get('id', ''))
message_thread_id = message.get('message_thread_id') # present for supergroup topics
text = message.get('text', '')
from_user = message.get('from', {})
timestamp = message.get('date', 0)
print(f'[tg-ingest] chat_id={chat_id} text_len={len(text)} update_id={update_id}')
if not chat_id or not text:
print(f'[tg-ingest] Dropping: chat_id={chat_id!r} text={text!r}')
return {'statusCode': 200, 'body': 'ok'}
# ── Send typing action (non-blocking, background thread) ──────────────
t = threading.Thread(target=send_typing, args=(chat_id, message_thread_id))
t.daemon = True
t.start()
# ── Enqueue to SQS FIFO ───────────────────────────────────────────────
sqs = boto3.client('sqs')
sqs.send_message(
QueueUrl=os.environ['MESSAGE_QUEUE_URL'],
MessageGroupId=chat_id,
MessageDeduplicationId=str(update_id),
MessageBody=json.dumps({
'channel': 'telegram',
'chat_id': chat_id,
'message_thread_id': message_thread_id, # None for regular chats, int for topics
'messages': [{
'text': text,
'from_id': str(from_user.get('id', '')),
'from_username': from_user.get('username', ''),
'from_name': f"{from_user.get('first_name', '')} {from_user.get('last_name', '')}".strip(),
}],
'update_id': update_id,
'timestamp': timestamp,
}),
)
return {'statusCode': 200, 'body': 'ok'}