1
0
mirror of https://github.com/eternnoir/pyTelegramBotAPI.git synced 2023-08-10 21:12:57 +03:00

Compare commits

...

54 Commits

Author SHA1 Message Date
Badiboy
01a6827542
Merge pull request #1462 from coder2020official/master
Allow using non-class states
2022-02-23 11:32:01 +03:00
_run
b960a9e574
Update custom_filters.py 2022-02-23 13:08:02 +05:00
_run
102fe3a8fb
Update asyncio_filters.py 2022-02-23 13:07:25 +05:00
Badiboy
292df419ba
Merge pull request #1456 from abdullaev388/master
I18N class for sync telebot and middleware for async
2022-02-22 22:37:13 +03:00
abdullaev388
7993e1d1c9 corrected setup middleware in async i18n middleware example 2022-02-21 20:08:03 +05:00
Badiboy
7309f92c36
Merge pull request #1459 from coder2020official/master
Fixed documentation and added link
2022-02-19 22:48:37 +03:00
_run
7875ff293d
Update README.md 2022-02-20 00:44:08 +05:00
_run
4adac4d852
Update quick_start.rst 2022-02-20 00:40:25 +05:00
abdullaev388
38bff65caf removed unused imports from util.py 2022-02-20 00:28:27 +05:00
Badiboy
9ecadf1bc1
Merge pull request #1458 from coder2020official/master
Bump documentation
2022-02-19 22:19:00 +03:00
abdullaev388
5d7ae385ec token removed. 2022-02-20 00:12:14 +05:00
abdullaev388
74e9780b30 BaseMiddleware returned to it's original place && I18N middleware is now only in examples 2022-02-20 00:08:14 +05:00
abdullaev388
9b20f41ece I18N class removed from telebot.util.py 2022-02-19 23:57:21 +05:00
Badiboy
967309120e
Merge pull request #1457 from Badiboy/master
Fix check of the regexp and commands types
2022-02-19 21:41:55 +03:00
Badiboy
94be2abdbd Typo 2022-02-19 21:39:52 +03:00
Badiboy
6c31b53cd9 Fix check of the regexp and commands types 2022-02-19 21:39:02 +03:00
abdullaev388
9bfc0b2c6f preventet breaking change 2022-02-19 23:37:03 +05:00
Badiboy
fc374ec57a
Merge pull request #1454 from Troshchk/message_handler_checking
Additional check of the regexp and commands types
2022-02-19 21:33:30 +03:00
_run
7a8e60ddc2
Update index.rst 2022-02-19 22:41:09 +05:00
coder2020official
7f43f26886 Add telebot 2022-02-19 22:21:15 +05:00
coder2020official
4521982837 Create .readthedocs.yml 2022-02-19 22:17:29 +05:00
coder2020official
30c43b557c Documentation Bump 2022-02-19 21:56:51 +05:00
abdullaev388
10b5886dcc Completed I18N examples descriptions 2022-02-19 18:56:27 +05:00
abdullaev388
93b97fc3fe I18N middleware example was added 2022-02-19 18:47:36 +05:00
abdullaev388
1f6e60fd74 I18N middleware implementation was added 2022-02-19 16:25:46 +05:00
abdullaev388
5337d4838d asyncio_middlewares.py was created && BaseMiddleware class was replaced to asyncio_middlewares.py 2022-02-19 16:02:14 +05:00
abdullaev388
ae5d183db0 slight TextFilter class improvement 2022-02-19 15:53:58 +05:00
abdullaev388
0d85a34551 an example for i18n class was added 2022-02-19 15:07:46 +05:00
abdullaev388
002c608d45 i18n class was added 2022-02-19 15:04:31 +05:00
Troshchk
ec766a3e43 Wrapping checking in private methods; warnings changed to errors 2022-02-16 14:05:54 +01:00
Badiboy
0ef8d04ed2
Merge pull request #1449 from abdullaev388/master
new advanced TextFilter was added && An example demostrating TextFilt…
2022-02-16 12:48:29 +03:00
abdullaev388
3a86916e72 example of TextFilter starts_with and ends_with usage simultaneously 2022-02-16 12:43:23 +05:00
abdullaev388
b41435f407 more descriptive exceptions 2022-02-16 12:29:27 +05:00
Badiboy
f689d90815
Merge pull request #1455 from Badiboy/master
Fix timer_bot.py
2022-02-15 19:55:32 +03:00
Badiboy
966f2e7ef7
Fix timer_bot.py 2022-02-15 19:55:12 +03:00
Troshchk
9075430210 Making first condition shorter, no change in functionality 2022-02-15 15:46:02 +01:00
Troshchk
68095ad69a Adding checks for the commands and regexp input types 2022-02-15 15:24:55 +01:00
abdullaev388
8c3d1e608c new TextFilter examples were added 2022-02-12 21:53:40 +05:00
abdullaev388
6822f18cbb multiple check patterns && multiple startwith, endswith fields 2022-02-12 21:41:10 +05:00
abdullaev388
6e4f2e19d6 async text contains filter was fixed 2022-02-12 20:36:10 +05:00
abdullaev388
8bbd062d13 text contains filter was fixed 2022-02-12 20:31:02 +05:00
abdullaev388
5f7ccc8c9b created async TextFilter 2022-02-12 17:33:29 +05:00
abdullaev388
5b1483f646 removed TextFilterKey in example, instead TextMatchFilter was modified 2022-02-12 17:07:59 +05:00
abdullaev388
3cd86d0e93 token. again. 2022-02-12 15:31:54 +05:00
abdullaev388
a893fbc358 async advanced callback_data example was added 2022-02-12 15:30:04 +05:00
abdullaev388
6fd2a38fe9 An asyncio example demostrating TextFilter usage 2022-02-12 15:12:30 +05:00
abdullaev388
b89ecb3e5a modified code 2022-02-12 14:32:59 +05:00
abdullaev388
2e5590b566
token removed :) 2022-02-12 14:11:16 +05:00
abdullaev388
733bb2ebbb new advanced TextFilter was added && An example demostrating TextFilter usage 2022-02-12 13:35:52 +05:00
Badiboy
64a22457e2
Merge pull request #1446 from abdullaev388/master
New CallbackData example
2022-02-10 12:26:12 +03:00
abdullaev388
0c8e94d2c6 advanced usage of callbackdata was added 2022-02-10 13:43:19 +05:00
abdullaev388
b9436821e0 callbackdata examples were separated into a directory 2022-02-10 13:33:44 +05:00
Badiboy
a8af9120de
Merge pull request #1444 from skelly37/patch-1
Update README.md
2022-02-09 09:51:24 +03:00
Kamil
0655a1f6b6
Update README.md
added my pyfram-telegram-bot
2022-02-09 00:27:18 +00:00
43 changed files with 2352 additions and 57 deletions

5
.gitignore vendored
View File

@ -64,3 +64,8 @@ testMain.py
#VS Code
.vscode/
.DS_Store
# documentation
_build/
_static/
_templates/

19
.readthedocs.yml Normal file
View File

@ -0,0 +1,19 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: doc_req.txt

View File

