This commit is contained in:
2024-08-29 17:07:22 +08:00
commit 6032bd1237
1790 changed files with 349463 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,454 @@
from abc import ABC
from typing import Optional, Union
from telebot.asyncio_handler_backends import State
from telebot import types
class SimpleCustomFilter(ABC):
"""
Simple Custom Filter base class.
Create child class with check() method.
Accepts only message, returns bool value, that is compared with given in handler.
Child classes should have .key property.
.. code-block:: python3
:caption: Example on creating a simple custom filter.
class ForwardFilter(SimpleCustomFilter):
# Check whether message was forwarded from channel or group.
key = 'is_forwarded'
def check(self, message):
return message.forward_date is not None
"""
key: str = None
async def check(self, message):
"""
Perform a check.
"""
pass
class AdvancedCustomFilter(ABC):
"""
Advanced Custom Filter base class.
Create child class with check() method.
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
message: Message class
text: Filter value given in handler
Child classes should have .key property.
.. code-block:: python3
:caption: Example on creating an advanced custom filter.
class TextStartsFilter(AdvancedCustomFilter):
# Filter to check whether message starts with some text.
key = 'text_startswith'
def check(self, message, text):
return message.text.startswith(text)
"""
key: str = None
async def check(self, message, text):
"""
Perform a check.
"""
pass
class TextFilter:
"""
Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll)
example of usage is in examples/asynchronous_telebot/custom_filters/advanced_text_filter.py
:param equals: string, True if object's text is equal to passed string
:type equals: :obj:`str`
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:type contains: list[str] or tuple[str]
:param starts_with: string, True if object's text starts with passed string
:type starts_with: :obj:`str`
:param ends_with: string, True if object's text starts with passed string
:type ends_with: :obj:`str`
:param ignore_case: bool (default False), case insensitive
:type ignore_case: :obj:`bool`
:raises ValueError: if incorrect value for a parameter was supplied
:return: None
"""
def __init__(self,
equals: Optional[str] = None,
contains: Optional[Union[list, tuple]] = None,
starts_with: Optional[Union[str, list, tuple]] = None,
ends_with: Optional[Union[str, list, tuple]] = None,
ignore_case: bool = False):
"""
:param equals: string, True if object's text is equal to passed string
:type equals: :obj:`str`
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:type contains: list[str] or tuple[str]
:param starts_with: string, True if object's text starts with passed string
:type starts_with: :obj:`str`
:param ends_with: string, True if object's text starts with passed string
:type ends_with: :obj:`str`
:param ignore_case: bool (default False), case insensitive
:type ignore_case: :obj:`bool`
:raises ValueError: if incorrect value for a parameter was supplied
:return: None
"""
to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with)))
if to_check == 0:
raise ValueError('None of the check modes was specified')
self.equals = equals
self.contains = self._check_iterable(contains, filter_name='contains')
self.starts_with = self._check_iterable(starts_with, filter_name='starts_with')
self.ends_with = self._check_iterable(ends_with, filter_name='ends_with')
self.ignore_case = ignore_case
def _check_iterable(self, iterable, filter_name):
if not iterable:
pass
elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple):
raise ValueError(f"Incorrect value of {filter_name!r}")
elif isinstance(iterable, str):
iterable = [iterable]
elif isinstance(iterable, list) or isinstance(iterable, tuple):
iterable = [i for i in iterable if isinstance(i, str)]
return iterable
async def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]):
"""
:meta private:
"""
if isinstance(obj, types.Poll):
text = obj.question
elif isinstance(obj, types.Message):
text = obj.text or obj.caption
elif isinstance(obj, types.CallbackQuery):
text = obj.data
elif isinstance(obj, types.InlineQuery):
text = obj.query
else:
return False
if self.ignore_case:
text = text.lower()
prepare_func = lambda string: str(string).lower()
else:
prepare_func = str
if self.equals:
result = prepare_func(self.equals) == text
if result:
return True
elif not result and not any((self.contains, self.starts_with, self.ends_with)):
return False
if self.contains:
result = any([prepare_func(i) in text for i in self.contains])
if result:
return True
elif not result and not any((self.starts_with, self.ends_with)):
return False
if self.starts_with:
result = any([text.startswith(prepare_func(i)) for i in self.starts_with])
if result:
return True
elif not result and not self.ends_with:
return False
if self.ends_with:
return any([text.endswith(prepare_func(i)) for i in self.ends_with])
return False
class TextMatchFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(text=['account'])
# your function
"""
key = 'text'
async def check(self, message, text):
"""
:meta private:
"""
if isinstance(text, TextFilter):
return await text.check(message)
elif type(text) is list:
return message.text in text
else:
return text == message.text
class TextContainsFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
key: text
.. code-block:: python3
:caption: Example on using this filter:
# Will respond if any message.text contains word 'account'
@bot.message_handler(text_contains=['account'])
# your function
"""
key = 'text_contains'
async def check(self, message, text):
"""
:meta private:
"""
if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple):
raise ValueError("Incorrect text_contains value")
elif isinstance(text, str):
text = [text]
elif isinstance(text, list) or isinstance(text, tuple):
text = [i for i in text if isinstance(i, str)]
return any([i in message.text for i in text])
class TextStartsFilter(AdvancedCustomFilter):
"""
Filter to check whether message starts with some text.
.. code-block:: python3
:caption: Example on using this filter:
# Will work if message.text starts with 'sir'.
@bot.message_handler(text_startswith='sir')
# your function
"""
key = 'text_startswith'
async def check(self, message, text):
"""
:meta private:
"""
return message.text.startswith(text)
class ChatFilter(AdvancedCustomFilter):
"""
Check whether chat_id corresponds to given chat_id.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(chat_id=[99999])
# your function
"""
key = 'chat_id'
async def check(self, message, text):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return message.message.chat.id in text
return message.chat.id in text
class ForwardFilter(SimpleCustomFilter):
"""
Check whether message was forwarded from channel or group.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_forwarded=True)
# your function
"""
key = 'is_forwarded'
async def check(self, message):
"""
:meta private:
"""
return message.forward_date is not None
class IsReplyFilter(SimpleCustomFilter):
"""
Check whether message is a reply.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_reply=True)
# your function
"""
key = 'is_reply'
async def check(self, message):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return message.message.reply_to_message is not None
return message.reply_to_message is not None
class LanguageFilter(AdvancedCustomFilter):
"""
Check users language_code.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(language_code=['ru'])
# your function
"""
key = 'language_code'
async def check(self, message, text):
"""
:meta private:
"""
if type(text) is list:
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
class IsAdminFilter(SimpleCustomFilter):
"""
Check whether the user is administrator / owner of the chat.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(chat_types=['supergroup'], is_chat_admin=True)
# your function
"""
key = 'is_chat_admin'
def __init__(self, bot):
self._bot = bot
async def check(self, message):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
result = await self._bot.get_chat_member(message.message.chat.id, message.from_user.id)
return result.status ('creator', 'administrator')
result = await self._bot.get_chat_member(message.chat.id, message.from_user.id)
return result.status in ['creator', 'administrator']
class StateFilter(AdvancedCustomFilter):
"""
Filter to check state.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(state=1)
# your function
"""
def __init__(self, bot):
self.bot = bot
key = 'state'
async def check(self, message, text):
"""
:meta private:
"""
if text == '*': return True
# needs to work with callbackquery
if isinstance(message, types.Message):
chat_id = message.chat.id
user_id = message.from_user.id
if isinstance(message, types.CallbackQuery):
chat_id = message.message.chat.id
user_id = message.from_user.id
message = message.message
if isinstance(text, list):
new_text = []
for i in text:
if isinstance(i, State): i = i.name
new_text.append(i)
text = new_text
elif isinstance(text, State):
text = text.name
if message.chat.type in ['group', 'supergroup']:
group_state = await self.bot.current_states.get_state(chat_id, user_id)
if group_state == text:
return True
elif type(text) is list and group_state in text:
return True
else:
user_state = await self.bot.current_states.get_state(chat_id, user_id)
if user_state == text:
return True
elif type(text) is list and user_state in text:
return True
class IsDigitFilter(SimpleCustomFilter):
"""
Filter to check whether the string is made up of only digits.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_digit=True)
# your function
"""
key = 'is_digit'
async def check(self, message):
"""
:meta private:
"""
return message.text.isdigit()
@@ -0,0 +1,137 @@
"""
File with all middleware classes, states.
"""
class BaseMiddleware:
"""
Base class for middleware.
Your middlewares should be inherited from this class.
Set update_sensitive=True if you want to get different updates on
different functions. For example, if you want to handle pre_process for
message update, then you will have to create pre_process_message function, and
so on. Same applies to post_process.
.. code-block:: python
:caption: Example of class-based middlewares
class MyMiddleware(BaseMiddleware):
def __init__(self):
self.update_sensitive = True
self.update_types = ['message', 'edited_message']
async def pre_process_message(self, message, data):
# only message update here
pass
async def post_process_message(self, message, data, exception):
pass # only message update here for post_process
async def pre_process_edited_message(self, message, data):
# only edited_message update here
pass
async def post_process_edited_message(self, message, data, exception):
pass # only edited_message update here for post_process
"""
update_sensitive: bool = False
def __init__(self):
pass
async def pre_process(self, message, data):
raise NotImplementedError
async def post_process(self, message, data, exception):
raise NotImplementedError
class State:
"""
Class representing a state.
.. code-block:: python3
class MyStates(StatesGroup):
my_state = State() # returns my_state:State string.
"""
def __init__(self) -> None:
self.name = None
def __str__(self) -> str:
return self.name
class StatesGroup:
"""
Class representing common states.
.. code-block:: python3
class MyStates(StatesGroup):
my_state = State() # returns my_state:State string.
"""
def __init_subclass__(cls) -> None:
state_list = []
for name, value in cls.__dict__.items():
if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable
value.name = ':'.join((cls.__name__, name))
value.group = cls
state_list.append(value)
cls._state_list = state_list
@property
def state_list(self):
return self._state_list
class SkipHandler:
"""
Class for skipping handlers.
Just return instance of this class
in middleware to skip handler.
Update will go to post_process,
but will skip execution of handler.
"""
def __init__(self) -> None:
pass
class CancelUpdate:
"""
Class for canceling updates.
Just return instance of this class
in middleware to skip update.
Update will skip handler and execution
of post_process in middlewares.
"""
def __init__(self) -> None:
pass
class ContinueHandling:
"""
Class for continue updates in handlers.
Just return instance of this class
in handlers to continue process.
.. code-block:: python3
:caption: Example of using ContinueHandling
@bot.message_handler(commands=['start'])
async def start(message):
await bot.send_message(message.chat.id, 'Hello World!')
return ContinueHandling()
@bot.message_handler(commands=['start'])
async def start2(message):
await bot.send_message(message.chat.id, 'Hello World2!')
"""
def __init__(self) -> None:
pass
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
from telebot.asyncio_storage.memory_storage import StateMemoryStorage
from telebot.asyncio_storage.redis_storage import StateRedisStorage
from telebot.asyncio_storage.pickle_storage import StatePickleStorage
from telebot.asyncio_storage.base_storage import StateContext,StateStorageBase
__all__ = [
'StateStorageBase', 'StateContext',
'StateMemoryStorage', 'StateRedisStorage', 'StatePickleStorage'
]
@@ -0,0 +1,68 @@
import copy
class StateStorageBase:
def __init__(self) -> None:
pass
async def set_data(self, chat_id, user_id, key, value):
"""
Set data for a user in a particular chat.
"""
raise NotImplementedError
async def get_data(self, chat_id, user_id):
"""
Get data for a user in a particular chat.
"""
raise NotImplementedError
async def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user.
! Note that you should create a
record if it does not exist, and
if a record with state already exists,
you need to update a record.
"""
raise NotImplementedError
async def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user.
"""
raise NotImplementedError
async def reset_data(self, chat_id, user_id):
"""
Reset data for a particular user in a chat.
"""
raise NotImplementedError
async def get_state(self, chat_id, user_id):
raise NotImplementedError
async def save(self, chat_id, user_id, data):
raise NotImplementedError
class StateContext:
"""
Class for data.
"""
def __init__(self, obj, chat_id, user_id):
self.obj = obj
self.data = None
self.chat_id = chat_id
self.user_id = user_id
async def __aenter__(self):
self.data = copy.deepcopy(await self.obj.get_data(self.chat_id, self.user_id))
return self.data
async def __aexit__(self, exc_type, exc_val, exc_tb):
return await self.obj.save(self.chat_id, self.user_id, self.data)
@@ -0,0 +1,66 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
class StateMemoryStorage(StateStorageBase):
def __init__(self) -> None:
self.data = {}
#
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
async def set_state(self, chat_id, user_id, state):
if hasattr(state, 'name'):
state = state.name
if chat_id in self.data:
if user_id in self.data[chat_id]:
self.data[chat_id][user_id]['state'] = state
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
return True
async def delete_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
del self.data[chat_id][user_id]
if chat_id == user_id:
del self.data[chat_id]
return True
return False
async def get_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['state']
return None
async def get_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['data']
return None
async def reset_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'] = {}
return True
return False
async def set_data(self, chat_id, user_id, key, value):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'][key] = value
return True
raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id))
def get_interactive_data(self, chat_id, user_id):
return StateContext(self, chat_id, user_id)
async def save(self, chat_id, user_id, data):
self.data[chat_id][user_id]['data'] = data
@@ -0,0 +1,110 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
import os
import pickle
class StatePickleStorage(StateStorageBase):
def __init__(self, file_path="./.state-save/states.pkl") -> None:
self.file_path = file_path
self.create_dir()
self.data = self.read()
async def convert_old_to_new(self):
# old looks like:
# {1: {'state': 'start', 'data': {'name': 'John'}}
# we should update old version pickle to new.
# new looks like:
# {1: {2: {'state': 'start', 'data': {'name': 'John'}}}}
new_data = {}
for key, value in self.data.items():
# this returns us id and dict with data and state
new_data[key] = {key: value} # convert this to new
# pass it to global data
self.data = new_data
self.update_data() # update data in file
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs, filename = os.path.split(self.file_path)
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path,'wb') as file:
pickle.dump({}, file)
def read(self):
file = open(self.file_path, 'rb')
data = pickle.load(file)
file.close()
return data
def update_data(self):
file = open(self.file_path, 'wb+')
pickle.dump(self.data, file, protocol=pickle.HIGHEST_PROTOCOL)
file.close()
async def set_state(self, chat_id, user_id, state):
if hasattr(state, 'name'):
state = state.name
if chat_id in self.data:
if user_id in self.data[chat_id]:
self.data[chat_id][user_id]['state'] = state
self.update_data()
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
self.update_data()
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
self.update_data()
return True
async def delete_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
del self.data[chat_id][user_id]
if chat_id == user_id:
del self.data[chat_id]
self.update_data()
return True
return False
async def get_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['state']
return None
async def get_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['data']
return None
async def reset_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'] = {}
self.update_data()
return True
return False
async def set_data(self, chat_id, user_id, key, value):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'][key] = value
self.update_data()
return True
raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id))
def get_interactive_data(self, chat_id, user_id):
return StateContext(self, chat_id, user_id)
async def save(self, chat_id, user_id, data):
self.data[chat_id][user_id]['data'] = data
self.update_data()
@@ -0,0 +1,179 @@
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
import json
redis_installed = True
is_actual_aioredis = False
try:
import aioredis
is_actual_aioredis = True
except ImportError:
try:
from redis import asyncio as aioredis
except ImportError:
redis_installed = False
class StateRedisStorage(StateStorageBase):
"""
This class is for Redis storage.
This will work only for states.
To use it, just pass this class to:
TeleBot(storage=StateRedisStorage())
"""
def __init__(self, host='localhost', port=6379, db=0, password=None, prefix='telebot_', redis_url=None):
if not redis_installed:
raise ImportError('AioRedis is not installed. Install it via "pip install aioredis"')
if is_actual_aioredis:
aioredis_version = tuple(map(int, aioredis.__version__.split(".")[0]))
if aioredis_version < (2,):
raise ImportError('Invalid aioredis version. Aioredis version should be >= 2.0.0')
if redis_url:
self.redis = aioredis.Redis.from_url(redis_url)
else:
self.redis = aioredis.Redis(host=host, port=port, db=db, password=password)
self.prefix = prefix
#self.con = Redis(connection_pool=self.redis) -> use this when necessary
#
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
async def get_record(self, key):
"""
Function to get record from database.
It has nothing to do with states.
Made for backward compatibility
"""
result = await self.redis.get(self.prefix+str(key))
if result: return json.loads(result)
return
async def set_record(self, key, value):
"""
Function to set record to database.
It has nothing to do with states.
Made for backward compatibility
"""
await self.redis.set(self.prefix+str(key), json.dumps(value))
return True
async def delete_record(self, key):
"""
Function to delete record from database.
It has nothing to do with states.
Made for backward compatibility
"""
await self.redis.delete(self.prefix+str(key))
return True
async def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if hasattr(state, 'name'):
state = state.name
if response:
if user_id in response:
response[user_id]['state'] = state
else:
response[user_id] = {'state': state, 'data': {}}
else:
response = {user_id: {'state': state, 'data': {}}}
await self.set_record(chat_id, response)
return True
async def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
del response[user_id]
if user_id == str(chat_id):
await self.delete_record(chat_id)
return True
else: await self.set_record(chat_id, response)
return True
return False
async def get_value(self, chat_id, user_id, key):
"""
Get value for a data of a user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
if key in response[user_id]['data']:
return response[user_id]['data'][key]
return None
async def get_state(self, chat_id, user_id):
"""
Get state of a user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
return response[user_id]['state']
return None
async def get_data(self, chat_id, user_id):
"""
Get data of particular user in a particular chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
return response[user_id]['data']
return None
async def reset_data(self, chat_id, user_id):
"""
Reset data of a user in a chat.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'] = {}
await self.set_record(chat_id, response)
return True
async def set_data(self, chat_id, user_id, key, value):
"""
Set data without interactive data.
"""
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'][key] = value
await self.set_record(chat_id, response)
return True
return False
def get_interactive_data(self, chat_id, user_id):
"""
Get Data in interactive way.
You can use with() with this function.
"""
return StateContext(self, chat_id, user_id)
async def save(self, chat_id, user_id, data):
response = await self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'] = data
await self.set_record(chat_id, response)
return True
@@ -0,0 +1,155 @@
"""
Callback data factory's file.
"""
"""
Copyright (c) 2017-2018 Alex Root Junior
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
This file was added during the pull request. The maintainers overlooked that it was copied
"as is" from another project and they do not consider it as a right way to develop a project.
However, due to backward compatibility we had to leave this file in the project with the above
copyright added, as it is required by the original project license.
"""
import typing
class CallbackDataFilter:
"""
Filter for CallbackData.
"""
def __init__(self, factory, config: typing.Dict[str, str]):
self.config = config
self.factory = factory
def check(self, query) -> bool:
"""
Checks if query.data appropriates to specified config
:param query: telebot.types.CallbackQuery
:type query: telebot.types.CallbackQuery
:return: True if query.data appropriates to specified config
:rtype: bool
"""
try:
data = self.factory.parse(query.data)
except ValueError:
return False
for key, value in self.config.items():
if isinstance(value, (list, tuple, set, frozenset)):
if data.get(key) not in value:
return False
elif data.get(key) != value:
return False
return True
class CallbackData:
"""
Callback data factory
This class will help you to work with CallbackQuery
"""
def __init__(self, *parts, prefix: str, sep=':'):
if not isinstance(prefix, str):
raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}')
if not prefix:
raise ValueError("Prefix can't be empty")
if sep in prefix:
raise ValueError(f"Separator {sep!r} can't be used in prefix")
self.prefix = prefix
self.sep = sep
self._part_names = parts
def new(self, *args, **kwargs) -> str:
"""
Generate callback data
:param args: positional parameters of CallbackData instance parts
:param kwargs: named parameters
:return: str
"""
args = list(args)
data = [self.prefix]
for part in self._part_names:
value = kwargs.pop(part, None)
if value is None:
if args:
value = args.pop(0)
else:
raise ValueError(f'Value for {part!r} was not passed!')
if value is not None and not isinstance(value, str):
value = str(value)
if self.sep in value:
raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values")
data.append(value)
if args or kwargs:
raise TypeError('Too many arguments were passed!')
callback_data = self.sep.join(data)
if len(callback_data.encode()) > 64:
raise ValueError('Resulted callback data is too long!')
return callback_data
def parse(self, callback_data: str) -> typing.Dict[str, str]:
"""
Parse data from the callback data
:param callback_data: string, use to telebot.types.CallbackQuery to parse it from string to a dict
:return: dict parsed from callback data
"""
prefix, *parts = callback_data.split(self.sep)
if prefix != self.prefix:
raise ValueError("Passed callback data can't be parsed with that prefix.")
elif len(parts) != len(self._part_names):
raise ValueError('Invalid parts count!')
result = {'@': prefix}
result.update(zip(self._part_names, parts))
return result
def filter(self, **config) -> CallbackDataFilter:
"""
Generate filter
:param config: specified named parameters will be checked with CallbackQuery.data
:return: CallbackDataFilter class
"""
for key in config.keys():
if key not in self._part_names:
raise ValueError(f'Invalid field name {key!r}')
return CallbackDataFilter(self, config)
@@ -0,0 +1,462 @@
from abc import ABC
from typing import Optional, Union
from telebot.handler_backends import State
from telebot import types
class SimpleCustomFilter(ABC):
"""
Simple Custom Filter base class.
Create child class with check() method.
Accepts only message, returns bool value, that is compared with given in handler.
Child classes should have .key property.
.. code-block:: python3
:caption: Example on creating a simple custom filter.
class ForwardFilter(SimpleCustomFilter):
# Check whether message was forwarded from channel or group.
key = 'is_forwarded'
def check(self, message):
return message.forward_date is not None
"""
key: str = None
def check(self, message):
"""
Perform a check.
"""
pass
class AdvancedCustomFilter(ABC):
"""
Advanced Custom Filter base class.
Create child class with check() method.
Accepts two parameters, returns bool: True - filter passed, False - filter failed.
message: Message class
text: Filter value given in handler
Child classes should have .key property.
.. code-block:: python3
:caption: Example on creating an advanced custom filter.
class TextStartsFilter(AdvancedCustomFilter):
# Filter to check whether message starts with some text.
key = 'text_startswith'
def check(self, message, text):
return message.text.startswith(text)
"""
key: str = None
def check(self, message, text):
"""
Perform a check.
"""
pass
class TextFilter:
"""
Advanced text filter to check (types.Message, types.CallbackQuery, types.InlineQuery, types.Poll)
example of usage is in examples/custom_filters/advanced_text_filter.py
:param equals: string, True if object's text is equal to passed string
:type equals: :obj:`str`
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:type contains: list[str] or tuple[str]
:param starts_with: string, True if object's text starts with passed string
:type starts_with: :obj:`str`
:param ends_with: string, True if object's text starts with passed string
:type ends_with: :obj:`str`
:param ignore_case: bool (default False), case insensitive
:type ignore_case: :obj:`bool`
:raises ValueError: if incorrect value for a parameter was supplied
:return: None
"""
def __init__(self,
equals: Optional[str] = None,
contains: Optional[Union[list, tuple]] = None,
starts_with: Optional[Union[str, list, tuple]] = None,
ends_with: Optional[Union[str, list, tuple]] = None,
ignore_case: bool = False):
"""
:param equals: string, True if object's text is equal to passed string
:type equals: :obj:`str`
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
:type contains: list[str] or tuple[str]
:param starts_with: string, True if object's text starts with passed string
:type starts_with: :obj:`str`
:param ends_with: string, True if object's text starts with passed string
:type ends_with: :obj:`str`
:param ignore_case: bool (default False), case insensitive
:type ignore_case: :obj:`bool`
:raises ValueError: if incorrect value for a parameter was supplied
:return: None
"""
to_check = sum((pattern is not None for pattern in (equals, contains, starts_with, ends_with)))
if to_check == 0:
raise ValueError('None of the check modes was specified')
self.equals = equals
self.contains = self._check_iterable(contains, filter_name='contains')
self.starts_with = self._check_iterable(starts_with, filter_name='starts_with')
self.ends_with = self._check_iterable(ends_with, filter_name='ends_with')
self.ignore_case = ignore_case
def _check_iterable(self, iterable, filter_name: str):
if not iterable:
pass
elif not isinstance(iterable, str) and not isinstance(iterable, list) and not isinstance(iterable, tuple):
raise ValueError(f"Incorrect value of {filter_name!r}")
elif isinstance(iterable, str):
iterable = [iterable]
elif isinstance(iterable, list) or isinstance(iterable, tuple):
iterable = [i for i in iterable if isinstance(i, str)]
return iterable
def check(self, obj: Union[types.Message, types.CallbackQuery, types.InlineQuery, types.Poll]):
"""
:meta private:
"""
if isinstance(obj, types.Poll):
text = obj.question
elif isinstance(obj, types.Message):
text = obj.text or obj.caption
elif isinstance(obj, types.CallbackQuery):
text = obj.data
elif isinstance(obj, types.InlineQuery):
text = obj.query
else:
return False
if self.ignore_case:
text = text.lower()
if self.equals:
self.equals = self.equals.lower()
elif self.contains:
self.contains = tuple(map(str.lower, self.contains))
elif self.starts_with:
self.starts_with = tuple(map(str.lower, self.starts_with))
elif self.ends_with:
self.ends_with = tuple(map(str.lower, self.ends_with))
if self.equals:
result = self.equals == text
if result:
return True
elif not result and not any((self.contains, self.starts_with, self.ends_with)):
return False
if self.contains:
result = any([i in text for i in self.contains])
if result:
return True
elif not result and not any((self.starts_with, self.ends_with)):
return False
if self.starts_with:
result = any([text.startswith(i) for i in self.starts_with])
if result:
return True
elif not result and not self.ends_with:
return False
if self.ends_with:
return any([text.endswith(i) for i in self.ends_with])
return False
class TextMatchFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(text=['account'])
# your function
"""
key = 'text'
def check(self, message, text):
"""
:meta private:
"""
if isinstance(text, TextFilter):
return text.check(message)
elif type(text) is list:
return message.text in text
else:
return text == message.text
class TextContainsFilter(AdvancedCustomFilter):
"""
Filter to check Text message.
key: text
.. code-block:: python3
:caption: Example on using this filter:
# Will respond if any message.text contains word 'account'
@bot.message_handler(text_contains=['account'])
# your function
"""
key = 'text_contains'
def check(self, message, text):
"""
:meta private:
"""
if not isinstance(text, str) and not isinstance(text, list) and not isinstance(text, tuple):
raise ValueError("Incorrect text_contains value")
elif isinstance(text, str):
text = [text]
elif isinstance(text, list) or isinstance(text, tuple):
text = [i for i in text if isinstance(i, str)]
return any([i in message.text for i in text])
class TextStartsFilter(AdvancedCustomFilter):
"""
Filter to check whether message starts with some text.
.. code-block:: python3
:caption: Example on using this filter:
# Will work if message.text starts with 'sir'.
@bot.message_handler(text_startswith='sir')
# your function
"""
key = 'text_startswith'
def check(self, message, text):
"""
:meta private:
"""
return message.text.startswith(text)
class ChatFilter(AdvancedCustomFilter):
"""
Check whether chat_id corresponds to given chat_id.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(chat_id=[99999])
# your function
"""
key = 'chat_id'
def check(self, message, text):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return message.message.chat.id in text
return message.chat.id in text
class ForwardFilter(SimpleCustomFilter):
"""
Check whether message was forwarded from channel or group.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_forwarded=True)
# your function
"""
key = 'is_forwarded'
def check(self, message):
"""
:meta private:
"""
return message.forward_date is not None
class IsReplyFilter(SimpleCustomFilter):
"""
Check whether message is a reply.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_reply=True)
# your function
"""
key = 'is_reply'
def check(self, message):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return message.message.reply_to_message is not None
return message.reply_to_message is not None
class LanguageFilter(AdvancedCustomFilter):
"""
Check users language_code.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(language_code=['ru'])
# your function
"""
key = 'language_code'
def check(self, message, text):
"""
:meta private:
"""
if type(text) is list:
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
class IsAdminFilter(SimpleCustomFilter):
"""
Check whether the user is administrator / owner of the chat.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(chat_types=['supergroup'], is_chat_admin=True)
# your function
"""
key = 'is_chat_admin'
def __init__(self, bot):
self._bot = bot
def check(self, message):
"""
:meta private:
"""
if isinstance(message, types.CallbackQuery):
return self._bot.get_chat_member(message.message.chat.id, message.from_user.id).status in ['creator', 'administrator']
return self._bot.get_chat_member(message.chat.id, message.from_user.id).status in ['creator', 'administrator']
class StateFilter(AdvancedCustomFilter):
"""
Filter to check state.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(state=1)
# your function
"""
def __init__(self, bot):
self.bot = bot
key = 'state'
def check(self, message, text):
"""
:meta private:
"""
if text == '*': return True
# needs to work with callbackquery
if isinstance(message, types.Message):
chat_id = message.chat.id
user_id = message.from_user.id
if isinstance(message, types.CallbackQuery):
chat_id = message.message.chat.id
user_id = message.from_user.id
message = message.message
if isinstance(text, list):
new_text = []
for i in text:
if isinstance(i, State): i = i.name
new_text.append(i)
text = new_text
elif isinstance(text, State):
text = text.name
if message.chat.type in ['group', 'supergroup']:
group_state = self.bot.current_states.get_state(chat_id, user_id)
if group_state == text:
return True
elif type(text) is list and group_state in text:
return True
else:
user_state = self.bot.current_states.get_state(chat_id, user_id)
if user_state == text:
return True
elif type(text) is list and user_state in text:
return True
class IsDigitFilter(SimpleCustomFilter):
"""
Filter to check whether the string is made up of only digits.
.. code-block:: python3
:caption: Example on using this filter:
@bot.message_handler(is_digit=True)
# your function
"""
key = 'is_digit'
def check(self, message):
"""
:meta private:
"""
return message.text.isdigit()
@@ -0,0 +1,3 @@
"""
A folder with asynchronous and synchronous extensions.
"""
@@ -0,0 +1,10 @@
"""
A folder with all the async extensions.
"""
from .webhooks import AsyncWebhookListener
__all__ = [
"AsyncWebhookListener"
]
@@ -0,0 +1,123 @@
"""
This file is used by AsyncTeleBot.run_webhooks() function.
Fastapi and starlette(0.20.2+) libraries are required to run this script.
"""
# modules required for running this script
fastapi_installed = True
try:
import fastapi
from fastapi.responses import JSONResponse
from fastapi.requests import Request
from uvicorn import Server, Config
except ImportError:
fastapi_installed = False
import asyncio
from telebot.types import Update
from typing import Optional
class AsyncWebhookListener:
def __init__(self, bot,
secret_token: str,
host: Optional[str]="127.0.0.1",
port: Optional[int]=443,
ssl_context: Optional[tuple]=None,
url_path: Optional[str]=None,
) -> None:
"""
Aynchronous implementation of webhook listener
for asynchronous version of telebot.
Not supposed to be used manually by user.
Use AsyncTeleBot.run_webhooks() instead.
:param bot: AsyncTeleBot instance.
:type bot: telebot.async_telebot.AsyncTeleBot
:param secret_token: Telegram secret token
:type secret_token: str
:param host: Webhook host
:type host: str
:param port: Webhook port
:type port: int
:param ssl_context: SSL context
:type ssl_context: tuple
:param url_path: Webhook url path
:type url_path: str
:raises ImportError: If FastAPI or uvicorn is not installed.
:raises ImportError: If Starlette version is too old.
:return: None
"""
self._check_dependencies()
self.app = fastapi.FastAPI()
self._secret_token = secret_token
self._bot = bot
self._port = port
self._host = host
self._ssl_context = ssl_context
self._url_path = url_path
self._prepare_endpoint_urls()
def _check_dependencies(self):
if not fastapi_installed:
raise ImportError('Fastapi or uvicorn is not installed. Please install it via pip.')
import starlette
if starlette.__version__ < '0.20.2':
raise ImportError('Starlette version is too old. Please upgrade it: `pip3 install starlette -U`')
return
def _prepare_endpoint_urls(self):
self.app.add_api_route(endpoint=self.process_update,path= self._url_path, methods=["POST"])
async def process_update(self, request: Request, update: dict):
"""
Processes updates.
:meta private:
"""
# header containsX-Telegram-Bot-Api-Secret-Token
if request.headers.get('X-Telegram-Bot-Api-Secret-Token') != self._secret_token:
# secret token didn't match
return JSONResponse(status_code=403, content={"error": "Forbidden"})
if request.headers.get('content-type') == 'application/json':
json_string = update
asyncio.create_task(self._bot.process_new_updates([Update.de_json(json_string)]))
return JSONResponse('', status_code=200)
return JSONResponse(status_code=403, content={"error": "Forbidden"})
async def run_app(self):
"""
Run app with the given parameters to init.
Not supposed to be used manually by user.
:return: None
"""
config = Config(app=self.app,
host=self._host,
port=self._port,
ssl_certfile=self._ssl_context[0],
ssl_keyfile=self._ssl_context[1]
)
server = Server(config)
await server.serve()
await self._bot.close_session()
@@ -0,0 +1,31 @@
from watchdog.events import FileSystemEventHandler
from watchdog.events import FileSystemEvent
import psutil
import os
import sys
import logging
logger = logging.getLogger('TeleBot')
class EventHandler(FileSystemEventHandler):
def on_any_event(self, event: FileSystemEvent):
logger.info('* Detected changes in: %s, reloading', (event.src_path))
restart_file()
def restart_file():
try:
p = psutil.Process(os.getpid())
for handler in p.open_files() + p.connections():
os.close(handler.fd)
except OSError:
pass
except Exception as e:
logger.error(e)
python = sys.executable
if os.name == 'nt':
os.execv(sys.executable, ['python'] + sys.argv)
else:
os.execl(python, python, *sys.argv)
@@ -0,0 +1,10 @@
"""
A folder with all the sync extensions.
"""
from .webhooks import SyncWebhookListener
__all__ = [
"SyncWebhookListener"
]
@@ -0,0 +1,116 @@
"""
This file is used by TeleBot.run_webhooks() function.
Fastapi is required to run this script.
"""
# modules required for running this script
fastapi_installed = True
try:
import fastapi
from fastapi.responses import JSONResponse
from fastapi.requests import Request
import uvicorn
except ImportError:
fastapi_installed = False
from telebot.types import Update
from typing import Optional
class SyncWebhookListener:
def __init__(self, bot,
secret_token: str,
host: Optional[str]="127.0.0.1",
port: Optional[int]=443,
ssl_context: Optional[tuple]=None,
url_path: Optional[str]=None,
) -> None:
"""
Synchronous implementation of webhook listener
for synchronous version of telebot.
Not supposed to be used manually by user.
Use TeleBot.run_webhooks() instead.
:param bot: TeleBot instance.
:type bot: telebot.TeleBot
:param secret_token: Telegram secret token
:type secret_token: str
:param host: Webhook host
:type host: str
:param port: Webhook port
:type port: int
:param ssl_context: SSL context
:type ssl_context: tuple
:param url_path: Webhook url path
:type url_path: str
:raises ImportError: If FastAPI or uvicorn is not installed.
:raises ImportError: If Starlette version is too old.
:return: None
"""
self._check_dependencies()
self.app = fastapi.FastAPI()
self._secret_token = secret_token
self._bot = bot
self._port = port
self._host = host
self._ssl_context = ssl_context
self._url_path = url_path
self._prepare_endpoint_urls()
@staticmethod
def _check_dependencies():
if not fastapi_installed:
raise ImportError('Fastapi or uvicorn is not installed. Please install it via pip.')
import starlette
if starlette.__version__ < '0.20.2':
raise ImportError('Starlette version is too old. Please upgrade it: `pip3 install starlette -U`')
return
def _prepare_endpoint_urls(self):
self.app.add_api_route(endpoint=self.process_update,path= self._url_path, methods=["POST"])
def process_update(self, request: Request, update: dict):
"""
Processes updates.
:meta private:
"""
# header containsX-Telegram-Bot-Api-Secret-Token
if request.headers.get('X-Telegram-Bot-Api-Secret-Token') != self._secret_token:
# secret token didn't match
return JSONResponse(status_code=403, content={"error": "Forbidden"})
if request.headers.get('content-type') == 'application/json':
self._bot.process_new_updates([Update.de_json(update)])
return JSONResponse('', status_code=200)
return JSONResponse(status_code=403, content={"error": "Forbidden"})
def run_app(self):
"""
Run app with the given parameters to init.
Not supposed to be used manually by user.
:return: None
"""
uvicorn.run(app=self.app,
host=self._host,
port=self._port,
ssl_certfile=self._ssl_context[0],
ssl_keyfile=self._ssl_context[1]
)
@@ -0,0 +1,360 @@
"""
Markdown & HTML formatting functions.
.. versionadded:: 4.5.1
"""
import html
import re
from typing import Optional
def format_text(*args, separator="\n"):
"""
Formats a list of strings into a single string.
.. code:: python3
format_text( # just an example
mbold('Hello'),
mitalic('World')
)
:param args: Strings to format.
:type args: :obj:`str`
:param separator: The separator to use between each string.
:type separator: :obj:`str`
:return: The formatted string.
:rtype: :obj:`str`
"""
return separator.join(args)
def escape_html(content: str) -> str:
"""
Escapes HTML characters in a string of HTML.
:param content: The string of HTML to escape.
:type content: :obj:`str`
:return: The escaped string.
:rtype: :obj:`str`
"""
return html.escape(content)
def escape_markdown(content: str) -> str:
"""
Escapes Markdown characters in a string of Markdown.
Credits to: simonsmh
:param content: The string of Markdown to escape.
:type content: :obj:`str`
:return: The escaped string.
:rtype: :obj:`str`
"""
parse = re.sub(r"([_*\[\]()~`>\#\+\-=|\.!\{\}\\])", r"\\\1", content)
reparse = re.sub(r"\\\\([_*\[\]()~`>\#\+\-=|\.!\{\}\\])", r"\1", parse)
return reparse
def mbold(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted bold string.
:param content: The string to bold.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '*{}*'.format(escape_markdown(content) if escape else content)
def hbold(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted bold string.
:param content: The string to bold.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<b>{}</b>'.format(escape_html(content) if escape else content)
def mitalic(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted italic string.
:param content: The string to italicize.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '_{}_\r'.format(escape_markdown(content) if escape else content)
def hitalic(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted italic string.
:param content: The string to italicize.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<i>{}</i>'.format(escape_html(content) if escape else content)
def munderline(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted underline string.
:param content: The string to underline.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '__{}__'.format(escape_markdown(content) if escape else content)
def hunderline(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted underline string.
:param content: The string to underline.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<u>{}</u>'.format(escape_html(content) if escape else content)
def mstrikethrough(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted strikethrough string.
:param content: The string to strikethrough.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '~{}~'.format(escape_markdown(content) if escape else content)
def hstrikethrough(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted strikethrough string.
:param content: The string to strikethrough.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<s>{}</s>'.format(escape_html(content) if escape else content)
def mspoiler(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted spoiler string.
:param content: The string to spoiler.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '||{}||'.format(escape_markdown(content) if escape else content)
def hspoiler(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted spoiler string.
:param content: The string to spoiler.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<tg-spoiler>{}</tg-spoiler>'.format(escape_html(content) if escape else content)
def mlink(content: str, url: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted link string.
:param content: The string to link.
:type content: :obj:`str`
:param url: The URL to link to.
:type url: str
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '[{}]({})'.format(escape_markdown(content), escape_markdown(url) if escape else content)
def hlink(content: str, url: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted link string.
:param content: The string to link.
:type content: :obj:`str`
:param url: The URL to link to.
:type url: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<a href="{}">{}</a>'.format(escape_html(url), escape_html(content) if escape else content)
def mcode(content: str, language: str="", escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted code string.
:param content: The string to code.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '```{}\n{}```'.format(language, escape_markdown(content) if escape else content)
def hcode(content: str, escape: Optional[bool]=True) -> str:
"""
Returns an HTML-formatted code string.
:param content: The string to code.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<code>{}</code>'.format(escape_html(content) if escape else content)
def hpre(content: str, escape: Optional[bool]=True, language: str="") -> str:
"""
Returns an HTML-formatted preformatted string.
:param content: The string to preformatted.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<pre><code class="{}">{}</code></pre>'.format(language, escape_html(content) if escape else content)
def hide_link(url: str) -> str:
"""
Hide url of an image.
:param url: The url of the image.
:type url: :obj:`str`
:return: The hidden url.
:rtype: :obj:`str`
"""
return f'<a href="{url}">&#8288;</a>'
def mcite(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a Markdown-formatted block-quotation string.
:param content: The string to bold.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
content = escape_markdown(content) if escape else content
content = '\n'.join(['>' + line for line in content.split('\n')])
return content
def hcite(content: str, escape: Optional[bool]=True) -> str:
"""
Returns a html-formatted block-quotation string.
:param content: The string to bold.
:type content: :obj:`str`
:param escape: True if you need to escape special characters. Defaults to True.
:type escape: :obj:`bool`
:return: The formatted string.
:rtype: :obj:`str`
"""
return '<blockquote>{}</blockquote>'.format(escape_html(content) if escape else content)
@@ -0,0 +1,295 @@
import os
import pickle
import threading
from telebot import apihelper
try:
from redis import Redis
redis_installed = True
except:
redis_installed = False
class HandlerBackend(object):
"""
Class for saving (next step|reply) handlers.
:meta private:
"""
def __init__(self, handlers=None):
if handlers is None:
handlers = {}
self.handlers = handlers
def register_handler(self, handler_group_id, handler):
raise NotImplementedError()
def clear_handlers(self, handler_group_id):
raise NotImplementedError()
def get_handlers(self, handler_group_id):
raise NotImplementedError()
class MemoryHandlerBackend(HandlerBackend):
"""
:meta private:
"""
def register_handler(self, handler_group_id, handler):
if handler_group_id in self.handlers:
self.handlers[handler_group_id].append(handler)
else:
self.handlers[handler_group_id] = [handler]
def clear_handlers(self, handler_group_id):
self.handlers.pop(handler_group_id, None)
def get_handlers(self, handler_group_id):
return self.handlers.pop(handler_group_id, None)
def load_handlers(self, filename, del_file_after_loading):
raise NotImplementedError()
class FileHandlerBackend(HandlerBackend):
"""
:meta private:
"""
def __init__(self, handlers=None, filename='./.handler-saves/handlers.save', delay=120):
super(FileHandlerBackend, self).__init__(handlers)
self.filename = filename
self.delay = delay
self.timer = threading.Timer(delay, self.save_handlers)
def register_handler(self, handler_group_id, handler):
if handler_group_id in self.handlers:
self.handlers[handler_group_id].append(handler)
else:
self.handlers[handler_group_id] = [handler]
self.start_save_timer()
def clear_handlers(self, handler_group_id):
self.handlers.pop(handler_group_id, None)
self.start_save_timer()
def get_handlers(self, handler_group_id):
handlers = self.handlers.pop(handler_group_id, None)
self.start_save_timer()
return handlers
def start_save_timer(self):
if not self.timer.is_alive():
if self.delay <= 0:
self.save_handlers()
else:
self.timer = threading.Timer(self.delay, self.save_handlers)
self.timer.start()
def save_handlers(self):
self.dump_handlers(self.handlers, self.filename)
def load_handlers(self, filename=None, del_file_after_loading=True):
if not filename:
filename = self.filename
tmp = self.return_load_handlers(filename, del_file_after_loading=del_file_after_loading)
if tmp is not None:
self.handlers.update(tmp)
@staticmethod
def dump_handlers(handlers, filename, file_mode="wb"):
dirs = filename.rsplit('/', maxsplit=1)[0]
os.makedirs(dirs, exist_ok=True)
with open(filename + ".tmp", file_mode) as file:
if (apihelper.CUSTOM_SERIALIZER is None):
pickle.dump(handlers, file)
else:
apihelper.CUSTOM_SERIALIZER.dump(handlers, file)
if os.path.isfile(filename):
os.remove(filename)
os.rename(filename + ".tmp", filename)
@staticmethod
def return_load_handlers(filename, del_file_after_loading=True):
if os.path.isfile(filename) and os.path.getsize(filename) > 0:
with open(filename, "rb") as file:
if (apihelper.CUSTOM_SERIALIZER is None):
handlers = pickle.load(file)
else:
handlers = apihelper.CUSTOM_SERIALIZER.load(file)
if del_file_after_loading:
os.remove(filename)
return handlers
class RedisHandlerBackend(HandlerBackend):
"""
:meta private:
"""
def __init__(self, handlers=None, host='localhost', port=6379, db=0, prefix='telebot', password=None):
super(RedisHandlerBackend, self).__init__(handlers)
if not redis_installed:
raise Exception("Redis is not installed. Install it via 'pip install redis'")
self.prefix = prefix
self.redis = Redis(host, port, db, password)
def _key(self, handle_group_id):
return ':'.join((self.prefix, str(handle_group_id)))
def register_handler(self, handler_group_id, handler):
handlers = []
value = self.redis.get(self._key(handler_group_id))
if value:
handlers = pickle.loads(value)
handlers.append(handler)
self.redis.set(self._key(handler_group_id), pickle.dumps(handlers))
def clear_handlers(self, handler_group_id):
self.redis.delete(self._key(handler_group_id))
def get_handlers(self, handler_group_id):
handlers = None
value = self.redis.get(self._key(handler_group_id))
if value:
handlers = pickle.loads(value)
self.clear_handlers(handler_group_id)
return handlers
class State:
"""
Class representing a state.
.. code-block:: python3
class MyStates(StatesGroup):
my_state = State() # returns my_state:State string.
"""
def __init__(self) -> None:
self.name = None
def __str__(self) -> str:
return self.name
class StatesGroup:
"""
Class representing common states.
.. code-block:: python3
class MyStates(StatesGroup):
my_state = State() # returns my_state:State string.
"""
def __init_subclass__(cls) -> None:
state_list = []
for name, value in cls.__dict__.items():
if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable
value.name = ':'.join((cls.__name__, name))
value.group = cls
state_list.append(value)
cls._state_list = state_list
@property
def state_list(self):
return self._state_list
class BaseMiddleware:
"""
Base class for middleware.
Your middlewares should be inherited from this class.
Set update_sensitive=True if you want to get different updates on
different functions. For example, if you want to handle pre_process for
message update, then you will have to create pre_process_message function, and
so on. Same applies to post_process.
.. note::
If you want to use middleware, you have to set use_class_middlewares=True in your
TeleBot instance.
.. code-block:: python3
:caption: Example of class-based middlewares.
class MyMiddleware(BaseMiddleware):
def __init__(self):
self.update_sensitive = True
self.update_types = ['message', 'edited_message']
def pre_process_message(self, message, data):
# only message update here
pass
def post_process_message(self, message, data, exception):
pass # only message update here for post_process
def pre_process_edited_message(self, message, data):
# only edited_message update here
pass
def post_process_edited_message(self, message, data, exception):
pass # only edited_message update here for post_process
"""
update_sensitive: bool = False
def __init__(self):
pass
def pre_process(self, message, data):
raise NotImplementedError
def post_process(self, message, data, exception):
raise NotImplementedError
class SkipHandler:
"""
Class for skipping handlers.
Just return instance of this class
in middleware to skip handler.
Update will go to post_process,
but will skip execution of handler.
"""
def __init__(self) -> None:
pass
class CancelUpdate:
"""
Class for canceling updates.
Just return instance of this class
in middleware to skip update.
Update will skip handler and execution
of post_process in middlewares.
"""
def __init__(self) -> None:
pass
class ContinueHandling:
"""
Class for continue updates in handlers.
Just return instance of this class
in handlers to continue process.
.. code-block:: python3
:caption: Example of using ContinueHandling
@bot.message_handler(commands=['start'])
def start(message):
bot.send_message(message.chat.id, 'Hello World!')
return ContinueHandling()
@bot.message_handler(commands=['start'])
def start2(message):
bot.send_message(message.chat.id, 'Hello World2!')
"""
def __init__(self) -> None:
pass
@@ -0,0 +1,84 @@
import random
import string
from io import BytesIO
try:
# noinspection PyPackageRequirements
from PIL import Image
pil_imported = True
except ImportError:
pil_imported = False
def is_string(var) -> bool:
"""
Returns True if the given object is a string.
"""
return isinstance(var, str)
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`
"""
return isinstance(var, dict)
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`
"""
return isinstance(var, bytes)
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`
"""
return pil_imported and isinstance(var, Image.Image)
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)
return photoBuffer
else:
raise RuntimeError('PIL module is not imported')
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]
def generate_random_token() -> str:
"""
Generates a random token consisting of letters and digits, 16 characters long.
:return: a random token
:rtype: :obj:`str`
"""
return ''.join(random.sample(string.ascii_letters, 16))
@@ -0,0 +1,13 @@
from telebot.storage.memory_storage import StateMemoryStorage
from telebot.storage.redis_storage import StateRedisStorage
from telebot.storage.pickle_storage import StatePickleStorage
from telebot.storage.base_storage import StateContext,StateStorageBase
__all__ = [
'StateStorageBase', 'StateContext',
'StateMemoryStorage', 'StateRedisStorage', 'StatePickleStorage'
]
@@ -0,0 +1,68 @@
import copy
class StateStorageBase:
def __init__(self) -> None:
pass
def set_data(self, chat_id, user_id, key, value):
"""
Set data for a user in a particular chat.
"""
raise NotImplementedError
def get_data(self, chat_id, user_id):
"""
Get data for a user in a particular chat.
"""
raise NotImplementedError
def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user.
! Note that you should create a
record if it does not exist, and
if a record with state already exists,
you need to update a record.
"""
raise NotImplementedError
def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user.
"""
raise NotImplementedError
def reset_data(self, chat_id, user_id):
"""
Reset data for a particular user in a chat.
"""
raise NotImplementedError
def get_state(self, chat_id, user_id):
raise NotImplementedError
def get_interactive_data(self, chat_id, user_id):
raise NotImplementedError
def save(self, chat_id, user_id, data):
raise NotImplementedError
class StateContext:
"""
Class for data.
"""
def __init__(self , obj, chat_id, user_id) -> None:
self.obj = obj
self.data = copy.deepcopy(obj.get_data(chat_id, user_id))
self.chat_id = chat_id
self.user_id = user_id
def __enter__(self):
return self.data
def __exit__(self, exc_type, exc_val, exc_tb):
return self.obj.save(self.chat_id, self.user_id, self.data)
@@ -0,0 +1,69 @@
from telebot.storage.base_storage import StateStorageBase, StateContext
class StateMemoryStorage(StateStorageBase):
def __init__(self) -> None:
super().__init__()
self.data = {}
#
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
def set_state(self, chat_id, user_id, state):
if hasattr(state, 'name'):
state = state.name
if chat_id in self.data:
if user_id in self.data[chat_id]:
self.data[chat_id][user_id]['state'] = state
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
return True
def delete_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
del self.data[chat_id][user_id]
if chat_id == user_id:
del self.data[chat_id]
return True
return False
def get_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['state']
return None
def get_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['data']
return None
def reset_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'] = {}
return True
return False
def set_data(self, chat_id, user_id, key, value):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'][key] = value
return True
raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id))
def get_interactive_data(self, chat_id, user_id):
return StateContext(self, chat_id, user_id)
def save(self, chat_id, user_id, data):
self.data[chat_id][user_id]['data'] = data
@@ -0,0 +1,116 @@
from telebot.storage.base_storage import StateStorageBase, StateContext
import os
import pickle
class StatePickleStorage(StateStorageBase):
def __init__(self, file_path="./.state-save/states.pkl") -> None:
super().__init__()
self.file_path = file_path
self.create_dir()
self.data = self.read()
def convert_old_to_new(self):
"""
Use this function to convert old storage to new storage.
This function is for people who was using pickle storage
that was in version <=4.3.1.
"""
# old looks like:
# {1: {'state': 'start', 'data': {'name': 'John'}}
# we should update old version pickle to new.
# new looks like:
# {1: {2: {'state': 'start', 'data': {'name': 'John'}}}}
new_data = {}
for key, value in self.data.items():
# this returns us id and dict with data and state
new_data[key] = {key: value} # convert this to new
# pass it to global data
self.data = new_data
self.update_data() # update data in file
def create_dir(self):
"""
Create directory .save-handlers.
"""
dirs, filename = os.path.split(self.file_path)
os.makedirs(dirs, exist_ok=True)
if not os.path.isfile(self.file_path):
with open(self.file_path,'wb') as file:
pickle.dump({}, file)
def read(self):
file = open(self.file_path, 'rb')
data = pickle.load(file)
file.close()
return data
def update_data(self):
file = open(self.file_path, 'wb+')
pickle.dump(self.data, file, protocol=pickle.HIGHEST_PROTOCOL)
file.close()
def set_state(self, chat_id, user_id, state):
if hasattr(state, 'name'):
state = state.name
if chat_id in self.data:
if user_id in self.data[chat_id]:
self.data[chat_id][user_id]['state'] = state
self.update_data()
return True
else:
self.data[chat_id][user_id] = {'state': state, 'data': {}}
self.update_data()
return True
self.data[chat_id] = {user_id: {'state': state, 'data': {}}}
self.update_data()
return True
def delete_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
del self.data[chat_id][user_id]
if chat_id == user_id:
del self.data[chat_id]
self.update_data()
return True
return False
def get_state(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['state']
return None
def get_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
return self.data[chat_id][user_id]['data']
return None
def reset_data(self, chat_id, user_id):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'] = {}
self.update_data()
return True
return False
def set_data(self, chat_id, user_id, key, value):
if self.data.get(chat_id):
if self.data[chat_id].get(user_id):
self.data[chat_id][user_id]['data'][key] = value
self.update_data()
return True
raise RuntimeError('chat_id {} and user_id {} does not exist'.format(chat_id, user_id))
def get_interactive_data(self, chat_id, user_id):
return StateContext(self, chat_id, user_id)
def save(self, chat_id, user_id, data):
self.data[chat_id][user_id]['data'] = data
self.update_data()
@@ -0,0 +1,183 @@
from telebot.storage.base_storage import StateStorageBase, StateContext
import json
redis_installed = True
try:
from redis import Redis, ConnectionPool
except:
redis_installed = False
class StateRedisStorage(StateStorageBase):
"""
This class is for Redis storage.
This will work only for states.
To use it, just pass this class to:
TeleBot(storage=StateRedisStorage())
"""
def __init__(self, host='localhost', port=6379, db=0, password=None, prefix='telebot_', redis_url=None):
super().__init__()
if redis_url:
self.redis = ConnectionPool.from_url(redis_url)
else:
self.redis = ConnectionPool(host=host, port=port, db=db, password=password)
#self.con = Redis(connection_pool=self.redis) -> use this when necessary
#
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
self.prefix = prefix
if not redis_installed:
raise Exception("Redis is not installed. Install it via 'pip install redis'")
def get_record(self, key):
"""
Function to get record from database.
It has nothing to do with states.
Made for backward compatibility
"""
connection = Redis(connection_pool=self.redis)
result = connection.get(self.prefix+str(key))
connection.close()
if result: return json.loads(result)
return
def set_record(self, key, value):
"""
Function to set record to database.
It has nothing to do with states.
Made for backward compatibility
"""
connection = Redis(connection_pool=self.redis)
connection.set(self.prefix+str(key), json.dumps(value))
connection.close()
return True
def delete_record(self, key):
"""
Function to delete record from database.
It has nothing to do with states.
Made for backward compatibility
"""
connection = Redis(connection_pool=self.redis)
connection.delete(self.prefix+str(key))
connection.close()
return True
def set_state(self, chat_id, user_id, state):
"""
Set state for a particular user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if hasattr(state, 'name'):
state = state.name
if response:
if user_id in response:
response[user_id]['state'] = state
else:
response[user_id] = {'state': state, 'data': {}}
else:
response = {user_id: {'state': state, 'data': {}}}
self.set_record(chat_id, response)
return True
def delete_state(self, chat_id, user_id):
"""
Delete state for a particular user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
del response[user_id]
if user_id == str(chat_id):
self.delete_record(chat_id)
return True
else: self.set_record(chat_id, response)
return True
return False
def get_value(self, chat_id, user_id, key):
"""
Get value for a data of a user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
if key in response[user_id]['data']:
return response[user_id]['data'][key]
return None
def get_state(self, chat_id, user_id):
"""
Get state of a user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
return response[user_id]['state']
return None
def get_data(self, chat_id, user_id):
"""
Get data of particular user in a particular chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
return response[user_id]['data']
return None
def reset_data(self, chat_id, user_id):
"""
Reset data of a user in a chat.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'] = {}
self.set_record(chat_id, response)
return True
def set_data(self, chat_id, user_id, key, value):
"""
Set data without interactive data.
"""
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'][key] = value
self.set_record(chat_id, response)
return True
return False
def get_interactive_data(self, chat_id, user_id):
"""
Get Data in interactive way.
You can use with() with this function.
"""
return StateContext(self, chat_id, user_id)
def save(self, chat_id, user_id, data):
response = self.get_record(chat_id)
user_id = str(user_id)
if response:
if user_id in response:
response[user_id]['data'] = data
self.set_record(chat_id, response)
return True
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,699 @@
# -*- coding: utf-8 -*-
import re
import threading
import traceback
from typing import Any, Callable, List, Dict, Optional, Union
import hmac
from hashlib import sha256
from urllib.parse import parse_qsl
# noinspection PyPep8Naming
import queue as Queue
import logging
from telebot import types
from telebot.service_utils import is_pil_image, is_dict, is_string, is_bytes, chunks, generate_random_token, pil_image_to_file
try:
import ujson as json
except ImportError:
import json
MAX_MESSAGE_LENGTH = 4096
logger = logging.getLogger('TeleBot')
thread_local = threading.local()
#: Contains all media content types.
content_type_media = [
'text', 'animation', 'audio', 'document', 'photo', 'sticker', 'story', 'video', 'video_note', 'voice', 'contact',
'dice', 'game', 'poll', 'venue', 'location', 'invoice', 'successful_payment', 'connected_website',
'passport_data', 'web_app_data',
]
#: Contains all service content types such as `User joined the group`.
content_type_service = [
'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo',
'group_chat_created', 'supergroup_chat_created', 'channel_chat_created', 'message_auto_delete_timer_changed',
'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message', 'users_shared', 'chat_shared',
'write_access_allowed', 'proximity_alert_triggered', 'forum_topic_created', 'forum_topic_edited',
'forum_topic_closed', 'forum_topic_reopened', 'general_forum_topic_hidden', 'general_forum_topic_unhidden',
'giveaway_created', 'giveaway', 'giveaway_winners', 'giveaway_completed', 'video_chat_scheduled',
'video_chat_started', 'video_chat_ended', 'video_chat_participants_invited',
]
#: All update types, should be used for allowed_updates parameter in polling.
update_types = [
"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", "message_reaction", "message_reaction_count", "chat_boost", "removed_chat_boost",
]
class WorkerThread(threading.Thread):
"""
:meta private:
"""
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
self.exception_info = None
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:
logger.debug(type(e).__name__ + " occurred, args=" + str(e.args) + "\n" + traceback.format_exc())
self.exception_info = e
self.exception_event.set()
if self.exception_callback:
self.exception_callback(self, self.exception_info)
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():
raise self.exception_info
def clear_exceptions(self):
self.exception_event.clear()
self.continue_event.set()
def stop(self):
self._running = False
class ThreadPool:
"""
:meta private:
"""
def __init__(self, telebot, num_threads=2):
self.telebot = telebot
self.tasks = Queue.Queue()
self.workers = [WorkerThread(self.on_exception, self.tasks) for _ in range(num_threads)]
self.num_threads = num_threads
self.exception_event = threading.Event()
self.exception_info = None
def put(self, func, *args, **kwargs):
self.tasks.put((func, args, kwargs))
def on_exception(self, worker_thread, exc_info):
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()
worker_thread.continue_event.set()
def raise_exceptions(self):
if self.exception_event.is_set():
raise self.exception_info
def clear_exceptions(self):
self.exception_event.clear()
def close(self):
for worker in self.workers:
worker.stop()
for worker in self.workers:
if worker != threading.current_thread():
worker.join()
class AsyncTask:
"""
:meta private:
"""
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)
except Exception as e:
self.result = e
self.done = True
def wait(self):
if not self.done:
self.thread.join()
if isinstance(self.result, BaseException):
raise self.result
else:
return self.result
class CustomRequestResponse:
"""
:meta private:
"""
def __init__(self, json_text, status_code=200, reason=""):
self.status_code = status_code
self.text = json_text
self.reason = reason
def json(self):
return json.loads(self.text)
def async_dec():
"""
:meta private:
"""
def decorator(fn):
def wrapper(*args, **kwargs):
return AsyncTask(fn, *args, **kwargs)
return wrapper
return decorator
def is_command(text: str) -> bool:
r"""
Checks if `text` is a command. Telegram chat commands start with the '/' character.
:param text: Text to check.
:type text: :obj:`str`
:return: True if `text` is a command, else False.
:rtype: :obj:`bool`
"""
if text is None: return False
return text.startswith('/')
def extract_command(text: str) -> Union[str, None]:
"""
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.
.. 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
:param text: String to extract the command from
:type text: :obj:`str`
:return: the command if `text` is a command (according to is_command), else None.
:rtype: :obj:`str` or :obj:`None`
"""
if text is None: return None
return text.split()[0].split('@')[0][1:] if is_command(text) else None
def extract_arguments(text: str) -> str or None:
"""
Returns the argument after the command.
.. code-block:: python3
:caption: Examples:
extract_arguments("/get name"): 'name'
extract_arguments("/get"): ''
extract_arguments("/get@botName name"): 'name'
:param text: String to extract the arguments from a command
:type text: :obj:`str`
:return: the arguments if `text` is a command (according to is_command), else None.
:rtype: :obj:`str` or :obj:`None`
"""
regexp = re.compile(r"/\w*(@\w*)*\s*([\s\S]*)", re.IGNORECASE)
result = regexp.match(text)
return result.group(2) if is_command(text) else None
def extract_entity(text: str, e: types.MessageEntity) -> str:
"""
Returns the content of the entity.
:param text: The text of the message the entity belongs to
:type text: :obj:`str`
:param e: The entity to extract
:type e: :obj:`MessageEntity`
:return: The content of the entity
:rtype: :obj:`str`
"""
offset = 0
start = 0
encoded_text = text.encode()
end = len(encoded_text)
i = 0
for byte in encoded_text:
if (byte & 0xc0) != 0x80:
if offset == e.offset:
start = i
elif offset - e.offset == e.length:
end = i
break
if byte >= 0xf0:
offset += 2
else:
offset += 1
i += 1
return encoded_text[start:end].decode()
def split_string(text: str, chars_per_string: int) -> List[str]:
"""
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
:type text: :obj:`str`
:param chars_per_string: The number of characters per line the text is split into.
:type chars_per_string: :obj:`int`
:return: The splitted text as a list of strings.
:rtype: :obj:`list` of :obj:`str`
"""
return [text[i:i + chars_per_string] for i in range(0, len(text), chars_per_string)]
def smart_split(text: str, chars_per_string: int = MAX_MESSAGE_LENGTH) -> List[str]:
r"""
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.
If `chars_per_string` > 4096: `chars_per_string` = 4096.
Splits by '\n', '. ' or ' ' in exactly this priority.
:param text: The text to split
:type text: :obj:`str`
:param chars_per_string: The number of maximum characters per part the text is split to.
:type chars_per_string: :obj:`int`
:return: The splitted text as a list of strings.
:rtype: :obj:`list` of :obj:`str`
"""
def _text_before_last(substr: str) -> str:
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]
if "\n" in part:
part = _text_before_last("\n")
elif ". " in part:
part = _text_before_last(". ")
elif " " in part:
part = _text_before_last(" ")
parts.append(part)
text = text[len(part):]
def escape(text: str) -> Optional[str]:
"""
Replaces the following chars in `text` ('&' with '&amp;', '<' with '&lt;' and '>' with '&gt;').
:param text: the text to escape
:return: the escaped text
"""
chars = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}
if text is None:
return None
for old, new in chars.items():
text = text.replace(old, new)
return text
def user_link(user: types.User, include_id: bool = False) -> str:
"""
Returns an HTML user link. This is useful for reports.
Attention: Don't forget to set parse_mode to 'HTML'!
.. 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.
:param user: the user (not the user_id)
:type user: :obj:`telebot.types.User`
:param include_id: include the user_id
:type include_id: :obj:`bool`
:return: HTML user link
:rtype: :obj:`str`
"""
name = escape(user.first_name)
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
+ (f" (<pre>{user.id}</pre>)" if include_id else ""))
def quick_markup(values: Dict[str, Dict[str, Any]], row_width: int = 2) -> types.InlineKeyboardMarkup:
"""
Returns a reply markup from a dict in this format: {'text': kwargs}
This is useful to avoid always typing 'btn1 = InlineKeyboardButton(...)' 'btn2 = InlineKeyboardButton(...)'
Example:
.. code-block:: python3
:caption: Using quick_markup:
from telebot.util import quick_markup
markup = quick_markup({
'Twitter': {'url': 'https://twitter.com'},
'Facebook': {'url': 'https://facebook.com'},
'Back': {'callback_data': 'whatever'}
}, row_width=2)
# returns an InlineKeyboardMarkup with two buttons in a row, one leading to Twitter, the other to facebook
# and a back button below
# kwargs can be:
{
'url': None,
'callback_data': None,
'switch_inline_query': None,
'switch_inline_query_current_chat': None,
'callback_game': None,
'pay': None,
'login_url': None,
'web_app': None
}
:param values: a dict containing all buttons to create in this format: {text: kwargs} {str:}
:type values: :obj:`dict`
:param row_width: number of :class:`telebot.types.InlineKeyboardButton` objects on each row
:type row_width: :obj:`int`
:return: InlineKeyboardMarkup
:rtype: :obj:`types.InlineKeyboardMarkup`
"""
markup = types.InlineKeyboardMarkup(row_width=row_width)
buttons = [
types.InlineKeyboardButton(text=text, **kwargs)
for text, kwargs in values.items()
]
markup.add(*buttons)
return markup
# CREDITS TO http://stackoverflow.com/questions/12317940#answer-12320352
def or_set(self):
"""
:meta private:
"""
self._set()
self.changed()
def or_clear(self):
"""
:meta private:
"""
self._clear()
self.changed()
def orify(e, changed_callback):
"""
:meta private:
"""
if not hasattr(e, "_set"):
e._set = e.set
if not hasattr(e, "_clear"):
e._clear = e.clear
e.changed = changed_callback
e.set = lambda: or_set(e)
e.clear = lambda: or_clear(e)
def OrEvent(*events):
"""
:meta private:
"""
or_event = threading.Event()
def changed():
bools = [ev.is_set() for ev in events]
if any(bools):
or_event.set()
else:
or_event.clear()
def busy_wait():
while not or_event.is_set():
# noinspection PyProtectedMember
or_event._wait(3)
for e in events:
orify(e, changed)
or_event._wait = or_event.wait
or_event.wait = busy_wait
changed()
return or_event
def per_thread(key, construct_value, reset=False):
"""
:meta private:
"""
if reset or not hasattr(thread_local, key):
value = construct_value()
setattr(thread_local, key, value)
return getattr(thread_local, key)
def deprecated(warn: bool = True, alternative: Optional[Callable] = None, deprecation_text=None):
"""
Use this decorator to mark functions as deprecated.
When the function is used, an info (or warning if `warn` is True) is logged.
:meta private:
:param warn: If True a warning is logged else an info
:type warn: :obj:`bool`
:param alternative: The new function to use instead
:type alternative: :obj:`Callable`
:param deprecation_text: Custom deprecation text
:type deprecation_text: :obj:`str`
:return: The decorated function
"""
def decorator(function):
def wrapper(*args, **kwargs):
info = f"`{function.__name__}` is deprecated."
if alternative:
info += f" Use `{alternative.__name__}` instead"
if deprecation_text:
info += " " + deprecation_text
if not warn:
logger.info(info)
else:
logger.warning(info)
return function(*args, **kwargs)
return wrapper
return decorator
# Cloud helpers
def webhook_google_functions(bot, request):
"""
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
"""
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'
def antiflood(function: Callable, *args, number_retries=5, **kwargs):
"""
Use this function inside loops in order to avoid getting TooManyRequests error.
Example:
.. code-block:: python3
from telebot.util import antiflood
for chat_id in chat_id_list:
msg = antiflood(bot.send_message, chat_id, text)
:param function: The function to call
:type function: :obj:`Callable`
:param number_retries: Number of retries to send
:type function: :obj:int
: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`
:return: None
"""
from telebot.apihelper import ApiTelegramException
from time import sleep
for _ in range(number_retries - 1):
try:
return function(*args, **kwargs)
except ApiTelegramException as ex:
if ex.error_code == 429:
sleep(ex.result_json['parameters']['retry_after'])
else:
raise
else:
return function(*args, **kwargs)
def parse_web_app_data(token: str, raw_init_data: str):
"""
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
"""
is_valid = validate_web_app_data(token, raw_init_data)
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
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
"""
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
__all__ = (
"content_type_media", "content_type_service", "update_types",
"WorkerThread", "AsyncTask", "CustomRequestResponse",
"async_dec", "deprecated",
"is_bytes", "is_string", "is_dict", "is_pil_image",
"chunks", "generate_random_token", "pil_image_to_file",
"is_command", "extract_command", "extract_arguments",
"split_string", "smart_split", "escape", "user_link", "quick_markup",
"antiflood", "parse_web_app_data", "validate_web_app_data",
"or_set", "or_clear", "orify", "OrEvent", "per_thread",
"webhook_google_functions"
)
@@ -0,0 +1,3 @@
# Versions should comply with PEP440.
# This line is parsed in setup.py:
__version__ = '4.16.1'