├── 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 | ![demo](img/code_and_bot.jpg) 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 | [![Documentation Status](https://readthedocs.org/projects/django-tgbot/badge/?version=latest)](https://django-tgbot.readthedocs.io/en/latest/?badge=latest) 2 | [![Bot API Version](https://img.shields.io/badge/Bot%20API-v4.9-blue)](https://core.telegram.org/bots/api) 3 | [![Community Chat](https://img.shields.io/badge/Community-Chat-informational?logo=telegram)](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 | ![demo](docs/img/code_and_bot.jpg) 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 | --------------------------------------------------------------------------------