@ -11,6 +11,8 @@
## <p align="center">Supporting Bot API version: <a href="https://core.telegram.org/bots/api#january-31-2022">5.7</a>!
<h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2>
## Contents
* [Getting started](#getting-started)
@ -860,5 +862,6 @@ Here are some examples of template:
* [GrandQuiz Bot](https://github.com/Carlosma7/TFM-GrandQuiz) by [Carlosma7](https://github.com/Carlosma7). This bot is a trivia game that allows you to play with people from different ages. This project addresses the use of a system through chatbots to carry out a social and intergenerational game as an alternative to traditional game development.
* [Diccionario de la RAE](https://t.me/dleraebot) ([source](https://github.com/studentenherz/dleraebot)) This bot lets you find difinitions of words in Spanish using [RAE's dictionary](https://dle.rae.es/). It features direct message and inline search.
* [remoteTelegramShell](https://github.com/EnriqueMoran/remoteTelegramShell) by [EnriqueMoran](https://github.com/EnriqueMoran). Control your LinuxOS computer through Telegram.
* [Pyfram-telegram-bot](https://github.com/skelly37/pyfram-telegram-bot) Query wolframalpha.com and make use of its API through Telegram.
**Want to have your bot listed here? Just make a pull request. Only bots with public source code are accepted.**

1
doc_req.txt Normal file
View File

@ -0,0 +1 @@
pytelegrambotapi

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -0,0 +1,51 @@
====================
AsyncTeleBot
====================
AsyncTeleBot methods
--------------------
.. automodule:: telebot.async_telebot
:members:
:undoc-members:
:show-inheritance:
Asyncio filters
---------------
.. automodule:: telebot.asyncio_filters
:members:
:undoc-members:
:show-inheritance:
Asynchronous storage for states
-------------------------------
.. automodule:: telebot.asyncio_storage
:members:
:undoc-members:
:show-inheritance:
asyncio_helper file
-------------------
.. automodule:: telebot.asyncio_helper
:members:
:undoc-members:
:show-inheritance:
Asyncio handler backends
------------------------
.. automodule:: telebot.asyncio_handler_backends
:members:
:undoc-members:
:show-inheritance:

13
docs/source/calldata.rst Normal file
View File

@ -0,0 +1,13 @@
=====================
Callback data factory
=====================
callback\_data file
-----------------------------
.. automodule:: telebot.callback_data
:members:
:undoc-members:
:show-inheritance:

66
docs/source/conf.py Normal file
View File

@ -0,0 +1,66 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'pyTelegramBotAPI'
copyright = '2022, coder2020official'
author = 'coder2020official'
# The full version, including alpha/beta/rc tags
release = '1.0'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx_rtd_theme',
'sphinx.ext.autosectionlabel',
'sphinx.ext.autodoc',
"sphinx.ext.autosummary",
"sphinx.ext.napoleon",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_logo = 'logo.png'
html_theme_options = {
'logo_only': True,
'display_version': False,
}

63
docs/source/index.rst Normal file
View File

@ -0,0 +1,63 @@
.. pyTelegramBotAPI documentation master file, created by
sphinx-quickstart on Fri Feb 18 20:58:37 2022.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pyTelegramBotAPI's documentation!
============================================
=======
TeleBot
=======
TeleBot is synchronous and asynchronous implementation of `Telegram Bot API <https://core.telegram.org/bots/api>`_.
Chats
-----
English chat: `Private chat <https://telegram.me/joinchat/Bn4ixj84FIZVkwhk2jag6A>`__
Russian chat: `@pytelegrambotapi_talks_ru <https://t.me/pytelegrambotapi_talks_ru>`__
News: `@pyTelegramBotAPI <https://t.me/pytelegrambotapi>`__
Pypi: `Pypi <https://pypi.org/project/pyTelegramBotAPI/>`__
Source: `Github repository <https://github.com/eternnoir/pyTelegramBotAPI>`__
Some features:
-------------
Easy to learn and use.
Easy to understand.
Both sync and async.
Examples on features.
States
And more...
Content
--------
.. toctree::
install
quick_start
types
sync_version/index
async_version/index
calldata
util
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

41
docs/source/install.rst Normal file
View File

@ -0,0 +1,41 @@
==================
Installation Guide
==================
:toctree
Using PIP
----------
.. code-block:: bash
$ pip install pyTelegramBotAPI
Using pipenv
------------
.. code-block:: bash
$ pipenv install pyTelegramBotAPI
By cloning repository
---------------------
.. code-block:: bash
$ git clone https://github.com/eternnoir/pyTelegramBotAPI.git
$ cd pyTelegramBotAPI
$ python setup.py install
Directly using pip
------------------
.. code-block:: bash
$ pip install git+https://github.com/eternnoir/pyTelegramBotAPI.git
It is generally recommended to use the first option.
While the API is production-ready, it is still under development and it has regular updates, do not forget to update it regularly by calling:
.. code-block:: bash
$ pip install pytelegrambotapi --upgrade

BIN
docs/source/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -0,0 +1,16 @@
===========
Quick start
===========
Synchronous TeleBot
-------------------
.. literalinclude:: ../../examples/echo_bot.py
:language: python
Asynchronous TeleBot
--------------------
.. literalinclude:: ../../examples/asynchronous_telebot/echo_bot.py
:language: python

View File

@ -0,0 +1,43 @@
===============
TeleBot version
===============
TeleBot methods
---------------
.. automodule:: telebot
:members:
:undoc-members:
:show-inheritance:
custom_filters file
------------------------------
.. automodule:: telebot.custom_filters
:members:
:undoc-members:
:show-inheritance:
Synchronous storage for states
-------------------------------
.. automodule:: telebot.storage
:members:
:undoc-members:
:show-inheritance:
handler_backends file
--------------------------------
.. automodule:: telebot.handler_backends
:members:
:undoc-members:
:show-inheritance:
apihelper
------------------------
.. automodule:: telebot.apihelper
:members:
:undoc-members:
:show-inheritance:

10
docs/source/types.rst Normal file
View File

@ -0,0 +1,10 @@
============
Types of API
============
.. automodule:: telebot.types
:members:
:undoc-members:
:show-inheritance:

12
docs/source/util.rst Normal file
View File

@ -0,0 +1,12 @@
============
Utils
============
util file
-------------------
.. automodule:: telebot.util
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,26 @@
from telebot import types
from telebot.async_telebot import AsyncTeleBot
from telebot.asyncio_filters import AdvancedCustomFilter
from telebot.callback_data import CallbackData, CallbackDataFilter
calendar_factory = CallbackData("year", "month", prefix="calendar")
calendar_zoom = CallbackData("year", prefix="calendar_zoom")
class CalendarCallbackFilter(AdvancedCustomFilter):
key = 'calendar_config'
async def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
return config.check(query=call)
class CalendarZoomCallbackFilter(AdvancedCustomFilter):
key = 'calendar_zoom_config'
async def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
return config.check(query=call)
def bind_filters(bot: AsyncTeleBot):
bot.add_custom_filter(CalendarCallbackFilter())
bot.add_custom_filter(CalendarZoomCallbackFilter())

View File

@ -0,0 +1,92 @@
import calendar
from datetime import date, timedelta
from filters import calendar_factory, calendar_zoom
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
EMTPY_FIELD = '1'
WEEK_DAYS = [calendar.day_abbr[i] for i in range(7)]
MONTHS = [(i, calendar.month_name[i]) for i in range(1, 13)]
def generate_calendar_days(year: int, month: int):
keyboard = InlineKeyboardMarkup(row_width=7)
today = date.today()
keyboard.add(
InlineKeyboardButton(
text=date(year=year, month=month, day=1).strftime('%b %Y'),
callback_data=EMTPY_FIELD
)
)
keyboard.add(*[
InlineKeyboardButton(
text=day,
callback_data=EMTPY_FIELD
)
for day in WEEK_DAYS
])
for week in calendar.Calendar().monthdayscalendar(year=year, month=month):
week_buttons = []
for day in week:
day_name = ' '
if day == today.day and today.year == year and today.month == month:
day_name = '🔘'
elif day != 0:
day_name = str(day)
week_buttons.append(
InlineKeyboardButton(
text=day_name,
callback_data=EMTPY_FIELD
)
)
keyboard.add(*week_buttons)
previous_date = date(year=year, month=month, day=1) - timedelta(days=1)
next_date = date(year=year, month=month, day=1) + timedelta(days=31)
keyboard.add(
InlineKeyboardButton(
text='Previous month',
callback_data=calendar_factory.new(year=previous_date.year, month=previous_date.month)
),
InlineKeyboardButton(
text='Zoom out',
callback_data=calendar_zoom.new(year=year)
),
InlineKeyboardButton(
text='Next month',
callback_data=calendar_factory.new(year=next_date.year, month=next_date.month)
),
)
return keyboard
def generate_calendar_months(year: int):
keyboard = InlineKeyboardMarkup(row_width=3)
keyboard.add(
InlineKeyboardButton(
text=date(year=year, month=1, day=1).strftime('Year %Y'),
callback_data=EMTPY_FIELD
)
)
keyboard.add(*[
InlineKeyboardButton(
text=month,
callback_data=calendar_factory.new(year=year, month=month_number)
)
for month_number, month in MONTHS
])
keyboard.add(
InlineKeyboardButton(
text='Previous year',
callback_data=calendar_zoom.new(year=year - 1)
),
InlineKeyboardButton(
text='Next year',
callback_data=calendar_zoom.new(year=year + 1)
)
)
return keyboard

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
"""
This Example will show you an advanced usage of CallbackData.
In this example calendar was implemented
"""
import asyncio
from datetime import date
from filters import calendar_factory, calendar_zoom, bind_filters
from keyboards import generate_calendar_days, generate_calendar_months, EMTPY_FIELD
from telebot import types
from telebot.async_telebot import AsyncTeleBot
API_TOKEN = ''
bot = AsyncTeleBot(API_TOKEN)
@bot.message_handler(commands='start')
async def start_command_handler(message: types.Message):
await bot.send_message(message.chat.id,
f"Hello {message.from_user.first_name}. This bot is an example of calendar keyboard."
"\nPress /calendar to see it.")
@bot.message_handler(commands='calendar')
async def calendar_command_handler(message: types.Message):
now = date.today()
await bot.send_message(message.chat.id, 'Calendar',
reply_markup=generate_calendar_days(year=now.year, month=now.month))
@bot.callback_query_handler(func=None, calendar_config=calendar_factory.filter())
async def calendar_action_handler(call: types.CallbackQuery):
callback_data: dict = calendar_factory.parse(callback_data=call.data)
year, month = int(callback_data['year']), int(callback_data['month'])
await bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
reply_markup=generate_calendar_days(year=year, month=month))
@bot.callback_query_handler(func=None, calendar_zoom_config=calendar_zoom.filter())
async def calendar_zoom_out_handler(call: types.CallbackQuery):
callback_data: dict = calendar_zoom.parse(callback_data=call.data)
year = int(callback_data.get('year'))
await bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
reply_markup=generate_calendar_months(year=year))
@bot.callback_query_handler(func=lambda call: call.data == EMTPY_FIELD)
async def callback_empty_field_handler(call: types.CallbackQuery):
await bot.answer_callback_query(call.id)
if __name__ == '__main__':
bind_filters(bot)
asyncio.run(bot.infinity_polling())

View File

@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
"""
This Example will show you usage of TextFilter
In this example you will see how to use TextFilter
with (message_handler, callback_query_handler, poll_handler)
"""
import asyncio
from telebot import types
from telebot.async_telebot import AsyncTeleBot
from telebot.asyncio_filters import TextMatchFilter, TextFilter, IsReplyFilter
bot = AsyncTeleBot("")
@bot.message_handler(text=TextFilter(equals='hello'))
async def hello_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(equals='hello', ignore_case=True))
async def hello_handler_ignore_case(message: types.Message):
await bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(contains=['good', 'bad']))
async def contains_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(contains=['good', 'bad'], ignore_case=True))
async def contains_handler_ignore_case(message: types.Message):
await bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(starts_with='st')) # stArk, steve, stONE
async def starts_with_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(starts_with='st', ignore_case=True)) # STark, sTeve, stONE
async def starts_with_handler_ignore_case(message: types.Message):
await bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(ends_with='ay')) # wednesday, SUNday, WeekDay
async def ends_with_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(ends_with='ay', ignore_case=True)) # wednesdAY, sundAy, WeekdaY
async def ends_with_handler_ignore_case(message: types.Message):
await bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(equals='/callback'))
async def send_callback(message: types.Message):
keyboard = types.InlineKeyboardMarkup(
keyboard=[
[types.InlineKeyboardButton(text='callback data', callback_data='example')],
[types.InlineKeyboardButton(text='ignore case callback data', callback_data='ExAmPLe')]
]
)
await bot.send_message(message.chat.id, message.text, reply_markup=keyboard)
@bot.callback_query_handler(func=None, text=TextFilter(equals='example'))
async def callback_query_handler(call: types.CallbackQuery):
await bot.answer_callback_query(call.id, call.data, show_alert=True)
@bot.callback_query_handler(func=None, text=TextFilter(equals='example', ignore_case=True))
async def callback_query_handler_ignore_case(call: types.CallbackQuery):
await bot.answer_callback_query(call.id, call.data + " ignore case", show_alert=True)
@bot.message_handler(text=TextFilter(equals='/poll'))
async def send_poll(message: types.Message):
await bot.send_poll(message.chat.id, question='When do you prefer to work?', options=['Morning', 'Night'])
await bot.send_poll(message.chat.id, question='WHEN DO you pRefeR to worK?', options=['Morning', 'Night'])
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?'))
async def poll_question_handler(poll: types.Poll):
print(poll.question)
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?', ignore_case=True))
async def poll_question_handler_ignore_case(poll: types.Poll):
print(poll.question + ' ignore case')
# either hi or contains one of (привет, salom)
@bot.message_handler(text=TextFilter(equals="hi", contains=('привет', 'salom'), ignore_case=True))
async def multiple_patterns_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
# starts with one of (mi, mea) for ex. minor, milk, meal, meat
@bot.message_handler(text=TextFilter(starts_with=['mi', 'mea'], ignore_case=True))
async def multiple_starts_with_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
# ends with one of (es, on) for ex. Jones, Davies, Johnson, Wilson
@bot.message_handler(text=TextFilter(ends_with=['es', 'on'], ignore_case=True))
async def multiple_ends_with_handler(message: types.Message):
await bot.send_message(message.chat.id, message.text)
# !ban /ban .ban !бан /бан .бан
@bot.message_handler(is_reply=True,
text=TextFilter(starts_with=('!', '/', '.'), ends_with=['ban', 'бан'], ignore_case=True))
async def ban_command_handler(message: types.Message):
if len(message.text) == 4 and message.chat.type != 'private':
try:
await bot.ban_chat_member(message.chat.id, message.reply_to_message.from_user.id)
await bot.reply_to(message.reply_to_message, 'Banned.')
except Exception as err:
print(err.args)
return
if __name__ == '__main__':
bot.add_custom_filter(TextMatchFilter())
bot.add_custom_filter(IsReplyFilter())
asyncio.run(bot.polling())

