TgrEzLi 0.1.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of TgrEzLi might be problematic. Click here for more details.

TgrEzLi/core.py CHANGED
@@ -1,449 +1,558 @@
1
- import logging, traceback, threading, asyncio, json
2
- import requests
3
- from functools import wraps
4
- from http.server import BaseHTTPRequestHandler, HTTPServer
5
- from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup
6
- from telegram.ext import ApplicationBuilder, ContextTypes, CallbackQueryHandler, MessageHandler, filters
7
- from PyCypher import Cy
8
- import os
9
-
10
- __version__="v0.1.2"
11
-
12
- # Logging configuration
13
- logger = logging.getLogger("TgrEzLi")
14
- logger.setLevel(logging.DEBUG)
15
- _file_handler = logging.FileHandler("TgrEzLi.log", encoding="utf-8")
16
- _file_handler.setLevel(logging.DEBUG)
17
- _formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(name)s - %(message)s")
18
- _file_handler.setFormatter(_formatter)
19
- logger.addHandler(_file_handler)
20
- _console_handler = logging.StreamHandler()
21
- _console_handler.setLevel(logging.INFO)
22
- _console_handler.setFormatter(_formatter)
23
- logger.addHandler(_console_handler)
24
-
25
- # Thread-local storage
26
- import threading
27
- _local = threading.local()
28
-
29
- class _TgMsgData:
30
- def __init__(self, text, msg_id, chat_id, user_id, user_name, timestamp, raw_update):
31
- self.text = text
32
- self.msgId = msg_id
33
- self.chatId = chat_id
34
- self.userId = user_id
35
- self.userName = user_name
36
- self.timestamp = timestamp
37
- self.raw_update = raw_update
38
-
39
- class _TgCmdData:
40
- def __init__(self, command, args, msg_id, chat_id, user_id, user_name, timestamp, raw_update):
41
- self.command = command
42
- self.args = args
43
- self.msgId = msg_id
44
- self.chatId = chat_id
45
- self.userId = user_id
46
- self.userName = user_name
47
- self.timestamp = timestamp
48
- self.raw_update = raw_update
49
-
50
- class _TgCbData:
51
- def __init__(self, text, value, msg_id, chat_id, user_id, user_name, timestamp, raw_update):
52
- self.text = text
53
- self.value = value
54
- self.msgId = msg_id
55
- self.chatId = chat_id
56
- self.userId = user_id
57
- self.userName = user_name
58
- self.timestamp = timestamp
59
- self.raw_update = raw_update
60
-
61
- class _TgArgsData:
62
- def __init__(self, data_dict: dict):
63
- self._data = data_dict
64
- def get(self, key, default=None):
65
- return self._data.get(key, default)
66
-
67
- class _TgMsgProxy:
68
- def __getattr__(self, item):
69
- if not hasattr(_local, "TgMsg") or _local.TgMsg is None:
70
- raise AttributeError("TgMsg not available in this context.")
71
- return getattr(_local.TgMsg, item)
72
-
73
- class _TgCmdProxy:
74
- def __getattr__(self, item):
75
- if not hasattr(_local, "TgCmd") or _local.TgCmd is None:
76
- raise AttributeError("TgCmd not available in this context.")
77
- return getattr(_local.TgCmd, item)
78
-
79
- class _TgCbProxy:
80
- def __getattr__(self, item):
81
- if not hasattr(_local, "TgCb") or _local.TgCb is None:
82
- raise AttributeError("TgCb not available in this context.")
83
- return getattr(_local.TgCb, item)
84
-
85
- class _TgArgsProxy:
86
- def __getattr__(self, item):
87
- if not hasattr(_local, "TgArgs") or _local.TgArgs is None:
88
- raise AttributeError("TgArgs not available in this context.")
89
- return getattr(_local.TgArgs, item)
90
-
91
- TgMsg = _TgMsgProxy()
92
- TgCmd = _TgCmdProxy()
93
- TgCb = _TgCbProxy()
94
- TgArgs = _TgArgsProxy()
1
+ """Main bot orchestrator: Telegram polling, handlers, optional API server."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import signal
6
+ import traceback
7
+ from threading import Event, Thread
8
+ from typing import Any
9
+
10
+ from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
11
+ from telegram.ext import (
12
+ ApplicationBuilder,
13
+ ContextTypes,
14
+ CallbackQueryHandler,
15
+ MessageHandler,
16
+ filters,
17
+ )
18
+
19
+ from TgrEzLi.api_server import ApiServer
20
+ from TgrEzLi.defaults import DEFAULT_BANNER_FILLER
21
+ from TgrEzLi.handlers import HandlerRegistry, call_user_function
22
+ from TgrEzLi.logger import get_default_logger
23
+ from TgrEzLi.types import TgCbData, TgCmdData, TgMsgData, TgrezliConfig
24
+
25
+ __version__ = "0.4.0"
26
+
27
+
28
+ def _print_banner(
29
+ logger: Any,
30
+ name: str,
31
+ version: str,
32
+ author: str,
33
+ filler: str = DEFAULT_BANNER_FILLER,
34
+ ) -> None:
35
+ banner = f"\n{filler * 50}\n{name} {version} by {author}\n{filler * 50}\n"
36
+ logger.info(banner)
37
+
95
38
 
96
39
  class TEL:
97
- def __init__(self):
98
- self.chat_ids = {}
99
- self.default_chat_name = None
100
- self._message_handlers = []
101
- self._command_handlers = {}
102
- self._callback_handlers = []
103
- self._api_routes = {}
104
- self._api_server_thread = None
105
- self._api_server_running = False
106
-
107
- self.application = None
108
- self._loop = None
109
- self._polling_thread = None
110
- self._connected = False
111
-
112
- self._save_log = True
113
- self._api_host = "localhost"
114
- self._api_port = 8080
115
- printBanner('TgrEzLi', __version__, 'by eaannist', '█')
116
-
117
- def setSaveLog(self, flag: bool):
118
- self._save_log = flag
119
- if not flag:
120
- if _file_handler in logger.handlers:
121
- logger.removeHandler(_file_handler)
122
- else:
123
- if _file_handler not in logger.handlers:
124
- logger.addHandler(_file_handler)
125
-
126
- def setHost(self, host: str):
127
- self._api_host = host
128
-
129
- def setPort(self, port: int):
130
- self._api_port = port
131
-
132
- def login(self, ppp):
133
- if os.path.exists('tgrdata.cy'):
134
- try:
135
- lines=Cy().decLines('tgrdata.cy').P(ppp)
136
- token=lines[0]
137
- chat_dict = {}
138
- for elemento in lines[1:]:
139
- chat_name, chat_id = elemento.split(":", 1)
140
- chat_dict[chat_name] = chat_id
141
- self.connect(token, chat_dict)
142
- except: raise Exception("Invalid password or broken tgrdata.cy file.")
143
- else: raise FileNotFoundError("File tgrdata.cy already created not found.")
144
-
145
- def signup(self, token, chat_dict, ppp):
146
- if os.path.exists('tgrdata.cy'):
147
- raise Exception("File tgrdata.cy already created.")
148
- lines = []
149
- lines.append(token)
150
- for chat_name, chat_id in chat_dict.items():
151
- line= chat_name + ":" + chat_id
152
- lines.append(line)
153
- Cy().encLines('tgrdata.cy').Lines(lines).P(ppp)
154
- self.login(ppp)
155
-
156
- def connect(self, token, chat_dict: dict):
157
- logger.info("Initialyzing BOT...")
40
+ """
41
+ Orchestrates Telegram polling, message/command/callback handlers,
42
+ and an optional embedded HTTP API server for custom POST routes.
43
+ """
44
+
45
+ def __init__(self, config: TgrezliConfig | None = None) -> None:
46
+ self.config = config or TgrezliConfig()
47
+ self.logger = get_default_logger(
48
+ save_log=self.config.save_log,
49
+ log_file=self.config.log_file,
50
+ )
51
+ self.registry = HandlerRegistry()
52
+ self._credential_manager: Any = None
53
+
54
+ self.chat_ids: dict[str, str] = {}
55
+ self.default_chat_name: str | None = None
56
+
57
+ self.application: Any = None
58
+ self.loop: asyncio.AbstractEventLoop | None = None
59
+ self.polling_thread: Thread | None = None
60
+ self._running = False # True while polling thread is active (send_msg works)
61
+ self._stop_event = Event() # set by stop() so run() returns
62
+
63
+ self.api_server: ApiServer | None = None
64
+
65
+ _print_banner(self.logger, "TgrEzLi", f"v{__version__}", "eaannist")
66
+
67
+ def _get_credential_manager(self) -> Any:
68
+ if self._credential_manager is None:
69
+ from TgrEzLi.crypto import CredentialManager
70
+ self._credential_manager = CredentialManager()
71
+ return self._credential_manager
72
+
73
+ def set_save_log(self, flag: bool) -> None:
74
+ """Enable or disable writing logs to the log file."""
75
+ self.config.save_log = flag
76
+ self.logger.set_save_log(flag)
77
+
78
+ def set_host(self, host: str) -> None:
79
+ """Set the API server host."""
80
+ self.config.api_host = host
81
+
82
+ def set_port(self, port: int) -> None:
83
+ """Set the API server port."""
84
+ self.config.api_port = port
85
+
86
+ def signup(self, token: str, chat_dict: dict[str, str], password: str) -> None:
87
+ """Encrypt and save token + chat_dict, then connect."""
88
+ self._get_credential_manager().signup(token, chat_dict, password)
89
+ self.login(password)
90
+
91
+ def login(self, password: str) -> None:
92
+ """Load token + chat_dict from encrypted file and connect."""
93
+ token, chat_dict = self._get_credential_manager().login(password)
94
+ self.connect(token, chat_dict)
95
+
96
+ def connect(self, token: str, chat_dict: dict[str, str]) -> None:
97
+ """Prepare the bot and start polling in a background thread. send_msg() works immediately; use run() to block the main thread until stop()."""
158
98
  if not chat_dict:
159
99
  raise ValueError("Chat dictionary is empty.")
160
100
  self.chat_ids = {str(k): str(v) for k, v in chat_dict.items()}
161
101
  self.default_chat_name = next(iter(self.chat_ids))
162
- self._loop = asyncio.new_event_loop()
102
+
103
+ self.loop = asyncio.new_event_loop()
163
104
  self.application = ApplicationBuilder().token(token).build()
105
+
164
106
  self.application.add_handler(MessageHandler(filters.COMMAND, self._command_handler), group=0)
165
- self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self._message_handler), group=1)
107
+ self.application.add_handler(
108
+ MessageHandler(filters.TEXT & ~filters.COMMAND, self._message_handler),
109
+ group=1,
110
+ )
166
111
  self.application.add_handler(CallbackQueryHandler(self._callback_handler), group=2)
167
112
  self.application.add_error_handler(self._error_handler)
168
- self._polling_thread = threading.Thread(target=self._run_loop, daemon=True)
169
- self._polling_thread.start()
170
- self._connected = True
171
- logger.info("BOT Connected and polling activated.")
172
113
 
173
- def _run_loop(self):
114
+ self.polling_thread = Thread(target=self._run_polling, daemon=True)
115
+ self.polling_thread.start()
116
+ self._running = True
117
+ self.logger.info("Bot connected and polling (background). send_msg() works; call run() to block until stop().")
118
+
119
+ def _run_polling(self) -> None:
174
120
  try:
175
- asyncio.set_event_loop(self._loop)
176
- logger.info("Starting run_polling() thread...")
177
- self._loop.run_until_complete(self.application.run_polling())
121
+ asyncio.set_event_loop(self.loop)
122
+ if self.application and self.loop:
123
+ self.loop.run_until_complete(self.application.run_polling())
178
124
  except Exception as e:
179
- logger.error(f"Error while run_polling(): {e}")
180
- logger.debug(traceback.format_exc())
125
+ self.logger.error("Error in run_polling: %s", e)
126
+ self.logger.debug(traceback.format_exc())
181
127
  finally:
182
- logger.info("Closing event loop.")
183
- self._loop.run_until_complete(self.application.shutdown())
184
- self._loop.close()
128
+ self._running = False
129
+ self.logger.info("Polling thread exiting.")
130
+ if self.application and self.loop:
131
+ try:
132
+ self.loop.run_until_complete(self.application.shutdown())
133
+ except Exception:
134
+ pass
135
+ if self.loop and self.loop.is_running():
136
+ self.loop.stop()
137
+ if self.loop:
138
+ self.loop.close()
139
+ self.loop = None
140
+ self.application = None
141
+
142
+ def run(self) -> None:
143
+ """Block the main thread until stop() is called (e.g. Ctrl+C). Starts the API server in a background thread if routes are registered. Polling is already active after connect()."""
144
+ if not self._running:
145
+ raise RuntimeError("Call connect() before run().")
146
+ if self.registry.api_routes and not self.api_server:
147
+ self.api_server = ApiServer(
148
+ self.logger, self.registry,
149
+ host=self.config.api_host,
150
+ port=self.config.api_port,
151
+ )
152
+ self.api_server.start()
153
+
154
+ def _on_signal(_sig: int, _frame: object) -> None:
155
+ self.logger.info("Shutdown requested (signal).")
156
+ self.stop()
185
157
 
186
- async def _message_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
187
- if not update.message: return
158
+ try:
159
+ signal.signal(signal.SIGINT, _on_signal)
160
+ except ValueError:
161
+ pass
162
+
163
+ self._stop_event.clear()
164
+ self.logger.info("Main thread blocking until stop() (Ctrl+C).")
165
+ self._stop_event.wait()
166
+
167
+ def stop(self) -> None:
168
+ """Stop polling and API server; unblocks run() if it was waiting. Safe to call from a signal handler or another thread."""
169
+ if self.application and getattr(self.application, "updater", None):
170
+ self.application.updater.stop()
171
+ if self.application:
172
+ self.application.stop()
173
+ if self.api_server:
174
+ self.api_server.stop()
175
+ self.api_server = None
176
+ self._stop_event.set()
177
+ if self.polling_thread and self.polling_thread.is_alive():
178
+ self.logger.info("Waiting for polling thread to exit...")
179
+ self.polling_thread.join(timeout=5)
180
+ self._running = False
181
+ self.logger.info("Bot stopped.")
182
+
183
+ async def _error_handler(self, update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
184
+ self.logger.error("Unhandled error!")
185
+ self.logger.error(str(context.error))
186
+ self.logger.debug(traceback.format_exc())
187
+
188
+ async def _message_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
189
+ if not update.message:
190
+ return
188
191
  chat_id = str(update.effective_chat.id)
189
192
  chat_name = self._get_chat_name_by_id(chat_id)
190
- if not chat_name: return
193
+ if not chat_name:
194
+ return
191
195
  text = update.message.text or ""
192
- tgmsg_data = _TgMsgData(text, update.message.message_id, chat_id,
193
- update.effective_user.id if update.effective_user else None,
194
- update.effective_user.username if update.effective_user else None,
195
- update.message.date, update)
196
- for (chats, func) in self._message_handlers:
196
+ tgmsg_data = TgMsgData(
197
+ text=text,
198
+ msg_id=update.message.message_id,
199
+ chat_id=chat_id,
200
+ user_id=update.effective_user.id if update.effective_user else None,
201
+ user_name=update.effective_user.username if update.effective_user else None,
202
+ timestamp=update.message.date,
203
+ raw_update=update,
204
+ )
205
+ for chats, func in self.registry.message_handlers:
197
206
  if chat_name in chats:
198
- self._call_user_function(func, TgMsg=tgmsg_data)
207
+ call_user_function(self.logger, func, TgMsg=tgmsg_data)
199
208
 
200
- async def _command_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
201
- if not update.message: return
209
+ async def _command_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
210
+ if not update.message:
211
+ return
202
212
  text = update.message.text or ""
203
213
  parts = text.strip().split(maxsplit=1)
204
214
  command_part = parts[0].lower()
205
215
  args_part = parts[1] if len(parts) > 1 else ""
206
- if '@' in command_part:
207
- command_part = command_part.split('@',1)[0]
216
+ if "@" in command_part:
217
+ command_part = command_part.split("@", 1)[0]
208
218
  command_name = command_part.lstrip("/")
219
+
209
220
  chat_id = str(update.effective_chat.id)
210
221
  chat_name = self._get_chat_name_by_id(chat_id)
211
- if not chat_name: return
212
- tgcmd_data = _TgCmdData(command_part, args_part, update.message.message_id, chat_id,
213
- update.effective_user.id if update.effective_user else None,
214
- update.effective_user.username if update.effective_user else None,
215
- update.message.date, update)
216
- if command_name in self._command_handlers:
217
- for (chats, func) in self._command_handlers[command_name]:
222
+ if not chat_name:
223
+ return
224
+
225
+ tgcmd_data = TgCmdData(
226
+ command=command_part,
227
+ args=args_part,
228
+ msg_id=update.message.message_id,
229
+ chat_id=chat_id,
230
+ user_id=update.effective_user.id if update.effective_user else None,
231
+ user_name=update.effective_user.username if update.effective_user else None,
232
+ timestamp=update.message.date,
233
+ raw_update=update,
234
+ )
235
+ if command_name in self.registry.command_handlers:
236
+ for chats, func in self.registry.command_handlers[command_name]:
218
237
  if chat_name in chats:
219
- self._call_user_function(func, TgCmd=tgcmd_data)
238
+ call_user_function(self.logger, func, TgCmd=tgcmd_data)
220
239
 
221
- async def _callback_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
222
- if not update.callback_query: return
240
+ async def _callback_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
241
+ if not update.callback_query:
242
+ return
243
+ cb_query = update.callback_query
223
244
  chat_id = str(update.effective_chat.id)
224
245
  chat_name = self._get_chat_name_by_id(chat_id)
225
- if not chat_name: return
226
- cb_query = update.callback_query
246
+ if not chat_name:
247
+ return
248
+
227
249
  button_text = None
228
250
  if cb_query.message and cb_query.message.reply_markup:
229
251
  for row in cb_query.message.reply_markup.inline_keyboard:
230
252
  for btn in row:
231
253
  if btn.callback_data == cb_query.data:
232
- button_text = btn.text; break
233
- tgcb_data = _TgCbData(button_text, cb_query.data,
234
- cb_query.message.message_id if cb_query.message else None,
235
- chat_id,
236
- cb_query.from_user.id if cb_query.from_user else None,
237
- cb_query.from_user.username if cb_query.from_user else None,
238
- cb_query.message.date if cb_query.message else None,
239
- update)
254
+ button_text = btn.text
255
+ break
256
+
257
+ tgcb_data = TgCbData(
258
+ text=button_text,
259
+ value=cb_query.data,
260
+ msg_id=cb_query.message.message_id if cb_query.message else None,
261
+ chat_id=chat_id,
262
+ user_id=cb_query.from_user.id if cb_query.from_user else None,
263
+ user_name=cb_query.from_user.username if cb_query.from_user else None,
264
+ timestamp=cb_query.message.date if cb_query.message else None,
265
+ raw_update=update,
266
+ )
240
267
  await cb_query.answer()
241
- for (chats, func) in self._callback_handlers:
268
+
269
+ for chats, func in self.registry.callback_handlers:
242
270
  if chat_name in chats:
243
- self._call_user_function(func, TgCb=tgcb_data)
271
+ call_user_function(self.logger, func, TgCb=tgcb_data)
244
272
 
245
- def onMessage(self, chat=None):
246
- def decorator(func):
247
- self._message_handlers.append((self._parse_chat(chat), func))
273
+ def on_message(self, chat: str | list[str] | set[str] | None = None) -> Any:
274
+ """Decorator for message handlers."""
275
+ def decorator(func: Any) -> Any:
276
+ self.registry.register_message_handler(func, self._parse_chat(chat))
248
277
  return func
249
278
  return decorator
250
279
 
251
- def onCommand(self, command, chat=None):
280
+ def on_command(self, command: str, chat: str | list[str] | set[str] | None = None) -> Any:
281
+ """Decorator for command handlers."""
252
282
  cmd = command.lstrip("/")
253
- def decorator(func):
254
- self._command_handlers.setdefault(cmd, []).append((self._parse_chat(chat), func))
283
+ def decorator(func: Any) -> Any:
284
+ self.registry.register_command_handler(cmd, func, self._parse_chat(chat))
255
285
  return func
256
286
  return decorator
257
287
 
258
- def onCallback(self, chat=None):
259
- def decorator(func):
260
- self._callback_handlers.append((self._parse_chat(chat), func))
288
+ def on_callback(self, chat: str | list[str] | set[str] | None = None) -> Any:
289
+ """Decorator for callback (inline keyboard) handlers."""
290
+ def decorator(func: Any) -> Any:
291
+ self.registry.register_callback_handler(func, self._parse_chat(chat))
261
292
  return func
262
293
  return decorator
263
294
 
264
- def onApiReq(self, endpoint, args=None, host=None, port=None):
265
- if args is None: args = []
266
- host = host or self._api_host
267
- port = port or self._api_port
268
- def decorator(func):
269
- self._api_routes[endpoint] = {"args": args, "func": func}
270
- if not self._api_server_running:
271
- self._start_api_server(host, port)
295
+ def on_api_req(
296
+ self,
297
+ endpoint: str,
298
+ args: list[str] | None = None,
299
+ host: str | None = None,
300
+ port: int | None = None,
301
+ ) -> Any:
302
+ """Decorator for custom API POST endpoints. Starts the API server if needed."""
303
+ if args is None:
304
+ args = []
305
+ def decorator(func: Any) -> Any:
306
+ self.registry.register_api_route(endpoint, args, func)
307
+ if not self.api_server:
308
+ h = host or self.config.api_host
309
+ p = port or self.config.api_port
310
+ self.api_server = ApiServer(self.logger, self.registry, host=h, port=p)
311
+ self.api_server.start()
272
312
  return func
273
313
  return decorator
274
314
 
275
- def sendMsg(self, text, chat=None):
315
+ def send_msg(self, text: str, chat: str | list[str] | set[str] | None = None) -> None:
276
316
  self._send(self.application.bot.send_message, {"text": text}, chat)
277
317
 
278
- def replyToMsg(self, text, msg_id, chat=None):
279
- self._send(self.application.bot.send_message, {"text": text, "reply_to_message_id": msg_id}, chat)
280
-
281
- def sendImg(self, photo_path, caption=None, chat=None):
282
- self._send(self.application.bot.send_photo, {"photo": photo_path, "caption": caption}, chat)
283
-
284
- def sendFile(self, file_path, caption=None, chat=None):
285
- self._send(self.application.bot.send_document, {"document": file_path, "caption": caption}, chat)
286
-
287
- def sendPosition(self, latitude, longitude, chat=None):
288
- self._send(self.application.bot.send_location, {"latitude": latitude, "longitude": longitude}, chat)
289
-
290
- def sendButtons(self, text, buttons, chat=None):
291
- kb = [[InlineKeyboardButton(btn["text"], callback_data=btn["value"]) for btn in row] for row in buttons]
318
+ def reply_to_msg(
319
+ self,
320
+ text: str,
321
+ msg_id: int,
322
+ chat: str | list[str] | set[str] | None = None,
323
+ ) -> None:
324
+ self._send(
325
+ self.application.bot.send_message,
326
+ {"text": text, "reply_to_message_id": msg_id},
327
+ chat,
328
+ )
329
+
330
+ def send_img(
331
+ self,
332
+ photo_path: str,
333
+ caption: str | None = None,
334
+ chat: str | list[str] | set[str] | None = None,
335
+ ) -> None:
336
+ self._send(
337
+ self.application.bot.send_photo,
338
+ {"photo": photo_path, "caption": caption},
339
+ chat,
340
+ )
341
+
342
+ def send_file(
343
+ self,
344
+ file_path: str,
345
+ caption: str | None = None,
346
+ chat: str | list[str] | set[str] | None = None,
347
+ ) -> None:
348
+ self._send(
349
+ self.application.bot.send_document,
350
+ {"document": file_path, "caption": caption},
351
+ chat,
352
+ )
353
+
354
+ def send_position(
355
+ self,
356
+ latitude: float,
357
+ longitude: float,
358
+ chat: str | list[str] | set[str] | None = None,
359
+ ) -> None:
360
+ self._send(
361
+ self.application.bot.send_location,
362
+ {"latitude": latitude, "longitude": longitude},
363
+ chat,
364
+ )
365
+
366
+ def send_buttons(
367
+ self,
368
+ text: str,
369
+ buttons: list[list[dict[str, str]]],
370
+ chat: str | list[str] | set[str] | None = None,
371
+ ) -> None:
372
+ kb = [
373
+ [InlineKeyboardButton(btn["text"], callback_data=btn["value"]) for btn in row]
374
+ for row in buttons
375
+ ]
292
376
  markup = InlineKeyboardMarkup(kb)
293
- self._send(self.application.bot.send_message, {"text": text, "reply_markup": markup}, chat)
294
-
295
- def _send(self, send_func, params: dict, chat):
296
- if not self._connected:
297
- raise RuntimeError("Not connected. Use connect() before using other methods.")
298
- for chat_name in self._parse_chat(chat):
377
+ self._send(
378
+ self.application.bot.send_message,
379
+ {"text": text, "reply_markup": markup},
380
+ chat,
381
+ )
382
+
383
+ def _send(
384
+ self,
385
+ send_func: Any,
386
+ params: dict[str, Any],
387
+ chat: str | list[str] | set[str] | None,
388
+ ) -> None:
389
+ if not self._running or not self.application or not self.loop:
390
+ raise RuntimeError("Bot is not connected. Call connect() first.")
391
+ chats = self._parse_chat(chat)
392
+ for chat_name in chats:
299
393
  if chat_name not in self.chat_ids:
300
- logger.error(f"Chat '{chat_name}' not found. Not sent.")
394
+ self.logger.error("Chat '%s' not found. Message not sent.", chat_name)
301
395
  continue
302
396
  params["chat_id"] = self.chat_ids[chat_name]
303
397
  coro = send_func(**params)
304
- asyncio.run_coroutine_threadsafe(coro, self._loop)
305
-
306
- def sendLog(self, limit=None, chat=None):
398
+ asyncio.run_coroutine_threadsafe(coro, self.loop)
399
+
400
+ def send_log(
401
+ self,
402
+ limit: int | None = None,
403
+ chat: str | list[str] | set[str] | None = None,
404
+ ) -> None:
405
+ """Send the last `limit` lines of the log file, or the full log if limit is None."""
307
406
  try:
308
- with open("TgrEzLi.log", "r", encoding="utf-8") as f:
407
+ with open(self.config.log_file, "r", encoding="utf-8") as f:
309
408
  lines = f.readlines()
310
409
  text = "".join(lines[-limit:]) if limit else "".join(lines)
311
- except Exception as e:
312
- text = f"Errore nella lettura del log: {e}"
313
- self.sendMsg(text, chat)
410
+ except OSError as e:
411
+ text = f"Error reading log file: {e}"
412
+ self.send_msg(text, chat)
314
413
 
315
- def sendInfo(self, chat=None):
414
+ def send_info(self, chat: str | list[str] | set[str] | None = None) -> None:
415
+ """Send bot info (chats, handlers, API server, log saving) to the given chat."""
316
416
  info = "Bot Info:\n"
317
417
  info += f"Chat IDs: {self.chat_ids}\n"
318
- info += f"onMessage handlers: {len(self._message_handlers)}\n"
319
- info += f"onCommand handlers: { {cmd: len(funcs) for cmd, funcs in self._command_handlers.items()} }\n"
320
- info += f"onCallback handlers: {len(self._callback_handlers)}\n"
321
- info += f"API Routes: {list(self._api_routes.keys())}\n"
322
- info += f"API Server: {self._api_host}:{self._api_port}\n"
323
- info += f"Salvataggio Log: {self._save_log}"
324
- self.sendMsg(info, chat)
325
-
326
- def sendRegisteredHandlers(self, chat=None):
327
- txt = "Handlers registrati:\n\nonMessage:\n"
328
- for chats, func in self._message_handlers:
329
- txt += f" - {func.__name__} su {chats}\n"
418
+ info += f"onMessage handlers: {len(self.registry.message_handlers)}\n"
419
+ info += "onCommand handlers: {\n"
420
+ for cmd, funcs in self.registry.command_handlers.items():
421
+ info += f" /{cmd} : {len(funcs)} handlers\n"
422
+ info += "}\n"
423
+ info += f"onCallback handlers: {len(self.registry.callback_handlers)}\n"
424
+ info += f"API Routes: {list(self.registry.api_routes.keys())}\n"
425
+ if self.api_server:
426
+ info += f"API Server: {self.api_server.host}:{self.api_server.port}\n"
427
+ else:
428
+ info += "API Server: Not started\n"
429
+ info += f"Log saving: {self.config.save_log}\n"
430
+ self.send_msg(info, chat)
431
+
432
+ def send_registered_handlers(
433
+ self,
434
+ chat: str | list[str] | set[str] | None = None,
435
+ ) -> None:
436
+ """Send a list of registered handlers for debugging."""
437
+ txt = "Registered handlers:\n\nonMessage:\n"
438
+ for chats, func in self.registry.message_handlers:
439
+ txt += f" - {func.__name__} on {chats}\n"
330
440
  txt += "\nonCommand:\n"
331
- for cmd, lst in self._command_handlers.items():
441
+ for cmd, lst in self.registry.command_handlers.items():
332
442
  for chats, func in lst:
333
- txt += f" - /{cmd} -> {func.__name__} su {chats}\n"
443
+ txt += f" - /{cmd} -> {func.__name__} on {chats}\n"
334
444
  txt += "\nonCallback:\n"
335
- for chats, func in self._callback_handlers:
336
- txt += f" - {func.__name__} su {chats}\n"
445
+ for chats, func in self.registry.callback_handlers:
446
+ txt += f" - {func.__name__} on {chats}\n"
337
447
  txt += "\nAPI Routes:\n"
338
- for route in self._api_routes:
448
+ for route in self.registry.api_routes:
339
449
  txt += f" - {route}\n"
340
- self.sendMsg(txt, chat)
450
+ self.send_msg(txt, chat)
341
451
 
342
- def _parse_chat(self, chat):
343
- if chat is None:
344
- return {self.default_chat_name}
345
- elif isinstance(chat, str):
346
- return {chat}
347
- elif isinstance(chat, (list, tuple, set)):
348
- return set(chat)
349
- else:
350
- raise ValueError(f"Invalid parameter: {chat}")
351
-
352
- def _get_chat_name_by_id(self, chat_id):
452
+ def _get_chat_name_by_id(self, chat_id: str) -> str | None:
353
453
  for name, cid in self.chat_ids.items():
354
454
  if cid == chat_id:
355
455
  return name
356
456
  return None
357
457
 
358
- def _call_user_function(self, func, TgMsg=None, TgCmd=None, TgCb=None, TgArgs=None):
359
- def worker():
360
- try:
361
- _local.TgMsg = TgMsg; _local.TgCmd = TgCmd; _local.TgCb = TgCb; _local.TgArgs = TgArgs
362
- func()
363
- except Exception as e:
364
- logger.error(f"User handler error: {e}")
365
- logger.debug(traceback.format_exc())
366
- finally:
367
- _local.TgMsg = _local.TgCmd = _local.TgCb = _local.TgArgs = None
368
- threading.Thread(target=worker, daemon=True).start()
369
-
370
- async def _error_handler(self, update: object, context: ContextTypes.DEFAULT_TYPE):
371
- logger.error("Unmanaged error!")
372
- logger.error(context.error)
373
- logger.debug(traceback.format_exc())
374
-
375
- def _start_api_server(self, host, port):
376
- self._api_server_running = True
377
- class _ApiRequestHandler(BaseHTTPRequestHandler):
378
- def do_POST(self_inner):
379
- path = self_inner.path
380
- length = int(self_inner.headers.get('Content-Length', 0))
381
- raw_body = self_inner.rfile.read(length) if length > 0 else b''
382
- try:
383
- data = json.loads(raw_body.decode('utf-8'))
384
- except:
385
- data = {}
386
- if path in self._api_routes:
387
- route_info = self._api_routes[path]
388
- func = route_info["func"]
389
- TgArgsDataObj = _TgArgsData(data)
390
- self._call_user_function(func, TgArgs=TgArgsDataObj)
391
- self_inner.send_response(200)
392
- self_inner.send_header("Content-type", "application/json; charset=utf-8")
393
- self_inner.end_headers()
394
- resp = {"status": "ok", "path": path, "data_received": data}
395
- self_inner.wfile.write(json.dumps(resp).encode('utf-8'))
396
- else:
397
- self_inner.send_response(404)
398
- self_inner.send_header("Content-type", "application/json; charset=utf-8")
399
- self_inner.end_headers()
400
- resp = {"status": "error", "message": "Route not found"}
401
- self_inner.wfile.write(json.dumps(resp).encode('utf-8'))
402
- def serve_forever():
403
- class ReusableHTTPServer(HTTPServer):
404
- allow_reuse_address = True
405
- with ReusableHTTPServer((host, port), _ApiRequestHandler) as httpd:
406
- logger.info(f"HTTP API listening on http://{host}:{port}")
407
- httpd.serve_forever()
408
- self._api_server_thread = threading.Thread(target=serve_forever, daemon=True)
409
- self._api_server_thread.start()
410
-
411
- class TReq:
412
- def __init__(self, endpoint: str):
413
- self.endpoint = endpoint
414
- self._host = "localhost"
415
- self._port = 9999
416
- self._body = {}
417
-
418
- def host(self, host: str):
419
- self._host = host
420
- return self
421
-
422
- def port(self, port: int):
423
- self._port = port
424
- return self
425
-
426
- def arg(self, name: str, value):
427
- self._body[name] = value
428
- return self
429
-
430
- def body(self, body_dict: dict):
431
- self._body = body_dict
432
- return self
433
-
434
- def send(self):
435
- url = f"http://{self._host}:{self._port}{self.endpoint}"
436
- try:
437
- response = requests.post(url, json=self._body)
438
- return response
439
- except Exception as e:
440
- raise Exception(f"Error sending request {url}: {e}")
441
-
442
- def printBanner(nome, versione, autore, filler):
443
- versione_width = len(versione)
444
- inner_width = max(len(nome) + versione_width, len(f">> {autore}")) + 4
445
- border = ' ' + filler * (inner_width + 4)
446
- line2 = f" {filler}{filler} {nome.ljust(inner_width - versione_width -2)}{versione.rjust(versione_width-2)} {filler}{filler}"
447
- line3 = f" {filler}{filler} {f">> {autore}".rjust(inner_width-2)} {filler}{filler}"
448
- banner = f"\n{border}\n{line2}\n{line3}\n{border}\n"
449
- print(banner)
458
+ def _parse_chat(
459
+ self,
460
+ chat: str | list[str] | set[str] | None,
461
+ ) -> set[str]:
462
+ if chat is None:
463
+ return {self.default_chat_name} if self.default_chat_name else set()
464
+ if isinstance(chat, str):
465
+ return {chat}
466
+ if isinstance(chat, (list, tuple, set)):
467
+ return set(chat)
468
+ raise ValueError(f"Invalid chat parameter: {chat}")
469
+
470
+ # Aliases for backward-friendly API (camelCase)
471
+ def setSaveLog(self, flag: bool) -> None:
472
+ self.set_save_log(flag)
473
+
474
+ def setHost(self, host: str) -> None:
475
+ self.set_host(host)
476
+
477
+ def setPort(self, port: int) -> None:
478
+ self.set_port(port)
479
+
480
+ def onMessage(self, chat: str | list[str] | set[str] | None = None) -> Any:
481
+ return self.on_message(chat)
482
+
483
+ def onCommand(self, command: str, chat: str | list[str] | set[str] | None = None) -> Any:
484
+ return self.on_command(command, chat)
485
+
486
+ def onCallback(self, chat: str | list[str] | set[str] | None = None) -> Any:
487
+ return self.on_callback(chat)
488
+
489
+ def onApiReq(
490
+ self,
491
+ endpoint: str,
492
+ args: list[str] | None = None,
493
+ host: str | None = None,
494
+ port: int | None = None,
495
+ ) -> Any:
496
+ return self.on_api_req(endpoint, args, host, port)
497
+
498
+ def sendMsg(self, text: str, chat: str | list[str] | set[str] | None = None) -> None:
499
+ self.send_msg(text, chat)
500
+
501
+ def replyToMsg(
502
+ self,
503
+ text: str,
504
+ msg_id: int,
505
+ chat: str | list[str] | set[str] | None = None,
506
+ ) -> None:
507
+ self.reply_to_msg(text, msg_id, chat)
508
+
509
+ def sendImg(
510
+ self,
511
+ photo_path: str,
512
+ caption: str | None = None,
513
+ chat: str | list[str] | set[str] | None = None,
514
+ ) -> None:
515
+ self.send_img(photo_path, caption, chat)
516
+
517
+ def sendFile(
518
+ self,
519
+ file_path: str,
520
+ caption: str | None = None,
521
+ chat: str | list[str] | set[str] | None = None,
522
+ ) -> None:
523
+ self.send_file(file_path, caption, chat)
524
+
525
+ def sendPosition(
526
+ self,
527
+ latitude: float,
528
+ longitude: float,
529
+ chat: str | list[str] | set[str] | None = None,
530
+ ) -> None:
531
+ self.send_position(latitude, longitude, chat)
532
+
533
+ def sendButtons(
534
+ self,
535
+ text: str,
536
+ buttons: list[list[dict[str, str]]],
537
+ chat: str | list[str] | set[str] | None = None,
538
+ ) -> None:
539
+ self.send_buttons(text, buttons, chat)
540
+
541
+ def sendLog(
542
+ self,
543
+ limit: int | None = None,
544
+ chat: str | list[str] | set[str] | None = None,
545
+ ) -> None:
546
+ self.send_log(limit, chat)
547
+
548
+ def sendInfo(self, chat: str | list[str] | set[str] | None = None) -> None:
549
+ self.send_info(chat)
550
+
551
+ def sendRegisteredHandlers(
552
+ self,
553
+ chat: str | list[str] | set[str] | None = None,
554
+ ) -> None:
555
+ self.send_registered_handlers(chat)
556
+
557
+
558
+ __all__ = ["TEL", "__version__"]