-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathmain.py
More file actions
193 lines (156 loc) · 7.63 KB
/
Copy pathmain.py
File metadata and controls
193 lines (156 loc) · 7.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import telegram
import os
import logging
import html
import traceback
from telegram.constants import ParseMode
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters
from telegram.request import HTTPXRequest
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Response
import uvicorn
from bot_logic import COMMANDS
from handlers.callbacks import button
from tg_ids import ROZEN_CHATID
from handlers.db import get_session
from models import ProcessedUpdate
async def error_handler(update, context):
"""Log the error and send a telegram message to notify the developer."""
logger = logging.getLogger("DCUBABOT")
logger.error("Exception while handling an update:", exc_info=context.error)
try:
tb_list = traceback.format_exception(None, context.error, context.error.__traceback__)
tb_string = "".join(tb_list)
message = f"⚠️ <b>Error en el bot:</b>\n<pre>{html.escape(tb_string)[:3800]}</pre>"
await context.bot.send_message(chat_id=ROZEN_CHATID, text=message, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"Failed to send error message to ROZEN: {e}")
async def log_update(update, context):
"""Log every update received by the bot."""
logger = logging.getLogger("DCUBABOT")
user = update.effective_user
chat = update.effective_chat
user_info = f"{user.id} ({user.username})" if user else None
chat_info = f"{chat.id} ({chat.title})" if chat else None
log_parts = ["Update received"]
if user_info:
log_parts.append(f"User: {user_info}")
if chat_info:
log_parts.append(f"Chat: {chat_info}")
if update.channel_post:
log_parts.append("Type: Channel Post")
if update.channel_post.text:
log_parts.append(f"Text: {update.channel_post.text}")
elif update.edited_channel_post:
log_parts.append("Type: Edited Channel Post")
elif update.poll:
log_parts.append(f"Type: Poll Update (id: {update.poll.id})")
elif update.poll_answer:
log_parts.append(f"Type: Poll Answer (poll_id: {update.poll_answer.poll_id})")
elif update.my_chat_member:
log_parts.append("Type: Bot Chat Member Status Update")
elif update.chat_member:
log_parts.append("Type: Chat Member Update")
elif update.inline_query:
log_parts.append(f"Type: Inline Query (query: {update.inline_query.query})")
if update.effective_message and update.effective_message.text:
log_parts.append(f"Message: {update.effective_message.text}")
elif update.callback_query:
log_parts.append(f"Callback Query: {update.callback_query.data}")
if not user_info and not chat_info and len(log_parts) == 1:
log_parts.append("Type: Unknown/System Update")
log_message = " | ".join(log_parts)
logger.info(log_message)
async def track_activity(update, context):
"""Track last activity time for validated groups."""
import datetime
from telegram import Update
from handlers.db import get_session
from models import Listable
chat = update.effective_chat
if chat and chat.type in ["group", "supergroup"]:
try:
chat_id_str = str(chat.id)
with get_session() as session:
group = session.query(Listable).filter_by(chat_id=chat_id_str).first()
if group and group.warned_at is None:
group.last_activity = datetime.datetime.utcnow()
except Exception as e:
logging.getLogger("DCUBABOT").error(f"Error tracking activity in group {chat.id}: {e}")
async def post_init(application: Application):
"""Set the bot commands on startup."""
from telegram import BotCommand
commands = []
for command_name, command_info in COMMANDS.items():
if 'description' in command_info and command_info['description']:
# Telegram commands must be lowercase and 1-32 chars
commands.append(BotCommand(command_name.lower(), command_info['description'][:256]))
try:
await application.bot.set_my_commands(commands)
logging.getLogger("DCUBABOT").info("Successfully set bot commands.")
except Exception as e:
logging.getLogger("DCUBABOT").error(f"Failed to set bot commands: {e}")
# Instancia global de la aplicación de Telegram
# Aumentamos el read_timeout y connection_pool_size para ser más tolerantes a lag en Cloud Run
t_request = HTTPXRequest(connection_pool_size=8, read_timeout=20.0, write_timeout=20.0, connect_timeout=20.0)
application = Application.builder().token(os.environ["TELEGRAM_BOT_TOKEN"]).request(t_request).post_init(post_init).build()
@asynccontextmanager
async def lifespan(app: FastAPI):
# Setup de la aplicación de Telegram al arrancar FastAPI
await application.initialize()
await application.start()
webhook_url = os.environ["WEBHOOK_URL"]
token = os.environ["TELEGRAM_BOT_TOKEN"]
url = f'{webhook_url.rstrip("/")}/{token}'
await application.bot.set_webhook(url=url)
yield
# Shutdown
await application.stop()
await application.shutdown()
app = FastAPI(lifespan=lifespan)
@app.post("/{token}")
async def telegram_webhook(token: str, request: Request):
if token != os.environ["TELEGRAM_BOT_TOKEN"]:
return Response(status_code=403)
try:
data = await request.json()
update = telegram.Update.de_json(data=data, bot=application.bot)
# Idempotency check: Have we processed this update_id already?
# Using an atomic insert to prevent race conditions when multiple Cloud Run
# instances receive the same update simultaneously.
from sqlalchemy.dialects.postgresql import insert
with get_session() as session:
stmt = insert(ProcessedUpdate).values(update_id=update.update_id).on_conflict_do_nothing()
result = session.execute(stmt)
session.commit()
# If rowcount is 0, the record already existed and ON CONFLICT prevented insertion
if result.rowcount == 0:
logging.getLogger("DCUBABOT").info(f"Skipping duplicate update_id {update.update_id}")
return Response(status_code=200)
# Await the processing SYNCHRONOUSLY before returning 200 OK
# This prevents Cloud Run from throttling the CPU while the bot is doing work
# TODO: If user traffic scales significantly and Telegram throws `RetryAfter` for
# generic `send_message` calls, consider offloading processing or outbound
# messages to Google Cloud Tasks / PubSub to decouple the HTTP response from the work.
await application.process_update(update)
except Exception as e:
logging.error(f"Failed to process update: {e}")
return Response(status_code=200)
def main():
"""Start the bot."""
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] - [%(name)s] - [%(levelname)s] - %(message)s',
)
application.add_error_handler(error_handler)
# Add the logging and tracking middleware handlers with high priority
application.add_handler(MessageHandler(filters.ALL, log_update), group=-1)
application.add_handler(MessageHandler(filters.ALL, track_activity), group=-2)
for command_name, command_info in COMMANDS.items():
application.add_handler(CommandHandler(command_name, command_info['handler']))
application.add_handler(CallbackQueryHandler(button))
# Run the FastAPI app via uvicorn
port = int(os.environ.get("PORT", 8080))
uvicorn.run(app, host="0.0.0.0", port=port)
if __name__ == "__main__":
main()