View File

@ -0,0 +1,120 @@
import contextvars
import gettext
import os
from telebot.asyncio_handler_backends import BaseMiddleware
try:
from babel.support import LazyProxy
babel_imported = True
except ImportError:
babel_imported = False
class I18N(BaseMiddleware):
"""
This middleware provides high-level tool for internationalization
It is based on gettext util.
"""
context_lang = contextvars.ContextVar('language', default=None)
def __init__(self, translations_path, domain_name: str):
super().__init__()
self.update_types = self.process_update_types()
self.path = translations_path
self.domain = domain_name
self.translations = self.find_translations()
@property
def available_translations(self):
return list(self.translations)
def gettext(self, text: str, lang: str = None):
"""
Singular translations
"""
if lang is None:
lang = self.context_lang.get()
if lang not in self.translations:
return text
translator = self.translations[lang]
return translator.gettext(text)
def ngettext(self, singular: str, plural: str, lang: str = None, n=1):
"""
Plural translations
"""
if lang is None:
lang = self.context_lang.get()
if lang not in self.translations:
if n == 1:
return singular
return plural
translator = self.translations[lang]
return translator.ngettext(singular, plural, n)
def lazy_gettext(self, text: str, lang: str = None):
if not babel_imported:
raise RuntimeError('babel module is not imported. Check that you installed it.')
return LazyProxy(self.gettext, text, lang, enable_cache=False)
def lazy_ngettext(self, singular: str, plural: str, lang: str = None, n=1):
if not babel_imported:
raise RuntimeError('babel module is not imported. Check that you installed it.')
return LazyProxy(self.ngettext, singular, plural, lang, n, enable_cache=False)
async def get_user_language(self, obj):
"""
You need to override this method and return user language
"""
raise NotImplementedError
def process_update_types(self) -> list:
"""
You need to override this method and return any update types which you want to be processed
"""
raise NotImplementedError
async def pre_process(self, message, data):
"""
context language variable will be set each time when update from 'process_update_types' comes
value is the result of 'get_user_language' method
"""
self.context_lang.set(await self.get_user_language(obj=message))
async def post_process(self, message, data, exception):
pass
def find_translations(self):
"""
Looks for translations with passed 'domain' in passed 'path'
"""
if not os.path.exists(self.path):
raise RuntimeError(f"Translations directory by path: {self.path!r} was not found")
result = {}
for name in os.listdir(self.path):
translations_path = os.path.join(self.path, name, 'LC_MESSAGES')
if not os.path.isdir(translations_path):
continue
po_file = os.path.join(translations_path, self.domain + '.po')
mo_file = po_file[:-2] + 'mo'
if os.path.isfile(po_file) and not os.path.isfile(mo_file):
raise FileNotFoundError(f"Translations for: {name!r} were not compiled!")
with open(mo_file, 'rb') as file:
result[name] = gettext.GNUTranslations(file)
return result

