├── django_tgbot
├── __init__.py
├── migrations
│ └── __init__.py
├── state_manager
│ ├── __init__.py
│ ├── state_types.py
│ ├── update_types.py
│ ├── message_types.py
│ ├── state_manager.py
│ ├── state.py
│ └── transition_condition.py
├── management
│ ├── bot_template
│ │ ├── credentials.py
│ │ ├── migrations
│ │ │ ├── __init__.py
│ │ │ └── 0001_initial.py
│ │ ├── __init__.py
│ │ ├── urls.py
│ │ ├── processors.py
│ │ ├── models.py
│ │ ├── bot.py
│ │ └── views.py
│ ├── commands
│ │ ├── tgbottoken.py
│ │ ├── tgbotwebhook.py
│ │ └── createtgbot.py
│ └── helpers.py
├── exceptions.py
├── types
│ ├── polloption.py
│ ├── responseparameters.py
│ ├── passportfile.py
│ ├── chatphoto.py
│ ├── voice.py
│ ├── location.py
│ ├── shippingaddress.py
│ ├── shippingoption.py
│ ├── keyboardbuttonpolltype.py
│ ├── videonote.py
│ ├── venue.py
│ ├── photosize.py
│ ├── userprofilephotos.py
│ ├── maskposition.py
│ ├── passportdata.py
│ ├── video.py
│ ├── file.py
│ ├── dice.py
│ ├── pollanswer.py
│ ├── replykeyboardremove.py
│ ├── successfulpayment.py
│ ├── messageentity.py
│ ├── labeledprice.py
│ ├── gamehighscore.py
│ ├── stickerset.py
│ ├── encryptedcredentials.py
│ ├── forcereply.py
│ ├── loginurl.py
│ ├── sticker.py
│ ├── shippingquery.py
│ ├── botcommand.py
│ ├── document.py
│ ├── inlinekeyboardmarkup.py
│ ├── audio.py
│ ├── animation.py
│ ├── precheckoutquery.py
│ ├── orderinfo.py
│ ├── game.py
│ ├── keyboardbutton.py
│ ├── inlinequery.py
│ ├── replykeyboardmarkup.py
│ ├── contact.py
│ ├── invoice.py
│ ├── user.py
│ ├── poll.py
│ ├── inlinekeyboardbutton.py
│ ├── choseninlineresult.py
│ ├── encryptedpassportelement.py
│ ├── chatpermissions.py
│ ├── callbackquery.py
│ ├── chatmember.py
│ ├── chat.py
│ ├── inputmessagecontent.py
│ ├── update.py
│ ├── __init__.py
│ ├── message.py
│ └── inlinequeryresult.py
├── decorators.py
├── bot.py
├── models.py
└── bot_api_user.py
├── docs
├── img
│ └── code_and_bot.jpg
├── models
│ ├── telegram_user.md
│ ├── telegram_chat.md
│ └── telegram_state.md
├── types
│ ├── replykeyboardmarkup.md
│ ├── update.md
│ ├── message.md
│ └── README.md
├── management_commands
│ ├── tgbottoken.md
│ ├── tgbotwebhook.md
│ └── createtgbot.md
├── getting_updates.md
├── index.md
├── classes
│ └── bot.md
└── processors.md
├── MANIFEST.in
├── pyproject.toml
├── .gitignore
├── .github
├── workflows
│ └── publish.yml
└── CONTRIBUTING.md
├── mkdocs.yml
├── LICENSE
├── setup.cfg
└── README.md
/django_tgbot/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_tgbot/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_tgbot/state_manager/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/credentials.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/__init__.py:
--------------------------------------------------------------------------------
1 | bot_token = ''
2 | app_name = ''
3 |
--------------------------------------------------------------------------------
/docs/img/code_and_bot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ali-Toosi/django-tgbot/HEAD/docs/img/code_and_bot.jpg
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.md
3 | recursive-include django_tgbot/management *
4 | recursive-include docs *
5 |
--------------------------------------------------------------------------------
/django_tgbot/state_manager/state_types.py:
--------------------------------------------------------------------------------
1 | All = '__ALL_STATES'
2 | Keep = '__KEEP_THE_STATE_NAME'
3 | Reset = '__RESET_STATE'
4 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.setuptools_scm]
--------------------------------------------------------------------------------
/django_tgbot/exceptions.py:
--------------------------------------------------------------------------------
1 | class ProcessFailure(Exception):
2 | pass
3 |
4 |
5 | class BotAPIRequestFailure(Exception):
6 | pass
7 |
8 |
9 | class APIInputError(Exception):
10 | pass
11 |
--------------------------------------------------------------------------------
/docs/models/telegram_user.md:
--------------------------------------------------------------------------------
1 | # Telegram User
2 |
3 | Holds basic information about a user:
4 |
5 | * id
6 | * first_name
7 | * last_name
8 | * username
9 | * is_bot
10 |
11 | Can be modified to store more information.
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from .views import handle_bot_request, poll_updates
3 |
4 | urlpatterns = [
5 | path('update/', handle_bot_request),
6 | path('poll/', poll_updates)
7 | ]
8 |
--------------------------------------------------------------------------------
/django_tgbot/types/polloption.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class PollOption(BasicType):
5 | fields = {
6 | 'text': str,
7 | 'voter_count': int
8 | }
9 |
10 | def __init__(self, obj=None):
11 | super(PollOption, self).__init__(obj)
12 |
--------------------------------------------------------------------------------
/docs/models/telegram_chat.md:
--------------------------------------------------------------------------------
1 | # Telegram Chat
2 |
3 | Holds basic information about a Telegram chat:
4 |
5 | * id
6 | * title (if it is not a private chat)
7 | * username (if it has one)
8 | * type (one of private, group, supergroup, channel)
9 |
10 | This model can be modified to store more information if needed.
--------------------------------------------------------------------------------
/django_tgbot/types/responseparameters.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class ResponseParameters(BasicType):
5 | fields = {
6 | 'migrate_to_chat_id': str,
7 | 'retry_after': int
8 | }
9 |
10 | def __init__(self, obj=None):
11 | super(ResponseParameters, self).__init__(obj)
12 |
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | *.egg-info
3 | build/
4 | venv/
5 | .idea/
6 | .python-version
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 | .DS_Store
11 | .AppleDouble
12 | .LSOverride
13 | site/
14 | release*
15 | .vscode
16 | db.sqlite3
17 |
18 | # uncomment credentials.py for keep the secret
19 | # [bot_folder]/credentials.py
20 |
--------------------------------------------------------------------------------
/django_tgbot/types/passportfile.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class PassportFile(BasicType):
5 | fields = {
6 | 'file_id': str,
7 | 'file_unique_id': str,
8 | 'file_size': int,
9 | 'file_date': int
10 | }
11 |
12 | def __init__(self, obj=None):
13 | super(PassportFile, self).__init__(obj)
14 |
--------------------------------------------------------------------------------
/django_tgbot/types/chatphoto.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class ChatPhoto(BasicType):
5 | fields = {
6 | 'small_file_id': str,
7 | 'small_file_unique_id': str,
8 | 'big_file_id': str,
9 | 'big_file_unique_id': str
10 | }
11 |
12 | def __init__(self, obj=None):
13 | super(ChatPhoto, self).__init__(obj)
--------------------------------------------------------------------------------
/django_tgbot/types/voice.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Voice(BasicType):
5 | fields = {
6 | 'file_id': str,
7 | 'file_unique_id': str,
8 | 'duration': int,
9 | 'mime_type': str,
10 | 'file_size': int
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(Voice, self).__init__(obj)
15 |
16 |
--------------------------------------------------------------------------------
/django_tgbot/types/location.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Location(BasicType):
5 | fields = {
6 | 'longitude': str,
7 | 'latitude': str
8 | }
9 |
10 | def __init__(self, obj=None):
11 | super(Location, self).__init__(obj)
12 |
13 | @classmethod
14 | def a(cls, latitude: str, longitude: str):
15 | return super().a(**locals())
16 |
--------------------------------------------------------------------------------
/django_tgbot/state_manager/update_types.py:
--------------------------------------------------------------------------------
1 | Message = 'message'
2 | EditedMessage = 'edited_message'
3 | ChannelPost = 'channel_post'
4 | EditedChannelPost = 'edited_channel_post'
5 | InlineQuery = 'inline_query'
6 | ChosenInlineResult = 'chosen_inline_result'
7 | CallbackQuery = 'callback_query'
8 | ShippingQuery = 'shipping_query'
9 | PreCheckoutQuery = 'pre_checkout_query'
10 | Poll = 'poll'
11 | PollAnswer = 'poll_answer'
12 |
--------------------------------------------------------------------------------
/django_tgbot/types/shippingaddress.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class ShippingAddress(BasicType):
5 | fields = {
6 | 'country_code': str,
7 | 'state': str,
8 | 'city': str,
9 | 'street_line1': str,
10 | 'street_line2': str,
11 | 'post_code': str
12 | }
13 |
14 | def __init__(self, obj=None):
15 | super(ShippingAddress, self).__init__(obj)
16 |
17 |
--------------------------------------------------------------------------------
/django_tgbot/types/shippingoption.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 | from . import labeledprice
3 |
4 |
5 | class ShippingOption(BasicType):
6 | fields = {
7 | 'id': str,
8 | 'title': str,
9 | 'prices': {
10 | 'class': labeledprice.LabeledPrice,
11 | 'array': True
12 | }
13 | }
14 |
15 | def __init__(self, obj=None):
16 | super(ShippingOption, self).__init__(obj)
--------------------------------------------------------------------------------
/django_tgbot/types/keyboardbuttonpolltype.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class KeyboardButtonPollType(BasicType):
7 | fields = {
8 | 'type': str,
9 | }
10 |
11 | def __init__(self, obj=None):
12 | super(KeyboardButtonPollType, self).__init__(obj)
13 |
14 | @classmethod
15 | def a(cls, type: Optional[str] = None):
16 | return super().a(**locals())
17 |
--------------------------------------------------------------------------------
/django_tgbot/types/videonote.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 | from . import photosize
3 |
4 |
5 | class VideoNote(BasicType):
6 | fields = {
7 | 'file_id': str,
8 | 'file_unique_id': str,
9 | 'length': int,
10 | 'duration': int,
11 | 'file_size': int,
12 | 'thumb': photosize.PhotoSize
13 | }
14 |
15 | def __init__(self, obj=None):
16 | super(VideoNote, self).__init__(obj)
17 |
--------------------------------------------------------------------------------
/django_tgbot/types/venue.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Venue(BasicType):
5 | fields = {
6 | 'title': str,
7 | 'address': str,
8 | 'foursquare_id': str,
9 | 'foursquare_type': str,
10 | }
11 |
12 | def __init__(self, obj=None):
13 | super(Venue, self).__init__(obj)
14 |
15 |
16 | from . import location
17 |
18 | Venue.fields.update({
19 | 'location': location.Location
20 | })
--------------------------------------------------------------------------------
/django_tgbot/types/photosize.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class PhotoSize(BasicType):
5 | fields = {
6 | 'file_id': str,
7 | 'file_unique_id': str,
8 | 'width': int,
9 | 'height': int,
10 | 'file_size': int
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(PhotoSize, self).__init__(obj)
15 |
16 | def get_file_id(self) -> str:
17 | return getattr(self, 'file_id', None)
18 |
--------------------------------------------------------------------------------
/django_tgbot/types/userprofilephotos.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class UserProfilePhotos(BasicType):
5 | fields = {
6 | 'total_count': int,
7 | }
8 |
9 | def __init__(self, obj=None):
10 | super(UserProfilePhotos, self).__init__(obj)
11 |
12 |
13 | from . import photosize
14 |
15 | UserProfilePhotos.fields.update({
16 | 'photos': {
17 | 'class': photosize.PhotoSize,
18 | 'array_of_array': True
19 | }
20 | })
--------------------------------------------------------------------------------
/django_tgbot/types/maskposition.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class MaskPosition(BasicType):
5 | fields = {
6 | 'point': str,
7 | 'x_shift': str,
8 | 'y_shift': str,
9 | 'scale': str
10 | }
11 |
12 | def __init__(self, obj=None):
13 | super(MaskPosition, self).__init__(obj)
14 |
15 | @classmethod
16 | def a(cls, point: str, x_shift: str, y_shift: str, scale: str):
17 | return super().a(**locals())
18 |
--------------------------------------------------------------------------------
/django_tgbot/types/passportdata.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 | from . import encryptedpassportelement, encryptedcredentials
3 |
4 |
5 | class PassportData(BasicType):
6 | fields = {
7 | 'data': {
8 | 'class': encryptedpassportelement.EncryptedPassportElement,
9 | 'array': True
10 | },
11 | 'credentials': encryptedcredentials.EncryptedCredentials
12 | }
13 |
14 | def __init__(self, obj=None):
15 | super(PassportData, self).__init__(obj)
16 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Pypi
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | pypi:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v3
11 | with:
12 | fetch-depth: 0
13 | - run: python3 -m pip install --upgrade build && python3 -m build
14 | - name: Publish package
15 | uses: pypa/gh-action-pypi-publish@release/v1
16 | with:
17 | password: ${{ secrets.PYPI_API_TOKEN }}
--------------------------------------------------------------------------------
/django_tgbot/types/video.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Video(BasicType):
5 | fields = {
6 | 'file_id': str,
7 | 'file_unique_id': str,
8 | 'width': int,
9 | 'height': int,
10 | 'duration': int,
11 | 'mime_type': str,
12 | 'file_size': int,
13 | }
14 |
15 | def __init__(self, obj=None):
16 | super(Video, self).__init__(obj)
17 |
18 |
19 | from . import photosize
20 |
21 | Video.fields.update({
22 | 'thumb': photosize.PhotoSize
23 | })
--------------------------------------------------------------------------------
/django_tgbot/types/file.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class File(BasicType):
5 | fields = {
6 | 'file_id': str,
7 | 'file_unique_id': str,
8 | 'file_path': str,
9 | 'file_size': int
10 | }
11 |
12 | def __init__(self, obj=None):
13 | super(File, self).__init__(obj)
14 |
15 | def get_file_id(self) -> str:
16 | return getattr(self, 'file_id', None)
17 |
18 | def get_file_unique_id(self) -> str:
19 | return getattr(self, 'file_unique_id', None)
20 |
--------------------------------------------------------------------------------
/django_tgbot/types/dice.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Dice(BasicType):
5 | fields = {
6 | 'emoji': str,
7 | 'value': int
8 | }
9 |
10 | def __init__(self, obj=None):
11 | super(Dice, self).__init__(obj)
12 |
13 | def get_emoji(self) -> str:
14 | return getattr(self, 'emoji')
15 |
16 | def get_value(self) -> int:
17 | return getattr(self, 'value')
18 |
19 | @classmethod
20 | def a(cls, emoji: str, value: int):
21 | return super().a(**locals())
22 |
--------------------------------------------------------------------------------
/django_tgbot/types/pollanswer.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class PollAnswer(BasicType):
5 | fields = {
6 | 'poll_id': str,
7 | 'option_ids': {
8 | 'class': int,
9 | 'array': True
10 | },
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(PollAnswer, self).__init__(obj)
15 |
16 | def get_user(self):
17 | return getattr(self, 'user', None)
18 |
19 |
20 | from . import user
21 |
22 | PollAnswer.fields.update({
23 | 'user': user.User
24 | })
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/processors.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.decorators import processor
2 | from django_tgbot.state_manager import message_types, update_types, state_types
3 | from django_tgbot.types.update import Update
4 | from .bot import state_manager
5 | from .models import TelegramState
6 | from .bot import TelegramBot
7 |
8 |
9 | @processor(state_manager, from_states=state_types.All)
10 | def hello_world(bot: TelegramBot, update: Update, state: TelegramState):
11 | bot.sendMessage(update.get_chat().get_id(), 'Hello!')
12 |
--------------------------------------------------------------------------------
/django_tgbot/types/replykeyboardremove.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class ReplyKeyboardRemove(BasicType):
7 | fields = {
8 | 'remove_keyboard': BasicType.bool_interpreter,
9 | 'selective': BasicType.bool_interpreter
10 | }
11 |
12 | def __init__(self, obj=None):
13 | super(ReplyKeyboardRemove, self).__init__(obj)
14 |
15 | @classmethod
16 | def a(cls, remove_keyboard: bool, selective: Optional[bool] = None):
17 | return super().a(**locals())
18 |
--------------------------------------------------------------------------------
/django_tgbot/types/successfulpayment.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 | from . import orderinfo
3 |
4 |
5 | class SuccessfulPayment(BasicType):
6 | fields = {
7 | 'currency': str,
8 | 'total_amount': int,
9 | 'invoice_payload': str,
10 | 'shipping_option_id': str,
11 | 'order_info': orderinfo.OrderInfo,
12 | 'telegram_payment_charge_id': str,
13 | 'provider_payment_charge_id': str,
14 | }
15 |
16 | def __init__(self, obj=None):
17 | super(SuccessfulPayment, self).__init__(obj)
--------------------------------------------------------------------------------
/django_tgbot/types/messageentity.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class MessageEntity(BasicType):
5 | fields = {
6 | 'type': str,
7 | 'offset': int,
8 | 'length': int,
9 | 'url': str,
10 | 'language': str,
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(MessageEntity, self).__init__(obj)
15 |
16 | def get_type(self) -> str:
17 | return getattr(self, 'type', None)
18 |
19 |
20 | from . import user
21 | MessageEntity.fields.update({
22 | 'user': user.User
23 | })
--------------------------------------------------------------------------------
/django_tgbot/types/labeledprice.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class LabeledPrice(BasicType):
5 | fields = {
6 | 'label': str,
7 | 'amount': int
8 | }
9 |
10 | def __init__(self, obj=None):
11 | super(LabeledPrice, self).__init__(obj)
12 |
13 | def get_label(self) -> str:
14 | return getattr(self, 'label', None)
15 |
16 | def get_amount(self) -> int:
17 | return getattr(self, 'amount', None)
18 |
19 | @classmethod
20 | def a(cls, label: str, amount: int):
21 | return super().a(**locals())
--------------------------------------------------------------------------------
/django_tgbot/types/gamehighscore.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class GameHighScore(BasicType):
5 | fields = {
6 | 'position': int,
7 | 'score': int
8 | }
9 |
10 | def __init__(self, obj=None):
11 | super(GameHighScore, self).__init__(obj)
12 |
13 | def get_user(self): # -> user.User:
14 | return getattr(self, 'user', None)
15 |
16 | def get_score(self) -> int:
17 | return getattr(self, 'score', None)
18 |
19 |
20 | from . import user
21 |
22 | GameHighScore.fields.update({
23 | 'user': user.User,
24 | })
--------------------------------------------------------------------------------
/django_tgbot/types/stickerset.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 | from . import sticker, photosize
3 |
4 |
5 | class StickerSet(BasicType):
6 | fields = {
7 | 'name': str,
8 | 'title': str,
9 | 'is_animated': BasicType.bool_interpreter,
10 | 'contains_masks': BasicType.bool_interpreter,
11 | 'stickers': {
12 | 'class': sticker.Sticker,
13 | 'array': True
14 | },
15 | 'thumb': photosize.PhotoSize
16 | }
17 |
18 | def __init__(self, obj=None):
19 | super(StickerSet, self).__init__(obj)
20 |
--------------------------------------------------------------------------------
/django_tgbot/types/encryptedcredentials.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class EncryptedCredentials(BasicType):
5 | fields = {
6 | 'data': str,
7 | 'hash': str,
8 | 'secret': str
9 | }
10 |
11 | def __init__(self, obj=None):
12 | super(EncryptedCredentials, self).__init__(obj)
13 |
14 | def get_data(self) -> str:
15 | return getattr(self, 'data', None)
16 |
17 | def get_hash(self) -> str:
18 | return getattr(self, 'hash', None)
19 |
20 | def get_secret(self) -> str:
21 | return getattr(self, 'secret', None)
--------------------------------------------------------------------------------
/django_tgbot/types/forcereply.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class ForceReply(BasicType):
7 | fields = {
8 | 'force_reply': BasicType.bool_interpreter,
9 | 'input_field_placeholder': str,
10 | 'selective': BasicType.bool_interpreter
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(ForceReply, self).__init__(obj)
15 |
16 | @classmethod
17 | def a(cls, force_reply: bool, input_field_placeholder: Optional[str] = None, selective: Optional[bool] = None):
18 | return super().a(**locals())
19 |
--------------------------------------------------------------------------------
/django_tgbot/types/loginurl.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class LoginUrl(BasicType):
7 | fields = {
8 | 'url': str,
9 | 'forward_text': str,
10 | 'bot_username': str,
11 | 'request_write_access': BasicType.bool_interpreter
12 | }
13 |
14 | def __init__(self, obj=None):
15 | super(LoginUrl, self).__init__(obj)
16 |
17 | @classmethod
18 | def a(cls, url: str, forward_text: Optional[str] = None, bot_username: Optional[str] = None, request_write_access: Optional[bool] = None):
19 | return super().a(**locals())
--------------------------------------------------------------------------------
/django_tgbot/types/sticker.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 | from . import photosize, maskposition
3 |
4 |
5 | class Sticker(BasicType):
6 | fields = {
7 | 'file_id': str,
8 | 'file_unique_id': str,
9 | 'width': int,
10 | 'height': int,
11 | 'is_animated': BasicType.bool_interpreter,
12 | 'thumb': photosize.PhotoSize,
13 | 'emoji': str,
14 | 'set_name': str,
15 | 'mask_position': maskposition.MaskPosition,
16 | 'file_size': int
17 | }
18 |
19 | def __init__(self, obj=None):
20 | super(Sticker, self).__init__(obj)
21 |
22 |
--------------------------------------------------------------------------------
/django_tgbot/types/shippingquery.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class ShippingQuery(BasicType):
5 | fields = {
6 | 'id': str,
7 | 'invoice_payload': str
8 | }
9 |
10 | def __init__(self, obj=None):
11 | super(ShippingQuery, self).__init__(obj)
12 |
13 | def get_user(self):
14 | return getattr(self, 'from', None)
15 |
16 | def get_from(self):
17 | return self.get_user()
18 |
19 |
20 | from . import user, shippingaddress
21 |
22 | ShippingQuery.fields.update({
23 | 'from': user.User,
24 | 'shipping_address': shippingaddress.ShippingAddress
25 | })
--------------------------------------------------------------------------------
/django_tgbot/types/botcommand.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class BotCommand(BasicType):
5 | fields = {
6 | 'command': str,
7 | 'description': str
8 | }
9 |
10 | def __init__(self, obj=None):
11 | super(BotCommand, self).__init__(obj)
12 |
13 | def get_command(self) -> str:
14 | return getattr(self, 'command')
15 |
16 | def get_description(self) -> str:
17 | return getattr(self, 'description')
18 |
19 | @classmethod
20 | def a(cls, command: str, description: str):
21 | command = str(command).lower()
22 | return super().a(**locals())
23 |
--------------------------------------------------------------------------------
/django_tgbot/types/document.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Document(BasicType):
5 | fields = {
6 | 'file_id': str,
7 | 'file_unique_id': str,
8 | 'file_name': str,
9 | 'mime_type': str,
10 | 'file_size': int
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(Document, self).__init__(obj)
15 |
16 | def get_file_id(self) -> str:
17 | return getattr(self, 'file_id', None)
18 |
19 |
20 | # Placed here to avoid import cycles
21 |
22 | from . import photosize
23 |
24 | Document.fields.update({
25 | 'thumb': photosize.PhotoSize
26 | })
27 |
--------------------------------------------------------------------------------
/docs/types/replykeyboardmarkup.md:
--------------------------------------------------------------------------------
1 | # ReplyKeyboardMarkup
2 | This object is used to create and send keyboards to Telegram as reply markups with the messages.
3 |
4 | Like other Type objects, this should be created using the `a` method. Note that the `keyboard` argument should be an array of
5 | array of `KeyboardButton`s.
6 |
7 | This is an example of a valid reply keyboard created:
8 |
9 | ```python
10 | keyboard = ReplyKeyboardMarkup.a(keyboard=[
11 | [KeyboardButton.a(text='Button A'), KeyboardButton.a(text='Button B')],
12 | [KeyboardButton.a(text='Button C', request_location=True)]
13 | ], resize_keyboard=True)
14 | ```
--------------------------------------------------------------------------------
/django_tgbot/types/inlinekeyboardmarkup.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from . import BasicType
4 | from . import inlinekeyboardbutton
5 |
6 |
7 | class InlineKeyboardMarkup(BasicType):
8 | fields = {
9 | 'inline_keyboard': {
10 | 'class': inlinekeyboardbutton.InlineKeyboardButton,
11 | 'array_of_array': True
12 | }
13 | }
14 |
15 | def __init__(self, obj=None):
16 | super(InlineKeyboardMarkup, self).__init__(obj)
17 |
18 | @classmethod
19 | def a(cls, inline_keyboard: List[List[inlinekeyboardbutton.InlineKeyboardButton]]):
20 | return super().a(**locals())
21 |
--------------------------------------------------------------------------------
/django_tgbot/types/audio.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Audio(BasicType):
5 | fields = {
6 | 'file_id': str,
7 | 'file_unique_id': str,
8 | 'duration': int,
9 | 'performer': str,
10 | 'title': str,
11 | 'mime_type': str,
12 | 'file_size': int,
13 | }
14 |
15 | def __init__(self, obj=None):
16 | super(Audio, self).__init__(obj)
17 |
18 | def get_file_id(self):
19 | return getattr(self, 'file_id', None)
20 |
21 |
22 | # Placed here to avoid import cycles
23 |
24 | from . import photosize
25 |
26 | Audio.fields.update({
27 | 'thumb': photosize.PhotoSize
28 | })
29 |
--------------------------------------------------------------------------------
/django_tgbot/types/animation.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Animation(BasicType):
5 | fields = {
6 | 'file_id': str,
7 | 'file_unique_id': str,
8 | 'width': int,
9 | 'height': int,
10 | 'duration': int,
11 | 'file_name': str,
12 | 'mime_type': str,
13 | 'file_size': int,
14 | }
15 |
16 | def __init__(self, obj=None):
17 | super(Animation, self).__init__(obj)
18 |
19 | def get_file_id(self):
20 | return getattr(self, 'file_id', None)
21 |
22 |
23 | # Placed here to avoid import cycles
24 |
25 | from . import photosize
26 |
27 | Animation.fields.update({
28 | 'thumb': photosize.PhotoSize
29 | })
30 |
--------------------------------------------------------------------------------
/django_tgbot/types/precheckoutquery.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class PreCheckoutQuery(BasicType):
5 | fields = {
6 | 'id': str,
7 | 'currency': str,
8 | 'total_amount': int,
9 | 'invoice_payload': str,
10 | 'shipping_option_id': str,
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(PreCheckoutQuery, self).__init__(obj)
15 |
16 | def get_user(self):
17 | return getattr(self, 'from', None)
18 |
19 | def get_from(self):
20 | return self.get_user()
21 |
22 |
23 | from . import user, orderinfo
24 |
25 | PreCheckoutQuery.fields.update({
26 | 'from': user.User,
27 | 'order_info': orderinfo.OrderInfo
28 | })
29 |
--------------------------------------------------------------------------------
/django_tgbot/types/orderinfo.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class OrderInfo(BasicType):
7 | fields = {
8 | 'name': str,
9 | 'phone_number': str,
10 | 'email': str,
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(OrderInfo, self).__init__(obj)
15 |
16 | def get_name(self) -> Optional[str]:
17 | return getattr(self, 'name', None)
18 |
19 | def get_shipping_address(self): # -> Optional[shippingaddress.ShippingAddress]:
20 | return getattr(self, 'shipping_address')
21 |
22 |
23 | from . import shippingaddress
24 |
25 | OrderInfo.fields.update({
26 | 'shipping_address': shippingaddress.ShippingAddress
27 | })
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.models import CASCADE
3 |
4 | from django_tgbot.models import AbstractTelegramUser, AbstractTelegramChat, AbstractTelegramState
5 |
6 |
7 | class TelegramUser(AbstractTelegramUser):
8 | pass
9 |
10 |
11 | class TelegramChat(AbstractTelegramChat):
12 | pass
13 |
14 |
15 | class TelegramState(AbstractTelegramState):
16 | telegram_user = models.ForeignKey(TelegramUser, related_name='telegram_states', on_delete=CASCADE, blank=True, null=True)
17 | telegram_chat = models.ForeignKey(TelegramChat, related_name='telegram_states', on_delete=CASCADE, blank=True, null=True)
18 |
19 | class Meta:
20 | unique_together = ('telegram_user', 'telegram_chat')
21 |
22 |
--------------------------------------------------------------------------------
/django_tgbot/types/game.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Game(BasicType):
5 | fields = {
6 | 'title': str,
7 | 'description': str,
8 | 'text': str
9 | }
10 |
11 | def __init__(self, obj=None):
12 | super(Game, self).__init__(obj)
13 |
14 | def get_title(self) -> str:
15 | return getattr(self, 'title', None)
16 |
17 |
18 | # Placed here to avoid import cycles
19 | from . import photosize, messageentity, animation
20 |
21 | Game.fields.update({
22 | 'photo': {
23 | 'class': photosize.PhotoSize,
24 | 'array': True
25 | },
26 | 'text_entities': {
27 | 'class': messageentity.MessageEntity,
28 | 'array': True
29 | },
30 | 'animation': animation.Animation
31 | })
32 |
33 |
--------------------------------------------------------------------------------
/django_tgbot/types/keyboardbutton.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 | from . import keyboardbuttonpolltype
5 |
6 |
7 | class KeyboardButton(BasicType):
8 | fields = {
9 | 'text': str,
10 | 'request_contact': BasicType.bool_interpreter,
11 | 'request_location': BasicType.bool_interpreter,
12 | 'request_poll': keyboardbuttonpolltype.KeyboardButtonPollType
13 | }
14 |
15 | def __init__(self, obj=None):
16 | super(KeyboardButton, self).__init__(obj)
17 |
18 | @classmethod
19 | def a(cls, text: str, request_contact: Optional[bool] = None, request_location: Optional[bool] = None,
20 | request_poll: Optional[keyboardbuttonpolltype.KeyboardButtonPollType] = None):
21 | return super().a(**locals())
22 |
--------------------------------------------------------------------------------
/django_tgbot/types/inlinequery.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class InlineQuery(BasicType):
5 | fields = {
6 | 'id': str,
7 | 'query': str,
8 | 'offset': str
9 | }
10 |
11 | def __init__(self, obj=None):
12 | super(InlineQuery, self).__init__(obj)
13 |
14 | def get_user(self): # -> user.User:
15 | return getattr(self, 'from', None)
16 |
17 | def get_id(self) -> str:
18 | return getattr(self, 'id', None)
19 |
20 | def get_query(self) -> str:
21 | return getattr(self, 'query', None)
22 |
23 | def get_offset(self) -> str:
24 | return getattr(self, 'offset', None)
25 |
26 |
27 | from . import user, location
28 |
29 | InlineQuery.fields.update({
30 | 'from': user.User,
31 | 'location': location.Location,
32 | })
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: django-tgbot
2 |
3 | nav:
4 | - Home: index.md
5 | - Creating bots: management_commands/createtgbot.md
6 | - Using the Bot: classes/bot.md
7 | - Processors: processors.md
8 | - Getting Updates: getting_updates.md
9 | - Types:
10 | - What's a type?: types/README.md
11 | - Update: types/update.md
12 | - Message: types/message.md
13 | - ReplyKeyboardMarkup: types/replykeyboardmarkup.md
14 | - Models:
15 | - Telegram User: models/telegram_user.md
16 | - Telegram Chat: models/telegram_chat.md
17 | - Telegram State: models/telegram_state.md
18 | - Management Commands:
19 | - createtgbot: management_commands/createtgbot.md
20 | - tgbottoken: management_commands/tgbottoken.md
21 | - tgbotwebhook: management_commands/tgbotwebhook.md
22 | theme: readthedocs
23 |
--------------------------------------------------------------------------------
/django_tgbot/types/replykeyboardmarkup.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 |
3 | from . import BasicType
4 | from . import keyboardbutton
5 |
6 |
7 | class ReplyKeyboardMarkup(BasicType):
8 | fields = {
9 | 'keyboard': {
10 | 'class': keyboardbutton.KeyboardButton,
11 | 'array_of_array': True
12 | },
13 | 'resize_keyboard': BasicType.bool_interpreter,
14 | 'one_time_keyboard': BasicType.bool_interpreter,
15 | 'selective': BasicType.bool_interpreter
16 | }
17 |
18 | def __init__(self, obj=None):
19 | super(ReplyKeyboardMarkup, self).__init__(obj)
20 |
21 | @classmethod
22 | def a(cls, keyboard: List[List[keyboardbutton.KeyboardButton]], resize_keyboard: Optional[bool] = None,
23 | one_time_keyboard: Optional[bool] = None, selective: Optional[bool] = None):
24 | return super().a(**locals())
25 |
26 |
--------------------------------------------------------------------------------
/django_tgbot/types/contact.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class Contact(BasicType):
7 | fields = {
8 | 'phone_number': str,
9 | 'first_name': str,
10 | 'last_name': str,
11 | 'user_id': str,
12 | 'vcard': str
13 | }
14 |
15 | def __init__(self, obj=None):
16 | super(Contact, self).__init__(obj)
17 |
18 | def get_phone_number(self) -> str:
19 | return getattr(self, 'phone_number', None)
20 |
21 | def get_first_name(self) -> str:
22 | return getattr(self, 'first_name', None)
23 |
24 | def get_last_name(self) -> Optional[str]:
25 | return getattr(self, 'last_name', None)
26 |
27 | @classmethod
28 | def a(cls, phone_number: str, first_name: str, last_name: Optional[str] = None, user_id: Optional[str] = None,
29 | vcard: Optional[str] = None):
30 | return super().a(**locals())
--------------------------------------------------------------------------------
/django_tgbot/types/invoice.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class Invoice(BasicType):
5 | fields = {
6 | 'title': str,
7 | 'description': str,
8 | 'start_parameter': str,
9 | 'currency': str,
10 | 'total_amount': int
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(Invoice, self).__init__(obj)
15 |
16 | def get_title(self) -> str:
17 | return getattr(self, 'title', None)
18 |
19 | def get_start_parameter(self) -> str:
20 | return getattr(self, 'start_parameter', None)
21 |
22 | def get_currency(self) -> str:
23 | return getattr(self, 'currency', None)
24 |
25 | def total_amount(self) -> int:
26 | return getattr(self, 'total_amount', None)
27 |
28 | @classmethod
29 | def a(cls, title: str, description: str, start_parameter: str, currency: str, total_amount: int):
30 | return super().a(**locals())
31 |
--------------------------------------------------------------------------------
/django_tgbot/state_manager/message_types.py:
--------------------------------------------------------------------------------
1 | Text = 'text'
2 | Audio = 'audio'
3 | Document = 'document'
4 | Animation = 'animation'
5 | Game = 'game'
6 | Photo = 'photo'
7 | Sticker = 'sticker'
8 | Video = 'video'
9 | Voice = 'voice'
10 | VideoNote = 'video_note'
11 | Contact = 'contact'
12 | Dice = 'dice'
13 | Location = 'location'
14 | Venue = 'venue'
15 | Poll = 'poll'
16 | NewChatMembers = 'new_chat_members'
17 | LeftChatMember = 'left_chat_member'
18 | NewChatTitle = 'new_chat_title'
19 | NewChatPhoto = 'new_chat_photo'
20 | DeleteChatPhoto = 'delete_chat_photo'
21 | GroupChatCreated = 'group_chat_created'
22 | SupergroupChatCreated = 'supergroup_chat_created'
23 | ChannelChatCreated = 'channel_chat_created'
24 | MigrateToChatId = 'migrate_to_chat_id'
25 | MigrateFromChatId = 'migrate_from_chat_id'
26 | PinnedMessage = 'pinned_message'
27 | Invoice = 'invoice'
28 | SuccessfulPayment = 'successful_payment'
29 | PassportData = 'passport_data'
30 |
--------------------------------------------------------------------------------
/django_tgbot/types/user.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class User(BasicType):
5 | fields = {
6 | 'id': str,
7 | 'is_bot': BasicType.bool_interpreter,
8 | 'first_name': str,
9 | 'last_name': str,
10 | 'username': str,
11 | 'language_code': str,
12 | 'can_join_groups': BasicType.bool_interpreter,
13 | 'can_read_all_group_messages': BasicType.bool_interpreter,
14 | 'supports_inline_queries': BasicType.bool_interpreter
15 | }
16 |
17 | def __init__(self, obj=None):
18 | super(User, self).__init__(obj)
19 |
20 | def get_id(self):
21 | return getattr(self, 'id', None)
22 |
23 | def get_username(self):
24 | return getattr(self, 'username', None)
25 |
26 | def get_first_name(self):
27 | return getattr(self, 'first_name', None)
28 |
29 | def get_last_name(self):
30 | return getattr(self, 'last_name', None)
31 |
--------------------------------------------------------------------------------
/django_tgbot/types/poll.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.types import messageentity
2 | from . import BasicType
3 | from . import polloption
4 |
5 |
6 | class Poll(BasicType):
7 | fields = {
8 | 'id': str,
9 | 'question': str,
10 | 'options': {
11 | 'class': polloption.PollOption,
12 | 'array': True
13 | },
14 | 'total_voter_count': int,
15 | 'is_closed': BasicType.bool_interpreter,
16 | 'is_anonymous': BasicType.bool_interpreter,
17 | 'type': str,
18 | 'allows_multiple_answers': BasicType.bool_interpreter,
19 | 'correct_option_id': int,
20 | 'explanation': str,
21 | 'explanation_entities': {
22 | 'class': messageentity.MessageEntity,
23 | 'array': True
24 | },
25 | 'open_period': int,
26 | 'close_date': int
27 | }
28 |
29 | def __init__(self, obj=None):
30 | super(Poll, self).__init__(obj)
31 |
32 |
--------------------------------------------------------------------------------
/docs/management_commands/tgbottoken.md:
--------------------------------------------------------------------------------
1 | # Updating your bot's token
2 |
3 | Another management command encapsulated in this package is `tgbottoken`. This command allows you to update the token for a bot you have created.
4 |
5 | ### Step by step guide
6 | 1. Open the Django project with `django-tgbot` installed in it
7 | 2. Enter this command in the command line (terminal / cmd):
8 |
9 | python manage.py tgbottoken
10 |
11 | 3. Enter the username for the bot you want to modify (without @):
12 |
13 | > python manage.py tgbottoken
14 | Enter bot username:
15 |
16 | 4. Enter your new API token:
17 |
18 | > python manage.py tgbottoken
19 | Enter bot username:
20 | Enter the bot token (retrieved from BotFather):
21 |
22 | Your token is updated! If you have set the webhook correctly you can now send messages to your bot and it should respond to the messages.
23 |
24 |
--------------------------------------------------------------------------------
/django_tgbot/types/inlinekeyboardbutton.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 | from . import loginurl
5 |
6 |
7 | class InlineKeyboardButton(BasicType):
8 | fields = {
9 | 'text': str,
10 | 'url': str,
11 | 'login_url': loginurl.LoginUrl,
12 | 'callback_data': str,
13 | 'switch_inline_query': str,
14 | 'switch_inline_query_current_chat': str,
15 | 'callback_game': str,
16 | 'pay': BasicType.bool_interpreter
17 | }
18 |
19 | def __init__(self, obj=None):
20 | super(InlineKeyboardButton, self).__init__(obj)
21 |
22 | @classmethod
23 | def a(cls, text: str, url: Optional[str] = None, login_url: Optional[loginurl.LoginUrl] = None,
24 | callback_data: Optional[str] = None, switch_inline_query: Optional[str] = None,
25 | switch_inline_query_current_chat: Optional[str] = None, callback_game: Optional[str] = None,
26 | pay: Optional[bool] = None):
27 | return super().a(**locals())
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Alireza Khoshghalb
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = django-tgbot
3 | version = 1.0.0
4 | description = A Django app for creating Telegram bots.
5 | long_description = file: README.md
6 | long_description_content_type = text/markdown
7 | url = https://www.github.com/Ali-Toosi/django-tgbot
8 | author = Ali Toosi
9 | author_email = alirezakhoshghalb@ymail.com
10 | license = MIT License
11 | classifiers =
12 | Environment :: Web Environment
13 | Framework :: Django
14 | Framework :: Django :: 3.0
15 | Intended Audience :: Developers
16 | License :: OSI Approved :: MIT License
17 | Operating System :: OS Independent
18 | Programming Language :: Python
19 | Programming Language :: Python :: 3
20 | Programming Language :: Python :: 3 :: Only
21 | Programming Language :: Python :: 3.6
22 | Programming Language :: Python :: 3.7
23 | Programming Language :: Python :: 3.8
24 | Programming Language :: Python :: 3.9
25 | Topic :: Internet :: WWW/HTTP
26 | Topic :: Internet :: WWW/HTTP :: Dynamic Content
27 |
28 | [options]
29 | include_package_data = true
30 | packages = find:
31 | install_requires =
32 | django
33 | requests
34 |
--------------------------------------------------------------------------------
/django_tgbot/types/choseninlineresult.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class ChosenInlineResult(BasicType):
7 | fields = {
8 | 'result_id': str,
9 | 'inline_message_id': str,
10 | 'query': str
11 | }
12 |
13 | def __init__(self, obj=None):
14 | super(ChosenInlineResult, self).__init__(obj)
15 |
16 | def get_user(self): # -> user.User:
17 | return getattr(self, 'from', None)
18 |
19 | def get_from(self):
20 | return self.get_user()
21 |
22 | def get_result_id(self) -> str:
23 | return getattr(self, 'result_id', None)
24 |
25 | def get_inline_message_id(self): # -> Optional[str]
26 | return getattr(self, 'inline_message_id', None)
27 |
28 | def get_query(self) -> str:
29 | return getattr(self, 'query', None)
30 |
31 | def get_location(self): # -> Optional[location.Location]
32 | return getattr(self, 'location', None)
33 |
34 |
35 | # Placed here to avoid import cycles
36 | from . import user, location
37 |
38 | ChosenInlineResult.fields.update({
39 | 'from': user.User,
40 | 'location': location.Location,
41 | })
42 |
--------------------------------------------------------------------------------
/django_tgbot/types/encryptedpassportelement.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 |
3 | from . import BasicType
4 | from . import passportfile
5 |
6 |
7 | class EncryptedPassportElement(BasicType):
8 | fields = {
9 | 'type': str,
10 | 'data': str,
11 | 'phone_number': str,
12 | 'email': str,
13 | 'files': {
14 | 'class': passportfile.PassportFile,
15 | 'array': True
16 | },
17 | 'front_side': passportfile.PassportFile,
18 | 'reverse_side': passportfile.PassportFile,
19 | 'selfie': passportfile.PassportFile,
20 | 'translation': {
21 | 'class': passportfile.PassportFile,
22 | 'array': True
23 | },
24 | 'hash': str
25 | }
26 |
27 | def __init__(self, obj=None):
28 | super(EncryptedPassportElement, self).__init__(obj)
29 |
30 | def get_type(self) -> str:
31 | return getattr(self, 'type', None)
32 |
33 | def get_data(self) -> Optional[str]:
34 | return getattr(self, 'data', None)
35 |
36 | def get_files(self) -> Optional[List[passportfile.PassportFile]]:
37 | return getattr(self, 'files', None)
38 |
--------------------------------------------------------------------------------
/docs/management_commands/tgbotwebhook.md:
--------------------------------------------------------------------------------
1 | # Updating your bot's webhook
2 |
3 | Another management command encapsulated in this package is `tgbotwebhook`. This command allows you to update the webhook address for a bot you have created.
4 |
5 | ### Step by step guide
6 | 1. Open the Django project with `django-tgbot` installed in it
7 | 2. Enter this command in the command line (terminal / cmd):
8 |
9 | python manage.py tgbotwebhook
10 |
11 | 3. Enter the username for the bot you want to modify (without @):
12 |
13 | > python manage.py tgbottoken
14 | Enter bot username:
15 |
16 | 4. You will be asked if you want to change the project URL and use default webhook address or give your own address as webhook.
17 |
18 | The difference is that if you choose to use the default address, you don't need to change the urls set for you in the project. However, if you choose to provide a customized webhook URL
19 | you need to take care of the urls' configuration yourself.
20 |
21 | 5. Enter the URL.
22 |
23 | Your webhook is updated! If you have set the webhook correctly you can now send messages to your bot and it should respond to the messages.
24 |
25 |
--------------------------------------------------------------------------------
/django_tgbot/types/chatpermissions.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class ChatPermissions(BasicType):
7 | fields = {
8 | 'can_send_messages': BasicType.bool_interpreter,
9 | 'can_send_media_messages': BasicType.bool_interpreter,
10 | 'can_send_polls': BasicType.bool_interpreter,
11 | 'can_send_other_messages': BasicType.bool_interpreter,
12 | 'can_add_web_page_previews': BasicType.bool_interpreter,
13 | 'can_change_info': BasicType.bool_interpreter,
14 | 'can_invite_users': BasicType.bool_interpreter,
15 | 'can_pin_messages': BasicType.bool_interpreter
16 | }
17 |
18 | def __init__(self, obj=None):
19 | super(ChatPermissions, self).__init__(obj)
20 |
21 | @classmethod
22 | def a(cls, can_send_messages: Optional[bool] = None, can_send_media_messages: Optional[bool] = None,
23 | can_send_polls: Optional[bool] = None, can_send_other_messages: Optional[bool] = None,
24 | can_add_web_page_previews: Optional[bool] = None, can_change_info: Optional[bool] = None,
25 | can_invite_users: Optional[bool] = None, can_pin_messages: Optional[bool] = None):
26 | return super().a(**locals())
27 |
28 |
--------------------------------------------------------------------------------
/docs/types/update.md:
--------------------------------------------------------------------------------
1 | # Update
2 |
3 | This object represents the [Update Type](https://core.telegram.org/bots/api#update) in the Bot API.
4 |
5 | ### Type
6 | An update can have one of several types indicated in the Bot API (e.g. message, channel_post, etc.).
7 |
8 | This class has a `type` method that will give you the type of this update. The returned value will be one of the
9 | available values in the `update_types` module. You can check the type by checking the string values but in order to avoid
10 | errors it is recommended to use the `update_types` module:
11 |
12 | ```python
13 | from django_tgbot.state_manager import update_types
14 | from django_tgbot.types.update import Update
15 |
16 | update: Update = ...
17 |
18 | type = update.type()
19 |
20 | if type == update_types.Message:
21 | print('Update is a message')
22 | elif type == update_types.EditedMessage:
23 | print('Update is an edited message')
24 | elif
25 | ...
26 | ```
27 |
28 |
29 | These are all of the available update types at the moment:
30 |
31 | * **Message**
32 | * **EditedMessage**
33 | * **ChannelPost**
34 | * **EditedChannelPost**
35 | * **InlineQuery**
36 | * **ChosenInlineResult**
37 | * **CallbackQuery**
38 | * **ShippingQuery**
39 | * **PreCheckoutQuery**
40 | * **Poll**
41 | * **PollAnswer**
42 |
--------------------------------------------------------------------------------
/django_tgbot/types/callbackquery.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from django_tgbot.types import chat
4 | from . import BasicType
5 |
6 |
7 | class CallbackQuery(BasicType):
8 | fields = {
9 | 'id': str,
10 | 'inline_message_id': str,
11 | 'chat_instance': str,
12 | 'data': str,
13 | 'game_short_name': str
14 | }
15 |
16 | def __init__(self, obj=None):
17 | super(CallbackQuery, self).__init__(obj)
18 |
19 | def get_id(self) -> str:
20 | return getattr(self, 'id', None)
21 |
22 | def get_user(self):
23 | return getattr(self, 'from', None)
24 |
25 | def get_from(self):
26 | return self.get_user()
27 |
28 | def get_message(self): # -> Optional[message.Message]:
29 | return getattr(self, 'message', None)
30 |
31 | def get_chat(self) -> Optional[chat.Chat]:
32 | if self.get_message() is None:
33 | return None
34 | return self.get_message().get_chat()
35 |
36 | def get_inline_message_id(self) -> Optional[str]:
37 | return getattr(self, 'inline_message_id', None)
38 |
39 | def get_data(self) -> Optional[str]:
40 | return getattr(self, 'data', None)
41 |
42 | # Placed here to avoid import cycles
43 |
44 | from . import user, message
45 |
46 | CallbackQuery.fields.update({
47 | 'from': user.User,
48 | 'message': message.Message
49 | })
50 |
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/bot.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.bot import AbstractTelegramBot
2 | from django_tgbot.state_manager.state_manager import StateManager
3 | from django_tgbot.types.update import Update
4 | from . import bot_token
5 | from .models import TelegramUser, TelegramChat, TelegramState
6 |
7 |
8 | class TelegramBot(AbstractTelegramBot):
9 | def __init__(self, token, state_manager):
10 | super(TelegramBot, self).__init__(token, state_manager)
11 |
12 | def get_db_user(self, telegram_id):
13 | return TelegramUser.objects.get_or_create(telegram_id=telegram_id)[0]
14 |
15 | def get_db_chat(self, telegram_id):
16 | return TelegramChat.objects.get_or_create(telegram_id=telegram_id)[0]
17 |
18 | def get_db_state(self, db_user, db_chat):
19 | return TelegramState.objects.get_or_create(telegram_user=db_user, telegram_chat=db_chat)[0]
20 |
21 | def pre_processing(self, update: Update, user, db_user, chat, db_chat, state):
22 | super(TelegramBot, self).pre_processing(update, user, db_user, chat, db_chat, state)
23 |
24 | def post_processing(self, update: Update, user, db_user, chat, db_chat, state):
25 | super(TelegramBot, self).post_processing(update, user, db_user, chat, db_chat, state)
26 |
27 |
28 | def import_processors():
29 | from . import processors
30 |
31 |
32 | state_manager = StateManager()
33 | bot = TelegramBot(bot_token, state_manager)
34 | import_processors()
35 |
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/views.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.http import HttpResponse
3 | from django.views.decorators.csrf import csrf_exempt
4 | from .bot import bot
5 | from django_tgbot.types.update import Update
6 |
7 | import logging
8 |
9 |
10 | @csrf_exempt
11 | def handle_bot_request(request):
12 | update = Update(request.body.decode("utf-8"))
13 | """
14 | All of the processing will happen in this part. It is wrapped in try-except block
15 | to make sure the returned HTTP status is 200. Otherwise, if your processors raise Exceptions
16 | causing this function to raise Exception and not return 200 status code, Telegram will stop
17 | sending updates to your webhook after a few tries. Instead, take the caught exception and handle it
18 | or log it to use for debugging later.
19 | """
20 | try:
21 | bot.handle_update(update)
22 | except Exception as e:
23 | if settings.DEBUG:
24 | raise e
25 | else:
26 | logging.exception(e)
27 | return HttpResponse("OK")
28 |
29 |
30 | def poll_updates(request):
31 | """
32 | Polls all waiting updates from the server. Note that webhook should not be set if polling is used.
33 | You can delete the webhook by passing an empty URL as the address.
34 | """
35 | count = bot.poll_updates_and_handle()
36 | return HttpResponse(f"Processed {count} update{'' if count == 1 else 's'}.")
37 |
--------------------------------------------------------------------------------
/django_tgbot/types/chatmember.py:
--------------------------------------------------------------------------------
1 | from . import BasicType
2 |
3 |
4 | class ChatMember(BasicType):
5 | fields = {
6 | 'status': str,
7 | 'custom_title': str,
8 | 'until_date': str,
9 | 'can_be_edited': BasicType.bool_interpreter,
10 | 'can_post_messages': BasicType.bool_interpreter,
11 | 'can_edit_messages': BasicType.bool_interpreter,
12 | 'can_delete_messages': BasicType.bool_interpreter,
13 | 'can_restrict_members': BasicType.bool_interpreter,
14 | 'can_promote_members': BasicType.bool_interpreter,
15 | 'can_change_info': BasicType.bool_interpreter,
16 | 'can_invite_users': BasicType.bool_interpreter,
17 | 'can_pin_messages': BasicType.bool_interpreter,
18 | 'is_member': BasicType.bool_interpreter,
19 | 'can_send_messages': BasicType.bool_interpreter,
20 | 'can_send_media_messages': BasicType.bool_interpreter,
21 | 'can_send_polls': BasicType.bool_interpreter,
22 | 'can_send_other_messages': BasicType.bool_interpreter,
23 | 'can_add_web_page_previews': BasicType.bool_interpreter
24 | }
25 |
26 | def __init__(self, obj=None):
27 | super(ChatMember, self).__init__(obj)
28 |
29 | def get_user(self):
30 | return getattr(self, 'user', None)
31 |
32 | def get_status(self) -> str:
33 | return getattr(self, 'status', None)
34 |
35 |
36 | # Placed here to avoid import cycles
37 | from . import user
38 |
39 | ChatMember.fields.update({
40 | 'user': user.User
41 | })
42 |
--------------------------------------------------------------------------------
/django_tgbot/types/chat.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from . import BasicType
3 |
4 |
5 | class Chat(BasicType):
6 | fields = {
7 | 'id': str,
8 | 'type': str,
9 | 'title': str,
10 | 'username': str,
11 | 'first_name': str,
12 | 'last_name': str,
13 | 'description': str,
14 | 'invite_link': str,
15 | 'slow_mode_delay': str,
16 | 'sticker_set_name': str
17 | }
18 |
19 | def __init__(self, obj=None):
20 | super(Chat, self).__init__(obj)
21 |
22 | def get_id(self) -> str:
23 | return getattr(self, 'id', None)
24 |
25 | def get_type(self) -> str:
26 | return getattr(self, 'type', None)
27 |
28 | def get_title(self) -> Optional[str]:
29 | return getattr(self, 'title', None)
30 |
31 | def get_username(self) -> Optional[str]:
32 | return getattr(self, 'username', None)
33 |
34 | def get_first_name(self) -> Optional[str]:
35 | return getattr(self, 'first_name', None)
36 |
37 | def get_last_name(self):
38 | return getattr(self, 'last_name', None)
39 |
40 | def get_photo(self):
41 | return getattr(self, 'photo', None)
42 |
43 |
44 | # These are placed here to avoid import cycles
45 |
46 | from . import chatphoto, message, chatpermissions
47 |
48 | Chat.fields.update({
49 | 'pinned_message': message.Message,
50 | 'can_set_sticker_set': BasicType.bool_interpreter,
51 | 'photo': chatphoto.ChatPhoto,
52 | 'permissions': chatpermissions.ChatPermissions
53 | })
54 |
--------------------------------------------------------------------------------
/django_tgbot/state_manager/state_manager.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.models import AbstractTelegramState
2 | from django_tgbot.state_manager.transition_condition import TransitionCondition
3 | from django_tgbot.types.update import Update
4 |
5 |
6 | class StateManager:
7 | def __init__(self):
8 | self.handling_states = {}
9 | self.default_message_types = None
10 | self.default_update_types = None
11 |
12 | def register_state(self, state: TransitionCondition, processor):
13 | self.handling_states[state] = processor
14 |
15 | def get_processors(self, update: Update, state: AbstractTelegramState):
16 | """
17 | Searches through all of the processors and creates a list of those that handle the current state
18 | :param update: The received update
19 | :param state: The current state
20 | :return: a list of processors
21 | """
22 | message = update.get_message()
23 | message_type = message.type() if message is not None else None
24 | update_type = update.type()
25 | state_name = state.name
26 | processors = []
27 | for handling_state in self.handling_states.keys():
28 | if handling_state.matches(state_name=state_name, update_type=update_type, message_type=message_type):
29 | processors.append(self.handling_states[handling_state])
30 | return processors
31 |
32 | def set_default_message_types(self, message_types):
33 | self.default_message_types = message_types
34 |
35 | def set_default_update_types(self, update_types):
36 | self.default_update_types = update_types
37 |
38 |
--------------------------------------------------------------------------------
/django_tgbot/management/commands/tgbottoken.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.conf import settings
4 | from django.core.management.base import BaseCommand
5 |
6 | from django_tgbot.management import helpers
7 |
8 |
9 | class Command(BaseCommand):
10 | help = 'Updates the token for an existing tgbot'
11 |
12 | def handle(self, *args, **options):
13 | bot_username = input('Enter the username of the bot (without @): ').lower()
14 | dst = os.path.join(settings.BASE_DIR, bot_username)
15 | if not os.path.isdir(dst):
16 | self.stdout.write(
17 | self.style.ERROR('No such bot found. Make sure you have created your bot with command `createtgbot`.')
18 | )
19 | return
20 |
21 | get_me_result, bot_token = helpers.prompt_token(self, prompt_message='Enter the new token for this bot: ')
22 | bot_name = str(get_me_result.get_first_name())
23 | received_username = str(get_me_result.get_username()).lower()
24 |
25 | if received_username != bot_username:
26 | self.stdout.write(self.style.ERROR('Given token does not belong to this bot.'))
27 | return
28 |
29 | with open(os.path.join(dst, 'credentials.py'), 'w') as f:
30 | f.write("# Do not remove these 2 lines:\nBOT_TOKEN = '{}'\nAPP_NAME = '{}'\n".format(bot_token, bot_username))
31 |
32 | with open(os.path.join(dst, '__init__.py'), 'w') as f:
33 | f.write(
34 | "from . import credentials\n\n\nbot_token = credentials.BOT_TOKEN\napp_name = credentials.APP_NAME\n"
35 | )
36 |
37 | self.stdout.write(self.style.SUCCESS('Successfully updated token for bot {}(@{}).'.format(bot_name, bot_username)))
38 |
--------------------------------------------------------------------------------
/docs/types/message.md:
--------------------------------------------------------------------------------
1 | # Message
2 |
3 | This object represents the [Message Type](https://core.telegram.org/bots/api#message) in the Bot API.
4 |
5 | ### Type
6 | Each message has a type. For example, it can be a text message, a picture or successful payment. To see a full list of
7 | message types check the `message_types` module from `django_tgbot.state_manager.message_types`.
8 |
9 | This class has a `type` method that will give you the type of this message. The returned value will be one of the
10 | available values in the `message_types` module. You can check the type by checking the string values but in order to avoid
11 | errors it is recommended to use the `message_types` module:
12 |
13 | ```python
14 | from django_tgbot.state_manager import message_types
15 | from django_tgbot.types.message import Message
16 |
17 | message: Message = ...
18 |
19 | type = message.type()
20 |
21 | if type == message_types.Game:
22 | print('Message is a Game!')
23 | elif type == message_types.Location:
24 | print('Message is a location')
25 | elif
26 | ...
27 | ```
28 |
29 | These are all of the available message types at the moment:
30 |
31 | * **Text **
32 | * **Audio **
33 | * **Document **
34 | * **Animation **
35 | * **Game **
36 | * **Photo **
37 | * **Sticker **
38 | * **Video **
39 | * **Voice **
40 | * **VideoNote **
41 | * **Contact **
42 | * **Location **
43 | * **Venue **
44 | * **Poll **
45 | * **NewChatMembers **
46 | * **LeftChatMembers **
47 | * **NewChatTitle **
48 | * **NewChatPhoto **
49 | * **DeleteChatPhoto **
50 | * **GroupChatCreated **
51 | * **SupergroupChatCreated **
52 | * **ChannelChatCreated **
53 | * **MigrateToChatId **
54 | * **MigrateFromChatId **
55 | * **PinnedMessage **
56 | * **Invoice **
57 | * **SuccessfulPayment **
58 | * **PassportData **
--------------------------------------------------------------------------------
/django_tgbot/state_manager/state.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.state_manager import state_types
2 |
3 |
4 | class State:
5 | def __init__(self, waiting_for=None, message_types=None, update_types=None, exclude_message_types=None,
6 | exclude_update_types=None):
7 | """
8 | Leave message_types / update_types for accepting all.
9 | :param waiting_for: the user's state in database
10 | :param message_types: accepted message_types
11 | :param update_types: accepted update_types
12 | """
13 | if message_types in ['all', '*', state_types.All]:
14 | message_types = None
15 |
16 | if update_types in ['all', '*', state_types.All]:
17 | update_types = None
18 |
19 | if waiting_for is None:
20 | waiting_for = ['', None]
21 | elif waiting_for in ['*', state_types.All]:
22 | waiting_for = ['*']
23 |
24 | args = locals()
25 | for var in args.keys():
26 | if var == 'self':
27 | continue
28 |
29 | result = args[var]
30 | if args[var] is None:
31 | result = []
32 | elif type(args[var]) == str:
33 | result = [args[var]]
34 |
35 | if type(result) != list:
36 | raise ValueError("Type of `{}` should be list".format(var))
37 |
38 | setattr(self, var, result)
39 |
40 | def matches(self, waiting_for, update_type, message_type=None):
41 | try:
42 | return (waiting_for in self.waiting_for or self.waiting_for == ['*']) and \
43 | (message_type is None or message_type in self.message_types or self.message_types == []) and \
44 | (message_type is None or message_type not in self.exclude_message_types) and \
45 | (update_type in self.update_types or self.update_types == []) and \
46 | (update_type not in self.exclude_update_types)
47 | except AttributeError:
48 | return False
49 |
--------------------------------------------------------------------------------
/django_tgbot/management/commands/tgbotwebhook.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.conf import settings
4 | from django.core.management.base import BaseCommand
5 |
6 | from django_tgbot.management import helpers
7 |
8 | import importlib.util
9 |
10 |
11 | class Command(BaseCommand):
12 | help = 'Updates the webhook address for an existing tgbot'
13 |
14 | def handle(self, *args, **options):
15 | bot_username = input('Enter the username of the bot (without @): ').lower()
16 | dst = os.path.join(settings.BASE_DIR, bot_username)
17 | credentials_file = os.path.join(dst, 'credentials.py')
18 |
19 | if not os.path.isfile(credentials_file):
20 | self.stdout.write(
21 | self.style.ERROR('No such bot found. Make sure you have created your bot with command `createtgbot`.')
22 | )
23 | return
24 |
25 | spec = importlib.util.spec_from_file_location("credentials", credentials_file)
26 | credentials_module = importlib.util.module_from_spec(spec)
27 | spec.loader.exec_module(credentials_module)
28 |
29 | bot_token = credentials_module.BOT_TOKEN
30 |
31 | again = True
32 | while again:
33 | choice = input("Please choose from the options:\n"
34 | " 1. Set the project URL and use default webhooks\n"
35 | " 2. Set a custom webhook address for this bot\n")
36 | try:
37 | if int(choice) == 1:
38 | helpers.prompt_project_url(self, bot_token, bot_username)
39 | again = False
40 | elif int(choice) == 2:
41 | helpers.prompt_webhook(self, bot_token, bot_username)
42 | again = False
43 | else:
44 | self.stdout.write('Did not recognize choice. Try again: ')
45 | again = True
46 | except ValueError:
47 | self.stdout.write('Respond only with the number of your selected option: ')
48 | again = True
49 |
--------------------------------------------------------------------------------
/django_tgbot/state_manager/transition_condition.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.state_manager import state_types
2 |
3 |
4 | class TransitionCondition:
5 | def __init__(self, from_states=None, message_types=None, update_types=None, exclude_message_types=None,
6 | exclude_update_types=None):
7 | """
8 | Leave message_types / update_types for accepting all.
9 | :param from_states: the user's state in database
10 | :param message_types: accepted message_types
11 | :param update_types: accepted update_types
12 | """
13 | if message_types in ['all', '*', state_types.All]:
14 | message_types = None
15 |
16 | if update_types in ['all', '*', state_types.All]:
17 | update_types = None
18 |
19 | if from_states is None or from_states == state_types.Reset:
20 | from_states = ['', None]
21 | elif from_states in ['*', state_types.All]:
22 | from_states = ['*']
23 |
24 | args = locals()
25 | for var in args.keys():
26 | if var == 'self':
27 | continue
28 |
29 | result = args[var]
30 | if args[var] is None:
31 | result = []
32 | elif type(args[var]) == str:
33 | result = [args[var]]
34 |
35 | if type(result) != list:
36 | raise ValueError("Type of `{}` should be list".format(var))
37 |
38 | setattr(self, var, result)
39 |
40 | def matches(self, state_name, update_type, message_type=None):
41 | try:
42 | return (state_name in self.from_states or self.from_states == ['*']) and \
43 | (message_type is None or message_type in self.message_types or self.message_types == []) and \
44 | (message_type is None or message_type not in self.exclude_message_types) and \
45 | (update_type in self.update_types or self.update_types == []) and \
46 | (update_type not in self.exclude_update_types)
47 | except AttributeError:
48 | return False
49 |
--------------------------------------------------------------------------------
/docs/getting_updates.md:
--------------------------------------------------------------------------------
1 | # Getting Updates
2 |
3 | There are 2 ways you can get updates from Telegram API: Webhooks and Polling.
4 |
5 | ### Webhook
6 | In order to use webhooks for getting updates from Telegram API, you need to have your project
7 | running and accessible with a public address. If you have not yet deployed your project publicly and it is
8 | only running on localhost, you can still use [Ngrok](http://ngrok.com) to make your local run accessible publicly.
9 |
10 | If you have followed the steps explained in the setup, all you need to do in order to create the webhook
11 | is to provide the public address your bot is running on (either your public host or Ngrok).
12 |
13 | You will be asked to provide this address once you are setting up your bot. If you have skipped that part
14 | or you are changing the address, you can still use manage.py commands to set the webhook address.
15 |
16 | Open a terminal and write command:
17 | ```plain
18 | python3 manage.py tgbotwebhook
19 | ```
20 |
21 | You will be asked to enter your bot's username. After you enter the username you will be asked about
22 | whether you are going to provide the project address (so the webhook will be set accordingly automatically) or
23 | you would like to enter the custom address yourself.
24 |
25 | The last step is to just enter the address (either the project address or your custom webhook address) and the webhook
26 | will be set.
27 |
28 | ### Polling
29 | Please note that this is still not suitable to be used in production and should only be used for local testing purposes.
30 |
31 | You can also choose not to use webhooks and instead, load the updates yourself whenever you need them.
32 | In order to do that, all you need to do is run the project (`python3 manage.py runserver`) then open this address in your browser:
33 | ```plain
34 | 127.0.0.1:8000/[BOT_USERNAME]/poll/
35 | ```
36 | Note that you should replace `8000` with the port you are using and `[BOT_USERNAME]` with your bot's username.
37 |
38 | This will fetch all of the pending updates from Telegram API and handle them. It will then print on the screen
39 | the number of updates that were handled. Whenever you want to handle new waiting updates, refresh the page.
--------------------------------------------------------------------------------
/django_tgbot/types/inputmessagecontent.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 |
5 |
6 | class InputMessageContent(BasicType):
7 | def __init__(self, obj=None):
8 | super(InputMessageContent, self).__init__(obj)
9 |
10 |
11 | class InputTextMessageContent(InputMessageContent):
12 | fields = {
13 | 'message_text': str,
14 | 'parse_mode': str,
15 | 'disable_web_page_preview': BasicType.bool_interpreter
16 | }
17 |
18 | def __init__(self, obj=None):
19 | super(InputTextMessageContent, self).__init__(obj)
20 |
21 | @classmethod
22 | def a(cls, message_text: str,
23 | parse_mode: Optional[str] = None, disable_web_page_preview: Optional[bool] = None):
24 | return super().a(**locals())
25 |
26 |
27 | class InputLocationMessageContent(InputMessageContent):
28 | fields = {
29 | 'latitude': str,
30 | 'longitude': str,
31 | 'live_period': int
32 | }
33 |
34 | def __init__(self, obj=None):
35 | super(InputLocationMessageContent, self).__init__(obj)
36 |
37 | @classmethod
38 | def a(cls, latitude: str, longitude: str, live_period: Optional[int] = None):
39 | return super().a(**locals())
40 |
41 |
42 | class InputVenueMessageContent(InputMessageContent):
43 | fields = {
44 | 'latitude': str,
45 | 'longitude': str,
46 | 'title': str,
47 | 'address': str,
48 | 'foursquare_id': str,
49 | 'foursquare_type': str
50 | }
51 |
52 | def __init__(self, obj=None):
53 | super(InputVenueMessageContent, self).__init__(obj)
54 |
55 | @classmethod
56 | def a(cls, latitude: str, longitude: str, title: str, address: str, foursquare_id: Optional[str] = None,
57 | foursquare_type: Optional[str] = None):
58 | return super().a(**locals())
59 |
60 |
61 | class InputContactMessageContent(InputMessageContent):
62 | fields = {
63 | 'phone_number': str,
64 | 'first_name': str,
65 | 'last_name': str,
66 | 'vcard': str
67 | }
68 |
69 | def __init__(self, obj=None):
70 | super(InputContactMessageContent, self).__init__(obj)
71 |
72 | @classmethod
73 | def a(cls, phone_number: str, first_name: str, last_name: Optional[str] = None, vcard: Optional[str] = None):
74 | return super().a(**locals())
75 |
76 |
77 |
--------------------------------------------------------------------------------
/docs/types/README.md:
--------------------------------------------------------------------------------
1 | # Types
2 |
3 | [Telegram Bot API](https://core.telegram.org/bots/api) uses several types for communications with the clients. All of them
4 | (with regard to the API version mentioned in this doc) are implemented in this package. You can find a complete list of available
5 | types in the API [here](https://core.telegram.org/bots/api#available-types).
6 |
7 | Examples of Types are: Update, Message, User, Chat, etc.
8 |
9 | ### Constructor
10 | All of the types have their default constructor taking an object holding information about this object. This object can be a dict
11 | or a JSON object. This constructor is designed to load the information from this object, which will be given to it by the Telegram API,
12 | Bot class and other Types.
13 |
14 | If you want to create an object of a Type, you should use the classmethod `a`. All of the Types have this method and they
15 | accept the exact same arguments as that type Fields (explained later in this document). You can see these arguments either from the
16 | [Telegram Bot API docs](https://core.telegram.org/bots/api#available-types) or from the `fields` attribute of that class.
17 |
18 | The `a` method will do some basic validations for the arguments you pass and will return an instance of the Type.
19 |
20 | Moreover, some of the more commonly used Types have the `a` method implemented inside their body with type hints in order to make it
21 | easier to use when working with a modern IDE.
22 |
23 | ### Fields
24 | Each Type has some fields with exact same names as they have in the API docs. The definitions of these fields can be seen
25 | from the `fields` attribute of the class or the API docs. When using the default constructor, the `parse_fields` method will
26 | read these fields from the given object. They will be set as an attribute to the object if and only if they are present in the given object.
27 |
28 | Basic validation when you are using the `a` method will use the types given in this attribute, unless you set `validation` equal
29 | to `False`.
30 |
31 | For more commonly used fields of the Types there is a `get_` method defined which you can use. If the field you
32 | want to use does not have this method you can still access it directly.
33 |
34 | For example if you want the message text from an update object you can do:
35 | ```python
36 | text = update_obj.get_message().get_text()
37 | ```
38 | which will use the `get_message` method of the update object and then the `get_text` method of the message object.
39 |
40 | You may, however, want to access the fields like this:
41 | ```python
42 | text = update_obj.message.text
43 | ```
44 |
45 |
--------------------------------------------------------------------------------
/django_tgbot/decorators.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.exceptions import ProcessFailure
2 | from django_tgbot.state_manager import state_types
3 | from django_tgbot.state_manager.transition_condition import TransitionCondition
4 | import inspect
5 | from django_tgbot.state_manager.state_manager import StateManager
6 |
7 |
8 | def processor(manager: StateManager, from_states=None, message_types=None, update_types=None,
9 | exclude_message_types=None, exclude_update_types=None, success=None, fail=None):
10 | def state_registrar(func):
11 | if func is None:
12 | raise ValueError("Passed processor is None.")
13 | all_args = inspect.getfullargspec(func)
14 | if not all([
15 | x in all_args[0] for x in ['bot', 'update', 'state']
16 | ]):
17 | raise ValueError("Passed processor does not have a valid signature.")
18 |
19 | def function_runner(bot, update, state, *args, **kwargs):
20 | current_state = state.name
21 | try:
22 | func(bot=bot, update=update, state=state, *args, **kwargs)
23 | if success == state_types.Reset:
24 | state.name = ''
25 | state.save()
26 | elif success == state_types.Keep:
27 | state.name = current_state
28 | state.save()
29 | elif success is not None:
30 | state.name = success
31 | state.save()
32 | except ProcessFailure:
33 | if fail == state_types.Reset:
34 | state.name = ''
35 | state.save()
36 | elif fail == state_types.Keep:
37 | state.name = current_state
38 | state.save()
39 | elif fail is not None:
40 | state.name = fail
41 | state.save()
42 |
43 | altered_message_types = message_types
44 | altered_update_types = update_types
45 |
46 | if altered_message_types is None:
47 | altered_message_types = manager.default_message_types
48 | if altered_update_types is None:
49 | altered_update_types = manager.default_update_types
50 |
51 | manager.register_state(TransitionCondition(
52 | from_states=from_states,
53 | message_types=altered_message_types,
54 | exclude_message_types=exclude_message_types,
55 | update_types=altered_update_types,
56 | exclude_update_types=exclude_update_types,
57 | ), processor=function_runner)
58 |
59 | return function_runner
60 |
61 | return state_registrar
62 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Django Telegram Bots
2 |
3 | This package can be used to have [Telegram Bots](https://core.telegram.org/bots) in your Django project.
4 | It is designed in a way allowing you to have multiple number of bots in the same project.
5 |
6 | Telegram Bot API version: 4.9
7 |
8 | 
9 |
10 |
11 | ## Setup
12 | 1. Install package from pip:
13 |
14 | pip install django-tgbot
15 |
16 |
17 | 2. Add `django_tgbot` to your Django project's `INSTALLED_APPS`
18 |
19 |
20 |
21 | ## Definitions
22 | It is important to understand these definitions in order to read the rest of the doc or to use the package.
23 |
24 | ##### Client
25 | Anything that can send messages or updates to the bot is called a **client**. This can be a user, another bot or a channel.
26 | Users can send messages to the bot in private chats or groups, bots can send messages in groups and channels can send messages in
27 | channels (of which our bot is a member). So when we talk about a **client** we are talking about one of these three.
28 |
29 | ##### Telegram State
30 |
31 | A client can be working with the bot in different settings. For example, a user can send messages to the bot in a private chat or in a group
32 | or different groups. A bot can send messages visible to our bot in different groups. These should be handled separately. If a user is working
33 | with the bot in 2 groups at the same time, we do not want their interactions with the bot in one group to interfere with their interactions
34 | with the bot in the other group or in the private chat. Here comes the **Telegram State**.
35 |
36 | **Telegram State** holds information about a client, the chat in which they are using the bot and some other auxiliary data
37 | for helping the bot to handle updates. These data are stored in the database under the model `TelegramState`. Please read its full documentation
38 | from [here](models/telegram_state.md).
39 |
40 | ##### Processor
41 | Whenever an update is received from Telegram, a `Telegram State` object will be assigned to it. One will be created if there is not any for this
42 | particular client and chat. Now we have a bot, an update to process and a state holding information about the client. It is time to process all of these
43 | information. A **processor** is a function that takes these 3 object (namely the bot, the update and the state) and processes it. Processors can respond
44 | to an update in whatever way they want. They can modify the state, they can send a message to this client or they can do nothing. Please read full documentation
45 | of **processors** from [here](processors.md).
46 |
47 | Let's have a look at how to [Create a new bot with django-tgbot](management_commands/createtgbot.md) next.
--------------------------------------------------------------------------------
/django_tgbot/management/bot_template/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2021-03-15 06:51
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='TelegramChat',
17 | fields=[
18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19 | ('telegram_id', models.CharField(max_length=128, unique=True)),
20 | ('type', models.CharField(choices=[('private', 'private'), ('group', 'group'), ('supergroup', 'supergroup'), ('channel', 'channel')], max_length=128)),
21 | ('title', models.CharField(blank=True, max_length=512, null=True)),
22 | ('username', models.CharField(blank=True, max_length=128, null=True)),
23 | ],
24 | options={
25 | 'abstract': False,
26 | },
27 | ),
28 | migrations.CreateModel(
29 | name='TelegramUser',
30 | fields=[
31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32 | ('telegram_id', models.CharField(max_length=128, unique=True)),
33 | ('is_bot', models.BooleanField(default=False)),
34 | ('first_name', models.CharField(max_length=128)),
35 | ('last_name', models.CharField(blank=True, max_length=128, null=True)),
36 | ('username', models.CharField(blank=True, max_length=128, null=True)),
37 | ],
38 | options={
39 | 'abstract': False,
40 | },
41 | ),
42 | migrations.CreateModel(
43 | name='TelegramState',
44 | fields=[
45 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
46 | ('memory', models.TextField(blank=True, null=True, verbose_name='Memory in JSON format')),
47 | ('name', models.CharField(blank=True, max_length=256, null=True)),
48 | ('telegram_chat', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='telegram_states', to='_BOT_USERNAME.telegramchat')),
49 | ('telegram_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='telegram_states', to='_BOT_USERNAME.telegramuser')),
50 | ],
51 | options={
52 | 'unique_together': {('telegram_user', 'telegram_chat')},
53 | },
54 | ),
55 | ]
56 |
--------------------------------------------------------------------------------
/django_tgbot/management/commands/createtgbot.py:
--------------------------------------------------------------------------------
1 | import errno
2 | import os
3 | import shutil
4 |
5 | from django.conf import settings
6 | from django.core.management.base import BaseCommand
7 |
8 | from django_tgbot.management import helpers
9 |
10 |
11 | class Command(BaseCommand):
12 | help = 'Creates a new Telegram bot'
13 |
14 | def handle(self, *args, **options):
15 |
16 | get_me_result, bot_token = helpers.prompt_token(self)
17 | bot_name = str(get_me_result.get_first_name())
18 | bot_username = str(get_me_result.get_username()).lower()
19 |
20 | self.stdout.write('Setting up @{} ...'.format(bot_username))
21 |
22 | helpers.prompt_project_url(self, bot_token, bot_username)
23 |
24 | dst = os.path.join(settings.BASE_DIR, bot_username)
25 | if os.path.isdir(dst):
26 | self.stdout.write(self.style.ERROR('Directory `{}` already exists.'.format(dst)))
27 | return
28 |
29 | src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'bot_template')
30 | try:
31 | shutil.copytree(src, dst)
32 | except OSError as exc:
33 | if exc.errno == errno.ENOTDIR:
34 | shutil.copy(src, dst)
35 | else:
36 | self.stdout.write(self.style.ERROR("Couldn't copy bot files."))
37 | return
38 |
39 | with open(os.path.join(dst, 'credentials.py'), 'w') as f:
40 | creds_help_text_lines = [
41 | "# Do not remove these 2 lines:",
42 | f"BOT_TOKEN = '{bot_token}' # You should consider using env variables or a secret manager for this.",
43 | f"APP_NAME = '{bot_username}'"
44 | ]
45 | f.write('\n'.join(creds_help_text_lines))
46 |
47 | with open(os.path.join(dst, '__init__.py'), 'w') as f:
48 | f.write(
49 | "from . import credentials\n\n\nbot_token = credentials.BOT_TOKEN\napp_name = credentials.APP_NAME\n"
50 | )
51 |
52 | with open(os.path.join(dst, os.path.join('migrations', '0001_initial.py')), 'r') as f:
53 | migration = f.read().replace('_BOT_USERNAME', bot_username)
54 | with open(os.path.join(dst, os.path.join('migrations', '0001_initial.py')), 'w') as f:
55 | f.write(migration)
56 |
57 | self.stdout.write(self.style.SUCCESS('Successfully created bot {}(@{}).'.format(bot_name, bot_username)))
58 |
59 | self.stdout.write("Next steps:")
60 | self.stdout.write("\t1. Add '{}' to INSTALLED_APPS in project settings".format(bot_username))
61 | self.stdout.write("\t2. Add `from {} import urls as {}_urls` to project urls file".format(bot_username, bot_username))
62 | self.stdout.write("\t3. Add `path('{}/', include({}_urls))` to project urls' urlpatterns".format(bot_username, bot_username))
63 | self.stdout.write("\t4. `python manage.py migrate`")
64 | self.stdout.write("Enjoy!")
65 |
--------------------------------------------------------------------------------
/django_tgbot/management/helpers.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.bot_api_user import BotAPIUser
2 | from django_tgbot.types.user import User
3 |
4 |
5 | def validate_token(bot_token):
6 | """
7 | If False, the second returned value is:
8 | 0 if token is not valid
9 | 1 if internet connection is not established
10 | If True, the second argument will be getMe result
11 | """
12 | api_user = BotAPIUser(bot_token)
13 | get_me_result = api_user.getMe()
14 | if type(get_me_result) != User:
15 | if 'no_connection' not in get_me_result.keys():
16 | return False, 0
17 | else:
18 | return False, 1
19 | return True, get_me_result
20 |
21 |
22 | def prompt_token(command_line, prompt_message='Enter the bot token (retrieved from BotFather): '):
23 | """
24 | Returns either None or getMe result
25 | """
26 | bot_token = input(prompt_message)
27 | validation = validate_token(bot_token)
28 | while not validation[0]:
29 | if validation[1] == 0:
30 | bot_token = input("Bot token is not valid. Please enter again: ")
31 | validation = validate_token(bot_token)
32 | elif validation[1] == 1:
33 | command_line.stdout.write(
34 | command_line.style.ERROR(
35 | "Connection failed. You need to be connected to the internet to run this command."
36 | )
37 | )
38 | return None, bot_token
39 | return validation[1], bot_token
40 |
41 |
42 | def set_webhook(bot_token, url):
43 | api_user = BotAPIUser(bot_token)
44 | res = api_user.setWebhook(url)
45 | return res
46 |
47 |
48 | def prompt_webhook(command_line, bot_token, bot_username):
49 | webhook_url = input("Enter the url to be set as webhook for @{}: ".format(bot_username))
50 | res = set_webhook(bot_token, webhook_url)
51 | if res['ok']:
52 | command_line.stdout.write(command_line.style.SUCCESS("Successfully set webhook."))
53 | else:
54 | command_line.stdout.write(command_line.style.WARNING("Couldn't set webhook:\n{}".format(res['description'])))
55 |
56 |
57 | def prompt_project_url(command_line, bot_token, bot_username):
58 | project_url = input("Enter the url of this project to set the webhook (Press Enter to skip): ")
59 | while len(project_url) > 0 and project_url[-1] == '/':
60 | project_url = project_url[:-1]
61 | if project_url != '':
62 | confirmed = False
63 | while not confirmed:
64 | confirmed_text = input("Bot webhook will be set to {}/{}/update/. Do you confirm? (Y/N): ".format(
65 | project_url,
66 | bot_username
67 | ))
68 | confirmed = confirmed_text.lower() in ['yes', 'y']
69 | if not confirmed:
70 | project_url = input("Enter the correct url: ")
71 | while len(project_url) > 0 and project_url[-1] == '/':
72 | project_url = project_url[:-1]
73 |
74 | res = set_webhook(bot_token, "{}/{}/update/".format(project_url, bot_username))
75 | if res['ok']:
76 | command_line.stdout.write(command_line.style.SUCCESS("Successfully set webhook."))
77 | else:
78 | command_line.stdout.write(command_line.style.WARNING("Couldn't set webhook:\n{}".format(res['description'])))
79 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to the package
2 |
3 | Thank you for your interest in making this package better for everyone!
4 |
5 | Please open your PRs to the **dev** branch. If you have your own ideas for improvement that's awesome go for it. If not,
6 | here's a list of features requested by people throughout the time that you could work on (in the order of priority):
7 | - A `processor` should be able to terminate the processing flow - meaning that it could prevent other processors from being run.
8 | This is particularly useful for when you want to handle exception cases and you don't want to have the exception scenario being checked in every processor.
9 | E.g. you want to cancel whatever process the user is undertaking if they enter `/cancel` - no more processors should run.
10 | - Catch up with Telegram API changes (we are currently upto V4.9, [Telegram's changelog](https://core.telegram.org/bots/api-changelog) could be used to see what features
11 | have been added ever since.)
12 | - Examples are a great way for people to quickly get the idea of how the package works and create more bots. I have
13 | created a few example bots in a separate repo, if you have developed bots (or are willing to do so, maybe as part of
14 | your acquaintance process with the package) please add them to that repo so other people could learn faster. This is the demo
15 | repo: https://github.com/Ali-Toosi/django-tgbot_demo
16 | - An additional filter could be implemented on `processor`s to allow only certain chats (i.e. certain Telegram ids) to enter a processor.
17 | - Add a dictionary of words for predefined texts (so instead of writing your own message to be sent in each processor,
18 | create a list of them somewhere else and only refer to them whenever needed). This helps a lot with changing your bot's messages
19 | without needing to go too deep inside the code. (Could potentially use something like Django's translation system as well.)
20 | - Create a dictionary of emojis so the developer doesn't have to copy paste it from somewhere else inside the code but just
21 | refer to them with their code/slug.
22 | - Add some automated tests to the package so we can make proceed with more contributions more confidently. (There are 0
23 | automated tests in the code right now so no matter how many you add it would be a great contribution.)
24 | - Descriptions of the manage.py commands for setting the bot web-hook are not completely clear to everyone.
25 | - Project docs only scratch the surface of all the things that could be done and don't cover everything. If you have worked with the package for some time and
26 | have found areas not covered in the docs, that would be great to write up something for them.
27 | - As of now, the bot only processes user's requests and answers them. A mechanism for sending messages to all users or a group
28 | of users who have used the bot in the past would be cool. Read this issue for more context and also a suggestion of how it can
29 | be implemented. ([related issue](https://github.com/Ali-Toosi/django-tgbot_demo/issues/2))
30 | - Although "commands" are basically text messages that start with `/`, some sort of special support for them could be added.
31 | ([related issue](https://github.com/Ali-Toosi/django-tgbot/issues/9))
32 |
33 | You could also join our Telegram group to have a discussion about these: https://t.me/DjangoTGBotChat
34 |
--------------------------------------------------------------------------------
/docs/management_commands/createtgbot.md:
--------------------------------------------------------------------------------
1 | # Creating a bot
2 |
3 | This package comes with a management command (namely `createtgbot`) to create new bots. Each bot will be an app in your Django project.
4 |
5 | This app created for a new bot you create will be a simple bot that responds to all messages by `Hello`.
6 |
7 | ### Step by step guide
8 | 1. Create a bot in Telegram using [BotFather](https://t.me/BotFather) and receive your API token
9 | 2. Open the Django project with `django-tgbot` installed in it
10 | 3. Enter this command in the command line (terminal / cmd):
11 |
12 | python manage.py createtgbot
13 |
14 | 4. Enter your API token:
15 |
16 | > python manage.py createtgbot
17 | Enter the bot token (retrieved from BotFather):
18 | Setting up @BotDevTestBot ...
19 |
20 | 5. Enter the URL your Django project is deployed on. If your project is not deployed yet and is not accessible, press Enter to skip. (If you have not deployed yet and want to test your bot, you can use services like [Ngrok](http://ngrok.com) to do so)
21 |
22 | Enter the url of this project to set the webhook (Press Enter to skip): https://URL.com
23 | Bot webhook will be set to https://URL.com/botdevtestbot/update/. Do you confirm? (Y/N): y
24 | Webhook was successfully set.
25 |
26 |
27 | 6. A new app will be created in your Django project. Add this app to your `INSTALLED_APPS`
28 | 7. Include this new app's urls in your project urls as described in the output of the above command
29 | 8. Update the database:
30 |
31 | python manage.py migrate
32 |
33 |
34 | Your bot is created! If you have set the webhook correctly you can now send messages to your bot and it responds all messages with `Hello!`.
35 |
36 | Overview of the process:
37 |
38 | ```
39 | > python manage.py createtgbot
40 | Enter the bot token (retrieved from BotFather): 521093911:AAEe6X-KTJHO98tK2skJLsYJsE7NRpjL8Ic
41 | Setting up @BotDevTestBot ...
42 | Enter the url of this project to set the webhook (Press Enter to skip): https://URL.com
43 | Bot webhook will be set to https://URL.com/botdevtestbot/update/. Do you confirm? (Y/N): y
44 | Webhook was successfully set.
45 | Successfully created bot Test Bot(@botdevtestbot).
46 | Next steps:
47 | 1. Add 'botdevtestbot' to INSTALLED_APPS in project settings
48 | 2. Add `from botdevtestbot import urls as botdevtestbot_urls` to project urls file
49 | 3. Add `path('botdevtestbot/', include(botdevtestbot_urls))` to project urls' urlpatterns
50 | 4. `python manage.py migrate`
51 | Enjoy!
52 | ```
53 |
54 |
55 |
56 | This app contains 3 models, inherited from base models in the package and the migration file is included in the app. You only need to `migrate`.
57 |
58 | Handling Telegram bots does not require much use of `views` and `urls`. A simple url and a view has been created for your bot in this app and you do not need to add anything else.
59 | However, you may add any urls or views in order to extend the functionality of your bot (e.g. adding a custom admin panel).
60 |
61 | There will be 2 other files in your bot's app: `processors.py` and `bot.py`. The first one is where you will put most of your code and it will be
62 | the core processing unit of your bot. The latter is an interface for working with the Bot API.
63 |
64 | Continue to:
65 |
66 | * [The Bot class](../classes/bot.md)
67 | * [Processors](../processors.md)
68 |
69 |
--------------------------------------------------------------------------------
/django_tgbot/bot.py:
--------------------------------------------------------------------------------
1 | from django_tgbot.bot_api_user import BotAPIUser
2 | from django_tgbot.types.update import Update
3 |
4 |
5 | class AbstractTelegramBot(BotAPIUser):
6 | def __init__(self, token, state_manager):
7 | super(AbstractTelegramBot, self).__init__(token)
8 | self.state_manager = state_manager
9 |
10 | def handle_update(self, update: Update):
11 | user = update.get_user()
12 | chat = update.get_chat()
13 |
14 | if user is not None:
15 | db_user = self.get_db_user(user.get_id())
16 | else:
17 | db_user = None
18 |
19 | if chat is not None:
20 | db_chat = self.get_db_chat(chat.get_id())
21 | else:
22 | db_chat = None
23 |
24 | db_state = self.get_db_state(db_user, db_chat)
25 |
26 | self.pre_processing(
27 | update,
28 | chat=chat,
29 | db_chat=db_chat,
30 | user=user,
31 | db_user=db_user,
32 | state=db_state
33 | )
34 |
35 | processors = self.state_manager.get_processors(update, db_state)
36 |
37 | for processor in processors:
38 | processor(self, update, db_state)
39 |
40 | self.post_processing(
41 | update,
42 | chat=chat,
43 | db_chat=db_chat,
44 | user=user,
45 | db_user=db_user,
46 | state=db_state
47 | )
48 |
49 | def poll_updates_and_handle(self):
50 | updates = self.getUpdates()
51 | offset = None
52 | total_count = 0
53 | while len(updates) > 0:
54 | total_count += len(updates)
55 | for update in updates:
56 | self.handle_update(update)
57 | offset = int(update.get_update_id()) + 1
58 | updates = self.getUpdates(offset=offset)
59 | return total_count
60 |
61 | def pre_processing(self, update: Update, user, db_user, chat, db_chat, state):
62 | if db_user is not None:
63 | db_user.first_name = user.get_first_name()
64 | db_user.last_name = user.get_last_name()
65 | db_user.username = user.get_username()
66 | db_user.save()
67 |
68 | if db_chat is not None:
69 | db_chat.type = chat.get_type()
70 | db_chat.username = chat.get_username()
71 | db_chat.title = chat.get_title()
72 | db_chat.save()
73 |
74 | def post_processing(self, update: Update, user, db_user, chat, db_chat, state):
75 | pass
76 |
77 | def get_db_user(self, telegram_id):
78 | """
79 | Should be implemented - Creates or retrieves the user object from database
80 | :param telegram_id: The telegram user's id
81 | :return: User object from database
82 | """
83 | pass
84 |
85 | def get_db_chat(self, telegram_id):
86 | """
87 | Should be implemented - Creates or retrieves the chat object from database
88 | :param telegram_id: The telegram chat's id
89 | :return: Chat object from database
90 | """
91 | pass
92 |
93 | def get_db_state(self, db_user, db_chat):
94 | """
95 | Should be implemented - Creates or retrieves a state object in the database for this user and chat
96 | :param db_user: The user creating this state for
97 | :param db_chat: The related chat
98 | :return: a state object from database
99 | """
100 | pass
101 |
--------------------------------------------------------------------------------
/django_tgbot/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.contenttypes.fields import GenericForeignKey
2 | from django.contrib.contenttypes.models import ContentType
3 | import json
4 | from django.db import models
5 | from django.db.models import CASCADE
6 |
7 |
8 | class AbstractTelegramChat(models.Model):
9 | """
10 | Represents a `chat` type in Bot API:
11 | https://core.telegram.org/bots/api#chat
12 | """
13 | CHAT_TYPES = (
14 | ("private", "private"),
15 | ("group", "group"),
16 | ("supergroup", "supergroup"),
17 | ("channel", "channel")
18 | )
19 |
20 | telegram_id = models.CharField(max_length=128, unique=True)
21 | type = models.CharField(choices=CHAT_TYPES, max_length=128)
22 | title = models.CharField(max_length=512, null=True, blank=True)
23 | username = models.CharField(max_length=128, null=True, blank=True)
24 |
25 | def is_private(self):
26 | return self.type == self.CHAT_TYPES[0][0]
27 |
28 | class Meta:
29 | abstract = True
30 |
31 | def __str__(self):
32 | return "{} ({})".format(self.title, self.telegram_id)
33 |
34 |
35 | class AbstractTelegramUser(models.Model):
36 | """
37 | Represented a `user` type in Bot API:
38 | https://core.telegram.org/bots/api#user
39 | """
40 | telegram_id = models.CharField(max_length=128, unique=True)
41 | is_bot = models.BooleanField(default=False)
42 | first_name = models.CharField(max_length=128)
43 | last_name = models.CharField(max_length=128, null=True, blank=True)
44 | username = models.CharField(max_length=128, null=True, blank=True)
45 |
46 | def get_chat_state(self, chat: AbstractTelegramChat):
47 | state, _ = AbstractTelegramState.objects.get_or_create(
48 | telegram_user__telegram_id=self.telegram_id,
49 | telegram_chat__telegram_id=chat.telegram_id
50 | )
51 | return state
52 |
53 | def get_private_chat_state(self):
54 | state, _ = AbstractTelegramState.objects.get_or_create(
55 | telegram_user__telegram_id=self.telegram_id,
56 | telegram_chat__telegram_id=self.telegram_id
57 | )
58 | return state
59 |
60 | class Meta:
61 | abstract = True
62 |
63 | def __str__(self):
64 | return "{} {} (@{})".format(self.first_name, self.last_name, self.username)
65 |
66 |
67 | class AbstractTelegramState(models.Model):
68 | memory = models.TextField(null=True, blank=True, verbose_name="Memory in JSON format")
69 | name = models.CharField(max_length=256, null=True, blank=True)
70 |
71 | class Meta:
72 | abstract = True
73 |
74 | def get_memory(self):
75 | """
76 | Gives a python object as the memory for this state.
77 | Use the set_memory method to set an object as memory. If invalid JSON used as memory, it will be cleared
78 | upon calling this method.
79 | """
80 | if self.memory in [None, '']:
81 | return {}
82 | try:
83 | return json.loads(self.memory)
84 | except ValueError:
85 | self.memory = ''
86 | self.save()
87 | return {}
88 |
89 | def set_memory(self, obj):
90 | """
91 | Sets a python object as memory for the state, in JSON format.
92 | If given object cannot be converted to JSON, function call will be ignored.
93 | :param obj: The memory object to be stored
94 | """
95 | try:
96 | self.memory = json.dumps(obj)
97 | self.save()
98 | except ValueError:
99 | pass
100 |
101 | def reset_memory(self):
102 | """
103 | Resets memory
104 | """
105 | self.set_memory({})
106 |
107 | def update_memory(self, obj):
108 | """
109 | Updates the memory in the exact way a Python dictionary is updated. New keys will be added and
110 | existing keys' value will be updated.
111 | :param obj: The dictionary to update based on
112 | """
113 | if type(obj) != dict:
114 | raise ValueError("Passed object should be type of dict")
115 | memory = self.get_memory()
116 | memory.update(obj)
117 | self.set_memory(memory)
118 |
119 | def set_name(self, name):
120 | self.name = name
121 | self.save()
122 |
--------------------------------------------------------------------------------
/django_tgbot/types/update.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from django_tgbot.state_manager import update_types
4 | from django_tgbot.types import chat
5 | from . import BasicType
6 | from . import message, inlinequery, choseninlineresult, callbackquery, shippingquery, precheckoutquery, poll, pollanswer
7 |
8 |
9 | class Update(BasicType):
10 | fields = {
11 | 'update_id': str,
12 | 'message': message.Message,
13 | 'edited_message': message.Message,
14 | 'channel_post': message.Message,
15 | 'edited_channel_post': message.Message,
16 | 'inline_query': inlinequery.InlineQuery,
17 | 'chosen_inline_result': choseninlineresult.ChosenInlineResult,
18 | 'callback_query': callbackquery.CallbackQuery,
19 | 'shipping_query': shippingquery.ShippingQuery,
20 | 'pre_checkout_query': precheckoutquery.PreCheckoutQuery,
21 | 'poll': poll.Poll,
22 | 'poll_answer': pollanswer.PollAnswer
23 | }
24 |
25 | def __init__(self, obj=None):
26 | super(Update, self).__init__(obj)
27 |
28 | def type(self):
29 | field_checks = {
30 | 'message': update_types.Message,
31 | 'edited_message': update_types.EditedMessage,
32 | 'channel_post': update_types.ChannelPost,
33 | 'edited_channel_post': update_types.EditedChannelPost,
34 | 'inline_query': update_types.InlineQuery,
35 | 'chosen_inline_result': update_types.ChosenInlineResult,
36 | 'callback_query': update_types.CallbackQuery,
37 | 'shipping_query': update_types.ShippingQuery,
38 | 'pre_checkout_query': update_types.PreCheckoutQuery,
39 | 'poll': update_types.Poll,
40 | 'poll_answer': update_types.PollAnswer
41 | }
42 | for key in field_checks.keys():
43 | if getattr(self, key, None) is not None:
44 | return field_checks[key]
45 | return None
46 |
47 | def get_update_id(self) -> str:
48 | return getattr(self, 'update_id')
49 |
50 | def get_message(self) -> Optional[message.Message]:
51 | for key in ['message', 'edited_message', 'channel_post', 'edited_channel_post']:
52 | if getattr(self, key, None) is not None:
53 | return getattr(self, key, None)
54 | return None
55 |
56 | def get_chat(self) -> Optional[chat.Chat]:
57 | if self.get_message() is not None:
58 | return self.get_message().get_chat()
59 | if self.is_callback_query():
60 | return self.get_callback_query().get_chat()
61 | return None
62 |
63 | def get_user(self):
64 | if self.get_message() is not None:
65 | return self.get_message().get_user()
66 | if self.is_inline_query():
67 | return self.get_inline_query().get_user()
68 | if self.is_chosen_inline_result():
69 | return self.get_chosen_inline_result().get_user()
70 | if self.is_callback_query():
71 | return self.get_callback_query().get_user()
72 | if self.is_shipping_query():
73 | return self.get_shipping_query().get_user()
74 | if self.is_pre_checkout_query():
75 | return self.get_pre_checkout_query().get_user()
76 | if self.is_poll_answer():
77 | return self.get_poll_answer().get_user()
78 | return None
79 |
80 | def get_callback_query(self) -> callbackquery.CallbackQuery:
81 | return getattr(self, 'callback_query', None)
82 |
83 | def get_inline_query(self) -> inlinequery.InlineQuery:
84 | return getattr(self, 'inline_query', None)
85 |
86 | def get_shipping_query(self) -> shippingquery.ShippingQuery:
87 | return getattr(self, 'shipping_query', None)
88 |
89 | def get_chosen_inline_result(self) -> choseninlineresult.ChosenInlineResult:
90 | return getattr(self, 'chosen_inline_result', None)
91 |
92 | def get_pre_checkout_query(self) -> precheckoutquery.PreCheckoutQuery:
93 | return getattr(self, 'pre_checkout_query', None)
94 |
95 | def get_poll_answer(self) -> pollanswer.PollAnswer:
96 | return getattr(self, 'poll_answer', None)
97 |
98 | def is_poll_answer(self) -> bool:
99 | return self.get_poll_answer() is not None
100 |
101 | def is_pre_checkout_query(self) -> bool:
102 | return self.get_pre_checkout_query() is not None
103 |
104 | def is_chosen_inline_result(self) -> bool:
105 | return self.get_chosen_inline_result() is not None
106 |
107 | def is_inline_query(self) -> bool:
108 | return self.get_inline_query() is not None
109 |
110 | def is_callback_query(self) -> bool:
111 | return self.get_callback_query() is not None
112 |
113 | def is_shipping_query(self) -> bool:
114 | return self.get_shipping_query() is not None
115 |
--------------------------------------------------------------------------------
/django_tgbot/types/__init__.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 |
4 | def validate_type_object(obj):
5 | if obj is None:
6 | return {}
7 | if type(obj) == dict:
8 | return obj
9 |
10 | if type(obj) == str:
11 | try:
12 | return json.loads(obj)
13 | except ValueError:
14 | return {}
15 |
16 | return {}
17 |
18 |
19 | def parse_field_configured(obj, config):
20 | """
21 | Parses an object to a Telegram Type based on the configuration given
22 | :param obj: The object to parse
23 | :param config: The configuration:
24 | - is array?
25 | - is array in array?
26 | - type of the class to be loaded to
27 | :return: the parsed object
28 | """
29 | foreign_type = config if type(config) != dict else config['class']
30 | if type(config) == dict:
31 |
32 | if 'array' in config and config['array'] is True:
33 | return [foreign_type(x) for x in obj]
34 |
35 | elif 'array_of_array' in config and config['array_of_array'] is True:
36 | res = []
37 | for inner_obj in obj:
38 | res.append([foreign_type(x) for x in inner_obj])
39 | return res
40 |
41 | return foreign_type(obj)
42 |
43 |
44 | class BasicType:
45 | """
46 | Types should have an attribute named `fields` with this format in order to be parsed:
47 | fields = {
48 | 'key': SomeType (Message, str, ...),
49 | 'key': {
50 | 'class': SomeType (Message, str, ...),
51 | 'array': True/False,
52 | 'array_of_array': True/False,
53 | 'validation': True/False
54 | }
55 | }
56 | """
57 | def __init__(self, obj=None):
58 | self.obj = validate_type_object(obj)
59 | self.parse_fields()
60 |
61 | def parse_fields(self):
62 | if self.obj is None:
63 | return
64 | for key in self.fields.keys():
65 | if key in self.obj:
66 | try:
67 | setattr(
68 | self,
69 | key,
70 | parse_field_configured(self.obj[key], self.fields[key])
71 | )
72 | except ValueError:
73 | pass
74 |
75 | def to_dict(self):
76 | result = {}
77 | for field in self.fields.keys():
78 | if not hasattr(self, field):
79 | continue
80 | if not hasattr(getattr(self, field), 'to_dict'):
81 | if type(getattr(self, field)) == list:
82 | result[field] = self.make_primitive(getattr(self, field))
83 | else:
84 | result[field] = getattr(self, field)
85 | else:
86 | result[field] = getattr(self, field).to_dict()
87 | return result
88 |
89 | @staticmethod
90 | def make_primitive(obj):
91 | if hasattr(obj, 'to_dict'):
92 | return obj.to_dict()
93 |
94 | if type(obj) == list:
95 | return [BasicType.make_primitive(x) for x in obj]
96 |
97 | return {}
98 |
99 | def to_json(self):
100 | return json.dumps(self.to_dict())
101 |
102 | @staticmethod
103 | def bool_interpreter(x):
104 | return True if x in [True, 'true', 'True', 1, 'T', 't', '1'] else False
105 |
106 | @classmethod
107 | def a(child_class, **kwargs):
108 | if not hasattr(child_class, 'fields'):
109 | return
110 |
111 | result = child_class()
112 |
113 | for key in child_class.fields.keys():
114 | if key not in kwargs or kwargs[key] is None:
115 | continue
116 |
117 | # Validating the argument
118 | if type(child_class.fields[key]) != dict \
119 | or 'validation' not in child_class.fields[key] \
120 | or child_class.fields[key]['validation']:
121 |
122 | if type(child_class.fields[key]) == dict:
123 | field = child_class.fields[key]
124 | if ('array' in field and field['array']) or ('array_of_array' in field and field['array_of_array']):
125 | expected_type = list
126 | else:
127 | expected_type = child_class.fields[key]['class']
128 | else:
129 | expected_type = child_class.fields[key]
130 |
131 | if expected_type == BasicType.bool_interpreter:
132 | expected_type = bool
133 |
134 | if type(kwargs[key]) != expected_type:
135 | raise ValueError('Type mismatch for argument {}.\nExpected type: {}\nActual type: {}'.format(
136 | key, expected_type, type(kwargs[key])
137 | ))
138 |
139 | setattr(result, key, kwargs[key])
140 |
141 | return result
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/docs/models/telegram_state.md:
--------------------------------------------------------------------------------
1 | # Telegram State
2 | A client can be working with the bot in different settings. For example, a user can send messages to the bot in a private chat or in a group
3 | or different groups. A bot can send messages visible to our bot in different groups. These should be handled separately. If a user is working
4 | with the bot in 2 groups at the same time, we do not want their interactions with the bot in one group to interfere with their interactions
5 | with the bot in the other group or in the private chat. Here comes the **Telegram State**.
6 |
7 | ## Information stored
8 | **Telegram State** holds information about a client, the chat in which they are using the bot and some other auxiliary data
9 | for helping the bot to handle updates. These auxiliary data are:
10 |
11 | * `telegram_user`: The user (either a person or a bot) this state belongs to. This field can be null (will be explained later in this document).
12 | * `telegram_chat`: The chat (private, group, supergroup or channel) this state belongs to. This field can be null (which will be explained later in this document).
13 | * `name`: The state that this client / chat currently has can be named so the bot can recognize it easier. This is basically how the bot
14 | handles the state. It sets a name for the state of this client / chat and it knows what to do when another update comes at a later time. Again,
15 | after processing that update, if it needs to move the user to another state, it will change this name. You can set this name by accessing it directly
16 | or you may use the `set_name` method:
17 | * `set_name`: Receives a string and sets it as the name.
18 |
19 | * `memory`: Basically, it is a JSON object you can store and it will be carried for you whenever a request comes regarding this client / chat.
20 | This memory will always be given to you as a dict and you can send a dict to be stored for you and given to you the next time you want it:
21 | * `set_memory`: Takes a dict and sets the memory equal to that.
22 | * `update_memory`: Takes a dict and updates the memory based on that - existing keys will be updates and non-existing keys will be created.
23 | * `reset_memory`: As the name suggests, resets the memory to empty dict.
24 |
25 |
26 | ##### Example
27 | Let's say we want to design a bot that takes a user's name and email address in their private chat and prints it. So the Telegram State object
28 | we will be working with, is for this particular user and their private chat with the bot.
29 |
30 | 1. The name of the state is initially empty. Look at this as a reset state. Now the user sends a message. The bot can accept this
31 | message as a start message and ask for the user's name. Now, it will change the state name to asked_for_name. **The advantage of
32 | having a name for the state is that the next time a message comes from this user in this chat, the bot knows what it was waiting for and hence,
33 | it will realize this new message has to be the user's name.**
34 |
35 | 2. The next message comes and the bot perceives it as the user's name. Now we want to ask for their email address. However, we shouldn't lose the name.
36 | We can create a new model for this or alter the existing models to store this but it does not make sense to create a new field in the database for
37 | every piece of information we need to keep throughout a simple process. Hence, we use the `memory` of this state. Bot sets the memory as this:
38 |
39 | state.set_memory({
40 | 'name':
41 | })
42 | and then asks for the email address. It should also change the state so it knows the next message that comes is the user's email address:
43 |
44 | state.set_name('asked_for_email')
45 |
46 | 3. Now the user will send another message which contains the email address. It's time to retrieve their name (which we have stored in the memory)
47 | and print it along with the email. We get the name, print it and then reset the state memory and name:
48 |
49 | name = state.get_memory()['name']
50 | print(...)
51 | state.reset_memory()
52 | state.set_name('')
53 |
54 | This example illustrates the use of state name and state memory. This example is implemented in the demo bot created with this package. You can see it [here](https://github.com/ARKhoshghalb/django-tgbot_demo).
55 |
56 |
57 | ## Possible settings for the user and the chat
58 | As it was mentioned earlier in this doc, the user and the chat in a state can be null. Now we will explain what each of these scenarios mean.
59 |
60 | * User: Non-blank, Chat: Non-blank: This is a regular conversation. Like a private chat or a user sending messages to a group.
61 | * User: Non-blank, Chat: Blank: This is when the user is working with the bot in inline mode. Telegram does not send you
62 | information about the chat that user is typing in and you only know the user. For more information please read the Telegram Bot API docs.
63 | * User: Blank, Chat: Non-blank: This is when your bot is a member of a channel and receives updates about that. There is no user sending messages.
64 | It's just the channel and you can define the state on that channel if you want to take actions in the future, with regards to these updates from the channel.
65 | * User: Blank, Chat: Blank: This will never happen. There is always at least one of these two with value.
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/django_tgbot/types/message.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from django_tgbot.state_manager import message_types
4 | from . import BasicType
5 |
6 |
7 | class Message(BasicType):
8 | """
9 | Represents Telegram Message type:
10 | https://core.telegram.org/bots/api#message
11 | """
12 | fields = {
13 | 'message_id': str,
14 | 'date': str,
15 | 'forward_from_message_id': str,
16 | 'forward_signature': str,
17 | 'forward_sender_name': str,
18 | 'forward_date': str,
19 | 'edit_date': str,
20 | 'media_group_id': str,
21 | 'author_signature': str,
22 | 'text': str,
23 | 'caption': str,
24 | 'new_chat_title': str,
25 | 'delete_chat_photo': BasicType.bool_interpreter,
26 | 'group_chat_created': BasicType.bool_interpreter,
27 | 'supergroup_chat_created': BasicType.bool_interpreter,
28 | 'channel_chat_created': BasicType.bool_interpreter,
29 | 'migrate_to_chat_id': str,
30 | 'migrate_from_chat_id': str,
31 | 'connected_website': str,
32 | }
33 |
34 | def __init__(self, obj=None):
35 | super(Message, self).__init__(obj)
36 |
37 | def type(self):
38 | field_checks = {
39 | 'text': message_types.Text,
40 | 'audio': message_types.Audio,
41 | 'document': message_types.Document,
42 | 'animation': message_types.Animation,
43 | 'game': message_types.Game,
44 | 'photo': message_types.Photo,
45 | 'sticker': message_types.Sticker,
46 | 'video': message_types.Video,
47 | 'voice': message_types.Voice,
48 | 'video_note': message_types.VideoNote,
49 | 'contact': message_types.Contact,
50 | 'dice': message_types.Dice,
51 | 'location': message_types.Location,
52 | 'venue': message_types.Venue,
53 | 'poll': message_types.Poll,
54 | 'new_chat_members': message_types.NewChatMembers,
55 | 'left_chat_member': message_types.LeftChatMember,
56 | 'new_chat_title': message_types.NewChatTitle,
57 | 'new_chat_photo': message_types.NewChatPhoto,
58 | 'delete_chat_photo': message_types.DeleteChatPhoto,
59 | 'group_chat_created': message_types.GroupChatCreated,
60 | 'supergroup_chat_created': message_types.SupergroupChatCreated,
61 | 'channel_chat_created': message_types.ChannelChatCreated,
62 | 'migrate_to_chat_id': message_types.MigrateToChatId,
63 | 'migrate_from_chat_id': message_types.MigrateFromChatId,
64 | 'pinned_message': message_types.PinnedMessage,
65 | 'invoice': message_types.Invoice,
66 | 'successful_payment': message_types.SuccessfulPayment,
67 | 'passport_data': message_types.PassportData
68 | }
69 |
70 | for key in field_checks.keys():
71 | if getattr(self, key, None) is not None:
72 | return field_checks[key]
73 | return None
74 |
75 | def get_message_id(self) -> str:
76 | return getattr(self, 'message_id', None)
77 |
78 | def get_chat(self):
79 | return getattr(self, 'chat', None)
80 |
81 | def get_user(self):
82 | return getattr(self, 'from', None)
83 |
84 | def get_from(self):
85 | return self.get_user()
86 |
87 | def get_reply_to_message(self):
88 | return getattr(self, 'reply_to_message', None)
89 |
90 | def get_text(self) -> Optional[str]:
91 | return getattr(self, 'text', None)
92 |
93 | def get_caption(self) -> Optional[str]:
94 | return getattr(self, 'caption', None)
95 |
96 | def get_reply_markup(self):
97 | return getattr(self, 'reply_markup', None)
98 |
99 |
100 | def get_photo(self):
101 | return getattr(self, 'photo', None)
102 |
103 |
104 | # Placed here to avoid import cycles
105 | from . import user, chat, messageentity, audio, document, animation, game, photosize, \
106 | inlinekeyboardmarkup, passportdata, successfulpayment, invoice, poll, \
107 | venue, location, contact, videonote, voice, video, sticker, dice
108 |
109 |
110 | Message.fields.update({
111 | 'reply_to_message': Message,
112 | 'via_bot': user.User,
113 | 'pinned_message': Message,
114 | 'from': user.User,
115 | 'chat': chat.Chat,
116 | 'forward_from': user.User,
117 | 'forward_from_chat': chat.Chat,
118 | 'entities': {
119 | 'class': messageentity.MessageEntity,
120 | 'array': True
121 | },
122 | 'caption_entities': {
123 | 'class': messageentity.MessageEntity,
124 | 'array': True
125 | },
126 | 'audio': audio.Audio,
127 | 'document': document.Document,
128 | 'animation': animation.Animation,
129 | 'game': game.Game,
130 | 'photo': {
131 | 'class': photosize.PhotoSize,
132 | 'array': True
133 | },
134 | 'sticker': sticker.Sticker,
135 | 'video': video.Video,
136 | 'voice': voice.Voice,
137 | 'video_note': videonote.VideoNote,
138 | 'contact': contact.Contact,
139 | 'dice': dice.Dice,
140 | 'location': location.Location,
141 | 'venue': venue.Venue,
142 | 'poll': poll.Poll,
143 | 'new_chat_members': {
144 | 'class': user.User,
145 | 'array': True
146 | },
147 | 'left_chat_member': user.User,
148 | 'new_chat_photo': {
149 | 'class': photosize.PhotoSize,
150 | 'array': True
151 | },
152 | 'invoice': invoice.Invoice,
153 | 'successful_payment': successfulpayment.SuccessfulPayment,
154 | 'passport_data': passportdata.PassportData,
155 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup
156 | })
157 |
--------------------------------------------------------------------------------
/docs/classes/bot.md:
--------------------------------------------------------------------------------
1 | # Bot class
2 |
3 | [Telegram Bot API](https://core.telegram.org/bots/api) has several types and methods.
4 | This class is designed to allow you to work with the Telegram's API methods easily and in an IDE-friendly manner.
5 |
6 | All of the API methods (with regard to the API version mentioned in this doc) are implemented as Python functions in the `BotAPIUser` class and this class inherits from it.
7 |
8 | This class receives an [Update](../types/update.md) object and is responsible for finding the related [processors](../processors.md) and run them.
9 | However, it runs a `pre_processing` method before running those processors and a `post_processing` after it.
10 |
11 | pre_processing
12 |
13 | By default, it only saves the current [User](../models/telegram_user.md) (if any), [Chat](../models/telegram_chat.md) (if any) and the
14 | [State](../models/telegram_state.md).
15 |
16 | You can change this method to do some other custom pre-processings.
17 |
18 |
19 | post_processing
20 |
21 | This will be an empty function in a fresh bot which will do nothing. You can implement it for custom functionality.
22 |
23 |
24 |
25 | ## API Methods
26 |
27 | The API methods on this class do not change any of the data you send and the name of the methods are exactly the same as
28 | on Telegram's API. The main purpose of implementing them is to be easy to use, by giving you a list of
29 | names and their parameters so you can easily make API calls, knowing exactly what parameters this method requires.
30 |
31 | Most of them are also equipped with type hints to facilitate developing if you are using a modern IDE.
32 |
33 | Each of the API methods return something upon successful or failed requests. If the request is successful, the response will be
34 | a [Type](../types/README.md) and if it fails, the response will be a JSON object explaining the reason of failure. The Bot class
35 | will convert this response:
36 |
37 | * If the request is successful, it will return the according [Type](../types/README.md) object
38 | * If the request fails, if will return a dict with the key `ok` equal to `False`
39 |
40 | ### Example
41 | This is an API call using the Bot class which will be successful:
42 | ```python
43 | result = bot.sendMessage(chat_id='93856963', text='This is django-tgbot!')
44 | ```
45 | In this scenario, the `result` variable will have a [Message](../types/message.md) object since the output of the `sendMessage` method
46 | will be the sent message. You can find the output for each API method in the [Telegram Bot API](https://core.telegram.org/bots/api) or
47 | you can use the method type hint in this package.
48 |
49 | This other API call will fail since the text argument is required for this method:
50 | ```python
51 | result = bot.sendMessage(chat_id='93856963')
52 | ```
53 | and `result` will be:
54 | ```python
55 | {
56 | 'ok': False,
57 | 'error_code': 400,
58 | 'description': 'Bad Request: message text is empty'
59 | }
60 | ```
61 |
62 |
63 |
64 | ## Methods' Arguments
65 |
66 | You should have a look at the API documentations while calling methods as they might have certain requirements on some arguments. For example, `parse_mode` argument on some methods
67 | accepts only from a fixed list of strings and inline keyboards have three optional fields that exactly one of them should be filled with a value.
68 |
69 | Regarding the fixed values for arguments, some of the commonly used arguments are defined in the Bot class for you to use, instead of writing the string value:
70 |
71 | * parse_mode:
72 | * `Bot.PARSE_MODE_MARKDOWN`
73 | * `Bot.PARSE_MODE_HTML`
74 |
75 |
76 | * poll_type:
77 | * `Bot.POLL_TYPE_REGULAR`
78 | * `Bot.POLL_TYPE_QUIZ`
79 |
80 | * chat_action:
81 | * `Bot.CHAT_ACTION_TYPING`
82 | * `Bot.CHAT_ACTION_PHOTO`
83 | * `Bot.CHAT_ACTION_VIDEO_RECORD`
84 | * `Bot.CHAT_ACTION_VIDEO_UPLOAD`
85 | * `Bot.CHAT_ACTION_AUDIO_RECORD`
86 | * `Bot.CHAT_ACTION_AUDIO_UPLOAD`
87 | * `Bot.CHAT_ACTION_DOCUMENT`
88 | * `Bot.CHAT_ACTION_LOCATION`
89 | * `Bot.CHAT_ACTION_VIDEO_NOTE_RECORD`
90 | * `Bot.CHAT_ACTION_VIDEO_NOTE_UPLOAD`
91 |
92 |
93 | Furthermore, you should also have a look at the accepted type for each argument. Some may only accept int, some may only accept bool, etc..
94 |
95 | In some cases, you may need to pass a parameter with the type of one of the predefined Telegram types.
96 | For example, if you want to send a keyboard along with a message, you need to pass a `ReplyKeyboardMarkup` as the `reply_markup` parameter.
97 | In these cases, you can either create a JSON object representing the value you want and pass it along or, alternatively,
98 | you can create the object with the needed type and pass the object itself - it will be then converted to JSON automatically. All of the
99 | Telegram types are defined here and you can create objects of those types.
100 |
101 | Hence, for the example explained above, for sending your keyboard you can create a [ReplyKeyboardMarkup](../types/replykeyboardmarkup.md)
102 | object pass it through as the `reply_markup` argument. If in some scenario, you need to have the JSON value of an object, all of the
103 | defined [Type](../types/README.md) in this package have a `to_dict` method to convert that object to a dict object and a `to_json`
104 | to convert it to JSON.
105 |
106 | If you want to create a [Type](../types/README.md) object, you should use a method called `a` and not the default constructor.
107 | For more information please read its [docs](../types/README.md).
108 |
109 | ### Example
110 |
111 | Let's send a keyboard with the sent message in our previous example:
112 |
113 | ```python
114 | result = bot.sendMessage(
115 |
116 | chat_id='93856963',
117 |
118 | text='This is django-tgbot!'
119 |
120 | reply_markup=ReplyKeyboardMarkup.a([
121 |
122 | [KeyboardButton.a('Left Button'), KeyboardButton.a('Right Button')]
123 |
124 | ])
125 |
126 | )
127 | ```
128 |
129 |
--------------------------------------------------------------------------------
/docs/processors.md:
--------------------------------------------------------------------------------
1 | # Processors
2 |
3 | To completely understand what processors are, you need to first read the Definitions section from [here](index.md).
4 |
5 | Processors are essentially Python functions that take (at least) these 3 named arguments:
6 |
7 | * `bot`: The bot instance - should be used for calling API methods. For more information read [Bot class docs](classes/bot.md).
8 | * `update`: The update object received from Telegram. For more information read about [Types](types/README.md) and [Updates](types/update.md).
9 | * `state`: The Telegram State object related to this update. For more information read [Telegram State docs](models/telegram_state.md).
10 |
11 |
12 | ## Creating processors
13 | As explained earlier, processors are just Python functions. So how should we say which functions are processors and which functions are not?
14 |
15 | ##### Processors module
16 | First of all, processors should be in a place that gets imported to the project at some point. When creating a new bot, a module named `processors.py` is
17 | created. This module will be imported in the code to load all of your processors. You may, however, realize that you are going to have a lot
18 | of processors and it is hard to have them all in one file and thus, you may need to separate them. To do so, you can remove this file and instead,
19 | create a package named `processors` in the same directory. Then put all of your processors in files under this package. Remember! You still need to
20 | import any file you create, in the `__init__.py` file in root directory of your package. For example:
21 |
22 | ```
23 | processors
24 | ├── __init__.py
25 | ├── greetings.py
26 | ├── signup.py
27 | └── ...
28 | ```
29 |
30 | Where `__init__.py` contains:
31 | ```
32 | from . import greetings, signup, ...
33 | ```
34 |
35 |
36 |
37 | ##### Registering with @processor
38 | Still, not every function you put in these modules will be considered as a processor. You may have a lot of functions, where you want only a few of them
39 | to be processors.
40 |
41 | If you want to declare a function as a processor, it should be registered with the `@processor` decorator:
42 | ```python
43 | @processor(...args...)
44 | def cool_processor(bot, update, state):
45 | pass
46 | ```
47 |
48 | An important note here is that any function you register as a processor should have the 3 named parameters explained above. If you register a processor
49 | that doesnt have at least one of these named parameters it will not be registered and an exception will be raised.
50 |
51 | Now let's move on to using this decorator.
52 |
53 | ## @processor decorator
54 | Each processor checks a few conditions before accepting an update. If those conditions are met then it will process the update. Otherwise, the update will not
55 | be sent to this processor.
56 |
57 | These conditions are these four:
58 |
59 | 1. What is the name of the [state](models/telegram_state.md) related to this update?
60 | 2. What is the type of the incoming update? Read more about update types [here](types/update.md)
61 | 3. What is the type of the message (if any) in this update? Read more about message types [here](types/message.md)
62 |
63 | Then, if the conditions are met, the processor will run. In most cases we want to change the state to something specific if running
64 | of the processor does not face any problems. This automation can be accomplished by the `success` and `fail` arguments:
65 |
66 | * `success`: Whatever value this argument has will be set as the name of the state if the processor runs with no problem.
67 | * `fail`: Whatever value this argument has will be set as the name of the state if the faces a problem while running.
68 |
69 | In the above definitions, a problem means a `ProcessFailure` exception being raised. So for example, if you receive a
70 | text message from the user and you find out it is not a valid response from them, you can raise a `ProcessFailure` and their state's name
71 | will change accordingly (it will get the value of `fail` argument).
72 |
73 | #### Arguments and descriptions
74 | We saw how a processor works. These are the arguments we can pass to the `@processor` decorator to register a processor:
75 |
76 | * **state_manager**: An instance of the state manager will be created with every new bot you create and you can import it from `bot.py`. This object
77 | will be responsible for storing all of the processors and checking their conditions. This is the only required argument.
78 | * **from_states**: This argument is to check the first condition from the list above. It takes a list or a single string and when
79 | a new update arrives, it will check to see if the state name is one of state names accepted by this processor. Special values:
80 | * If you want to accept updates with all state names, pass **state_types.All**
81 | * If you want to accept updates with reset states (i.e. empty state names), leave blank or pass an empty string or pass **state_types.Reset**
82 | * **update_types**: This argument is to check the second condition from the list above. It takes a list or a single value and
83 | checks the type of the received update. Use the predefined [update types](types/update.md) to fill this argument. For example, you can pass
84 | `[update_types.Message, update_types.EditedMessage]` to catch messages and edited messages or `update_types.ChannelPost` to get only channel posts.
85 | Special values:
86 | * If you want to accept all update types, leave this argument blank
87 | * **exclude_update_types**: This argument is also for checking the second condition. The only difference with `update_types` is that here
88 | you provide the update types you do NOT want to accept. Follows the same logic and values.
89 | * **message_types**: This argument is for checking the third condition in the list above. It only works if the received update contains
90 | a message. It takes a list or a single value and checks the type of the message in the update. Use the predefined [message types](types/message.md) for
91 | this argument. For example you can pass `[message_types.Voice, message_types.Audio]` to only catch messages containing an audio file. Special values:
92 | * If you want to accept all message types, leave this argument blank
93 | * **exclude_message_types**: This argument is also for checking the third condition. The only difference with `message_types` is that here
94 | you provide the message types you do NOT want to accept. Follows the same logic and values.
95 | * **success**: The value you want the state name to get in case the processor runs successfully. Special values:
96 | * If you don't want the state to be affected by this and want to change it manually yourself, leave it blank.
97 | * If you want to reset the state (to become empty), pass `state_types.Reset`.
98 | * If you want to keep the current state, pass `state_types.Keep`. Please note that this is different than leaving it as blank.
99 | When using this, no matter what changes you manually perform on the state name, after the processor is run the state name will
100 | become whatever it was before running the processor.
101 | * **fail**: The value you want the state name to get in case the processor raises `ProcessFailure`. Special values:
102 | * Exactly like the `success`.
103 |
104 | These are 2 examples of processors registered with `@processor`:
105 | ```python
106 | @processor(
107 | state_manager, from_states='asked_for_name',
108 | update_types=[update_types.Message, update_types.EditedMessage],
109 | message_types=message_types.Text,
110 | success='asked_for_email', fail=state_types.Keep
111 | )
112 | def get_name(bot, update, state):
113 | pass
114 | ```
115 |
116 | ```python
117 | @processor(
118 | state_manager, from_states=['asked_for_media', 'asked_for_file'],
119 | update_types=update_types.Message,
120 | exclude_message_types=[message_types.Text, message_types.PinnedMessage],
121 | success=state_types.Reset, fail=state_types.Keep
122 | )
123 | def get_anything_but_text(bot, update, state):
124 | pass
125 | ```
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://django-tgbot.readthedocs.io/en/latest/?badge=latest)
2 | [](https://core.telegram.org/bots/api)
3 | [](https://t.me/DjangoTGBotChat)
4 |
5 |
6 | # Telegram Bots in Django
7 |
8 | Bot API version 4.9 ([Telegram bot API docs](https://core.telegram.org/bots/api))
9 |
10 | If you have questions about how you can use the package (or if you think you'd be able to help others) please join the Telegram group (@DjangoTGBotChat).
11 | If you would like to contribute, read [this page](https://github.com/Ali-Toosi/django-tgbot/blob/master/.github/CONTRIBUTING.md) for a list of suggestions of what you could work on.
12 |
13 | Please note that all types and methods implemented here are defined exactly as they have been in [Telegram Bot API](https://core.telegram.org/bots/api). This means, although some type annotations
14 | and method comments have been provided in this package, you can find additional explanations for all types and methods on Telegram Bot API docs and use them in this package exactly the same way.
15 |
16 |
17 | 
18 |
19 | ### Setup
20 |
21 | 1. Install package from pip:
22 | ```
23 | pip install django-tgbot
24 | ```
25 |
26 | 2. Add `django_tgbot` to your Django project's `INSTALLED_APPS`
27 |
28 | That's it :) You installed django-tgbot.
29 |
30 |
31 |
32 | ### Create a new Telegram bot
33 | 1. Create a bot in Telegram using [BotFather](https://t.me/BotFather) and receive your API token
34 | 2. Open the Django project with `django-tgbot` installed in it
35 | 3. Enter this command in the command line (terminal / cmd):
36 | ```
37 | python manage.py createtgbot
38 | ```
39 | 4. Enter your API token:
40 | ```
41 | > python manage.py createtgbot
42 | Enter the bot token (retrieved from BotFather):
43 | Setting up @BotDevTestBot ...
44 | ```
45 | 5. Enter the URL your Django project is deployed on. If your project is not deployed yet and is not accessible, press Enter to skip. (If you have not deployed yet and want to test your bot, you can use services like [Ngrok](http://ngrok.com) to do so)
46 | ```
47 | Enter the url of this project to set the webhook (Press Enter to skip): https://URL.com
48 | Bot webhook will be set to https://URL.com/botdevtestbot/update/. Do you confirm? (Y/N): y
49 | Webhook was successfully set.
50 | ```
51 |
52 | 6. A new app will be created in your Django project. Add this app to your `INSTALLED_APPS`
53 | 7. Include this new app's urls in your project urls as described in the output of the above command
54 | 8. Update the database:
55 | ```
56 | python manage.py migrate
57 | ```
58 |
59 | Your bot is created! If you have set the webhook correctly you can now send messages to your bot and it responds all messages with `Hello!`.
60 |
61 | Overview of the process:
62 |
63 | ```
64 | > python manage.py createtgbot
65 | Enter the bot token (retrieved from BotFather): 521093911:AAEe6X-KTJHO98tK2skJLsYJsE7NRpjL8Ic
66 | Setting up @BotDevTestBot ...
67 | Enter the url of this project to set the webhook (Press Enter to skip): https://URL.com
68 | Bot webhook will be set to https://URL.com/botdevtestbot/update/. Do you confirm? (Y/N): y
69 | Webhook was successfully set.
70 | Successfully created bot Test Bot(@botdevtestbot).
71 | Next steps:
72 | 1. Add 'botdevtestbot' to INSTALLED_APPS in project settings
73 | 2. Add `from botdevtestbot import urls as botdevtestbot_urls` to project urls file
74 | 3. Add `path('botdevtestbot/', include(botdevtestbot_urls))` to project urls' urlpatterns
75 | 4. `python manage.py migrate`
76 | Enjoy!
77 | ```
78 |
79 |
80 |
81 | ### Definitions
82 | It is important to understand these definitions in order to read the rest of the doc or to use the package. We encourage you to also read the
83 | Definitions section in the full documentations. You can find a link to the full documentations at the end of this document.
84 |
85 | This is an overview of the flow:
86 |
87 | Bot receives a message/update --> Telegram sends an Update to your webhook --> Django receives this request and passes it to the app created for this bot (In steps above) --> `django_tgbot` creates an `Update` object from the HTTP request --> One or several `processor`s will be assigned to handle this `Update` --> All of these `processor`s will run and possibly make some calls to Telegram API.
88 |
89 | #### Types
90 | Telegram uses some methods and `Type`s (think of this types as classes) to handle everything. All of these types are implemented as Python classes and you can use them from `django_tgbot.types`. You can view a full list of all Telegram available types [here](https://core.telegram.org/bots/api#available-types) (The API version based on which this package is designed is written at the top of this document. With newer versions there might be new types not implemented in this package)
91 |
92 | #### Models
93 | Each new bot you create comes prepackaged with 3 models:
94 | * TelegramUser: represents a user in Telegram. Can be a person or a bot. Basically, it is an entity that can send messages.
95 | * TelegramChat: represents a chat in Telegram. Can be a private chat, a group, a supergroup or a channel. Basically, it is a place that messages can be sent from one or several parties.
96 | * TelegramState: From the bot's perspective, each user in a chat or a chat by itself or a user by itself, are in a state. This state is stored in `TelegramState` model. It holds the user (can be blank), the chat (can be blank), a memory and a name. This name helps the bot to easily determine what this state is and what needs to be done.
97 |
98 | #### Bot
99 | When you create a new bot, an app will be created which has a new class named `TelegramBot` and a bot instantiated from this class. This is your interface for working with the Telegram Bot API.
100 |
101 | For every request it receives, it will do some preprocessing and then finds the `processor`s responsible for this request and runs all of them. Finally, it will do some post-processings.
102 |
103 | The mentioned preprocessing and post-processings can be changed or removed from the Bot class in `bot.py` in your newly created app. By default, for all requests, the user's `id`, `first_name`, `last_name` and `username` and the chat's `id`, `title` and `type` will be stored in database in the preprocessing and nothing is done in the postprocessing.
104 |
105 | #### Processors
106 |
107 | This is the core of your bot's functionality and for this you write most of your codes.
108 |
109 | So far, we have seen every client of our bot (a user with/without a chat or a chat itself) has a state (i.e. is in a state). Processors take a client with its state and based on their state and the sent update, they will forward this client to another state.
110 |
111 | Each processor should declare the states from which clients can enter and the state to which clients should be sent in case processor ran successfully or in case it failed.
112 |
113 | Processors are just Python functions that take the bot instance, the update received from Telegram and the state of the client as input.
114 |
115 |
116 |
117 |
118 | ### Developing your bot
119 |
120 | When you create a new bot, a new app will be created in your Django project which will contain a file named `processors.py`. This module is where all your processors lie. You can also remove this file and instead, create a package with the same name in the same directory since the number of processors might get large and it's a good idea to keep them separated in different modules. If you do so, do not forget to import all of these modules in the `__init__.py` of `processors` package.
121 | If you replace `processors.py` with a `processors` package:
122 |
123 | ```
124 | processors
125 | ├── __init__.py
126 | ├── greetings.py
127 | ├── signup.py
128 | └── ...
129 | ```
130 |
131 | Where `__init__.py` contains:
132 | ```
133 | from . import greetings, signup, ...
134 | ```
135 |
136 | Initially, there is a `hello_world` processor available in `processors.py`. As you can see, all of your processors should get registered by `@processor` decorator in order to be recognized later by the bot. They should also take three named parameters (like the `hello_world` sample):
137 | * bot: An instance of the TelegramBot. Can be used to call Telegram API (sending messages, etc.)
138 | * update: The received update loaded into the `Update` type explained above.
139 | * state: The state of this client, a TelegramState instance. You can change their state or memory using this.
140 |
141 | As said earlier, each processor should declare what states it accepts to process and if processing is done successfully, what should the client's state become and what should it become if the processing fails.
142 |
143 | These are declared above the function definition in the `@processor` arguments:
144 | * from_states: Name of the accepting states for this processor. It can be a string, a list or `state_types.All` which will accept all states. If you want to accept the empty (reset) state (the client's state is initially empty and it's a good idea to use empty string as a reset state), leave it blank or set it as `from_states = ''` or `from_states = state_types.Reset`.
145 | * success: The new state of the client, if processor runs successfully. "successfully" means being run without raising `ProcessFailure` exception.
146 | * fail: The new state of the client, if processor fails to run. If you want to fail the processor you should raise `ProcessFailure`. This exception can be imported from `django_tgbot.exceptions`.
147 |
148 | You may use `state_types.Keep` as the value for `success` or `fail` to not change the state or use `state_types.Reset` to reset the state.
149 |
150 | Additionally, you can define the update types and the message types you want this processor to handle. For example, you may want to have a processor that only handles requests regarding a message getting edited (which is a different update type than receiving a new message) or you may want to have a processor only handling requests of video messages (which is a different message type than text messages). To see a full list of different updates types and message types read Telegram Bot API docs. Parameters for `@processor`:
151 | * message_types: Can be a single value or a list of values. Leave unfilled to accept any message type. Use values from `message_types` module to fill in the parameter. For example you can say `message_types = message_types.Text` to only handle text messages or `message_types = [message_types.NewChatMembers, message_types.LeftChatMembers]` to handle updates about group members coming and going.
152 | * exclude_message_types: Works exactly like `message_types` except that values passed here will be excluded from the valid message types to handle.
153 | * update_types: Can be a single value or a list of values. Leave unfilled to accept any update type. Like the message_updates, available update_types are accessible from the `update_types` module. For example, `update_types = [update_types.ChannelPost, update_types.EditedMessage]` makes the processor handle only updates about a new post being sent to a channel or a message being edited.
154 | * exclude_update_types: Works exactly like `update_types` except that values passed here will be excluded from the acceptable update types to handle.
155 |
156 | Please note that the first parameter for `@processor` should be always an state manager. An state manager is created automatically when you create a new bot and it's imported in the processors module. You can use that and give it to all of the processors. You may change this state manager to have different behaviors in your bot, which is not a common case and will be explained in advanced documentations.
157 |
158 | Example of a processor definition:
159 | ```python
160 | @processor(state_manager, from_states='asked_for_name', success='got_their_name', fail=state_types.Keep, message_types=message_types.Text, update_types=update_types.Message)
161 | def say_hello(bot, update, state):
162 | bot.sendMessage(update.get_chat().get_id(), "Hello {}!".format(update.get_message().get_text()))
163 | ```
164 |
165 | You may also leave the `success` and `fail` arguments in order to not change the state automatically, if you want to change it yourself in the processor:
166 |
167 | ```python
168 | @processor(state_manager, from_states='asked_for_name', message_types=message_types.Text, update_types=update_types.Message)
169 | def say_hello(bot, update, state):
170 | text = update.get_message().get_text()
171 | if text == 'Alireza':
172 | bot.sendMessage(update.get_chat().get_id(), "Hello Alireza!")
173 | state.name = 'got_their_name'
174 | state.save()
175 | else:
176 | bot.sendMessage(update.get_chat().get_id(), "Nah")
177 | state.name = 'failed_to_give_name'
178 | state.save()
179 | ```
180 |
181 | Please note that leaving the `success` and `fail` parameters without a value is NOT the same as setting them to `state_types.Keep`. Leaving them will not change them and allows you to set them in the processor's run time. However, setting them to `state_types.Keep` will force the state to be the same as what is was before entering the processor.
182 |
183 | ### Using the API methods
184 |
185 | The interface you can use for sending requests to the Bot API is the TelegramBot class and an instance of it will be created when you start a new bot.
186 |
187 | All of the methods on Telegram API are implemented in this class. Furthermore, if you want to send any custom request or call any method, you can use the method `send_request` to do so.
188 |
189 | You should have a look at the API documentations while calling methods as they might have certain requirements on some arguments. For example, `parse_mode` argument on some methods,
190 | only accepts a few fixed strings and inline keyboards have three optional fields that exactly one of them should be filled with a value.
191 |
192 | You might need to use the defined `Type`s to create and pass data more easily, in cases that Telegram expects a certain type as the value for some parameter. For example,
193 | if you want to send a keyboard as the `reply_markup` for some message, you can create the keyboard object with the `ReplyKeyboardMarkup` object and pass
194 | it as the value. Please note that `Type` classes' constructor is designed to accept a JSON object. If you want to create an instance of such classes,
195 | you should use another method called `a`.
196 |
197 | All of the `Type`s have a method called `a` that allow you to create an object of that type. They get the parameters and validate them and return an object of that type.
198 | For example, if you want to create a keyboard with two buttons one saying `A` and other saying `B` this is how you create it:
199 | ```python
200 | keyboard = ReplyKeyboardMarkup.a(keyboard=[
201 | [KeyboardButton.a(text='A'), KeyboardButton.a(text='B')]
202 | ])
203 | ```
204 |
205 | All `Type` classes also have a `to_dict` and `to_json` method that may help you in some scenarios. For more information see the package docs.
206 |
207 |
208 |
209 | ### Final Notes
210 | * As explained earlier, some API methods have certain requirements for some of their parameters. One of them is the `reply_markup` parameter for all of the methods that have it.
211 | This parameter should be passed as a JSON object. However, since it is a very common parameter to use in a lot of methods, if you pass this as a `Type` (e.g. `ReplyKeyboardMarkup` or `InlineKeyboardMarkup`)
212 | it will be converted to JSON automatically.
213 | * To send a file when using methods that accept it (such as `sendPhoto` or `sendDocument`), if you are going to upload the file (and not providing the file url or file id), set the `upload` argument to `True` and pass the opened file to the according argument. For example:
214 | `bot.sendPhoto(chat_id, photo=open('my_file.png', 'rb'), upload=True)`
215 |
216 |
217 | ### Links
218 | * Some demo bots created with `django-tgbot`: [https://github.com/Ali-Toosi/django-tgbot_demo](https://github.com/Ali-Toosi/django-tgbot_demo)
219 | * Full documentation: [https://django-tgbot.readthedocs.io/en/latest/](https://django-tgbot.readthedocs.io/en/latest/)
220 |
--------------------------------------------------------------------------------
/django_tgbot/types/inlinequeryresult.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import BasicType
4 | from . import inputmessagecontent, inlinekeyboardmarkup
5 |
6 |
7 | class InlineQueryResult(BasicType):
8 | def __init__(self, obj=None):
9 | super(InlineQueryResult, self).__init__(obj)
10 |
11 |
12 | class InlineQueryResultArticle(InlineQueryResult):
13 | fields = {
14 | 'type': str,
15 | 'id': str,
16 | 'title': str,
17 | 'input_message_content': {
18 | 'class': inputmessagecontent.InputMessageContent,
19 | 'validation': False
20 | },
21 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
22 | 'url': str,
23 | 'hide_url': BasicType.bool_interpreter,
24 | 'description': str,
25 | 'thumb_url': str,
26 | 'thumb_width': int,
27 | 'thumb_height': int
28 | }
29 |
30 | def __init__(self, obj=None):
31 | super(InlineQueryResultArticle, self).__init__(obj)
32 |
33 | @classmethod
34 | def a(cls, id: str, title: str, input_message_content: inputmessagecontent.InputMessageContent,
35 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None, url: Optional[str] = None,
36 | hide_url: Optional[bool] = None, description: Optional[str] = None, thumb_url: Optional[str] = None,
37 | thumb_width: Optional[int] = None, thumb_height: Optional[int] = None):
38 | type = 'article'
39 | return super().a(**locals())
40 |
41 |
42 | class InlineQueryResultPhoto(InlineQueryResult):
43 | fields = {
44 | 'type': str,
45 | 'id': str,
46 | 'photo_url': str,
47 | 'thumb_url': str,
48 | 'photo_width': int,
49 | 'photo_height': int,
50 | 'title': str,
51 | 'description': str,
52 | 'caption': str,
53 | 'parse_mode': str,
54 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
55 | 'input_message_content': {
56 | 'class': inputmessagecontent.InputMessageContent,
57 | 'validation': False
58 | }
59 | }
60 |
61 | def __init__(self, obj=None):
62 | super(InlineQueryResultPhoto, self).__init__(obj)
63 |
64 | @classmethod
65 | def a(cls, id: str, photo_url: str, thumb_url: str, photo_width: Optional[int] = None,
66 | photo_height: Optional[int] = None, title: Optional[str] = None, description: Optional[str] = None,
67 | caption: Optional[str] = None, parse_mode: Optional[str] = None,
68 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
69 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
70 | type = 'photo'
71 | return super().a(**locals())
72 |
73 |
74 | class InlineQueryResultGif(InlineQueryResult):
75 | fields = {
76 | 'type': str,
77 | 'id': str,
78 | 'gif_url': str,
79 | 'gif_width': int,
80 | 'gif_height': int,
81 | 'gif_duration': int,
82 | 'thumb_url': str,
83 | 'thumb_mime_type': str,
84 | 'title': str,
85 | 'caption': str,
86 | 'parse_mode': str,
87 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
88 | 'input_message_content': {
89 | 'class': inputmessagecontent.InputMessageContent,
90 | 'validation': False
91 | }
92 | }
93 |
94 | def __init__(self, obj=None):
95 | super(InlineQueryResultGif, self).__init__(obj)
96 |
97 | @classmethod
98 | def a(cls, id: str, gif_url: str, thumb_url: str, thumb_mime_type: str = None, gif_width: Optional[int] = None, gif_height: Optional[int] = None,
99 | gif_duration: Optional[int] = None, title: Optional[str] = None, caption: Optional[str] = None,
100 | parse_mode: Optional[str] = None, input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
101 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
102 | type = 'gif'
103 | return super().a(**locals())
104 |
105 |
106 | class InlineQueryResultMpeg4Gif(InlineQueryResult):
107 | fields = {
108 | 'type': str,
109 | 'id': str,
110 | 'mpeg4_url': str,
111 | 'mpeg4_width': int,
112 | 'mpeg4_height': int,
113 | 'mpeg4_duration': int,
114 | 'thumb_url': str,
115 | 'thumb_mime_type': str,
116 | 'title': str,
117 | 'caption': str,
118 | 'parse_mode': str,
119 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
120 | 'input_message_content': {
121 | 'class': inputmessagecontent.InputMessageContent,
122 | 'validation': False
123 | }
124 | }
125 |
126 | def __init__(self, obj=None):
127 | super(InlineQueryResultMpeg4Gif, self).__init__(obj)
128 |
129 | @classmethod
130 | def a(cls, id: str, mpeg4_url: str, thumb_url: str, thumb_mime_type: str = None, mpeg4_width: Optional[int] = None,
131 | mpeg4_height: Optional[int] = None, mpeg4_duration: Optional[int] = None, title: Optional[str] = None,
132 | caption: Optional[str] = None, parse_mode: Optional[str] = None,
133 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
134 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
135 | type = 'mpeg4_gif'
136 | return super().a(**locals())
137 |
138 |
139 | class InlineQueryResultVideo(InlineQueryResult):
140 | fields = {
141 | 'type': str,
142 | 'id': str,
143 | 'video_url': str,
144 | 'mime_type': str,
145 | 'thumb_url': str,
146 | 'title': str,
147 | 'caption': str,
148 | 'parse_mode': str,
149 | 'video_width': int,
150 | 'video_height': int,
151 | 'video_duration': int,
152 | 'description': str,
153 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
154 | 'input_message_content': {
155 | 'class': inputmessagecontent.InputMessageContent,
156 | 'validation': False
157 | }
158 | }
159 |
160 | def __init__(self, obj=None):
161 | super(InlineQueryResultVideo, self).__init__(obj)
162 |
163 | @classmethod
164 | def a(cls, id: str, video_url: str, mime_type: str, thumb_url: str, title: str,
165 | caption: Optional[str] = None, parse_mode: Optional[str] = None, video_width: Optional[int] = None,
166 | video_height: Optional[int] = None, video_duration: Optional[int] = None, description: Optional[str] = None,
167 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
168 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
169 | type = 'video'
170 | return super().a(**locals())
171 |
172 |
173 | class InlineQueryResultAudio(InlineQueryResult):
174 | fields = {
175 | 'type': str,
176 | 'id': str,
177 | 'audio_url': str,
178 | 'title': str,
179 | 'caption': str,
180 | 'performer': str,
181 | 'audio_duration': int,
182 | 'parse_mode': str,
183 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
184 | 'input_message_content': {
185 | 'class': inputmessagecontent.InputMessageContent,
186 | 'validation': False
187 | }
188 | }
189 |
190 | def __init__(self, obj=None):
191 | super(InlineQueryResultAudio, self).__init__(obj)
192 |
193 | @classmethod
194 | def a(cls, id: str, audio_url: str, title: str, caption: Optional[str] = None,
195 | parse_mode: Optional[str] = None, performer: Optional[str] = None, audio_duration: Optional[int] = None,
196 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
197 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
198 | type = 'audio'
199 | return super().a(**locals())
200 |
201 |
202 | class InlineQueryResultVoice(InlineQueryResult):
203 | fields = {
204 | 'type': str,
205 | 'id': str,
206 | 'voice_url': str,
207 | 'voice_duration': int,
208 | 'title': str,
209 | 'caption': str,
210 | 'parse_mode': str,
211 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
212 | 'input_message_content': {
213 | 'class': inputmessagecontent.InputMessageContent,
214 | 'validation': False
215 | }
216 | }
217 |
218 | def __init__(self, obj=None):
219 | super(InlineQueryResultVoice, self).__init__(obj)
220 |
221 | @classmethod
222 | def a(cls, id: str, voice_url: str, title: str, caption: Optional[str] = None,
223 | parse_mode: Optional[str] = None, voice_duration: Optional[int] = None,
224 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
225 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
226 | type = 'voice'
227 | return super().a(**locals())
228 |
229 |
230 | class InlineQueryResultDocument(InlineQueryResult):
231 | fields = {
232 | 'type': str,
233 | 'id': str,
234 | 'document_url': str,
235 | 'mime_type': str,
236 | 'description': str,
237 | 'thumb_url': str,
238 | 'thumb_width': int,
239 | 'thumb_height': int,
240 | 'title': str,
241 | 'caption': str,
242 | 'parse_mode': str,
243 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
244 | 'input_message_content': {
245 | 'class': inputmessagecontent.InputMessageContent,
246 | 'validation': False
247 | }
248 | }
249 |
250 | def __init__(self, obj=None):
251 | super(InlineQueryResultDocument, self).__init__(obj)
252 |
253 | @classmethod
254 | def a(cls, id: str, title: str, document_url: str, mime_type: str, caption: Optional[str] = None,
255 | parse_mode: Optional[str] = None, description: Optional[str] = None, thumb_url: Optional[str] = None,
256 | thumb_width: Optional[int] = None, thumb_height: Optional[int] = None,
257 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
258 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
259 | type = 'document'
260 | return super().a(**locals())
261 |
262 |
263 | class InlineQueryResultLocation(InlineQueryResult):
264 | fields = {
265 | 'type': str,
266 | 'id': str,
267 | 'latitude': str,
268 | 'longitude': str,
269 | 'title': str,
270 | 'live_period': int,
271 | 'thumb_width': int,
272 | 'thumb_height': int,
273 | 'thumb_url': str,
274 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
275 | 'input_message_content': {
276 | 'class': inputmessagecontent.InputMessageContent,
277 | 'validation': False
278 | }
279 | }
280 |
281 | def __init__(self, obj=None):
282 | super(InlineQueryResultLocation, self).__init__(obj)
283 |
284 | @classmethod
285 | def a(cls, id: str, latitude: str, longitude: str, title: str, live_period: Optional[int] = None,
286 | thumb_url: Optional[str] = None, thumb_width: Optional[int] = None, thumb_height: Optional[int] = None,
287 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
288 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
289 | type = 'document'
290 | return super().a(**locals())
291 |
292 |
293 | class InlineQueryResultVenue(InlineQueryResult):
294 | fields = {
295 | 'type': str,
296 | 'id': str,
297 | 'latitude': str,
298 | 'longitude': str,
299 | 'title': str,
300 | 'address': str,
301 | 'foursquare_id': str,
302 | 'foursquare_type': str,
303 | 'thumb_url': str,
304 | 'thumb_width': int,
305 | 'thumb_height': int,
306 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
307 | 'input_message_content': {
308 | 'class': inputmessagecontent.InputMessageContent,
309 | 'validation': False
310 | }
311 | }
312 |
313 | def __init__(self, obj=None):
314 | super(InlineQueryResultVenue, self).__init__(obj)
315 |
316 | @classmethod
317 | def a(cls, id: str, latitude: str, longitude: str, title: str, address: str,
318 | foursquare_id: Optional[str] = None, foursqure_type: Optional[str] = None, thumb_url: Optional[str] = None,
319 | thumb_width: Optional[int] = None, thumb_height: Optional[int] = None,
320 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
321 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
322 | type = 'venue'
323 | return super().a(**locals())
324 |
325 |
326 | class InlineQueryResultContact(InlineQueryResult):
327 | fields = {
328 | 'type': str,
329 | 'id': str,
330 | 'phone_number': str,
331 | 'first_name': str,
332 | 'last_name': str,
333 | 'vcard': str,
334 | 'thumb_url': str,
335 | 'thumb_width': int,
336 | 'thumb_height': int,
337 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
338 | 'input_message_content': {
339 | 'class': inputmessagecontent.InputMessageContent,
340 | 'validation': False
341 | }
342 | }
343 |
344 | def __init__(self, obj=None):
345 | super(InlineQueryResultContact, self).__init__(obj)
346 |
347 | @classmethod
348 | def a(cls, id: str, phone_number: str, first_name: str, last_name: Optional[str] = None,
349 | vcard: Optional[str] = None, thumb_url: Optional[str] = None, thumb_width: Optional[int] = None,
350 | thumb_height: Optional[int] = None,
351 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
352 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
353 | type = 'contact'
354 | return super().a(**locals())
355 |
356 |
357 | class InlineQueryResultGame(InlineQueryResult):
358 | fields = {
359 | 'type': str,
360 | 'id': str,
361 | 'game_short_name': str,
362 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
363 | }
364 |
365 | def __init__(self, obj=None):
366 | super(InlineQueryResultGame, self).__init__(obj)
367 |
368 | @classmethod
369 | def a(cls, id: str, game_short_name: str,
370 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
371 | type = 'game'
372 | return super().a(**locals())
373 |
374 |
375 | class InlineQueryResultCachedPhoto(InlineQueryResult):
376 | fields = {
377 | 'type': str,
378 | 'id': str,
379 | 'photo_file_id': str,
380 | 'description': str,
381 | 'thumb_url': str,
382 | 'title': str,
383 | 'caption': str,
384 | 'parse_mode': str,
385 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
386 | 'input_message_content': {
387 | 'class': inputmessagecontent.InputMessageContent,
388 | 'validation': False
389 | }
390 | }
391 |
392 | def __init__(self, obj=None):
393 | super(InlineQueryResultCachedPhoto, self).__init__(obj)
394 |
395 | @classmethod
396 | def a(cls, id: str, photo_file_id: str, title: Optional[str] = None,
397 | description: Optional[str] = None, caption: Optional[str] = None,
398 | parse_mode: Optional[str] = None,
399 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
400 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
401 | type = 'photo'
402 | return super().a(**locals())
403 |
404 |
405 | class InlineQueryResultCachedGif(InlineQueryResult):
406 | fields = {
407 | 'type': str,
408 | 'id': str,
409 | 'gif_file_id': str,
410 | 'title': str,
411 | 'caption': str,
412 | 'parse_mode': str,
413 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
414 | 'input_message_content': {
415 | 'class': inputmessagecontent.InputMessageContent,
416 | 'validation': False
417 | }
418 | }
419 |
420 | def __init__(self, obj=None):
421 | super(InlineQueryResultCachedGif, self).__init__(obj)
422 |
423 | @classmethod
424 | def a(cls, id: str, gif_file_id: str, title: Optional[str] = None,
425 | caption: Optional[str] = None,
426 | parse_mode: Optional[str] = None,
427 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
428 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
429 | type = 'gif'
430 | return super().a(**locals())
431 |
432 |
433 | class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
434 | fields = {
435 | 'type': str,
436 | 'id': str,
437 | 'mpeg4_file_id': str,
438 | 'title': str,
439 | 'caption': str,
440 | 'parse_mode': str,
441 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
442 | 'input_message_content': {
443 | 'class': inputmessagecontent.InputMessageContent,
444 | 'validation': False
445 | }
446 | }
447 |
448 | def __init__(self, obj=None):
449 | super(InlineQueryResultCachedMpeg4Gif, self).__init__(obj)
450 |
451 | @classmethod
452 | def a(cls, id: str, mpeg4_file_id: str, title: Optional[str] = None,
453 | caption: Optional[str] = None,
454 | parse_mode: Optional[str] = None,
455 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
456 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
457 | type = 'mpeg4_gif'
458 | return super().a(**locals())
459 |
460 |
461 | class InlineQueryResultCachedSticker(InlineQueryResult):
462 | fields = {
463 | 'type': str,
464 | 'id': str,
465 | 'sticker_file_id': str,
466 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
467 | 'input_message_content': {
468 | 'class': inputmessagecontent.InputMessageContent,
469 | 'validation': False
470 | }
471 | }
472 |
473 | def __init__(self, obj=None):
474 | super(InlineQueryResultCachedSticker, self).__init__(obj)
475 |
476 | @classmethod
477 | def a(cls, id: str, sticker_file_id: str,
478 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
479 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
480 | type = 'sticker'
481 | return super().a(**locals())
482 |
483 |
484 | class InlineQueryResultCachedDocument(InlineQueryResult):
485 | fields = {
486 | 'type': str,
487 | 'id': str,
488 | 'title': str,
489 | 'caption': str,
490 | 'description': str,
491 | 'document_file_id': str,
492 | 'parse_mode': str,
493 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
494 | 'input_message_content': {
495 | 'class': inputmessagecontent.InputMessageContent,
496 | 'validation': False
497 | }
498 | }
499 |
500 | def __init__(self, obj=None):
501 | super(InlineQueryResultCachedDocument, self).__init__(obj)
502 |
503 | @classmethod
504 | def a(cls, id: str, document_file_id: str, title: str,
505 | description: Optional[str] = None, caption: Optional[str] = None,
506 | parse_mode: Optional[str] = None,
507 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
508 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
509 | type = 'document'
510 | return super().a(**locals())
511 |
512 |
513 | class InlineQueryResultCachedVideo(InlineQueryResult):
514 | fields = {
515 | 'type': str,
516 | 'id': str,
517 | 'video_file_id': str,
518 | 'title': str,
519 | 'caption': str,
520 | 'description': str,
521 | 'parse_mode': str,
522 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
523 | 'input_message_content': {
524 | 'class': inputmessagecontent.InputMessageContent,
525 | 'validation': False
526 | }
527 | }
528 |
529 | def __init__(self, obj=None):
530 | super(InlineQueryResultCachedVideo, self).__init__(obj)
531 |
532 | @classmethod
533 | def a(cls, id: str, video_file_id: str, title: str,
534 | description: Optional[str] = None, caption: Optional[str] = None,
535 | parse_mode: Optional[str] = None,
536 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
537 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
538 | type = 'video'
539 | return super().a(**locals())
540 |
541 |
542 | class InlineQueryResultCachedVoice(InlineQueryResult):
543 | fields = {
544 | 'type': str,
545 | 'id': str,
546 | 'voice_file_id': str,
547 | 'title': str,
548 | 'caption': str,
549 | 'parse_mode': str,
550 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
551 | 'input_message_content': {
552 | 'class': inputmessagecontent.InputMessageContent,
553 | 'validation': False
554 | }
555 | }
556 |
557 | def __init__(self, obj=None):
558 | super(InlineQueryResultCachedVoice, self).__init__(obj)
559 |
560 | @classmethod
561 | def a(cls, id: str, voice_file_id: str, title: str,
562 | caption: Optional[str] = None,
563 | parse_mode: Optional[str] = None,
564 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
565 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
566 | type = 'voice'
567 | return super().a(**locals())
568 |
569 |
570 | class InlineQueryResultCachedAudio(InlineQueryResult):
571 | fields = {
572 | 'type': str,
573 | 'id': str,
574 | 'audio_file_id': str,
575 | 'caption': str,
576 | 'parse_mode': str,
577 | 'reply_markup': inlinekeyboardmarkup.InlineKeyboardMarkup,
578 | 'input_message_content': {
579 | 'class': inputmessagecontent.InputMessageContent,
580 | 'validation': False
581 | }
582 | }
583 |
584 | def __init__(self, obj=None):
585 | super(InlineQueryResultCachedAudio, self).__init__(obj)
586 |
587 | @classmethod
588 | def a(cls, id: str, audio_file_id: str,
589 | caption: Optional[str] = None,
590 | parse_mode: Optional[str] = None,
591 | input_message_content: Optional[inputmessagecontent.InputMessageContent] = None,
592 | reply_markup: Optional[inlinekeyboardmarkup.InlineKeyboardMarkup] = None):
593 | type = 'audio'
594 | return super().a(**locals())
595 |
--------------------------------------------------------------------------------
/django_tgbot/bot_api_user.py:
--------------------------------------------------------------------------------
1 | from typing import Union, List
2 |
3 | import requests
4 | import time
5 | import json
6 | import inspect
7 |
8 | from django_tgbot.exceptions import BotAPIRequestFailure, APIInputError
9 | from django_tgbot.types.botcommand import BotCommand
10 | from django_tgbot.types.chat import Chat
11 | from django_tgbot.types.chatmember import ChatMember
12 | from django_tgbot.types.file import File
13 | from django_tgbot.types.forcereply import ForceReply
14 | from django_tgbot.types.inlinekeyboardmarkup import InlineKeyboardMarkup
15 | from django_tgbot.types.message import Message
16 | from django_tgbot.types.messageentity import MessageEntity
17 | from django_tgbot.types.poll import Poll
18 | from django_tgbot.types.replykeyboardmarkup import ReplyKeyboardMarkup
19 | from django_tgbot.types.replykeyboardremove import ReplyKeyboardRemove
20 | from django_tgbot.types.stickerset import StickerSet
21 | from django_tgbot.types.update import Update
22 | from django_tgbot.types.user import User
23 | from django_tgbot.types.userprofilephotos import UserProfilePhotos
24 |
25 |
26 | def create_params_from_args(args=None, exclude=None):
27 | """
28 | The args should usually be `locals()` and exclude should be the args you want to exclude
29 | 'self' is automatically added to exclude list.
30 | :return: a dictionary made from the arguments
31 | """
32 | if args is None:
33 | args = {}
34 | if exclude is None:
35 | exclude = []
36 | if 'self' not in exclude:
37 | exclude.append('self')
38 | result = {}
39 | for arg in args.keys():
40 | if arg in exclude or args[arg] is None:
41 | continue
42 | if hasattr(args[arg], 'to_dict'):
43 | result[arg] = args[arg].to_dict()
44 | else:
45 | result[arg] = args[arg]
46 |
47 | if arg == 'reply_markup' and type(result[arg]) != str:
48 | result[arg] = json.dumps(result[arg])
49 |
50 | if (type(result[arg])) == list:
51 | to_be_json_list = []
52 | for item in result[arg]:
53 | if hasattr(item, 'to_dict'):
54 | to_be_json_list.append(item.to_dict())
55 | else:
56 | to_be_json_list.append(item)
57 |
58 | result[arg] = json.dumps(to_be_json_list)
59 |
60 | return result
61 |
62 |
63 | class BotAPIUser:
64 | MAX_TRIES = 5
65 |
66 | PARSE_MODE_MARKDOWN = 'Markdown'
67 | PARSE_MODE_HTML = 'HTML'
68 |
69 | POLL_TYPE_QUIZ = 'quiz'
70 | POLL_TYPE_REGULAR = 'regular'
71 |
72 | CHAT_ACTION_TYPING = 'typing'
73 | CHAT_ACTION_PHOTO = 'upload_photo'
74 | CHAT_ACTION_VIDEO_RECORD = 'record_video'
75 | CHAT_ACTION_VIDEO_UPLOAD = 'upload_video'
76 | CHAT_ACTION_AUDIO_RECORD = 'record_audio'
77 | CHAT_ACTION_AUDIO_UPLOAD = 'upload_audio'
78 | CHAT_ACTION_DOCUMENT = 'upload_document'
79 | CHAT_ACTION_LOCATION = 'find_location'
80 | CHAT_ACTION_VIDEO_NOTE_RECORD = 'record_video_note'
81 | CHAT_ACTION_VIDEO_NOTE_UPLOAD = 'upload_video_note'
82 |
83 | def __init__(self, token):
84 | self.token = ''
85 | self.api_url = ''
86 | self.set_token(token)
87 |
88 | def set_token(self, token):
89 | self.token = token
90 | self.api_url = 'https://api.telegram.org/bot{}'.format(self.token)
91 |
92 | def send_request(self, method, data=None, files=None):
93 | if data is None:
94 | data = {}
95 | url = '{}/{}'.format(self.api_url, method)
96 | r = None
97 | sleep_time = 1
98 | for i in range(self.MAX_TRIES):
99 | try:
100 | if files is None:
101 | r = requests.post(url, data)
102 | else:
103 | r = requests.post(url, data=data, files=files)
104 | except requests.RequestException:
105 | time.sleep(sleep_time)
106 | sleep_time += 0.2
107 | continue
108 | else:
109 | break
110 |
111 | if r is not None:
112 | python_result = json.loads(r.text)
113 | if 'ok' not in python_result:
114 | python_result['ok'] = bool(r)
115 | return python_result
116 | return {'ok': False, 'description': 'Max tries exceeded.', 'no_connection': True}
117 |
118 | def request_and_result(self, data, result_type, files=None):
119 | """
120 | Should be called by a method with exact same name as the API method
121 |
122 | :param result_type: can be either a type class or a list containing one type class which will parse
123 | the result to be a list of that type. For example: Message if the result is a message or [Message] if the
124 | result if a list of messages
125 | """
126 | res = self.send_request(inspect.stack()[1].function, data=data, files=files)
127 | if res['ok']:
128 | if type(result_type) == list and len(result_type) > 0:
129 | if len(result_type) > 1:
130 | raise ValueError("Passed `result_type` cannot have more than one element if it is a list.")
131 | return list(map(result_type[0], list(res['result'])))
132 | else:
133 | return result_type(res['result'])
134 | else:
135 | return res
136 |
137 | def getMe(self) -> User:
138 | return self.request_and_result(create_params_from_args(), User)
139 |
140 | def getMyCommands(self) -> List[BotCommand]:
141 | return self.request_and_result(create_params_from_args(), [BotCommand])
142 |
143 | def setMyCommands(self, commands: List[BotCommand]) -> bool:
144 | return self.request_and_result(create_params_from_args(locals()), bool)
145 |
146 | def getUpdates(self, offset=None, limit=100, timeout=0, allow_updates=None) -> List[Update]:
147 | """
148 | Returns a list of UNPARSED updates. The json objects in the list should still be sent to Update class to become
149 | Update objects.
150 | :param offset: The update_id of the first update you wish to receive
151 | :param limit: The number of updates returned
152 | :param timeout: Timeout in seconds for long polling.
153 | :param allow_updates: List of the update types you want your bot to receive. Can be either a list of JSON string
154 | :return: a list of json updates
155 | """
156 | updates = self.request_and_result(create_params_from_args(locals()), [Update])
157 | if type(updates) == dict and not updates['ok']:
158 | raise BotAPIRequestFailure(f"Error code {updates['error_code']} ({updates['description']})")
159 | return updates
160 |
161 | def setWebhook(self, url):
162 | return self.send_request('setWebhook', {'url': url})
163 |
164 | def sendMessage(self, chat_id, text, parse_mode=None, entities: List[MessageEntity] = None,
165 | disable_web_page_preview=None, disable_notification=None, reply_to_message_id=None,
166 | reply_markup: Union[
167 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
168 | return self.request_and_result(create_params_from_args(locals()), Message)
169 |
170 | def forwardMessage(self, chat_id, from_chat_id, message_id, disable_notification=None):
171 | return self.request_and_result(create_params_from_args(locals()), Message)
172 |
173 | def sendPhoto(self, chat_id, photo, upload=False, caption=None, parse_mode=None, disable_notification=None,
174 | reply_to_message_id=None,
175 | reply_markup: Union[
176 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
177 | if not upload:
178 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
179 | else:
180 | return self.request_and_result(
181 | create_params_from_args(locals(), ['upload', 'photo']),
182 | Message,
183 | files={'photo': photo}
184 | )
185 |
186 | def sendAudio(self, chat_id, audio, upload=False, caption=None, parse_mode=None, duration=None, performer=None,
187 | title=None,
188 | thumb=None, disable_notification=None, reply_to_message_id=None,
189 | reply_markup: Union[
190 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
191 |
192 | if not upload:
193 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
194 | else:
195 | return self.request_and_result(
196 | create_params_from_args(locals(), ['upload', 'audio']),
197 | Message,
198 | files={'audio': audio}
199 | )
200 |
201 | def sendDocument(self, chat_id, document, upload=False, thumb=None, caption=None, parse_mode=None,
202 | disable_notification=None,
203 | reply_to_message_id=None,
204 | reply_markup: Union[
205 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
206 |
207 | if not upload:
208 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
209 | else:
210 | return self.request_and_result(
211 | create_params_from_args(locals(), ['upload', 'document']),
212 | Message,
213 | files={'document': document}
214 | )
215 |
216 | def sendVideo(self, chat_id, video, upload=False, duration=None, width=None, height=None, thumb=None, caption=None,
217 | parse_mode=None, supports_streaming=None, disable_notification=None, reply_to_message_id=None,
218 | reply_markup: Union[
219 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
220 |
221 | if not upload:
222 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
223 | else:
224 | return self.request_and_result(
225 | create_params_from_args(locals(), ['upload', 'video']),
226 | Message,
227 | files={'video': video}
228 | )
229 |
230 | def sendAnimation(self, chat_id, animation, upload=False, duration=None, width=None, height=None, thumb=None,
231 | caption=None,
232 | parse_mode=None, disable_notification=None, reply_to_message_id=None,
233 | reply_markup: Union[
234 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
235 |
236 | if not upload:
237 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
238 | else:
239 | return self.request_and_result(
240 | create_params_from_args(locals(), ['upload', 'animation']),
241 | Message,
242 | files={'animation': animation}
243 | )
244 |
245 | def sendVoice(self, chat_id, voice, upload=False, caption=None, parse_mode=None, duration=None,
246 | disable_notification=None,
247 | reply_to_message_id=None,
248 | reply_markup: Union[
249 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
250 |
251 | if not upload:
252 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
253 | else:
254 | return self.request_and_result(
255 | create_params_from_args(locals(), ['upload', 'voice']),
256 | Message,
257 | files={'voice': voice}
258 | )
259 |
260 | def sendVideoNote(self, chat_id, video_note, upload=False, duration=None, length=None, thumb=None,
261 | disable_notification=None,
262 | reply_to_message_id=None,
263 | reply_markup: Union[
264 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
265 |
266 | if not upload:
267 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
268 | else:
269 | return self.request_and_result(
270 | create_params_from_args(locals(), ['upload', 'video_note']),
271 | Message,
272 | files={'video_note': video_note}
273 | )
274 |
275 | def sendMediaGroup(self, chat_id, media, upload=False, disable_notification=None, reply_to_message_id=None):
276 | if not upload:
277 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
278 | else:
279 | return self.request_and_result(
280 | create_params_from_args(locals(), ['upload', 'media']),
281 | Message,
282 | files={'media': media}
283 | )
284 |
285 | def sendLocation(self, chat_id, latitude, longitude, live_period=None, disable_notification=None,
286 | reply_to_message_id=None,
287 | reply_markup: Union[
288 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
289 |
290 | return self.request_and_result(create_params_from_args(locals()), Message)
291 |
292 | def editMessageLiveLocation(self, chat_id, latitude, longitude, message_id=None, inline_message_id=None,
293 | reply_markup: Union[
294 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
295 |
296 | return self.request_and_result(create_params_from_args(locals()), Message)
297 |
298 | def stopMessageLiveLocation(self, chat_id, message_id=None, inline_message_id=None,
299 | reply_markup: Union[
300 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
301 |
302 | return self.request_and_result(create_params_from_args(locals()), Message)
303 |
304 | def sendVenue(self, chat_id, latitude, longitude, title, address, foursquare_id=None, foursquare_type=None,
305 | disable_notification=None, reply_to_message_id=None,
306 | reply_markup: Union[
307 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
308 |
309 | return self.request_and_result(create_params_from_args(locals()), Message)
310 |
311 | def sendContact(self, chat_id, phone_number, first_name, last_name=None, vcard=None, disable_notification=None,
312 | reply_to_message_id=None,
313 | reply_markup: Union[
314 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
315 |
316 | return self.request_and_result(create_params_from_args(locals()), Message)
317 |
318 | def sendPoll(self, chat_id, question, options, is_anonymous=None, type=None, allows_multiple_answers=None,
319 | correct_option_id: int = None, explanation: str = None, explanation_parse_mode: str = None,
320 | explanation_entities: List[MessageEntity] = None, open_period: int = None, close_date: int = None,
321 | is_closed=None, disable_notification=None,
322 | reply_to_message_id=None,
323 | reply_markup: Union[
324 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
325 |
326 | if open_period is not None and close_date is not None:
327 | raise APIInputError("Polls cannot use both open_period and close_date.")
328 |
329 | return self.request_and_result(create_params_from_args(locals()), Message)
330 |
331 | def sendChatAction(self, chat_id, action):
332 | return self.request_and_result(create_params_from_args(locals()), bool)
333 |
334 | def getUserProfilePhotos(self, user_id, offset=None, limit=None):
335 | return self.request_and_result(create_params_from_args(locals()), UserProfilePhotos)
336 |
337 | def getFile(self, file_id):
338 | return self.request_and_result(create_params_from_args(locals()), File)
339 |
340 | def kickChatMember(self, chat_id, user_id, until_date=None):
341 | return self.request_and_result(create_params_from_args(locals()), bool)
342 |
343 | def unbanChatMember(self, chat_id, user_id):
344 | return self.request_and_result(create_params_from_args(locals()), bool)
345 |
346 | def restrictChatMember(self, chat_id, user_id, permissions, until_date=None):
347 | return self.request_and_result(create_params_from_args(locals()), bool)
348 |
349 | def promoteChatMember(self, chat_id, user_id, can_change_info=None, can_post_messages=None, can_edit_messages=None,
350 | can_delete_messages=None, can_invite_users=None, can_restrict_members=None,
351 | can_pin_messages=None, can_promote_members=None):
352 | return self.request_and_result(create_params_from_args(locals()), bool)
353 |
354 | def setChatAdministratorCustomTitle(self, chat_id, user_id, custom_title):
355 | return self.request_and_result(create_params_from_args(locals()), bool)
356 |
357 | def setChatPermissions(self, chat_id, permissions):
358 | return self.request_and_result(create_params_from_args(locals()), bool)
359 |
360 | def exportChatInviteLink(self, chat_id):
361 | return self.request_and_result(create_params_from_args(locals()), bool)
362 |
363 | def setChatPhoto(self, chat_id, photo):
364 | return self.request_and_result(create_params_from_args(locals(), ['photo']), bool, files={'photo': photo})
365 |
366 | def deleteChatPhoto(self, chat_id):
367 | return self.request_and_result(create_params_from_args(locals()), bool)
368 |
369 | def setChatTitle(self, chat_id, title):
370 | return self.request_and_result(create_params_from_args(locals()), bool)
371 |
372 | def setChatDescription(self, chat_id, description=None):
373 | return self.request_and_result(create_params_from_args(locals()), bool)
374 |
375 | def pinChatMessage(self, chat_id, message_id, disable_notification=None):
376 | return self.request_and_result(create_params_from_args(locals()), bool)
377 |
378 | def unpinChatMessage(self, chat_id):
379 | return self.request_and_result(create_params_from_args(locals()), bool)
380 |
381 | def leaveChat(self, chat_id):
382 | return self.request_and_result(create_params_from_args(locals()), bool)
383 |
384 | def getChat(self, chat_id):
385 | return self.request_and_result(create_params_from_args(locals()), Chat)
386 |
387 | # TODO getChatAdministrators
388 |
389 | def getChatMembersCount(self, chat_id):
390 | return self.request_and_result(create_params_from_args(locals()), int)
391 |
392 | def getChatMember(self, chat_id, user_id):
393 | return self.request_and_result(create_params_from_args(locals()), ChatMember)
394 |
395 | def setChatStickerSet(self, chat_id, sticker_set_name):
396 | return self.request_and_result(create_params_from_args(locals()), bool)
397 |
398 | def deleteChatStickerSet(self, chat_id):
399 | return self.request_and_result(create_params_from_args(locals()), bool)
400 |
401 | def answerCallbackQuery(self, callback_query_id, text=None, show_alert=None, url=None, cache_time=None):
402 | return self.request_and_result(create_params_from_args(locals()), bool)
403 |
404 | def editMessageText(self, text, chat_id=None, message_id=None, inline_message_id=None, parse_mode=None,
405 | disable_web_page_preview=None,
406 | reply_markup: Union[
407 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
408 |
409 | return self.request_and_result(create_params_from_args(locals()), Message)
410 |
411 | def editMessageCaption(self, chat_id=None, message_id=None, inline_message_id=None, caption=None, parse_mode=None,
412 | reply_markup: Union[
413 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
414 |
415 | return self.request_and_result(create_params_from_args(locals()), Message)
416 |
417 | def editMessageMedia(self, media, chat_id=None, message_id=None, inline_message_id=None,
418 | reply_markup: Union[
419 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
420 |
421 | return self.request_and_result(create_params_from_args(locals()), Message)
422 |
423 | def editMessageReplyMarkup(self, chat_id=None, message_id=None, inline_message_id=None,
424 | reply_markup: Union[
425 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
426 |
427 | return self.request_and_result(create_params_from_args(locals()), Message)
428 |
429 | def stopPoll(self, chat_id, message_id, reply_markup=None):
430 | return self.request_and_result(create_params_from_args(locals()), Poll)
431 |
432 | def deleteMessage(self, chat_id, message_id):
433 | return self.request_and_result(create_params_from_args(locals()), bool)
434 |
435 | def sendSticker(self, chat_id, sticker, upload=False, disable_notification=None, reply_to_message_id=None,
436 | reply_markup: Union[
437 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
438 |
439 | if not upload:
440 | return self.request_and_result(create_params_from_args(locals(), ['upload']), Message)
441 | else:
442 | return self.request_and_result(
443 | create_params_from_args(locals(), ['upload', 'sticker']),
444 | Message,
445 | files={'sticker': sticker}
446 | )
447 |
448 | def getStickerSet(self, name):
449 | return self.request_and_result(create_params_from_args(locals()), StickerSet)
450 |
451 | def uploadStickerFile(self, user_id, png_sticker):
452 | return self.request_and_result(create_params_from_args(locals(), ['png_sticker']), File,
453 | files={'png_sticker': png_sticker})
454 |
455 | def createNewStickerSet(self, user_id, name, title, emojis, png_sticker=None, tgs_sticker=None, upload=False,
456 | contains_masks=None, mask_position=None):
457 | if png_sticker is not None and tgs_sticker is not None:
458 | raise APIInputError("Only one of png_sticker and tgs_sticker should be used.")
459 |
460 | if png_sticker is None and tgs_sticker is None:
461 | raise APIInputError("You must use exactly one of png_sticker and tgs_sticker. They are both None.")
462 |
463 | ignore_list = ['upload']
464 | files = {}
465 |
466 | if png_sticker is None:
467 | ignore_list.append('png_sticker')
468 | files['tgs_sticker'] = tgs_sticker
469 | if tgs_sticker is None:
470 | ignore_list.append('tgs_sticker')
471 | files['png_sticker'] = png_sticker
472 |
473 | if not upload:
474 | return self.request_and_result(create_params_from_args(locals(), ignore_list), bool)
475 | else:
476 | return self.request_and_result(
477 | create_params_from_args(locals(), ['upload', 'png_sticker', 'tgs_sticker']),
478 | bool,
479 | files=files
480 | )
481 |
482 | def addStickerToSet(self, user_id, name, emojis, png_sticker=None, tgs_sticker=None, upload=False,
483 | mask_position=None):
484 | if png_sticker is not None and tgs_sticker is not None:
485 | raise APIInputError("Only one of png_sticker and tgs_sticker should be used.")
486 |
487 | if png_sticker is None and tgs_sticker is None:
488 | raise APIInputError("You must use exactly one of png_sticker and tgs_sticker. They are both None.")
489 |
490 | ignore_list = ['upload']
491 | files = {}
492 |
493 | if png_sticker is None:
494 | ignore_list.append('png_sticker')
495 | files['tgs_sticker'] = tgs_sticker
496 | if tgs_sticker is None:
497 | ignore_list.append('tgs_sticker')
498 | files['png_sticker'] = png_sticker
499 |
500 | if not upload:
501 | return self.request_and_result(create_params_from_args(locals(), ignore_list), bool)
502 | else:
503 | return self.request_and_result(
504 | create_params_from_args(locals(), ['upload', 'png_sticker', 'tgs_sticker']),
505 | bool,
506 | files=files
507 | )
508 |
509 | def setStickerPositionInSet(self, sticker, position):
510 | return self.request_and_result(create_params_from_args(locals()), bool)
511 |
512 | def setStickerSetThumb(self, name: str, user_id, thumb=None, upload=False) -> bool:
513 | if upload and thumb is None:
514 | raise APIInputError("Param `upload` is True but no thumbnail is given.")
515 | if not upload:
516 | return self.request_and_result(create_params_from_args(locals(), ['upload']), bool)
517 | else:
518 | return self.request_and_result(
519 | create_params_from_args(locals(), ['upload', 'thumb']),
520 | bool,
521 | files={'thumb': thumb}
522 | )
523 |
524 | def deleteStickerFromSet(self, sticker):
525 | return self.request_and_result(create_params_from_args(locals()), bool)
526 |
527 | def answerInlineQuery(self, inline_query_id, results, cache_time=None, is_personal=None, next_offset=None,
528 | switch_pm_text=None, switch_pm_parameter=None):
529 | return self.request_and_result(create_params_from_args(locals()), bool)
530 |
531 | def sendInvoice(self, chat_id, title, description, payload, provider_token, start_parameter, currency, prices,
532 | provider_data=None, photo_url=None, photo_size=None, photo_width=None, photo_height=None,
533 | need_name=None, need_phone_number=None, need_email=None, need_shipping_address=None,
534 | send_phone_number_to_provider=None, send_email_to_provider=None, is_flexible=None,
535 | disable_notification=None, reply_to_message_id=None,
536 | reply_markup: Union[
537 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None) -> Message:
538 |
539 | return self.request_and_result(create_params_from_args(locals()), Message)
540 |
541 | def answerShippingQuery(self, shipping_query_id, ok, shipping_options=None, error_message=None):
542 | return self.request_and_result(create_params_from_args(locals()), bool)
543 |
544 | def answerPreCheckoutQuery(self, pre_checkout_query_id, ok, error_message=None):
545 | return self.request_and_result(create_params_from_args(locals()), bool)
546 |
547 | def sendGame(self, chat_id, game_short_name, disable_notification=None, reply_to_message_id=None,
548 | reply_markup=None):
549 | return self.request_and_result(create_params_from_args(locals()), Message)
550 |
551 | def setGameScore(self, user_id, score, force=None, disable_edit_message=None, chat_id=None, message_id=None,
552 | inline_message_id=None):
553 | return self.request_and_result(create_params_from_args(locals()), Message)
554 |
555 | # TODO getGameHighScores
556 |
557 | def sendDice(
558 | self, chat_id, emoji=None, disable_notification=None, reply_to_message_id=None,
559 | allow_sending_without_reply=None, reply_markup: Union[
560 | None, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply] = None):
561 | return self.request_and_result(create_params_from_args(locals()), Message)
562 |
--------------------------------------------------------------------------------