2015-08-31 12:46:18 +03:00
|
|
|
# -*- coding: utf-8 -*-
|
2018-01-15 16:08:50 +03:00
|
|
|
import random
|
2018-08-17 12:54:26 +03:00
|
|
|
import re
|
2018-01-15 16:08:50 +03:00
|
|
|
import string
|
2015-08-31 12:46:18 +03:00
|
|
|
import threading
|
2017-06-20 15:45:18 +03:00
|
|
|
import traceback
|
2021-06-30 14:08:05 +03:00
|
|
|
from typing import Any, Callable, List, Dict, Optional, Union
|
2022-05-30 12:22:49 +03:00
|
|
|
import hmac
|
|
|
|
from hashlib import sha256
|
|
|
|
from urllib.parse import parse_qsl
|
2018-08-17 12:54:26 +03:00
|
|
|
|
2021-08-18 22:16:30 +03:00
|
|
|
# noinspection PyPep8Naming
|
2020-08-24 16:02:35 +03:00
|
|
|
import queue as Queue
|
2018-03-10 09:41:34 +03:00
|
|
|
import logging
|
2021-06-18 23:35:49 +03:00
|
|
|
|
|
|
|
from telebot import types
|
2015-08-31 12:46:18 +03:00
|
|
|
|
2021-11-03 18:48:46 +03:00
|
|
|
try:
|
|
|
|
import ujson as json
|
|
|
|
except ImportError:
|
|
|
|
import json
|
|
|
|
|
2020-07-31 08:39:04 +03:00
|
|
|
try:
|
2021-08-18 22:16:30 +03:00
|
|
|
# noinspection PyPackageRequirements
|
2020-08-29 21:57:41 +03:00
|
|
|
from PIL import Image
|
2020-07-31 08:39:04 +03:00
|
|
|
from io import BytesIO
|
2022-02-19 13:04:31 +03:00
|
|
|
|
2020-07-31 08:39:04 +03:00
|
|
|
pil_imported = True
|
|
|
|
except:
|
|
|
|
pil_imported = False
|
|
|
|
|
2021-06-03 19:51:32 +03:00
|
|
|
MAX_MESSAGE_LENGTH = 4096
|
|
|
|
|
2018-03-10 09:41:34 +03:00
|
|
|
logger = logging.getLogger('TeleBot')
|
2015-08-31 12:46:18 +03:00
|
|
|
|
2017-07-19 01:35:19 +03:00
|
|
|
thread_local = threading.local()
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
#: Contains all media content types.
|
2020-11-07 12:52:51 +03:00
|
|
|
content_type_media = [
|
2022-11-13 13:20:26 +03:00
|
|
|
'text', 'audio', 'document', 'animation', 'game', 'photo', 'sticker', 'video', 'video_note', 'voice', 'contact',
|
|
|
|
'location', 'venue', 'dice', 'invoice', 'successful_payment', 'connected_website', 'poll', 'passport_data',
|
|
|
|
'web_app_data',
|
2020-11-07 12:52:51 +03:00
|
|
|
]
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
#: Contains all service content types such as `User joined the group`.
|
2020-11-07 12:52:51 +03:00
|
|
|
content_type_service = [
|
2022-02-19 21:57:21 +03:00
|
|
|
'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
|
2021-06-21 18:39:13 +03:00
|
|
|
'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
|
2022-04-23 15:03:54 +03:00
|
|
|
'proximity_alert_triggered', 'video_chat_scheduled', 'video_chat_started', 'video_chat_ended',
|
2022-11-13 13:20:26 +03:00
|
|
|
'video_chat_participants_invited', 'message_auto_delete_timer_changed', 'forum_topic_created', 'forum_topic_closed',
|
|
|
|
'forum_topic_reopened',
|
2020-11-07 12:52:51 +03:00
|
|
|
]
|
2017-07-19 01:35:19 +03:00
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
#: All update types, should be used for allowed_updates parameter in polling.
|
2021-06-23 20:57:44 +03:00
|
|
|
update_types = [
|
2022-11-13 13:20:26 +03:00
|
|
|
"message", "edited_message", "channel_post", "edited_channel_post", "inline_query", "chosen_inline_result",
|
|
|
|
"callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer", "my_chat_member", "chat_member",
|
|
|
|
"chat_join_request",
|
2021-06-23 17:09:40 +03:00
|
|
|
]
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2022-02-19 13:04:31 +03:00
|
|
|
|
2015-10-01 23:03:54 +03:00
|
|
|
class WorkerThread(threading.Thread):
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2020-07-04 20:45:48 +03:00
|
|
|
count = 0
|
|
|
|
|
|
|
|
def __init__(self, exception_callback=None, queue=None, name=None):
|
|
|
|
if not name:
|
|
|
|
name = "WorkerThread{0}".format(self.__class__.count + 1)
|
|
|
|
self.__class__.count += 1
|
|
|
|
if not queue:
|
|
|
|
queue = Queue.Queue()
|
|
|
|
|
|
|
|
threading.Thread.__init__(self, name=name)
|
|
|
|
self.queue = queue
|
|
|
|
self.daemon = True
|
|
|
|
|
|
|
|
self.received_task_event = threading.Event()
|
|
|
|
self.done_event = threading.Event()
|
|
|
|
self.exception_event = threading.Event()
|
|
|
|
self.continue_event = threading.Event()
|
|
|
|
|
|
|
|
self.exception_callback = exception_callback
|
2020-08-24 16:02:35 +03:00
|
|
|
self.exception_info = None
|
2020-07-04 20:45:48 +03:00
|
|
|
self._running = True
|
|
|
|
self.start()
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
while self._running:
|
|
|
|
try:
|
|
|
|
task, args, kwargs = self.queue.get(block=True, timeout=.5)
|
|
|
|
self.continue_event.clear()
|
|
|
|
self.received_task_event.clear()
|
|
|
|
self.done_event.clear()
|
|
|
|
self.exception_event.clear()
|
|
|
|
logger.debug("Received task")
|
|
|
|
self.received_task_event.set()
|
|
|
|
|
|
|
|
task(*args, **kwargs)
|
|
|
|
logger.debug("Task complete")
|
|
|
|
self.done_event.set()
|
|
|
|
except Queue.Empty:
|
|
|
|
pass
|
|
|
|
except Exception as e:
|
2020-11-07 14:43:17 +03:00
|
|
|
logger.debug(type(e).__name__ + " occurred, args=" + str(e.args) + "\n" + traceback.format_exc())
|
2020-08-24 16:02:35 +03:00
|
|
|
self.exception_info = e
|
2020-07-04 20:45:48 +03:00
|
|
|
self.exception_event.set()
|
|
|
|
if self.exception_callback:
|
2020-08-24 16:02:35 +03:00
|
|
|
self.exception_callback(self, self.exception_info)
|
2020-07-04 20:45:48 +03:00
|
|
|
self.continue_event.wait()
|
|
|
|
|
|
|
|
def put(self, task, *args, **kwargs):
|
|
|
|
self.queue.put((task, args, kwargs))
|
|
|
|
|
|
|
|
def raise_exceptions(self):
|
|
|
|
if self.exception_event.is_set():
|
2020-08-24 16:02:35 +03:00
|
|
|
raise self.exception_info
|
2020-07-04 20:45:48 +03:00
|
|
|
|
|
|
|
def clear_exceptions(self):
|
|
|
|
self.exception_event.clear()
|
|
|
|
self.continue_event.set()
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self._running = False
|
2015-08-31 12:46:18 +03:00
|
|
|
|
|
|
|
|
2015-10-01 23:03:54 +03:00
|
|
|
class ThreadPool:
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2022-02-01 23:58:57 +03:00
|
|
|
def __init__(self, telebot, num_threads=2):
|
|
|
|
self.telebot = telebot
|
2015-10-01 23:03:54 +03:00
|
|
|
self.tasks = Queue.Queue()
|
|
|
|
self.workers = [WorkerThread(self.on_exception, self.tasks) for _ in range(num_threads)]
|
2015-08-31 12:46:18 +03:00
|
|
|
self.num_threads = num_threads
|
|
|
|
|
2015-10-01 23:03:54 +03:00
|
|
|
self.exception_event = threading.Event()
|
2020-08-24 16:02:35 +03:00
|
|
|
self.exception_info = None
|
2015-10-01 23:03:54 +03:00
|
|
|
|
2015-08-31 12:46:18 +03:00
|
|
|
def put(self, func, *args, **kwargs):
|
|
|
|
self.tasks.put((func, args, kwargs))
|
|
|
|
|
2015-10-01 23:03:54 +03:00
|
|
|
def on_exception(self, worker_thread, exc_info):
|
2022-02-01 23:58:57 +03:00
|
|
|
if self.telebot.exception_handler is not None:
|
|
|
|
handled = self.telebot.exception_handler.handle(exc_info)
|
|
|
|
else:
|
|
|
|
handled = False
|
|
|
|
if not handled:
|
|
|
|
self.exception_info = exc_info
|
|
|
|
self.exception_event.set()
|
2015-10-01 23:03:54 +03:00
|
|
|
worker_thread.continue_event.set()
|
|
|
|
|
|
|
|
def raise_exceptions(self):
|
|
|
|
if self.exception_event.is_set():
|
2020-08-24 16:02:35 +03:00
|
|
|
raise self.exception_info
|
2015-10-01 23:03:54 +03:00
|
|
|
|
|
|
|
def clear_exceptions(self):
|
|
|
|
self.exception_event.clear()
|
|
|
|
|
2015-08-31 12:46:18 +03:00
|
|
|
def close(self):
|
|
|
|
for worker in self.workers:
|
|
|
|
worker.stop()
|
|
|
|
for worker in self.workers:
|
|
|
|
worker.join()
|
|
|
|
|
2015-10-01 12:33:23 +03:00
|
|
|
|
2015-08-31 12:46:18 +03:00
|
|
|
class AsyncTask:
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2015-08-31 12:46:18 +03:00
|
|
|
def __init__(self, target, *args, **kwargs):
|
|
|
|
self.target = target
|
|
|
|
self.args = args
|
|
|
|
self.kwargs = kwargs
|
|
|
|
|
|
|
|
self.done = False
|
|
|
|
self.thread = threading.Thread(target=self._run)
|
|
|
|
self.thread.start()
|
|
|
|
|
|
|
|
def _run(self):
|
|
|
|
try:
|
|
|
|
self.result = self.target(*self.args, **self.kwargs)
|
2020-08-24 16:02:35 +03:00
|
|
|
except Exception as e:
|
|
|
|
self.result = e
|
2015-08-31 12:46:18 +03:00
|
|
|
self.done = True
|
|
|
|
|
|
|
|
def wait(self):
|
|
|
|
if not self.done:
|
|
|
|
self.thread.join()
|
2015-10-01 23:03:54 +03:00
|
|
|
if isinstance(self.result, BaseException):
|
2020-08-24 16:02:35 +03:00
|
|
|
raise self.result
|
2015-08-31 12:46:18 +03:00
|
|
|
else:
|
|
|
|
return self.result
|
|
|
|
|
|
|
|
|
2021-11-03 18:48:46 +03:00
|
|
|
class CustomRequestResponse():
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2022-02-19 21:57:21 +03:00
|
|
|
def __init__(self, json_text, status_code = 200, reason = ""):
|
2021-11-03 18:48:46 +03:00
|
|
|
self.status_code = status_code
|
|
|
|
self.text = json_text
|
|
|
|
self.reason = reason
|
|
|
|
|
|
|
|
def json(self):
|
|
|
|
return json.loads(self.text)
|
|
|
|
|
|
|
|
|
2018-07-02 18:13:11 +03:00
|
|
|
def async_dec():
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2015-08-31 12:46:18 +03:00
|
|
|
def decorator(fn):
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
return AsyncTask(fn, *args, **kwargs)
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
def is_string(var) -> bool:
|
|
|
|
"""
|
|
|
|
Returns True if the given object is a string.
|
|
|
|
"""
|
2020-08-24 16:02:35 +03:00
|
|
|
return isinstance(var, str)
|
2015-08-31 12:46:18 +03:00
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
def is_dict(var) -> bool:
|
|
|
|
"""
|
|
|
|
Returns True if the given object is a dictionary.
|
|
|
|
|
|
|
|
:param var: object to be checked
|
|
|
|
:type var: :obj:`object`
|
|
|
|
|
|
|
|
:return: True if the given object is a dictionary.
|
|
|
|
:rtype: :obj:`bool`
|
|
|
|
"""
|
2020-07-31 23:30:38 +03:00
|
|
|
return isinstance(var, dict)
|
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
def is_bytes(var) -> bool:
|
|
|
|
"""
|
|
|
|
Returns True if the given object is a bytes object.
|
|
|
|
|
|
|
|
:param var: object to be checked
|
|
|
|
:type var: :obj:`object`
|
|
|
|
|
|
|
|
:return: True if the given object is a bytes object.
|
|
|
|
:rtype: :obj:`bool`
|
|
|
|
"""
|
2020-07-31 23:30:38 +03:00
|
|
|
return isinstance(var, bytes)
|
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
def is_pil_image(var) -> bool:
|
|
|
|
"""
|
|
|
|
Returns True if the given object is a PIL.Image.Image object.
|
|
|
|
|
|
|
|
:param var: object to be checked
|
|
|
|
:type var: :obj:`object`
|
|
|
|
|
|
|
|
:return: True if the given object is a PIL.Image.Image object.
|
|
|
|
:rtype: :obj:`bool`
|
|
|
|
"""
|
2020-08-29 21:57:41 +03:00
|
|
|
return pil_imported and isinstance(var, Image.Image)
|
2020-07-31 08:39:04 +03:00
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2020-07-31 08:39:04 +03:00
|
|
|
def pil_image_to_file(image, extension='JPEG', quality='web_low'):
|
|
|
|
if pil_imported:
|
|
|
|
photoBuffer = BytesIO()
|
|
|
|
image.convert('RGB').save(photoBuffer, extension, quality=quality)
|
|
|
|
photoBuffer.seek(0)
|
2022-02-19 13:04:31 +03:00
|
|
|
|
2020-07-31 08:39:04 +03:00
|
|
|
return photoBuffer
|
|
|
|
else:
|
|
|
|
raise RuntimeError('PIL module is not imported')
|
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2021-06-03 20:51:33 +03:00
|
|
|
def is_command(text: str) -> bool:
|
2022-03-19 11:49:36 +03:00
|
|
|
r"""
|
2015-08-31 12:46:18 +03:00
|
|
|
Checks if `text` is a command. Telegram chat commands start with the '/' character.
|
2022-03-19 11:49:36 +03:00
|
|
|
|
2015-08-31 12:46:18 +03:00
|
|
|
:param text: Text to check.
|
2022-07-24 21:14:09 +03:00
|
|
|
:type text: :obj:`str`
|
|
|
|
|
2015-08-31 12:46:18 +03:00
|
|
|
:return: True if `text` is a command, else False.
|
2022-07-24 21:14:09 +03:00
|
|
|
:rtype: :obj:`bool`
|
2015-08-31 12:46:18 +03:00
|
|
|
"""
|
2021-06-19 18:59:55 +03:00
|
|
|
if text is None: return False
|
2015-08-31 12:46:18 +03:00
|
|
|
return text.startswith('/')
|
|
|
|
|
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
def extract_command(text: str) -> Union[str, None]:
|
2015-08-31 12:46:18 +03:00
|
|
|
"""
|
|
|
|
Extracts the command from `text` (minus the '/') if `text` is a command (see is_command).
|
|
|
|
If `text` is not a command, this function returns None.
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
.. code-block:: python3
|
|
|
|
:caption: Examples:
|
|
|
|
|
|
|
|
extract_command('/help'): 'help'
|
|
|
|
extract_command('/help@BotName'): 'help'
|
|
|
|
extract_command('/search black eyed peas'): 'search'
|
|
|
|
extract_command('Good day to you'): None
|
2015-08-31 12:46:18 +03:00
|
|
|
|
|
|
|
:param text: String to extract the command from
|
2022-07-24 21:14:09 +03:00
|
|
|
:type text: :obj:`str`
|
|
|
|
|
2015-08-31 12:46:18 +03:00
|
|
|
:return: the command if `text` is a command (according to is_command), else None.
|
2022-07-24 21:14:09 +03:00
|
|
|
:rtype: :obj:`str` or :obj:`None`
|
2015-08-31 12:46:18 +03:00
|
|
|
"""
|
2021-06-19 18:59:55 +03:00
|
|
|
if text is None: return None
|
2015-08-31 12:46:18 +03:00
|
|
|
return text.split()[0].split('@')[0][1:] if is_command(text) else None
|
|
|
|
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
def extract_arguments(text: str) -> str or None:
|
2021-06-03 20:06:53 +03:00
|
|
|
"""
|
2021-06-03 20:51:33 +03:00
|
|
|
Returns the argument after the command.
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
.. code-block:: python3
|
|
|
|
:caption: Examples:
|
|
|
|
|
|
|
|
extract_arguments("/get name"): 'name'
|
|
|
|
extract_arguments("/get"): ''
|
|
|
|
extract_arguments("/get@botName name"): 'name'
|
2021-06-03 20:06:53 +03:00
|
|
|
|
2021-06-03 20:51:33 +03:00
|
|
|
:param text: String to extract the arguments from a command
|
2022-07-24 21:14:09 +03:00
|
|
|
:type text: :obj:`str`
|
|
|
|
|
2021-06-03 20:51:33 +03:00
|
|
|
:return: the arguments if `text` is a command (according to is_command), else None.
|
2022-07-24 21:14:09 +03:00
|
|
|
:rtype: :obj:`str` or :obj:`None`
|
2021-06-03 20:06:53 +03:00
|
|
|
"""
|
2021-06-19 18:59:55 +03:00
|
|
|
regexp = re.compile(r"/\w*(@\w*)*\s*([\s\S]*)", re.IGNORECASE)
|
2021-06-03 20:51:33 +03:00
|
|
|
result = regexp.match(text)
|
|
|
|
return result.group(2) if is_command(text) else None
|
2021-06-03 20:06:53 +03:00
|
|
|
|
|
|
|
|
2021-06-03 20:51:33 +03:00
|
|
|
def split_string(text: str, chars_per_string: int) -> List[str]:
|
2015-08-31 12:46:18 +03:00
|
|
|
"""
|
|
|
|
Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string.
|
|
|
|
This is very useful for splitting one giant message into multiples.
|
|
|
|
|
|
|
|
:param text: The text to split
|
2022-07-24 21:14:09 +03:00
|
|
|
:type text: :obj:`str`
|
|
|
|
|
2015-08-31 12:46:18 +03:00
|
|
|
:param chars_per_string: The number of characters per line the text is split into.
|
2022-07-24 21:14:09 +03:00
|
|
|
:type chars_per_string: :obj:`int`
|
|
|
|
|
2015-08-31 12:46:18 +03:00
|
|
|
:return: The splitted text as a list of strings.
|
2022-07-24 21:14:09 +03:00
|
|
|
:rtype: :obj:`list` of :obj:`str`
|
2015-08-31 12:46:18 +03:00
|
|
|
"""
|
|
|
|
return [text[i:i + chars_per_string] for i in range(0, len(text), chars_per_string)]
|
2015-10-01 23:03:54 +03:00
|
|
|
|
2021-06-03 19:51:32 +03:00
|
|
|
|
2022-02-19 21:57:21 +03:00
|
|
|
def smart_split(text: str, chars_per_string: int=MAX_MESSAGE_LENGTH) -> List[str]:
|
2022-03-07 14:10:44 +03:00
|
|
|
r"""
|
2021-06-03 19:51:32 +03:00
|
|
|
Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string.
|
|
|
|
This is very useful for splitting one giant message into multiples.
|
2021-06-19 18:59:55 +03:00
|
|
|
If `chars_per_string` > 4096: `chars_per_string` = 4096.
|
2021-06-03 19:51:32 +03:00
|
|
|
Splits by '\n', '. ' or ' ' in exactly this priority.
|
|
|
|
|
|
|
|
:param text: The text to split
|
2022-07-24 21:14:09 +03:00
|
|
|
:type text: :obj:`str`
|
|
|
|
|
2021-06-03 19:51:32 +03:00
|
|
|
:param chars_per_string: The number of maximum characters per part the text is split to.
|
2022-07-24 21:14:09 +03:00
|
|
|
:type chars_per_string: :obj:`int`
|
|
|
|
|
2021-06-03 19:51:32 +03:00
|
|
|
:return: The splitted text as a list of strings.
|
2022-07-24 21:14:09 +03:00
|
|
|
:rtype: :obj:`list` of :obj:`str`
|
2021-06-03 19:51:32 +03:00
|
|
|
"""
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2021-06-03 20:51:33 +03:00
|
|
|
def _text_before_last(substr: str) -> str:
|
2021-06-03 19:51:32 +03:00
|
|
|
return substr.join(part.split(substr)[:-1]) + substr
|
|
|
|
|
|
|
|
if chars_per_string > MAX_MESSAGE_LENGTH: chars_per_string = MAX_MESSAGE_LENGTH
|
|
|
|
|
|
|
|
parts = []
|
|
|
|
while True:
|
|
|
|
if len(text) < chars_per_string:
|
|
|
|
parts.append(text)
|
|
|
|
return parts
|
|
|
|
|
|
|
|
part = text[:chars_per_string]
|
|
|
|
|
2022-02-19 21:57:21 +03:00
|
|
|
if "\n" in part: part = _text_before_last("\n")
|
|
|
|
elif ". " in part: part = _text_before_last(". ")
|
|
|
|
elif " " in part: part = _text_before_last(" ")
|
2021-06-03 19:51:32 +03:00
|
|
|
|
|
|
|
parts.append(part)
|
|
|
|
text = text[len(part):]
|
|
|
|
|
2021-06-03 20:51:33 +03:00
|
|
|
|
|
|
|
def escape(text: str) -> str:
|
|
|
|
"""
|
|
|
|
Replaces the following chars in `text` ('&' with '&', '<' with '<' and '>' with '>').
|
|
|
|
|
|
|
|
:param text: the text to escape
|
|
|
|
:return: the escaped text
|
|
|
|
"""
|
2022-02-07 00:57:33 +03:00
|
|
|
chars = {"&": "&", "<": "<", ">": ">"}
|
2022-12-03 16:11:07 +03:00
|
|
|
if text is None:
|
2022-12-03 15:33:22 +03:00
|
|
|
return None
|
2022-12-03 16:23:10 +03:00
|
|
|
for old, new in chars.items():
|
|
|
|
text = text.replace(old, new)
|
|
|
|
return text
|
2021-06-03 20:51:33 +03:00
|
|
|
|
|
|
|
|
2022-02-19 21:57:21 +03:00
|
|
|
def user_link(user: types.User, include_id: bool=False) -> str:
|
2021-06-03 20:51:33 +03:00
|
|
|
"""
|
|
|
|
Returns an HTML user link. This is useful for reports.
|
|
|
|
Attention: Don't forget to set parse_mode to 'HTML'!
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
|
|
|
|
.. code-block:: python3
|
|
|
|
:caption: Example:
|
|
|
|
|
|
|
|
bot.send_message(your_user_id, user_link(message.from_user) + ' started the bot!', parse_mode='HTML')
|
|
|
|
|
|
|
|
.. note::
|
|
|
|
You can use formatting.* for all other formatting options(bold, italic, links, and etc.)
|
|
|
|
This method is kept for backward compatibility, and it is recommended to use formatting.* for
|
|
|
|
more options.
|
2021-06-03 20:51:33 +03:00
|
|
|
|
|
|
|
:param user: the user (not the user_id)
|
2022-07-24 21:14:09 +03:00
|
|
|
:type user: :obj:`telebot.types.User`
|
|
|
|
|
2021-06-03 20:51:33 +03:00
|
|
|
:param include_id: include the user_id
|
2022-07-24 21:14:09 +03:00
|
|
|
:type include_id: :obj:`bool`
|
|
|
|
|
2021-06-03 20:51:33 +03:00
|
|
|
:return: HTML user link
|
2022-07-24 21:14:09 +03:00
|
|
|
:rtype: :obj:`str`
|
2021-06-03 20:51:33 +03:00
|
|
|
"""
|
|
|
|
name = escape(user.first_name)
|
2022-02-19 13:04:31 +03:00
|
|
|
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
|
2022-02-19 21:57:21 +03:00
|
|
|
+ (f" (<pre>{user.id}</pre>)" if include_id else ""))
|
2021-06-03 20:51:33 +03:00
|
|
|
|
|
|
|
|
2022-02-19 21:57:21 +03:00
|
|
|
def quick_markup(values: Dict[str, Dict[str, Any]], row_width: int=2) -> types.InlineKeyboardMarkup:
|
2021-06-17 21:28:53 +03:00
|
|
|
"""
|
|
|
|
Returns a reply markup from a dict in this format: {'text': kwargs}
|
|
|
|
This is useful to avoid always typing 'btn1 = InlineKeyboardButton(...)' 'btn2 = InlineKeyboardButton(...)'
|
|
|
|
|
|
|
|
Example:
|
2022-03-07 12:24:28 +03:00
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
.. code-block:: python3
|
|
|
|
:caption: Using quick_markup:
|
2022-03-07 12:24:28 +03:00
|
|
|
|
|
|
|
quick_markup({
|
|
|
|
'Twitter': {'url': 'https://twitter.com'},
|
|
|
|
'Facebook': {'url': 'https://facebook.com'},
|
|
|
|
'Back': {'callback_data': 'whatever'}
|
|
|
|
}, row_width=2):
|
2022-07-24 21:14:09 +03:00
|
|
|
# returns an InlineKeyboardMarkup with two buttons in a row, one leading to Twitter, the other to facebook
|
|
|
|
# and a back button below
|
2022-03-07 12:24:28 +03:00
|
|
|
|
|
|
|
# kwargs can be:
|
|
|
|
{
|
|
|
|
'url': None,
|
|
|
|
'callback_data': None,
|
|
|
|
'switch_inline_query': None,
|
|
|
|
'switch_inline_query_current_chat': None,
|
|
|
|
'callback_game': None,
|
|
|
|
'pay': None,
|
2022-05-21 15:10:57 +03:00
|
|
|
'login_url': None,
|
|
|
|
'web_app': None
|
2022-03-07 12:24:28 +03:00
|
|
|
}
|
2021-06-17 21:28:53 +03:00
|
|
|
|
|
|
|
:param values: a dict containing all buttons to create in this format: {text: kwargs} {str:}
|
2022-07-24 21:14:09 +03:00
|
|
|
:type values: :obj:`dict`
|
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
:param row_width: int row width
|
2022-07-24 21:14:09 +03:00
|
|
|
:type row_width: :obj:`int`
|
|
|
|
|
2021-06-17 21:28:53 +03:00
|
|
|
:return: InlineKeyboardMarkup
|
2022-07-24 21:14:09 +03:00
|
|
|
:rtype: :obj:`types.InlineKeyboardMarkup`
|
2021-06-17 21:28:53 +03:00
|
|
|
"""
|
|
|
|
markup = types.InlineKeyboardMarkup(row_width=row_width)
|
2021-07-19 20:01:37 +03:00
|
|
|
buttons = [
|
|
|
|
types.InlineKeyboardButton(text=text, **kwargs)
|
|
|
|
for text, kwargs in values.items()
|
|
|
|
]
|
2021-06-17 21:28:53 +03:00
|
|
|
markup.add(*buttons)
|
|
|
|
return markup
|
2021-06-03 20:51:33 +03:00
|
|
|
|
|
|
|
|
2015-10-01 23:03:54 +03:00
|
|
|
# CREDITS TO http://stackoverflow.com/questions/12317940#answer-12320352
|
|
|
|
def or_set(self):
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2015-10-01 23:03:54 +03:00
|
|
|
self._set()
|
|
|
|
self.changed()
|
|
|
|
|
|
|
|
|
|
|
|
def or_clear(self):
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2015-10-01 23:03:54 +03:00
|
|
|
self._clear()
|
|
|
|
self.changed()
|
|
|
|
|
|
|
|
|
|
|
|
def orify(e, changed_callback):
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2021-01-14 03:44:37 +03:00
|
|
|
if not hasattr(e, "_set"):
|
|
|
|
e._set = e.set
|
|
|
|
if not hasattr(e, "_clear"):
|
|
|
|
e._clear = e.clear
|
2015-10-01 23:03:54 +03:00
|
|
|
e.changed = changed_callback
|
|
|
|
e.set = lambda: or_set(e)
|
|
|
|
e.clear = lambda: or_clear(e)
|
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2015-10-01 23:03:54 +03:00
|
|
|
def OrEvent(*events):
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2015-10-01 23:03:54 +03:00
|
|
|
or_event = threading.Event()
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2015-10-01 23:03:54 +03:00
|
|
|
def changed():
|
2021-01-14 03:44:37 +03:00
|
|
|
bools = [ev.is_set() for ev in events]
|
2015-10-01 23:03:54 +03:00
|
|
|
if any(bools):
|
|
|
|
or_event.set()
|
|
|
|
else:
|
|
|
|
or_event.clear()
|
2015-10-03 13:48:56 +03:00
|
|
|
|
|
|
|
def busy_wait():
|
|
|
|
while not or_event.is_set():
|
2022-01-24 22:38:35 +03:00
|
|
|
# noinspection PyProtectedMember
|
2015-10-03 13:48:56 +03:00
|
|
|
or_event._wait(3)
|
|
|
|
|
2015-10-01 23:03:54 +03:00
|
|
|
for e in events:
|
|
|
|
orify(e, changed)
|
2015-10-03 13:48:56 +03:00
|
|
|
or_event._wait = or_event.wait
|
|
|
|
or_event.wait = busy_wait
|
2015-10-01 23:03:54 +03:00
|
|
|
changed()
|
|
|
|
return or_event
|
2016-03-17 08:18:08 +03:00
|
|
|
|
2017-07-19 01:35:19 +03:00
|
|
|
|
2018-09-06 12:42:44 +03:00
|
|
|
def per_thread(key, construct_value, reset=False):
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
:meta private:
|
|
|
|
"""
|
2018-09-06 12:42:44 +03:00
|
|
|
if reset or not hasattr(thread_local, key):
|
2017-07-19 01:35:19 +03:00
|
|
|
value = construct_value()
|
|
|
|
setattr(thread_local, key, value)
|
2018-09-06 12:42:44 +03:00
|
|
|
|
|
|
|
return getattr(thread_local, key)
|
2018-01-15 16:08:50 +03:00
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2020-07-31 23:30:38 +03:00
|
|
|
def chunks(lst, n):
|
|
|
|
"""Yield successive n-sized chunks from lst."""
|
|
|
|
# https://stackoverflow.com/a/312464/9935473
|
|
|
|
for i in range(0, len(lst), n):
|
|
|
|
yield lst[i:i + n]
|
2018-01-15 16:08:50 +03:00
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
def generate_random_token() -> str:
|
|
|
|
"""
|
|
|
|
Generates a random token consisting of letters and digits, 16 characters long.
|
|
|
|
|
|
|
|
:return: a random token
|
|
|
|
:rtype: :obj:`str`
|
|
|
|
"""
|
2020-05-02 13:09:52 +03:00
|
|
|
return ''.join(random.sample(string.ascii_letters, 16))
|
|
|
|
|
2021-06-19 18:59:55 +03:00
|
|
|
|
2022-04-23 15:03:54 +03:00
|
|
|
def deprecated(warn: bool=True, alternative: Optional[Callable]=None, deprecation_text=None):
|
2021-06-30 14:08:05 +03:00
|
|
|
"""
|
|
|
|
Use this decorator to mark functions as deprecated.
|
|
|
|
When the function is used, an info (or warning if `warn` is True) is logged.
|
2022-07-24 21:14:09 +03:00
|
|
|
|
|
|
|
:meta private:
|
2022-03-19 11:49:36 +03:00
|
|
|
|
2021-06-30 14:08:05 +03:00
|
|
|
:param warn: If True a warning is logged else an info
|
2022-07-24 21:14:09 +03:00
|
|
|
:type warn: :obj:`bool`
|
|
|
|
|
2021-06-30 14:08:05 +03:00
|
|
|
:param alternative: The new function to use instead
|
2022-07-24 21:14:09 +03:00
|
|
|
:type alternative: :obj:`Callable`
|
|
|
|
|
2022-04-23 15:03:54 +03:00
|
|
|
:param deprecation_text: Custom deprecation text
|
2022-07-24 21:14:09 +03:00
|
|
|
:type deprecation_text: :obj:`str`
|
|
|
|
|
|
|
|
:return: The decorated function
|
2021-06-30 14:08:05 +03:00
|
|
|
"""
|
|
|
|
def decorator(function):
|
|
|
|
def wrapper(*args, **kwargs):
|
2022-04-23 15:03:54 +03:00
|
|
|
info = f"`{function.__name__}` is deprecated."
|
|
|
|
if alternative:
|
|
|
|
info += f" Use `{alternative.__name__}` instead"
|
|
|
|
if deprecation_text:
|
|
|
|
info += " " + deprecation_text
|
2021-06-30 14:08:05 +03:00
|
|
|
if not warn:
|
2022-01-10 16:40:33 +03:00
|
|
|
logger.info(info)
|
2021-06-30 14:08:05 +03:00
|
|
|
else:
|
2022-01-10 16:40:33 +03:00
|
|
|
logger.warning(info)
|
2021-06-30 14:08:05 +03:00
|
|
|
return function(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
2021-08-24 14:01:10 +03:00
|
|
|
|
|
|
|
# Cloud helpers
|
2021-08-25 15:17:25 +03:00
|
|
|
def webhook_google_functions(bot, request):
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
A webhook endpoint for Google Cloud Functions FaaS.
|
|
|
|
|
|
|
|
:param bot: The bot instance
|
|
|
|
:type bot: :obj:`telebot.TeleBot` or :obj:`telebot.async_telebot.AsyncTeleBot`
|
|
|
|
|
|
|
|
:param request: The request object
|
|
|
|
:type request: :obj:`flask.Request`
|
|
|
|
|
|
|
|
:return: The response object
|
|
|
|
"""
|
2021-08-24 14:01:10 +03:00
|
|
|
if request.is_json:
|
|
|
|
try:
|
|
|
|
request_json = request.get_json()
|
|
|
|
update = types.Update.de_json(request_json)
|
|
|
|
bot.process_new_updates([update])
|
|
|
|
return ''
|
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
|
|
|
return 'Bot FAIL', 400
|
|
|
|
else:
|
|
|
|
return 'Bot ON'
|
2021-11-03 17:30:10 +03:00
|
|
|
|
2022-02-19 13:04:31 +03:00
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
def antiflood(function: Callable, *args, **kwargs):
|
2021-11-03 17:30:10 +03:00
|
|
|
"""
|
|
|
|
Use this function inside loops in order to avoid getting TooManyRequests error.
|
|
|
|
Example:
|
2022-03-07 12:24:28 +03:00
|
|
|
|
|
|
|
.. code-block:: python3
|
|
|
|
|
|
|
|
from telebot.util import antiflood
|
|
|
|
for chat_id in chat_id_list:
|
2021-11-03 17:30:10 +03:00
|
|
|
msg = antiflood(bot.send_message, chat_id, text)
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
:param function: The function to call
|
|
|
|
:type function: :obj:`Callable`
|
|
|
|
|
|
|
|
:param args: The arguments to pass to the function
|
|
|
|
:type args: :obj:`tuple`
|
|
|
|
|
|
|
|
:param kwargs: The keyword arguments to pass to the function
|
|
|
|
:type kwargs: :obj:`dict`
|
|
|
|
|
2022-03-07 12:24:28 +03:00
|
|
|
:return: None
|
2021-11-03 17:30:10 +03:00
|
|
|
"""
|
|
|
|
from telebot.apihelper import ApiTelegramException
|
|
|
|
from time import sleep
|
2022-09-10 16:46:16 +03:00
|
|
|
|
2021-11-03 17:30:10 +03:00
|
|
|
try:
|
2022-09-10 16:46:16 +03:00
|
|
|
return function(*args, **kwargs)
|
2021-11-03 17:30:10 +03:00
|
|
|
except ApiTelegramException as ex:
|
|
|
|
if ex.error_code == 429:
|
|
|
|
sleep(ex.result_json['parameters']['retry_after'])
|
2022-09-10 16:46:16 +03:00
|
|
|
return function(*args, **kwargs)
|
2022-09-10 10:59:40 +03:00
|
|
|
else:
|
|
|
|
raise
|
2022-05-30 12:22:49 +03:00
|
|
|
|
|
|
|
|
2022-05-30 14:51:33 +03:00
|
|
|
def parse_web_app_data(token: str, raw_init_data: str):
|
2022-07-24 21:14:09 +03:00
|
|
|
"""
|
|
|
|
Parses web app data.
|
|
|
|
|
|
|
|
:param token: The bot token
|
|
|
|
:type token: :obj:`str`
|
|
|
|
|
|
|
|
:param raw_init_data: The raw init data
|
|
|
|
:type raw_init_data: :obj:`str`
|
|
|
|
|
|
|
|
:return: The parsed init data
|
|
|
|
"""
|
2022-07-08 11:15:50 +03:00
|
|
|
is_valid = validate_web_app_data(token, raw_init_data)
|
2022-05-30 12:22:49 +03:00
|
|
|
if not is_valid:
|
|
|
|
return False
|
|
|
|
|
|
|
|
result = {}
|
|
|
|
for key, value in parse_qsl(raw_init_data):
|
|
|
|
try:
|
|
|
|
value = json.loads(value)
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
result[key] = value
|
|
|
|
else:
|
|
|
|
result[key] = value
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2022-07-24 21:14:09 +03:00
|
|
|
def validate_web_app_data(token: str, raw_init_data: str):
|
|
|
|
"""
|
|
|
|
Validates web app data.
|
|
|
|
|
|
|
|
:param token: The bot token
|
|
|
|
:type token: :obj:`str`
|
|
|
|
|
|
|
|
:param raw_init_data: The raw init data
|
|
|
|
:type raw_init_data: :obj:`str`
|
|
|
|
|
|
|
|
:return: The parsed init data
|
|
|
|
"""
|
2022-05-30 12:22:49 +03:00
|
|
|
try:
|
|
|
|
parsed_data = dict(parse_qsl(raw_init_data))
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
if "hash" not in parsed_data:
|
|
|
|
return False
|
|
|
|
|
|
|
|
init_data_hash = parsed_data.pop('hash')
|
|
|
|
data_check_string = "\n".join(f"{key}={value}" for key, value in sorted(parsed_data.items()))
|
|
|
|
secret_key = hmac.new(key=b"WebAppData", msg=token.encode(), digestmod=sha256)
|
|
|
|
|
|
|
|
return hmac.new(secret_key.digest(), data_check_string.encode(), sha256).hexdigest() == init_data_hash
|
|
|
|
|
|
|
|
|