View File

@ -0,0 +1,34 @@
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup, KeyboardButton
def languages_keyboard():
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text="English", callback_data='en'),
InlineKeyboardButton(text="Русский", callback_data='ru'),
InlineKeyboardButton(text="O'zbekcha", callback_data='uz_Latn')
]
]
)
def clicker_keyboard(_):
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text=_("click"), callback_data='click'),
]
]
)
def menu_keyboard(_):
keyboard = ReplyKeyboardMarkup(resize_keyboard=True)
keyboard.add(
KeyboardButton(text=_("My user id")),
KeyboardButton(text=_("My user name")),
KeyboardButton(text=_("My first name"))
)
return keyboard

View File

@ -0,0 +1,81 @@
# English translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr ""
#: keyboards.py:29
msgid "My user id"
msgstr ""
#: keyboards.py:30
msgid "My user name"
msgstr ""
#: keyboards.py:31
msgid "My first name"
msgstr ""
#: main.py:97
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example\n"
"/menu - text menu example"
msgstr ""
#: main.py:121
msgid "Language has been changed"
msgstr ""
#: main.py:130 main.py:150
#, fuzzy
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] ""
msgstr[1] ""
#: main.py:135 main.py:155
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
#: main.py:163
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
msgstr ""
#: main.py:203
msgid "Seems you confused language"
msgstr ""
#~ msgid ""
#~ "Hello, {user_fist_name}!\n"
#~ "This is the example of multilanguage bot.\n"
#~ "Available commands:\n"
#~ "\n"
#~ "/lang - change your language\n"
#~ "/plural - pluralization example"
#~ msgstr ""

View File

@ -0,0 +1,82 @@
# Russian translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ru\n"
"Language-Team: ru <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr "Клик"
#: keyboards.py:29
msgid "My user id"
msgstr "Мой user id"
#: keyboards.py:30
msgid "My user name"
msgstr "Мой user name"
#: keyboards.py:31
msgid "My first name"
msgstr "Мой first name"
#: main.py:97
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example\n"
"/menu - text menu example"
msgstr ""
"Привет, {user_fist_name}!\n"
"Это пример мультиязычного бота.\n"
"Доступные команды:\n"
"\n"
"/lang - изменить язык\n"
"/plural - пример плюрализации\n"
"/menu - Пример текстового меню"
#: main.py:121
msgid "Language has been changed"
msgstr "Язык был сменён"
#: main.py:130 main.py:150
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] "У вас {number} клик"
msgstr[1] "У вас {number} клика"
msgstr[2] "У вас {number} кликов"
#: main.py:135 main.py:155
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
"Это кликер.\n"
"\n"
#: main.py:163
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
msgstr "Это пример ReplyKeyboardMarkup меню в мультиязычном боте."
#: main.py:203
msgid "Seems you confused language"
msgstr "Кажется, вы перепутали язык"

View File

@ -0,0 +1,80 @@
# Uzbek (Latin) translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-19 18:37+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: uz_Latn\n"
"Language-Team: uz_Latn <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr "clik"
#: keyboards.py:29
msgid "My user id"
msgstr "Mani user id"
#: keyboards.py:30
msgid "My user name"
msgstr "Mani user name"
#: keyboards.py:31
msgid "My first name"
msgstr "Mani first name"
#: main.py:97
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example\n"
"/menu - text menu example"
msgstr ""
"Salom, {user_fist_name}!\n"
"Bu multilanguage bot misoli.\n"
"Mavjud buyruqlar:\n"
"\n"
"/lang - tilni ozgartirish\n"
"/plural - pluralizatsiya misoli\n"
"/menu - text menu misoli"
#: main.py:121
msgid "Language has been changed"
msgstr "Til ozgartirildi"
#: main.py:130 main.py:150
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] "Sizda {number}ta clik"
msgstr[1] "Sizda {number}ta clik"
#: main.py:135 main.py:155
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
"Bu clicker.\n"
"\n"
#: main.py:163
msgid "This is ReplyKeyboardMarkup menu example in multilanguage bot."
msgstr "Bu multilanguage bot da replykeyboardmarkup menyu misoli."
#: main.py:203
msgid "Seems you confused language"
msgstr "Tilni adashtirdiz"

View File

@ -0,0 +1,214 @@
"""
In this example you will learn how to adapt your bot to different languages
Using built-in middleware I18N.
You need to install babel package 'https://pypi.org/project/Babel/'
Babel provides a command-line interface for working with message catalogs
After installing babel package you have a script called 'pybabel'
Too see all the commands open terminal and type 'pybabel --help'
Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html'
Create a directory 'locales' where our translations will be stored
First we need to extract texts:
pybabel extract -o locales/{domain_name}.pot --input-dirs .
{domain_name}.pot - is the file where all translations are saved
The name of this file should be the same as domain which you pass to I18N class
In this example domain_name will be 'messages'
For gettext (singular texts) we use '_' alias and it works perfect
You may also you some alias for ngettext (plural texts) but you can face with a problem that
your plural texts are not being extracted
That is because by default 'pybabel extract' recognizes the following keywords:
_, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_
To add your own keyword you can use '-k' flag
In this example for 'ngettext' i will assign double underscore alias '__'
Full command with pluralization support will look so:
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
Then create directories with translations (get list of all locales: 'pybabel --list-locales'):
pybabel init -i locales/{domain_name}.pot -d locales -l en
pybabel init -i locales/{domain_name}.pot -d locales -l ru
pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn
Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po
After you translated all the texts you need to compile .po files:
pybabel compile -d locales
When you delete/update your texts you also need to update them in .po files:
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
pybabel update -i locales/{domain_name}.pot -d locales
- translate
pybabel compile -d locales
If you have any exceptions check:
- you have installed babel
- translations are ready, so you just compiled it
- in the commands above you replaced {domain_name} to messages
- you are writing commands from correct path in terminal
"""
import asyncio
from typing import Union
import keyboards
from telebot import types
from telebot.async_telebot import AsyncTeleBot
from telebot.asyncio_filters import TextMatchFilter, TextFilter
from i18n_base_midddleware import I18N
from telebot.asyncio_storage.memory_storage import StateMemoryStorage
class I18NMiddleware(I18N):
def process_update_types(self) -> list:
"""
Here you need to return a list of update types which you want to be processed
"""
return ['message', 'callback_query']
async def get_user_language(self, obj: Union[types.Message, types.CallbackQuery]):
"""
This method is called when new update comes (only updates which you return in 'process_update_types' method)
Returned language will be used in 'pre_process' method of parent class
Returned language will be set to context language variable.
If you need to get translation with user's actual language you don't have to pass it manually
It will be automatically passed from context language value.
However if you need some other language you can always pass it.
"""
user_id = obj.from_user.id
if user_id not in users_lang:
users_lang[user_id] = 'en'
return users_lang[user_id]
storage = StateMemoryStorage()
bot = AsyncTeleBot("", state_storage=storage)
i18n = I18NMiddleware(translations_path='locales', domain_name='messages')
_ = i18n.gettext # for singular translations
__ = i18n.ngettext # for plural translations
# These are example storages, do not use it in a production development
users_lang = {}
users_clicks = {}
@bot.message_handler(commands='start')
async def start_handler(message: types.Message):
text = _("Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n\n"
"/lang - change your language\n"
"/plural - pluralization example\n"
"/menu - text menu example")
# remember don't use f string for interpolation, use .format method instead
text = text.format(user_fist_name=message.from_user.first_name)
await bot.send_message(message.from_user.id, text)
@bot.message_handler(commands='lang')
async def change_language_handler(message: types.Message):
await bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
reply_markup=keyboards.languages_keyboard())
@bot.callback_query_handler(func=None, text=TextFilter(contains=['en', 'ru', 'uz_Latn']))
async def language_handler(call: types.CallbackQuery):
lang = call.data
users_lang[call.from_user.id] = lang
# When you changed user language, you have to pass it manually beacause it is not changed in context
await bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
@bot.message_handler(commands='plural')
async def pluralization_handler(message: types.Message):
if not users_clicks.get(message.from_user.id):
users_clicks[message.from_user.id] = 0
clicks = users_clicks[message.from_user.id]
text = __(
singular="You have {number} click",
plural="You have {number} clicks",
n=clicks
)
text = _("This is clicker.\n\n") + text.format(number=clicks)
await bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_))
@bot.callback_query_handler(func=None, text=TextFilter(equals='click'))
async def click_handler(call: types.CallbackQuery):
if not users_clicks.get(call.from_user.id):
users_clicks[call.from_user.id] = 1
else:
users_clicks[call.from_user.id] += 1
clicks = users_clicks[call.from_user.id]
text = __(
singular="You have {number} click",
plural="You have {number} clicks",
n=clicks
)
text = _("This is clicker.\n\n") + text.format(number=clicks)
await bot.edit_message_text(text, call.from_user.id, call.message.message_id,
reply_markup=keyboards.clicker_keyboard(_))
@bot.message_handler(commands='menu')
async def menu_handler(message: types.Message):
text = _("This is ReplyKeyboardMarkup menu example in multilanguage bot.")
await bot.send_message(message.chat.id, text, reply_markup=keyboards.menu_keyboard(_))
# For lazy tranlations
# lazy gettext is used when you don't know user's locale
# It can be used for example to handle text buttons in multilanguage bot
# The actual translation will be delayed until update comes and context language is set
l_ = i18n.lazy_gettext
# Handlers below will handle text according to user's language
@bot.message_handler(text=l_("My user id"))
async def return_user_id(message: types.Message):
await bot.send_message(message.chat.id, str(message.from_user.id))
@bot.message_handler(text=l_("My user name"))
async def return_user_id(message: types.Message):
username = message.from_user.username
if not username:
username = '-'
await bot.send_message(message.chat.id, username)
# You can make it case insensitive
@bot.message_handler(text=TextFilter(equals=l_("My first name"), ignore_case=True))
async def return_user_id(message: types.Message):
await bot.send_message(message.chat.id, message.from_user.first_name)
all_menu_texts = []
for language in i18n.available_translations:
for menu_text in ("My user id", "My user name", "My first name"):
all_menu_texts.append(_(menu_text, language))
# When user confused language. (handles all menu buttons texts)
@bot.message_handler(text=TextFilter(contains=all_menu_texts, ignore_case=True))
async def missed_message(message: types.Message):
await bot.send_message(message.chat.id, _("Seems you confused language"), reply_markup=keyboards.menu_keyboard(_))
if __name__ == '__main__':
bot.setup_middleware(i18n)
bot.add_custom_filter(TextMatchFilter())
asyncio.run(bot.infinity_polling())

