Inserted Python API for bot and simple echo bot
This commit is contained in:
parent
2cbf3ecc14
commit
fbb74dd6a9
18
bot.py
18
bot.py
@ -1,3 +1,19 @@
|
||||
# Work in Progress
|
||||
# Api-Key: 5228016873:AAGFrh0P6brag7oD3gxXjCh5gnLLE8JMvMs
|
||||
# text bot at t.me/projektaktienbot
|
||||
# text bot at t.me/projektaktienbot
|
||||
# API Documentation https://core.telegram.org/bots/api
|
||||
# Code examples https://github.com/eternnoir/pyTelegramBotAPI#getting-started
|
||||
|
||||
import telebot
|
||||
|
||||
bot = telebot.TeleBot("5228016873:AAGFrh0P6brag7oD3gxXjCh5gnLLE8JMvMs")
|
||||
|
||||
@bot.message_handler(commands=['start', 'help'])
|
||||
def send_welcome(message):
|
||||
bot.reply_to(message, "Thank you for using this bot")
|
||||
|
||||
@bot.message_handler(func=lambda message: True)
|
||||
def echo_all(message):
|
||||
bot.reply_to(message, message.text)
|
||||
|
||||
bot.infinity_polling()
|
3887
telebot/__init__.py
Normal file
3887
telebot/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
telebot/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
telebot/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/__pycache__/apihelper.cpython-39.pyc
Normal file
BIN
telebot/__pycache__/apihelper.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/__pycache__/custom_filters.cpython-39.pyc
Normal file
BIN
telebot/__pycache__/custom_filters.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/__pycache__/handler_backends.cpython-39.pyc
Normal file
BIN
telebot/__pycache__/handler_backends.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/__pycache__/types.cpython-39.pyc
Normal file
BIN
telebot/__pycache__/types.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/__pycache__/util.cpython-39.pyc
Normal file
BIN
telebot/__pycache__/util.cpython-39.pyc
Normal file
Binary file not shown.
1776
telebot/apihelper.py
Normal file
1776
telebot/apihelper.py
Normal file
File diff suppressed because it is too large
Load Diff
3357
telebot/async_telebot.py
Normal file
3357
telebot/async_telebot.py
Normal file
File diff suppressed because it is too large
Load Diff
328
telebot/asyncio_filters.py
Normal file
328
telebot/asyncio_filters.py
Normal file
@ -0,0 +1,328 @@
|
||||
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.
|
||||
"""
|
||||
|
||||
async def check(self, message):
|
||||
"""
|
||||
Perform a check.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class AdvancedCustomFilter(ABC):
|
||||
"""
|
||||
Simple 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
|
||||
"""
|
||||
|
||||
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/custom_filters/advanced_text_filter.py
|
||||
"""
|
||||
|
||||
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
|
||||
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
|
||||
:param starts_with: string, True if object's text starts with passed string
|
||||
:param ends_with: string, True if object's text starts with passed string
|
||||
:param ignore_case: bool (default False), case insensitive
|
||||
"""
|
||||
|
||||
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]):
|
||||
|
||||
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.
|
||||
key: text
|
||||
|
||||
Example:
|
||||
@bot.message_handler(text=['account'])
|
||||
"""
|
||||
|
||||
key = 'text'
|
||||
|
||||
async def check(self, message, text):
|
||||
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
|
||||
|
||||
Example:
|
||||
# Will respond if any message.text contains word 'account'
|
||||
@bot.message_handler(text_contains=['account'])
|
||||
"""
|
||||
|
||||
key = 'text_contains'
|
||||
|
||||
async def check(self, message, text):
|
||||
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.
|
||||
|
||||
Example:
|
||||
# Will work if message.text starts with 'Sir'.
|
||||
@bot.message_handler(text_startswith='Sir')
|
||||
"""
|
||||
|
||||
key = 'text_startswith'
|
||||
|
||||
async def check(self, message, text):
|
||||
return message.text.startswith(text)
|
||||
|
||||
|
||||
class ChatFilter(AdvancedCustomFilter):
|
||||
"""
|
||||
Check whether chat_id corresponds to given chat_id.
|
||||
|
||||
Example:
|
||||
@bot.message_handler(chat_id=[99999])
|
||||
"""
|
||||
|
||||
key = 'chat_id'
|
||||
|
||||
async def check(self, message, text):
|
||||
return message.chat.id in text
|
||||
|
||||
|
||||
class ForwardFilter(SimpleCustomFilter):
|
||||
"""
|
||||
Check whether message was forwarded from channel or group.
|
||||
|
||||
Example:
|
||||
|
||||
@bot.message_handler(is_forwarded=True)
|
||||
"""
|
||||
|
||||
key = 'is_forwarded'
|
||||
|
||||
async def check(self, message):
|
||||
return message.forward_from_chat is not None
|
||||
|
||||
|
||||
class IsReplyFilter(SimpleCustomFilter):
|
||||
"""
|
||||
Check whether message is a reply.
|
||||
|
||||
Example:
|
||||
|
||||
@bot.message_handler(is_reply=True)
|
||||
"""
|
||||
|
||||
key = 'is_reply'
|
||||
|
||||
async def check(self, message):
|
||||
return message.reply_to_message is not None
|
||||
|
||||
|
||||
class LanguageFilter(AdvancedCustomFilter):
|
||||
"""
|
||||
Check users language_code.
|
||||
|
||||
Example:
|
||||
|
||||
@bot.message_handler(language_code=['ru'])
|
||||
"""
|
||||
|
||||
key = 'language_code'
|
||||
|
||||
async def check(self, message, text):
|
||||
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.
|
||||
|
||||
Example:
|
||||
@bot.message_handler(chat_types=['supergroup'], is_chat_admin=True)
|
||||
"""
|
||||
|
||||
key = 'is_chat_admin'
|
||||
|
||||
def __init__(self, bot):
|
||||
self._bot = bot
|
||||
|
||||
async def check(self, message):
|
||||
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.
|
||||
|
||||
Example:
|
||||
@bot.message_handler(state=1)
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
key = 'state'
|
||||
|
||||
async def check(self, message, text):
|
||||
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 == 'group':
|
||||
group_state = await self.bot.current_states.get_state(user_id, chat_id)
|
||||
if group_state == text:
|
||||
return True
|
||||
elif group_state in text and type(text) is list:
|
||||
return True
|
||||
|
||||
|
||||
else:
|
||||
user_state = await self.bot.current_states.get_state(user_id, chat_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.
|
||||
|
||||
Example:
|
||||
@bot.message_handler(is_digit=True)
|
||||
"""
|
||||
key = 'is_digit'
|
||||
|
||||
async def check(self, message):
|
||||
return message.text.isdigit()
|
56
telebot/asyncio_handler_backends.py
Normal file
56
telebot/asyncio_handler_backends.py
Normal file
@ -0,0 +1,56 @@
|
||||
class BaseMiddleware:
|
||||
"""
|
||||
Base class for middleware.
|
||||
Your middlewares should be inherited from this class.
|
||||
"""
|
||||
|
||||
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:
|
||||
def __init__(self) -> None:
|
||||
self.name = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class StatesGroup:
|
||||
def __init_subclass__(cls) -> None:
|
||||
|
||||
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))
|
||||
|
||||
|
||||
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
|
1741
telebot/asyncio_helper.py
Normal file
1741
telebot/asyncio_helper.py
Normal file
File diff suppressed because it is too large
Load Diff
13
telebot/asyncio_storage/__init__.py
Normal file
13
telebot/asyncio_storage/__init__.py
Normal file
@ -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'
|
||||
]
|
68
telebot/asyncio_storage/base_storage.py
Normal file
68
telebot/asyncio_storage/base_storage.py
Normal file
@ -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)
|
66
telebot/asyncio_storage/memory_storage.py
Normal file
66
telebot/asyncio_storage/memory_storage.py
Normal file
@ -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 isinstance(state, object):
|
||||
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
|
109
telebot/asyncio_storage/pickle_storage.py
Normal file
109
telebot/asyncio_storage/pickle_storage.py
Normal file
@ -0,0 +1,109 @@
|
||||
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 = self.file_path.rsplit('/', maxsplit=1)[0]
|
||||
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 isinstance(state, object):
|
||||
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': {}}}
|
||||
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()
|
171
telebot/asyncio_storage/redis_storage.py
Normal file
171
telebot/asyncio_storage/redis_storage.py
Normal file
@ -0,0 +1,171 @@
|
||||
from telebot.asyncio_storage.base_storage import StateStorageBase, StateContext
|
||||
import json
|
||||
|
||||
redis_installed = True
|
||||
try:
|
||||
import aioredis
|
||||
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_'):
|
||||
if not redis_installed:
|
||||
raise ImportError('AioRedis is not installed. Install it via "pip install 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')
|
||||
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 backend 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 backend 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 backend 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 isinstance(state, object):
|
||||
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'] = dict(data, **response[user_id]['data'])
|
||||
await self.set_record(chat_id, response)
|
||||
return True
|
115
telebot/callback_data.py
Normal file
115
telebot/callback_data.py
Normal file
@ -0,0 +1,115 @@
|
||||
import typing
|
||||
|
||||
|
||||
class CallbackDataFilter:
|
||||
|
||||
def __init__(self, factory, config: typing.Dict[str, str]):
|
||||
self.config = config
|
||||
self.factory = factory
|
||||
|
||||
def check(self, query):
|
||||
"""
|
||||
Checks if query.data appropriates to specified config
|
||||
:param query: telebot.types.CallbackQuery
|
||||
:return: 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 CallbackQury.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)
|
336
telebot/custom_filters.py
Normal file
336
telebot/custom_filters.py
Normal file
@ -0,0 +1,336 @@
|
||||
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.
|
||||
"""
|
||||
|
||||
def check(self, message):
|
||||
"""
|
||||
Perform a check.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class AdvancedCustomFilter(ABC):
|
||||
"""
|
||||
Simple 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
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
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
|
||||
:param contains: list[str] or tuple[str], True if any string element of iterable is in text
|
||||
:param starts_with: string, True if object's text starts with passed string
|
||||
:param ends_with: string, True if object's text starts with passed string
|
||||
:param ignore_case: bool (default False), case insensitive
|
||||
"""
|
||||
|
||||
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]):
|
||||
|
||||
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.
|
||||
key: text
|
||||
|
||||
Example:
|
||||
@bot.message_handler(text=['account'])
|
||||
"""
|
||||
|
||||
key = 'text'
|
||||
|
||||
def check(self, message, text):
|
||||
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
|
||||
|
||||
Example:
|
||||
# Will respond if any message.text contains word 'account'
|
||||
@bot.message_handler(text_contains=['account'])
|
||||
"""
|
||||
|
||||
key = 'text_contains'
|
||||
|
||||
def check(self, message, text):
|
||||
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.
|
||||
|
||||
Example:
|
||||
# Will work if message.text starts with 'Sir'.
|
||||
@bot.message_handler(text_startswith='Sir')
|
||||
"""
|
||||
|
||||
key = 'text_startswith'
|
||||
|
||||
def check(self, message, text):
|
||||
return message.text.startswith(text)
|
||||
|
||||
|
||||
class ChatFilter(AdvancedCustomFilter):
|
||||
"""
|
||||
Check whether chat_id corresponds to given chat_id.
|
||||
|
||||
Example:
|
||||
@bot.message_handler(chat_id=[99999])
|
||||
"""
|
||||
|
||||
key = 'chat_id'
|
||||
|
||||
def check(self, message, text):
|
||||
return message.chat.id in text
|
||||
|
||||
|
||||
class ForwardFilter(SimpleCustomFilter):
|
||||
"""
|
||||
Check whether message was forwarded from channel or group.
|
||||
|
||||
Example:
|
||||
|
||||
@bot.message_handler(is_forwarded=True)
|
||||
"""
|
||||
|
||||
key = 'is_forwarded'
|
||||
|
||||
def check(self, message):
|
||||
return message.forward_from_chat is not None
|
||||
|
||||
|
||||
class IsReplyFilter(SimpleCustomFilter):
|
||||
"""
|
||||
Check whether message is a reply.
|
||||
|
||||
Example:
|
||||
|
||||
@bot.message_handler(is_reply=True)
|
||||
"""
|
||||
|
||||
key = 'is_reply'
|
||||
|
||||
def check(self, message):
|
||||
return message.reply_to_message is not None
|
||||
|
||||
|
||||
class LanguageFilter(AdvancedCustomFilter):
|
||||
"""
|
||||
Check users language_code.
|
||||
|
||||
Example:
|
||||
|
||||
@bot.message_handler(language_code=['ru'])
|
||||
"""
|
||||
|
||||
key = 'language_code'
|
||||
|
||||
def check(self, message, text):
|
||||
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.
|
||||
|
||||
Example:
|
||||
@bot.message_handler(chat_types=['supergroup'], is_chat_admin=True)
|
||||
"""
|
||||
|
||||
key = 'is_chat_admin'
|
||||
|
||||
def __init__(self, bot):
|
||||
self._bot = bot
|
||||
|
||||
def check(self, message):
|
||||
return self._bot.get_chat_member(message.chat.id, message.from_user.id).status in ['creator', 'administrator']
|
||||
|
||||
|
||||
class StateFilter(AdvancedCustomFilter):
|
||||
"""
|
||||
Filter to check state.
|
||||
|
||||
Example:
|
||||
@bot.message_handler(state=1)
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
key = 'state'
|
||||
|
||||
def check(self, message, text):
|
||||
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 == 'group':
|
||||
group_state = self.bot.current_states.get_state(user_id, chat_id)
|
||||
if group_state == text:
|
||||
return True
|
||||
elif group_state in text and type(text) is list:
|
||||
return True
|
||||
|
||||
|
||||
else:
|
||||
user_state = self.bot.current_states.get_state(user_id, chat_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.
|
||||
|
||||
Example:
|
||||
@bot.message_handler(is_digit=True)
|
||||
"""
|
||||
key = 'is_digit'
|
||||
|
||||
def check(self, message):
|
||||
return message.text.isdigit()
|
206
telebot/handler_backends.py
Normal file
206
telebot/handler_backends.py
Normal file
@ -0,0 +1,206 @@
|
||||
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
|
||||
"""
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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:
|
||||
def __init__(self) -> None:
|
||||
self.name = None
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
|
||||
class StatesGroup:
|
||||
def __init_subclass__(cls) -> None:
|
||||
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))
|
||||
|
||||
|
||||
class BaseMiddleware:
|
||||
"""
|
||||
Base class for middleware.
|
||||
Your middlewares should be inherited from this class.
|
||||
"""
|
||||
|
||||
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
|
13
telebot/storage/__init__.py
Normal file
13
telebot/storage/__init__.py
Normal file
@ -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'
|
||||
]
|
BIN
telebot/storage/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
telebot/storage/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/storage/__pycache__/base_storage.cpython-39.pyc
Normal file
BIN
telebot/storage/__pycache__/base_storage.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/storage/__pycache__/memory_storage.cpython-39.pyc
Normal file
BIN
telebot/storage/__pycache__/memory_storage.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/storage/__pycache__/pickle_storage.cpython-39.pyc
Normal file
BIN
telebot/storage/__pycache__/pickle_storage.cpython-39.pyc
Normal file
Binary file not shown.
BIN
telebot/storage/__pycache__/redis_storage.cpython-39.pyc
Normal file
BIN
telebot/storage/__pycache__/redis_storage.cpython-39.pyc
Normal file
Binary file not shown.
65
telebot/storage/base_storage.py
Normal file
65
telebot/storage/base_storage.py
Normal file
@ -0,0 +1,65 @@
|
||||
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 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)
|
67
telebot/storage/memory_storage.py
Normal file
67
telebot/storage/memory_storage.py
Normal file
@ -0,0 +1,67 @@
|
||||
from telebot.storage.base_storage import StateStorageBase, StateContext
|
||||
|
||||
class StateMemoryStorage(StateStorageBase):
|
||||
def __init__(self) -> None:
|
||||
self.data = {}
|
||||
#
|
||||
# {chat_id: {user_id: {'state': None, 'data': {}}, ...}, ...}
|
||||
|
||||
|
||||
def set_state(self, chat_id, user_id, state):
|
||||
if isinstance(state, object):
|
||||
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
|
115
telebot/storage/pickle_storage.py
Normal file
115
telebot/storage/pickle_storage.py
Normal file
@ -0,0 +1,115 @@
|
||||
from telebot.storage.base_storage import StateStorageBase, StateContext
|
||||
import os
|
||||
|
||||
|
||||
import pickle
|
||||
|
||||
|
||||
class StatePickleStorage(StateStorageBase):
|
||||
# noinspection PyMissingConstructor
|
||||
def __init__(self, file_path="./.state-save/states.pkl") -> None:
|
||||
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 = self.file_path.rsplit('/', maxsplit=1)[0]
|
||||
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 isinstance(state, object):
|
||||
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': {}}}
|
||||
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()
|
180
telebot/storage/redis_storage.py
Normal file
180
telebot/storage/redis_storage.py
Normal file
@ -0,0 +1,180 @@
|
||||
from pyclbr import Class
|
||||
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_'):
|
||||
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 backend 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 backend 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 backend 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 isinstance(state, object):
|
||||
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'] = dict(data, **response[user_id]['data'])
|
||||
self.set_record(chat_id, response)
|
||||
return True
|
||||
|
2888
telebot/types.py
Normal file
2888
telebot/types.py
Normal file
File diff suppressed because it is too large
Load Diff
512
telebot/util.py
Normal file
512
telebot/util.py
Normal file
@ -0,0 +1,512 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import threading
|
||||
import traceback
|
||||
from typing import Any, Callable, List, Dict, Optional, Union
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
import queue as Queue
|
||||
import logging
|
||||
|
||||
from telebot import types
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
try:
|
||||
# noinspection PyPackageRequirements
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
pil_imported = True
|
||||
except:
|
||||
pil_imported = False
|
||||
|
||||
MAX_MESSAGE_LENGTH = 4096
|
||||
|
||||
logger = logging.getLogger('TeleBot')
|
||||
|
||||
thread_local = threading.local()
|
||||
|
||||
content_type_media = [
|
||||
'text', 'audio', 'animation', 'document', 'photo', 'sticker', 'video', 'video_note', 'voice', 'contact', 'dice', 'poll',
|
||||
'venue', 'location'
|
||||
]
|
||||
|
||||
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', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message',
|
||||
'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended',
|
||||
'voice_chat_participants_invited', 'message_auto_delete_timer_changed'
|
||||
]
|
||||
|
||||
update_types = [
|
||||
"update_id", "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"
|
||||
]
|
||||
|
||||
|
||||
class WorkerThread(threading.Thread):
|
||||
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:
|
||||
|
||||
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:
|
||||
worker.join()
|
||||
|
||||
|
||||
class AsyncTask:
|
||||
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():
|
||||
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():
|
||||
def decorator(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
return AsyncTask(fn, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def is_string(var):
|
||||
return isinstance(var, str)
|
||||
|
||||
|
||||
def is_dict(var):
|
||||
return isinstance(var, dict)
|
||||
|
||||
|
||||
def is_bytes(var):
|
||||
return isinstance(var, bytes)
|
||||
|
||||
|
||||
def is_pil_image(var):
|
||||
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 is_command(text: str) -> bool:
|
||||
"""
|
||||
Checks if `text` is a command. Telegram chat commands start with the '/' character.
|
||||
:param text: Text to check.
|
||||
:return: True if `text` is a command, else False.
|
||||
"""
|
||||
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.
|
||||
|
||||
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
|
||||
:return: the command if `text` is a command (according to is_command), else 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:
|
||||
"""
|
||||
Returns the argument after the command.
|
||||
|
||||
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
|
||||
:return: the arguments if `text` is a command (according to is_command), else 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 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
|
||||
:param chars_per_string: The number of characters per line the text is split into.
|
||||
:return: The splitted text as a list of strings.
|
||||
"""
|
||||
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
|
||||
:param chars_per_string: The number of maximum characters per part the text is split to.
|
||||
:return: The splitted text as a list of strings.
|
||||
"""
|
||||
|
||||
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) -> str:
|
||||
"""
|
||||
Replaces the following chars in `text` ('&' with '&', '<' with '<' and '>' with '>').
|
||||
|
||||
:param text: the text to escape
|
||||
:return: the escaped text
|
||||
"""
|
||||
chars = {"&": "&", "<": "<", ">": ">"}
|
||||
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'!
|
||||
|
||||
Example:
|
||||
bot.send_message(your_user_id, user_link(message.from_user) + ' started the bot!', parse_mode='HTML')
|
||||
|
||||
:param user: the user (not the user_id)
|
||||
:param include_id: include the user_id
|
||||
:return: HTML user link
|
||||
"""
|
||||
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:: python
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
:param values: a dict containing all buttons to create in this format: {text: kwargs} {str:}
|
||||
:param row_width: int row width
|
||||
:return: 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):
|
||||
self._set()
|
||||
self.changed()
|
||||
|
||||
|
||||
def or_clear(self):
|
||||
self._clear()
|
||||
self.changed()
|
||||
|
||||
|
||||
def orify(e, changed_callback):
|
||||
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):
|
||||
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):
|
||||
if reset or not hasattr(thread_local, key):
|
||||
value = construct_value()
|
||||
setattr(thread_local, key, value)
|
||||
|
||||
return getattr(thread_local, key)
|
||||
|
||||
|
||||
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():
|
||||
return ''.join(random.sample(string.ascii_letters, 16))
|
||||
|
||||
|
||||
def deprecated(warn: bool=True, alternative: Optional[Callable]=None):
|
||||
"""
|
||||
Use this decorator to mark functions as deprecated.
|
||||
When the function is used, an info (or warning if `warn` is True) is logged.
|
||||
:param warn: If True a warning is logged else an info
|
||||
:param alternative: The new function to use instead
|
||||
"""
|
||||
def decorator(function):
|
||||
def wrapper(*args, **kwargs):
|
||||
info = f"`{function.__name__}` is deprecated." + (f" Use `{alternative.__name__}` instead" if alternative else "")
|
||||
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."""
|
||||
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, *args, **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:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return: None
|
||||
"""
|
||||
from telebot.apihelper import ApiTelegramException
|
||||
from time import sleep
|
||||
msg = None
|
||||
try:
|
||||
msg = function(*args, **kwargs)
|
||||
except ApiTelegramException as ex:
|
||||
if ex.error_code == 429:
|
||||
sleep(ex.result_json['parameters']['retry_after'])
|
||||
msg = function(*args, **kwargs)
|
||||
finally:
|
||||
return msg
|
3
telebot/version.py
Normal file
3
telebot/version.py
Normal file
@ -0,0 +1,3 @@
|
||||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '4.4.0'
|
Loading…
Reference in New Issue
Block a user