View File

@ -0,0 +1,25 @@
import telebot
from telebot import types, AdvancedCustomFilter
from telebot.callback_data import CallbackData, CallbackDataFilter
calendar_factory = CallbackData("year", "month", prefix="calendar")
calendar_zoom = CallbackData("year", prefix="calendar_zoom")
class CalendarCallbackFilter(AdvancedCustomFilter):
key = 'calendar_config'
def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
return config.check(query=call)
class CalendarZoomCallbackFilter(AdvancedCustomFilter):
key = 'calendar_zoom_config'
def check(self, call: types.CallbackQuery, config: CallbackDataFilter):
return config.check(query=call)
def bind_filters(bot: telebot.TeleBot):
bot.add_custom_filter(CalendarCallbackFilter())
bot.add_custom_filter(CalendarZoomCallbackFilter())

View File

@ -0,0 +1,92 @@
import calendar
from datetime import date, timedelta
from examples.callback_data_examples.advanced_calendar_example.filters import calendar_factory, calendar_zoom
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
EMTPY_FIELD = '1'
WEEK_DAYS = [calendar.day_abbr[i] for i in range(7)]
MONTHS = [(i, calendar.month_name[i]) for i in range(1, 13)]
def generate_calendar_days(year: int, month: int):
keyboard = InlineKeyboardMarkup(row_width=7)
today = date.today()
keyboard.add(
InlineKeyboardButton(
text=date(year=year, month=month, day=1).strftime('%b %Y'),
callback_data=EMTPY_FIELD
)
)
keyboard.add(*[
InlineKeyboardButton(
text=day,
callback_data=EMTPY_FIELD
)
for day in WEEK_DAYS
])
for week in calendar.Calendar().monthdayscalendar(year=year, month=month):
week_buttons = []
for day in week:
day_name = ' '
if day == today.day and today.year == year and today.month == month:
day_name = '🔘'
elif day != 0:
day_name = str(day)
week_buttons.append(
InlineKeyboardButton(
text=day_name,
callback_data=EMTPY_FIELD
)
)
keyboard.add(*week_buttons)
previous_date = date(year=year, month=month, day=1) - timedelta(days=1)
next_date = date(year=year, month=month, day=1) + timedelta(days=31)
keyboard.add(
InlineKeyboardButton(
text='Previous month',
callback_data=calendar_factory.new(year=previous_date.year, month=previous_date.month)
),
InlineKeyboardButton(
text='Zoom out',
callback_data=calendar_zoom.new(year=year)
),
InlineKeyboardButton(
text='Next month',
callback_data=calendar_factory.new(year=next_date.year, month=next_date.month)
),
)
return keyboard
def generate_calendar_months(year: int):
keyboard = InlineKeyboardMarkup(row_width=3)
keyboard.add(
InlineKeyboardButton(
text=date(year=year, month=1, day=1).strftime('Year %Y'),
callback_data=EMTPY_FIELD
)
)
keyboard.add(*[
InlineKeyboardButton(
text=month,
callback_data=calendar_factory.new(year=year, month=month_number)
)
for month_number, month in MONTHS
])
keyboard.add(
InlineKeyboardButton(
text='Previous year',
callback_data=calendar_zoom.new(year=year - 1)
),
InlineKeyboardButton(
text='Next year',
callback_data=calendar_zoom.new(year=year + 1)
)
)
return keyboard

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""
This Example will show you an advanced usage of CallbackData.
In this example calendar was implemented
"""
from datetime import date
from examples.callback_data_examples.advanced_calendar_example.keyboards import generate_calendar_days, \
generate_calendar_months, EMTPY_FIELD
from filters import calendar_factory, calendar_zoom, bind_filters
from telebot import types, TeleBot
API_TOKEN = ''
bot = TeleBot(API_TOKEN)
@bot.message_handler(commands='start')
def start_command_handler(message: types.Message):
bot.send_message(message.chat.id,
f"Hello {message.from_user.first_name}. This bot is an example of calendar keyboard."
"\nPress /calendar to see it.")
@bot.message_handler(commands='calendar')
def calendar_command_handler(message: types.Message):
now = date.today()
bot.send_message(message.chat.id, 'Calendar', reply_markup=generate_calendar_days(year=now.year, month=now.month))
@bot.callback_query_handler(func=None, calendar_config=calendar_factory.filter())
def calendar_action_handler(call: types.CallbackQuery):
callback_data: dict = calendar_factory.parse(callback_data=call.data)
year, month = int(callback_data['year']), int(callback_data['month'])
bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
reply_markup=generate_calendar_days(year=year, month=month))
@bot.callback_query_handler(func=None, calendar_zoom_config=calendar_zoom.filter())
def calendar_zoom_out_handler(call: types.CallbackQuery):
callback_data: dict = calendar_zoom.parse(callback_data=call.data)
year = int(callback_data.get('year'))
bot.edit_message_reply_markup(call.message.chat.id, call.message.id,
reply_markup=generate_calendar_months(year=year))
@bot.callback_query_handler(func=lambda call: call.data == EMTPY_FIELD)
def callback_empty_field_handler(call: types.CallbackQuery):
bot.answer_callback_query(call.id)
if __name__ == '__main__':
bind_filters(bot)
bot.infinity_polling()

View File

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
"""
This Example will show you usage of TextFilter
In this example you will see how to use TextFilter
with (message_handler, callback_query_handler, poll_handler)
"""
from telebot import TeleBot, types
from telebot.custom_filters import TextFilter, TextMatchFilter, IsReplyFilter
bot = TeleBot("")
@bot.message_handler(text=TextFilter(equals='hello'))
def hello_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(equals='hello', ignore_case=True))
def hello_handler_ignore_case(message: types.Message):
bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(contains=['good', 'bad']))
def contains_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(contains=['good', 'bad'], ignore_case=True))
def contains_handler_ignore_case(message: types.Message):
bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(starts_with='st')) # stArk, steve, stONE
def starts_with_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(starts_with='st', ignore_case=True)) # STark, sTeve, stONE
def starts_with_handler_ignore_case(message: types.Message):
bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(ends_with='ay')) # wednesday, SUNday, WeekDay
def ends_with_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
@bot.message_handler(text=TextFilter(ends_with='ay', ignore_case=True)) # wednesdAY, sundAy, WeekdaY
def ends_with_handler_ignore_case(message: types.Message):
bot.send_message(message.chat.id, message.text + ' ignore case')
@bot.message_handler(text=TextFilter(equals='/callback'))
def send_callback(message: types.Message):
keyboard = types.InlineKeyboardMarkup(
keyboard=[
[types.InlineKeyboardButton(text='callback data', callback_data='example')],
[types.InlineKeyboardButton(text='ignore case callback data', callback_data='ExAmPLe')]
]
)
bot.send_message(message.chat.id, message.text, reply_markup=keyboard)
@bot.callback_query_handler(func=None, text=TextFilter(equals='example'))
def callback_query_handler(call: types.CallbackQuery):
bot.answer_callback_query(call.id, call.data, show_alert=True)
@bot.callback_query_handler(func=None, text=TextFilter(equals='example', ignore_case=True))
def callback_query_handler_ignore_case(call: types.CallbackQuery):
bot.answer_callback_query(call.id, call.data + " ignore case", show_alert=True)
@bot.message_handler(text=TextFilter(equals='/poll'))
def send_poll(message: types.Message):
bot.send_poll(message.chat.id, question='When do you prefer to work?', options=['Morning', 'Night'])
bot.send_poll(message.chat.id, question='WHEN DO you pRefeR to worK?', options=['Morning', 'Night'])
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?'))
def poll_question_handler(poll: types.Poll):
print(poll.question)
@bot.poll_handler(func=None, text=TextFilter(equals='When do you prefer to work?', ignore_case=True))
def poll_question_handler_ignore_case(poll: types.Poll):
print(poll.question + ' ignore case')
# either hi or contains one of (привет, salom)
@bot.message_handler(text=TextFilter(equals="hi", contains=('привет', 'salom'), ignore_case=True))
def multiple_patterns_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
# starts with one of (mi, mea) for ex. minor, milk, meal, meat
@bot.message_handler(text=TextFilter(starts_with=['mi', 'mea'], ignore_case=True))
def multiple_starts_with_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
# ends with one of (es, on) for ex. Jones, Davies, Johnson, Wilson
@bot.message_handler(text=TextFilter(ends_with=['es', 'on'], ignore_case=True))
def multiple_ends_with_handler(message: types.Message):
bot.send_message(message.chat.id, message.text)
# !ban /ban .ban !бан /бан .бан
@bot.message_handler(is_reply=True,
text=TextFilter(starts_with=('!', '/', '.'), ends_with=['ban', 'бан'], ignore_case=True))
def ban_command_handler(message: types.Message):
if len(message.text) == 4 and message.chat.type != 'private':
try:
bot.ban_chat_member(message.chat.id, message.reply_to_message.from_user.id)
bot.reply_to(message.reply_to_message, 'Banned.')
except Exception as err:
print(err.args)
return
if __name__ == '__main__':
bot.add_custom_filter(TextMatchFilter())
bot.add_custom_filter(IsReplyFilter())
bot.infinity_polling()

View File

@ -0,0 +1,66 @@
import gettext
import os
class I18N:
"""
This class provides high-level tool for internationalization
It is based on gettext util.
"""
def __init__(self, translations_path, domain_name: str):
self.path = translations_path
self.domain = domain_name
self.translations = self.find_translations()
@property
def available_translations(self):
return list(self.translations)
def gettext(self, text: str, lang: str = None):
"""
Singular translations
"""
if not lang or lang not in self.translations:
return text
translator = self.translations[lang]
return translator.gettext(text)
def ngettext(self, singular: str, plural: str, lang: str = None, n=1):
"""
Plural translations
"""
if not lang or lang not in self.translations:
if n == 1:
return singular
return plural
translator = self.translations[lang]
return translator.ngettext(singular, plural, n)
def find_translations(self):
"""
Looks for translations with passed 'domain' in passed 'path'
"""
if not os.path.exists(self.path):
raise RuntimeError(f"Translations directory by path: {self.path!r} was not found")
result = {}
for name in os.listdir(self.path):
translations_path = os.path.join(self.path, name, 'LC_MESSAGES')
if not os.path.isdir(translations_path):
continue
po_file = os.path.join(translations_path, self.domain + '.po')
mo_file = po_file[:-2] + 'mo'
if os.path.isfile(po_file) and not os.path.isfile(mo_file):
raise FileNotFoundError(f"Translations for: {name!r} were not compiled!")
with open(mo_file, 'rb') as file:
result[name] = gettext.GNUTranslations(file)
return result

View File

@ -0,0 +1,23 @@
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
def languages_keyboard():
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text="English", callback_data='en'),
InlineKeyboardButton(text="Русский", callback_data='ru'),
InlineKeyboardButton(text="O'zbekcha", callback_data='uz_Latn')
]
]
)
def clicker_keyboard(_, lang):
return InlineKeyboardMarkup(
keyboard=[
[
InlineKeyboardButton(text=_("click", lang=lang), callback_data='click'),
]
]
)

View File

@ -0,0 +1,51 @@
# English translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-18 17:54+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr ""
#: main.py:78
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example"
msgstr ""
#: main.py:102
msgid "Language has been changed"
msgstr ""
#: main.py:114
#, fuzzy
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] ""
msgstr[1] ""
#: main.py:120
msgid ""
"This is clicker.\n"
"\n"
msgstr ""

View File

@ -0,0 +1,60 @@
# Russian translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-18 17:54+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ru\n"
"Language-Team: ru <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr "Клик"
#: main.py:78
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example"
msgstr ""
"Привет, {user_fist_name}!\n"
"Это пример мультиязычного бота.\n"
"Доступные команды:\n"
"\n"
"/lang - изменить язык\n"
"/plural - пример плюрализации"
#: main.py:102
msgid "Language has been changed"
msgstr "Язык был сменён"
#: main.py:114
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] "У вас {number} клик"
msgstr[1] "У вас {number} клика"
msgstr[2] "У вас {number} кликов"
#: main.py:120
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
"Это кликер.\n"
"\n"

View File

@ -0,0 +1,58 @@
# Uzbek (Latin) translations for PROJECT.
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-02-18 17:54+0500\n"
"PO-Revision-Date: 2022-02-18 16:22+0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: uz_Latn\n"
"Language-Team: uz_Latn <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"
#: keyboards.py:20
msgid "click"
msgstr "clik"
#: main.py:78
msgid ""
"Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n"
"\n"
"/lang - change your language\n"
"/plural - pluralization example"
msgstr ""
"Salom, {user_fist_name}!\n"
"Bu multilanguage bot misoli.\n"
"Mavjud buyruqlar:\n"
"\n"
"/lang - tilni ozgartirish\n"
"/plural - pluralizatsiya misoli"
#: main.py:102
msgid "Language has been changed"
msgstr "Til ozgartirildi"
#: main.py:114
msgid "You have {number} click"
msgid_plural "You have {number} clicks"
msgstr[0] "Sizda {number}ta clik"
msgstr[1] "Sizda {number}ta clik"
#: main.py:120
msgid ""
"This is clicker.\n"
"\n"
msgstr ""
"Bu clicker.\n"
"\n"

View File

@ -0,0 +1,153 @@
"""
In this example you will learn how to adapt your bot to different languages
Using built-in class I18N.
You need to install babel package 'https://pypi.org/project/Babel/'
Babel provides a command-line interface for working with message catalogs
After installing babel package you have a script called 'pybabel'
Too see all the commands open terminal and type 'pybabel --help'
Full description for pybabel commands can be found here: 'https://babel.pocoo.org/en/latest/cmdline.html'
Create a directory 'locales' where our translations will be stored
First we need to extract texts:
pybabel extract -o locales/{domain_name}.pot --input-dirs .
{domain_name}.pot - is the file where all translations are saved
The name of this file should be the same as domain which you pass to I18N class
In this example domain_name will be 'messages'
For gettext (singular texts) we use '_' alias and it works perfect
You may also you some alias for ngettext (plural texts) but you can face with a problem that
your plural texts are not being extracted
That is because by default 'pybabel extract' recognizes the following keywords:
_, gettext, ngettext, ugettext, ungettext, dgettext, dngettext, N_
To add your own keyword you can use '-k' flag
In this example for 'ngettext' i will assign double underscore alias '__'
Full command with pluralization support will look so:
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
Then create directories with translations (get list of all locales: 'pybabel --list-locales'):
pybabel init -i locales/{domain_name}.pot -d locales -l en
pybabel init -i locales/{domain_name}.pot -d locales -l ru
pybabel init -i locales/{domain_name}.pot -d locales -l uz_Latn
Now you can translate the texts located in locales/{language}/LC_MESSAGES/{domain_name}.po
After you translated all the texts you need to compile .po files:
pybabel compile -d locales
When you delete/update your texts you also need to update them in .po files:
pybabel extract -o locales/{domain_name}.pot -k __:1,2 --input-dirs .
pybabel update -i locales/{domain_name}.pot -d locales
- translate
pybabel compile -d locales
If you have any exceptions check:
- you have installed babel
- translations are ready, so you just compiled it
- in the commands above you replaced {domain_name} to messages
- you are writing commands from correct path in terminal
"""
from functools import wraps
import keyboards
from telebot import TeleBot, types, custom_filters
from telebot.storage.memory_storage import StateMemoryStorage
from i18n_class import I18N
storage = StateMemoryStorage()
bot = TeleBot("", state_storage=storage)
i18n = I18N(translations_path='locales', domain_name='messages')
_ = i18n.gettext # for singular translations
__ = i18n.ngettext # for plural translations
# These are example storages, do not use it in a production development
users_lang = {}
users_clicks = {}
def get_user_language(func):
"""
This decorator will pass to your handler current user's language
"""
@wraps(func)
def inner(*args, **kwargs):
obj = args[0]
kwargs.update(lang=users_lang.get(obj.from_user.id, 'en'))
return func(*args, **kwargs)
return inner
@bot.message_handler(commands='start')
@get_user_language
def start_handler(message: types.Message, lang):
text = _("Hello, {user_fist_name}!\n"
"This is the example of multilanguage bot.\n"
"Available commands:\n\n"
"/lang - change your language\n"
"/plural - pluralization example", lang=lang)
# remember don't use f string for interpolation, use .format method instead
text = text.format(user_fist_name=message.from_user.first_name)
bot.send_message(message.from_user.id, text)
@bot.message_handler(commands='lang')
def change_language_handler(message: types.Message):
bot.send_message(message.chat.id, "Choose language\nВыберите язык\nTilni tanlang",
reply_markup=keyboards.languages_keyboard())
@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(contains=['en', 'ru', 'uz_Latn']))
def language_handler(call: types.CallbackQuery):
lang = call.data
users_lang[call.from_user.id] = lang
bot.edit_message_text(_("Language has been changed", lang=lang), call.from_user.id, call.message.id)
bot.delete_state(call.from_user.id)
@bot.message_handler(commands='plural')
@get_user_language
def pluralization_handler(message: types.Message, lang):
if not users_clicks.get(message.from_user.id):
users_clicks[message.from_user.id] = 0
clicks = users_clicks[message.from_user.id]
text = __(
singular="You have {number} click",
plural="You have {number} clicks",
n=clicks,
lang=lang
)
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
bot.send_message(message.chat.id, text, reply_markup=keyboards.clicker_keyboard(_, lang))
@bot.callback_query_handler(func=None, text=custom_filters.TextFilter(equals='click'))
@get_user_language
def click_handler(call: types.CallbackQuery, lang):
if not users_clicks.get(call.from_user.id):
users_clicks[call.from_user.id] = 1
else:
users_clicks[call.from_user.id] += 1
clicks = users_clicks[call.from_user.id]
text = __(
singular="You have {number} click",
plural="You have {number} clicks",
n=clicks,
lang=lang
)
text = _("This is clicker.\n\n", lang=lang) + text.format(number=clicks)
bot.edit_message_text(text, call.from_user.id, call.message.message_id,
reply_markup=keyboards.clicker_keyboard(_, lang))
if __name__ == '__main__':
bot.add_custom_filter(custom_filters.TextMatchFilter())
bot.infinity_polling()

View File

@ -32,7 +32,7 @@ def set_timer(message):
@bot.message_handler(commands=['unset'])
def unset_timer(message):
schedule.clean(message.chat.id)
schedule.clear(message.chat.id)
if __name__ == '__main__':

View File

@ -2456,7 +2456,7 @@ class TeleBot:
:param emojis:
:param png_sticker: Required if `tgs_sticker` is None
:param tgs_sticker: Required if `png_sticker` is None
:webm_sticker:
:param webm_sticker:
:param mask_position:
:return:
"""
@ -2741,6 +2741,16 @@ class TeleBot:
"""
self.add_middleware_handler(callback, update_types)
@staticmethod
def check_commands_input(commands, method_name):
if not isinstance(commands, list) or not all(isinstance(item, str) for item in commands):
logger.error(f"{method_name}: Commands filter should be list of strings (commands), unknown type supplied to the 'commands' filter list. Not able to use the supplied type.")
@staticmethod
def check_regexp_input(regexp, method_name):
if not isinstance(regexp, str):
logger.error(f"{method_name}: Regexp filter should be string. Not able to use the supplied type.")
def message_handler(self, commands=None, regexp=None, func=None, content_types=None, chat_types=None, **kwargs):
"""
Message handler decorator.
@ -2783,9 +2793,15 @@ class TeleBot:
if content_types is None:
content_types = ["text"]
if isinstance(commands, str):
logger.warning("message_handler: 'commands' filter should be List of strings (commands), not string.")
commands = [commands]
method_name = "message_handler"
if commands is not None:
self.check_commands_input(commands, method_name)
if isinstance(commands, str):
commands = [commands]
if regexp is not None:
self.check_regexp_input(regexp, method_name)
if isinstance(content_types, str):
logger.warning("message_handler: 'content_types' filter should be List of strings (content types), not string.")
@ -2824,9 +2840,15 @@ class TeleBot:
:param pass_bot: Pass TeleBot to handler.
:return: decorated function
"""
if isinstance(commands, str):
logger.warning("register_message_handler: 'commands' filter should be List of strings (commands), not string.")
commands = [commands]
method_name = "register_message_handler"
if commands is not None:
self.check_commands_input(commands, method_name)
if isinstance(commands, str):
commands = [commands]
if regexp is not None:
self.check_regexp_input(regexp, method_name)
if isinstance(content_types, str):
logger.warning("register_message_handler: 'content_types' filter should be List of strings (content types), not string.")
@ -2856,9 +2878,15 @@ class TeleBot:
if content_types is None:
content_types = ["text"]
if isinstance(commands, str):
logger.warning("edited_message_handler: 'commands' filter should be List of strings (commands), not string.")
commands = [commands]
method_name = "edited_message_handler"
if commands is not None:
self.check_commands_input(commands, method_name)
if isinstance(commands, str):
commands = [commands]
if regexp is not None:
self.check_regexp_input(regexp, method_name)
if isinstance(content_types, str):
logger.warning("edited_message_handler: 'content_types' filter should be List of strings (content types), not string.")
@ -2897,9 +2925,15 @@ class TeleBot:
:param pass_bot: Pass TeleBot to handler.
:return: decorated function
"""
if isinstance(commands, str):
logger.warning("register_edited_message_handler: 'commands' filter should be List of strings (commands), not string.")
commands = [commands]
method_name = "register_edited_message_handler"
if commands is not None:
self.check_commands_input(commands, method_name)
if isinstance(commands, str):
commands = [commands]
if regexp is not None:
self.check_regexp_input(regexp, method_name)
if isinstance(content_types, str):
logger.warning("register_edited_message_handler: 'content_types' filter should be List of strings (content types), not string.")
@ -2929,9 +2963,15 @@ class TeleBot:
if content_types is None:
content_types = ["text"]
if isinstance(commands, str):
logger.warning("channel_post_handler: 'commands' filter should be List of strings (commands), not string.")
commands = [commands]
method_name = "channel_post_handler"
if commands is not None:
self.check_commands_input(commands, method_name)
if isinstance(commands, str):
commands = [commands]
if regexp is not None:
self.check_regexp_input(regexp, method_name)
if isinstance(content_types, str):
logger.warning("channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
@ -2968,9 +3008,15 @@ class TeleBot:
:param pass_bot: Pass TeleBot to handler.
:return: decorated function
"""
if isinstance(commands, str):
logger.warning("register_channel_post_handler: 'commands' filter should be List of strings (commands), not string.")
commands = [commands]
method_name = "register_channel_post_handler"
if commands is not None:
self.check_commands_input(commands, method_name)
if isinstance(commands, str):
commands = [commands]
if regexp is not None:
self.check_regexp_input(regexp, method_name)
if isinstance(content_types, str):
logger.warning("register_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
@ -2998,9 +3044,15 @@ class TeleBot:
if content_types is None:
content_types = ["text"]
if isinstance(commands, str):
logger.warning("edited_channel_post_handler: 'commands' filter should be List of strings (commands), not string.")
commands = [commands]
method_name = "edited_channel_post_handler"
if commands is not None:
self.check_commands_input(commands, method_name)
if isinstance(commands, str):
commands = [commands]
if regexp is not None:
self.check_regexp_input(regexp, method_name)
if isinstance(content_types, str):
logger.warning("edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")
@ -3037,9 +3089,15 @@ class TeleBot:
:param pass_bot: Pass TeleBot to handler.
:return: decorated function
"""
if isinstance(commands, str):
logger.warning("register_edited_channel_post_handler: 'commands' filter should be List of strings (commands), not string.")
commands = [commands]
method_name = "register_edited_channel_post_handler"
if commands is not None:
self.check_commands_input(commands, method_name)
if isinstance(commands, str):
commands = [commands]
if regexp is not None:
self.check_regexp_input(regexp, method_name)
if isinstance(content_types, str):
logger.warning("register_edited_channel_post_handler: 'content_types' filter should be List of strings (content types), not string.")

View File

@ -1,4 +1,8 @@
from abc import ABC
from typing import Optional, Union
from telebot import types
class SimpleCustomFilter(ABC):
"""
@ -30,6 +34,95 @@ class AdvancedCustomFilter(ABC):
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.
@ -42,8 +135,13 @@ class TextMatchFilter(AdvancedCustomFilter):
key = 'text'
async def check(self, message, text):
if type(text) is list:return message.text in text
else: return text == 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):
"""
@ -58,7 +156,15 @@ class TextContainsFilter(AdvancedCustomFilter):
key = 'text_contains'
async def check(self, message, text):
return text in 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):
"""
@ -70,8 +176,10 @@ class TextStartsFilter(AdvancedCustomFilter):
"""
key = 'text_startswith'
async def check(self, message, text):
return message.text.startswith(text)
return message.text.startswith(text)
class ChatFilter(AdvancedCustomFilter):
"""
@ -82,9 +190,11 @@ class ChatFilter(AdvancedCustomFilter):
"""
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.
@ -99,6 +209,7 @@ class ForwardFilter(SimpleCustomFilter):
async def check(self, message):
return message.forward_from_chat is not None
class IsReplyFilter(SimpleCustomFilter):
"""
Check whether message is a reply.
@ -114,7 +225,6 @@ class IsReplyFilter(SimpleCustomFilter):
return message.reply_to_message is not None
class LanguageFilter(AdvancedCustomFilter):
"""
Check users language_code.
@ -127,8 +237,11 @@ class LanguageFilter(AdvancedCustomFilter):
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
if type(text) is list:
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
class IsAdminFilter(SimpleCustomFilter):
"""
@ -147,6 +260,7 @@ class IsAdminFilter(SimpleCustomFilter):
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.
@ -154,34 +268,40 @@ class StateFilter(AdvancedCustomFilter):
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
if isinstance(text, list):
new_text = [i.name for i in text]
new_text = []
for i in text:
if isclass(i): i = i.name
new_text.append(i)
text = new_text
elif isinstance(text, object):
text = text.name
if message.chat.type == 'group':
group_state = await self.bot.current_states.get_state(message.chat.id, message.from_user.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(message.chat.id,message.from_user.id)
user_state = await self.bot.current_states.get_state(message.chat.id, message.from_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.

View File

@ -1,14 +1,15 @@
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
@ -16,9 +17,10 @@ class BaseMiddleware:
class State:
def __init__(self) -> None:
self.name = None
def __str__(self) -> str:
return self.name
class StatesGroup:
def __init_subclass__(cls) -> None:
@ -26,4 +28,4 @@ class StatesGroup:
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.name = ':'.join((cls.__name__, name))

View File

@ -1,4 +1,9 @@
from abc import ABC
from typing import Optional, Union
from telebot import types
class SimpleCustomFilter(ABC):
"""
Simple Custom Filter base class.
@ -29,6 +34,100 @@ class AdvancedCustomFilter(ABC):
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.
@ -41,8 +140,13 @@ class TextMatchFilter(AdvancedCustomFilter):
key = 'text'
def check(self, message, text):
if type(text) is list:return message.text in text
else: return text == 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):
"""
@ -57,7 +161,15 @@ class TextContainsFilter(AdvancedCustomFilter):
key = 'text_contains'
def check(self, message, text):
return text in 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):
"""
@ -69,8 +181,10 @@ class TextStartsFilter(AdvancedCustomFilter):
"""
key = 'text_startswith'
def check(self, message, text):
return message.text.startswith(text)
return message.text.startswith(text)
class ChatFilter(AdvancedCustomFilter):
"""
@ -81,9 +195,11 @@ class ChatFilter(AdvancedCustomFilter):
"""
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.
@ -98,6 +214,7 @@ class ForwardFilter(SimpleCustomFilter):
def check(self, message):
return message.forward_from_chat is not None
class IsReplyFilter(SimpleCustomFilter):
"""
Check whether message is a reply.
@ -113,7 +230,6 @@ class IsReplyFilter(SimpleCustomFilter):
return message.reply_to_message is not None
class LanguageFilter(AdvancedCustomFilter):
"""
Check users language_code.
@ -126,8 +242,11 @@ class LanguageFilter(AdvancedCustomFilter):
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
if type(text) is list:
return message.from_user.language_code in text
else:
return message.from_user.language_code == text
class IsAdminFilter(SimpleCustomFilter):
"""
@ -145,6 +264,7 @@ class IsAdminFilter(SimpleCustomFilter):
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.
@ -152,15 +272,20 @@ class StateFilter(AdvancedCustomFilter):
Example:
@bot.message_handler(state=1)
"""
def __init__(self, bot):
self.bot = bot
key = 'state'
def check(self, message, text):
if text == '*': return True
if isinstance(text, list):
new_text = [i.name for i in text]
new_text = []
for i in text:
if isclass(i): i = i.name
new_text.append(i)
text = new_text
elif isinstance(text, object):
text = text.name
@ -170,14 +295,16 @@ class StateFilter(AdvancedCustomFilter):
return True
elif group_state in text and type(text) is list:
return True
else:
user_state = self.bot.current_states.get_state(message.chat.id,message.from_user.id)
user_state = self.bot.current_states.get_state(message.chat.id, message.from_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.

View File

@ -21,6 +21,7 @@ try:
# noinspection PyPackageRequirements
from PIL import Image
from io import BytesIO
pil_imported = True
except:
pil_imported = False
@ -39,16 +40,17 @@ content_type_media = [
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',
'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",
"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
@ -217,7 +219,7 @@ def pil_image_to_file(image, extension='JPEG', quality='web_low'):
photoBuffer = BytesIO()
image.convert('RGB').save(photoBuffer, extension, quality=quality)
photoBuffer.seek(0)
return photoBuffer
else:
raise RuntimeError('PIL module is not imported')
@ -338,7 +340,7 @@ def user_link(user: types.User, include_id: bool=False) -> str:
:return: HTML user link
"""
name = escape(user.first_name)
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
return (f"<a href='tg://user?id={user.id}'>{name}</a>"
+ (f" (<pre>{user.id}</pre>)" if include_id else ""))
@ -477,6 +479,7 @@ def webhook_google_functions(bot, request):
else:
return 'Bot ON'
def antiflood(function, *args, **kwargs):
"""
Use this function inside loops in order to avoid getting TooManyRequests error.