├── .gitignore ├── LICENSE ├── README.md ├── lesson-01 ├── bot.py └── config.py ├── lesson-02 ├── bot.py ├── config.py ├── db_map.py ├── demo-media │ ├── files │ │ └── very important text file.txt │ ├── ogg │ │ └── Rick_Astley_-_Never_Gonna_Give_You_Up.ogg │ ├── pics │ │ ├── kitten0.jpg │ │ ├── kitten1.jpg │ │ ├── kitten2.jpg │ │ └── kitten3.jpg │ ├── videoNotes │ │ └── cute-puppy.mp4 │ └── videos │ │ └── hedgehog.mp4 └── upload_my_files.py ├── lesson-03 ├── bot.py ├── config.py ├── messages.py └── utils.py ├── lesson-04 ├── config.py ├── messages.py ├── payments-minimum_bot.py └── payments_bot.py └── lesson-05 ├── bot.py ├── config.py └── keyboards.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | 4 | .DS_store 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Suren Khorenyan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Асинхронный Telegram бот на языке Python 3 с использованием библиотеки aiogram 2 | 3 | ## Текстовая серия уроков устарела технически, хоть и остаётся актуальной концептуально. 4 | Есть [новая текстовая серия уроков](https://mastergroosha.github.io/aiogram-3-guide/) от автора, вдохновившего меня на создание этой серии уроков (вот так вот мы зациклились). 5 | 6 | Также [на канале автора этого текстового курса](https://www.youtube.com/@SurenKhorenyan) есть [серия видеоуроков по aiogram](https://www.youtube.com/playlist?list=PLYnH8mpFQ4am8cFYqn2KsPLb-HrhcYgtC) **по новой версии aiogram 3**. 7 | 8 | К сожалению, ресурс, на котором были расположены текстовые курсы, "закончился" и остался в законсервированном виде. Там нельзя ничего обновлять, нет админки, поэтому там не оставить никакую подсказку об изменениях. 9 | 10 | ___ 11 | 12 | Для понимания уроков необходимо хотя бы базовое знание языка Python версии 3. 13 | 14 | Код из всех уроков доступен на [GitHub](https://github.com/surik00/aiogram-lessons). 15 | 16 | > **Важно!** Автор не является профессионалом, в уроках от вас не требуется поступать точно так же. Данный учебник является дружеской рекомендацией, поэтому обо всех ошибках и недочетах можно и **нужно** писать в комментариях или обсуждении. Советы, как поступить было бы лучше, тоже приветствуются. 17 | 18 | ### Q&A: 19 | 20 | **Q:** Почему [aiogram](http://aiogram.readthedocs.io/en/latest/index.html), а не, например, [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI)? 21 | **A:** Автор сам [начинал знакомство с разработкой Телеграм ботов](https://www.gitbook.com/book/groosha/telegram-bot-lessons/details), используя pyTelegramBotAPI, однако поведение библиотеки перестало удовлетворять на больших проектах, у неё странная многопоточность, [FSM](https://en.wikipedia.org/wiki/Finite-state_machine) приходилось создавать самостоятельно \(есть даже [порт FSM из aiogram в pyTelegramBotAPI](https://github.com/Ars2014/FSMTelegramBotAPI)\), плохо реализованное логгирование, aiogram позволяет создавать middleware, например то же [логгирование](https://github.com/aiogram/aiogram/blob/master/aiogram/contrib/middlewares/logging.py), [антифлуд](https://github.com/aiogram/aiogram/blob/master/examples/middleware_and_antiflood.py), ну и просто, [почему нет](https://goo.gl/ngtT8u)? 22 | 23 | 24 | ### **Оглавление:** 25 | 26 | * [Урок 1. Быстрый старт. Эхо-бот](https://surik00.gitbooks.io/aiogram-lessons/content/chapter1.html) 27 | * [Урок 2. Медиа, разметка, эмоджи и щепотка логирования](http://surik00.gitbooks.io/aiogram-lessons/content/chapter2.html) 28 | * [Урок 3. Машина состояний и то самое логгирование](http://surik00.gitbooks.io/aiogram-lessons/content/chapter3.html) 29 | * [Урок 4. Платежи в Telegram](http://surik00.gitbooks.io/aiogram-lessons/content/chapter4.html) 30 | * [Урок 5. Клавиатуры и кнопки](http://surik00.gitbooks.io/aiogram-lessons/content/chapter5.html) 31 | -------------------------------------------------------------------------------- /lesson-01/bot.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, types 2 | from aiogram.dispatcher import Dispatcher 3 | from aiogram.utils import executor 4 | 5 | from config import TOKEN 6 | 7 | 8 | bot = Bot(token=TOKEN) 9 | dp = Dispatcher(bot) 10 | 11 | 12 | @dp.message_handler(commands=['start']) 13 | async def process_start_command(message: types.Message): 14 | await message.reply("Привет!\nНапиши мне что-нибудь!") 15 | 16 | 17 | @dp.message_handler(commands=['help']) 18 | async def process_help_command(message: types.Message): 19 | await message.reply("Напиши мне что-нибудь, и я отпрпавлю этот текст тебе в ответ!") 20 | 21 | 22 | @dp.message_handler() 23 | async def echo_message(msg: types.Message): 24 | await bot.send_message(msg.from_user.id, msg.text) 25 | 26 | 27 | if __name__ == '__main__': 28 | executor.start_polling(dp) 29 | -------------------------------------------------------------------------------- /lesson-01/config.py: -------------------------------------------------------------------------------- 1 | TOKEN = 'YOUR:own_private_secure-Bot_token' 2 | -------------------------------------------------------------------------------- /lesson-02/bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiogram import Bot, types 5 | from aiogram.utils import executor 6 | from aiogram.utils.emoji import emojize 7 | from aiogram.dispatcher import Dispatcher 8 | from aiogram.types.message import ContentType 9 | from aiogram.utils.markdown import text, bold, italic, code, pre 10 | from aiogram.types import ParseMode, InputMediaPhoto, InputMediaVideo, ChatActions 11 | 12 | from config import TOKEN 13 | 14 | logging.basicConfig(format=u'%(filename)s [ LINE:%(lineno)+3s ]#%(levelname)+8s [%(asctime)s] %(message)s', 15 | level=logging.INFO) 16 | 17 | CAT_BIG_EYES = 'AgADAgADNqkxG3hu6Eov3mINslrI7jUWnA4ABAX7PAfFIfbONj0AAgI' 18 | KITTENS = [ 19 | 'AgADAgADN6kxG3hu6EqJjqtjb2_dtnztAw4ABMPliaCdHTFDDxsCAAEC', 20 | 'AgADAgADNakxG3hu6Epaq9GtKVQcmEPqAw4ABKKK02zsSoEJtRwCAAEC', 21 | 'AgADAgADNKkxG3hu6EoNC-hZek5IUkeZQw4ABPbUDtX7JTIZmjwAAgI', 22 | ] 23 | VOICE = 'AwADAgADXQEAAnhu6EqAvqdylJRvBgI' 24 | VIDEO = 'BAADAgADXAEAAnhu6ErDHE-xNjIzMgI' 25 | TEXT_FILE = 'BQADAgADWgEAAnhu6ErgyjSYkwOL6AI' 26 | VIDEO_NOTE = 'DQADAgADWwEAAnhu6EoFqDa-fStSmgI' 27 | 28 | 29 | bot = Bot(token=TOKEN) 30 | dp = Dispatcher(bot) 31 | 32 | 33 | @dp.message_handler(commands=['start']) 34 | async def process_start_command(message: types.Message): 35 | await message.reply('Привет!\nИспользуй /help, ' 36 | 'чтобы узнать список доступных команд!') 37 | 38 | 39 | @dp.message_handler(commands=['help']) 40 | async def process_help_command(message: types.Message): 41 | msg = text(bold('Я могу ответить на следующие команды:'), 42 | '/voice', '/photo', '/group', '/note', '/file, /testpre', sep='\n') 43 | await message.reply(msg, parse_mode=ParseMode.MARKDOWN) 44 | 45 | 46 | @dp.message_handler(commands=['voice']) 47 | async def process_voice_command(message: types.Message): 48 | await bot.send_voice(message.from_user.id, VOICE, 49 | reply_to_message_id=message.message_id) 50 | 51 | 52 | @dp.message_handler(commands=['photo']) 53 | async def process_photo_command(message: types.Message): 54 | caption = 'Какие глазки! :eyes:' 55 | await bot.send_photo(message.from_user.id, CAT_BIG_EYES, 56 | caption=emojize(caption), 57 | reply_to_message_id=message.message_id) 58 | 59 | 60 | @dp.message_handler(commands=['group']) 61 | async def process_group_command(message: types.Message): 62 | media = [InputMediaVideo(VIDEO, 'ёжик и котятки')] 63 | for photo_id in KITTENS: 64 | media.append(InputMediaPhoto(photo_id)) 65 | await bot.send_media_group(message.from_user.id, media) 66 | 67 | 68 | @dp.message_handler(commands=['note']) 69 | async def process_note_command(message: types.Message): 70 | user_id = message.from_user.id 71 | await bot.send_chat_action(user_id, ChatActions.RECORD_VIDEO_NOTE) 72 | await asyncio.sleep(1) # конвертируем видео и отправляем его пользователю 73 | await bot.send_video_note(message.from_user.id, VIDEO_NOTE) 74 | 75 | 76 | @dp.message_handler(commands=['file']) 77 | async def process_file_command(message: types.Message): 78 | user_id = message.from_user.id 79 | await bot.send_chat_action(user_id, ChatActions.UPLOAD_DOCUMENT) 80 | await asyncio.sleep(1) # скачиваем файл и отправляем его пользователю 81 | await bot.send_document(user_id, TEXT_FILE, 82 | caption='Этот файл специально для тебя!') 83 | 84 | 85 | @dp.message_handler(commands=['testpre']) 86 | async def process_testpre_command(message: types.Message): 87 | message_text = pre(emojize('''@dp.message_handler(commands=['testpre']) 88 | async def process_testpre_command(message: types.Message): 89 | message_text = pre(emojize('Ха! Не в этот раз :smirk:')) 90 | await bot.send_message(message.from_user.id, message_text)''')) 91 | await bot.send_message(message.from_user.id, message_text, 92 | parse_mode=ParseMode.MARKDOWN) 93 | 94 | 95 | @dp.message_handler() 96 | async def echo_message(msg: types.Message): 97 | await bot.send_message(msg.from_user.id, msg.text) 98 | 99 | 100 | @dp.message_handler(content_types=ContentType.ANY) 101 | async def unknown_message(msg: types.Message): 102 | message_text = text(emojize('Я не знаю, что с этим делать :astonished:'), 103 | italic('\nЯ просто напомню,'), 'что есть', 104 | code('команда'), '/help') 105 | await msg.reply(message_text, parse_mode=ParseMode.MARKDOWN) 106 | 107 | 108 | if __name__ == '__main__': 109 | executor.start_polling(dp) 110 | -------------------------------------------------------------------------------- /lesson-02/config.py: -------------------------------------------------------------------------------- 1 | TOKEN = 'YOUR:own_private_secure-Bot_token' 2 | MY_ID = 77777 3 | 4 | DB_FILENAME = 'botuploads.db' 5 | -------------------------------------------------------------------------------- /lesson-02/db_map.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | from sqlalchemy.ext.declarative import declarative_base 3 | 4 | 5 | Base = declarative_base() 6 | 7 | 8 | class MediaIds(Base): 9 | __tablename__ = 'Media ids' 10 | id = Column(Integer, primary_key=True) 11 | file_id = Column(String(255)) 12 | filename = Column(String(255)) 13 | -------------------------------------------------------------------------------- /lesson-02/demo-media/files/very important text file.txt: -------------------------------------------------------------------------------- 1 | Lasciate ogni speranza, voi ch’entrate 2 | -------------------------------------------------------------------------------- /lesson-02/demo-media/ogg/Rick_Astley_-_Never_Gonna_Give_You_Up.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahenzon/aiogram-lessons/2210eed18028dc9f044b564bdb5af8f6f6a221b0/lesson-02/demo-media/ogg/Rick_Astley_-_Never_Gonna_Give_You_Up.ogg -------------------------------------------------------------------------------- /lesson-02/demo-media/pics/kitten0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahenzon/aiogram-lessons/2210eed18028dc9f044b564bdb5af8f6f6a221b0/lesson-02/demo-media/pics/kitten0.jpg -------------------------------------------------------------------------------- /lesson-02/demo-media/pics/kitten1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahenzon/aiogram-lessons/2210eed18028dc9f044b564bdb5af8f6f6a221b0/lesson-02/demo-media/pics/kitten1.jpg -------------------------------------------------------------------------------- /lesson-02/demo-media/pics/kitten2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahenzon/aiogram-lessons/2210eed18028dc9f044b564bdb5af8f6f6a221b0/lesson-02/demo-media/pics/kitten2.jpg -------------------------------------------------------------------------------- /lesson-02/demo-media/pics/kitten3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahenzon/aiogram-lessons/2210eed18028dc9f044b564bdb5af8f6f6a221b0/lesson-02/demo-media/pics/kitten3.jpg -------------------------------------------------------------------------------- /lesson-02/demo-media/videoNotes/cute-puppy.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahenzon/aiogram-lessons/2210eed18028dc9f044b564bdb5af8f6f6a221b0/lesson-02/demo-media/videoNotes/cute-puppy.mp4 -------------------------------------------------------------------------------- /lesson-02/demo-media/videos/hedgehog.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahenzon/aiogram-lessons/2210eed18028dc9f044b564bdb5af8f6f6a221b0/lesson-02/demo-media/videos/hedgehog.mp4 -------------------------------------------------------------------------------- /lesson-02/upload_my_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import logging 4 | from aiogram import Bot 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.orm import scoped_session, sessionmaker 7 | 8 | from db_map import Base, MediaIds 9 | from config import TOKEN, MY_ID, DB_FILENAME 10 | 11 | logging.basicConfig(format=u'%(filename)s [ LINE:%(lineno)+3s ]#%(levelname)+8s [%(asctime)s] %(message)s', 12 | level=logging.DEBUG) 13 | 14 | engine = create_engine(f'sqlite:///{DB_FILENAME}') 15 | 16 | if not os.path.isfile(f'./{DB_FILENAME}'): 17 | Base.metadata.create_all(engine) 18 | 19 | session_factory = sessionmaker(bind=engine) 20 | Session = scoped_session(session_factory) 21 | 22 | bot = Bot(token=TOKEN) 23 | 24 | 25 | BASE_MEDIA_PATH = './demo-media' 26 | 27 | 28 | async def uploadMediaFiles(folder, method, file_attr): 29 | folder_path = os.path.join(BASE_MEDIA_PATH, folder) 30 | for filename in os.listdir(folder_path): 31 | if filename.startswith('.'): 32 | continue 33 | 34 | logging.info(f'Started processing {filename}') 35 | with open(os.path.join(folder_path, filename), 'rb') as file: 36 | msg = await method(MY_ID, file, disable_notification=True) 37 | if file_attr == 'photo': 38 | file_id = msg.photo[-1].file_id 39 | else: 40 | file_id = getattr(msg, file_attr).file_id 41 | session = Session() 42 | newItem = MediaIds(file_id=file_id, filename=filename) 43 | try: 44 | session.add(newItem) 45 | session.commit() 46 | except Exception as e: 47 | logging.error( 48 | 'Couldn\'t upload {}. Error is {}'.format(filename, e)) 49 | else: 50 | logging.info( 51 | f'Successfully uploaded and saved to DB file {filename} with id {file_id}') 52 | finally: 53 | session.close() 54 | 55 | loop = asyncio.get_event_loop() 56 | 57 | tasks = [ 58 | loop.create_task(uploadMediaFiles('pics', bot.send_photo, 'photo')), 59 | loop.create_task(uploadMediaFiles('videos', bot.send_video, 'video')), 60 | loop.create_task(uploadMediaFiles('videoNotes', bot.send_video_note, 'video_note')), 61 | loop.create_task(uploadMediaFiles('files', bot.send_document, 'document')), 62 | loop.create_task(uploadMediaFiles('ogg', bot.send_voice, 'voice')), 63 | ] 64 | 65 | wait_tasks = asyncio.wait(tasks) 66 | 67 | loop.run_until_complete(wait_tasks) 68 | loop.close() 69 | Session.remove() 70 | -------------------------------------------------------------------------------- /lesson-03/bot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiogram import Bot, types 4 | from aiogram.utils import executor 5 | from aiogram.dispatcher import Dispatcher 6 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 7 | from aiogram.contrib.middlewares.logging import LoggingMiddleware 8 | 9 | from config import TOKEN 10 | from utils import TestStates 11 | from messages import MESSAGES 12 | 13 | 14 | logging.basicConfig(format=u'%(filename)+13s [ LINE:%(lineno)-4s] %(levelname)-8s [%(asctime)s] %(message)s', 15 | level=logging.DEBUG) 16 | 17 | 18 | bot = Bot(token=TOKEN) 19 | dp = Dispatcher(bot, storage=MemoryStorage()) 20 | 21 | dp.middleware.setup(LoggingMiddleware()) 22 | 23 | 24 | @dp.message_handler(commands=['start']) 25 | async def process_start_command(message: types.Message): 26 | await message.reply(MESSAGES['start']) 27 | 28 | 29 | @dp.message_handler(commands=['help']) 30 | async def process_help_command(message: types.Message): 31 | await message.reply(MESSAGES['help']) 32 | 33 | 34 | @dp.message_handler(state='*', commands=['setstate']) 35 | async def process_setstate_command(message: types.Message): 36 | argument = message.get_args() 37 | state = dp.current_state(user=message.from_user.id) 38 | if not argument: 39 | await state.reset_state() 40 | return await message.reply(MESSAGES['state_reset']) 41 | 42 | if (not argument.isdigit()) or (not int(argument) < len(TestStates.all())): 43 | return await message.reply(MESSAGES['invalid_key'].format(key=argument)) 44 | 45 | await state.set_state(TestStates.all()[int(argument)]) 46 | await message.reply(MESSAGES['state_change'], reply=False) 47 | 48 | 49 | @dp.message_handler(state=TestStates.TEST_STATE_1) 50 | async def first_test_state_case_met(message: types.Message): 51 | await message.reply('Первый!', reply=False) 52 | 53 | 54 | @dp.message_handler(state=TestStates.TEST_STATE_2[0]) 55 | async def second_test_state_case_met(message: types.Message): 56 | await message.reply('Второй!', reply=False) 57 | 58 | 59 | @dp.message_handler(state=TestStates.TEST_STATE_3 | TestStates.TEST_STATE_4) 60 | async def third_or_fourth_test_state_case_met(message: types.Message): 61 | await message.reply('Третий или четвертый!', reply=False) 62 | 63 | 64 | @dp.message_handler(state=TestStates.all()) 65 | async def some_test_state_case_met(message: types.Message): 66 | with dp.current_state(user=message.from_user.id) as state: 67 | text = MESSAGES['current_state'].format( 68 | current_state=await state.get_state(), 69 | states=TestStates.all() 70 | ) 71 | await message.reply(text, reply=False) 72 | 73 | 74 | @dp.message_handler() 75 | async def echo_message(msg: types.Message): 76 | await bot.send_message(msg.from_user.id, msg.text) 77 | 78 | 79 | async def shutdown(dispatcher: Dispatcher): 80 | await dispatcher.storage.close() 81 | await dispatcher.storage.wait_closed() 82 | 83 | 84 | if __name__ == '__main__': 85 | executor.start_polling(dp, on_shutdown=shutdown) 86 | -------------------------------------------------------------------------------- /lesson-03/config.py: -------------------------------------------------------------------------------- 1 | TOKEN = 'YOUR:own_private_secure-Bot_token' 2 | -------------------------------------------------------------------------------- /lesson-03/messages.py: -------------------------------------------------------------------------------- 1 | from utils import TestStates 2 | 3 | 4 | help_message = 'Для того, чтобы изменить текущее состояние пользователя, ' \ 5 | f'отправь команду "/setstate x", где x - число от 0 до {len(TestStates.all()) - 1}.\n' \ 6 | 'Чтобы сбросить текущее состояние, отправь "/setstate" без аргументов.' 7 | 8 | start_message = 'Привет! Это демонстрация работы FSM.\n' + help_message 9 | invalid_key_message = 'Ключ "{key}" не подходит.\n' + help_message 10 | state_change_success_message = 'Текущее состояние успешно изменено' 11 | state_reset_message = 'Состояние успешно сброшено' 12 | current_state_message = 'Текущее состояние - "{current_state}", что удовлетворяет условию "один из {states}"' 13 | 14 | MESSAGES = { 15 | 'start': start_message, 16 | 'help': help_message, 17 | 'invalid_key': invalid_key_message, 18 | 'state_change': state_change_success_message, 19 | 'state_reset': state_reset_message, 20 | 'current_state': current_state_message, 21 | } 22 | -------------------------------------------------------------------------------- /lesson-03/utils.py: -------------------------------------------------------------------------------- 1 | from aiogram.utils.helper import Helper, HelperMode, ListItem 2 | 3 | 4 | class TestStates(Helper): 5 | mode = HelperMode.snake_case 6 | 7 | TEST_STATE_0 = ListItem() 8 | TEST_STATE_1 = ListItem() 9 | TEST_STATE_2 = ListItem() 10 | TEST_STATE_3 = ListItem() 11 | TEST_STATE_4 = ListItem() 12 | TEST_STATE_5 = ListItem() 13 | 14 | 15 | if __name__ == '__main__': 16 | print(TestStates.all()) 17 | -------------------------------------------------------------------------------- /lesson-04/config.py: -------------------------------------------------------------------------------- 1 | BOT_TOKEN = 'YOUR:own_private_secure-Bot_token' 2 | PAYMENTS_PROVIDER_TOKEN = '012345678:TEST:1234' 3 | 4 | TIME_MACHINE_IMAGE_URL = 'http://erkelzaar.tsudao.com/models/perrotta/TIME_MACHINE.jpg' 5 | -------------------------------------------------------------------------------- /lesson-04/messages.py: -------------------------------------------------------------------------------- 1 | help_message = ''' 2 | Через этого бота можно купить машину времени, чтобы посмотреть, как происходит покупка и оплата в Telegram. 3 | Отправьте команду /buy, чтобы перейти к покупке. 4 | Узнать правила и положения можно воспользовавшись командой /terms. 5 | ''' 6 | 7 | start_message = 'Привет! Это демонстрация работы платежей в Telegram!\n' + help_message 8 | 9 | pre_buy_demo_alert = '''\ 10 | Так как сейчас я запущен в тестовом режиме, для оплаты нужно использовать карточку с номером `4242 4242 4242 4242` 11 | Счёт для оплаты: 12 | ''' 13 | 14 | terms = '''\ 15 | *Спасибо, что выбрали нашего бота. Мы надеемся, вам понравится ваша новая машина времени!* 16 | 17 | 1. Если машина времени не будет доставлена вовремя, пожалуйста, произведите переосмысление вашей концепции времени и попробуйте снова. 18 | 2. Если вы обнаружите, что машина времени не работает, будьте добры связаться с нашими сервисными мастерскими будущего с экзопланеты Trappist-1e. Они будут доступны в любом месте в период с мая 2075 года по ноябрь 4000 года нашей эры. 19 | 3. Если вы хотите вернуть деньги, будьте так любезны подать заявку вчера, и мы немедленно совершим возврат. 20 | ''' 21 | 22 | tm_title = 'Самая настоящая Машина Времени' 23 | tm_description = '''\ 24 | Хотите познакомиться со своими пра-пра-пра-пра-бабушкой и дедушкой? 25 | Сделать состояние на ставках? 26 | Пожать руку Хаммурапи и прогуляться по Висячим садам Семирамиды? 27 | Закажите Машину Времени у нас прямо сейчас! 28 | ''' 29 | 30 | AU_error = '''\ 31 | К сожалению, наши курьеры боятся кенгуру, а телепорт не может так далеко отправлять. 32 | Попробуйте выбрать другой адрес! 33 | ''' 34 | 35 | wrong_email = '''\ 36 | Нам кажется, что указанный имейл не действителен. 37 | Попробуйте указать другой имейл 38 | ''' 39 | 40 | successful_payment = ''' 41 | Ура! Платеж на сумму `{total_amount} {currency}` совершен успешно! Приятного пользования новенькой машиной времени! 42 | Правила возврата средств смотрите в /terms 43 | Купить ещё одну машину времени своему другу - /buy 44 | ''' 45 | 46 | 47 | MESSAGES = { 48 | 'start': start_message, 49 | 'help': help_message, 50 | 'pre_buy_demo_alert': pre_buy_demo_alert, 51 | 'terms': terms, 52 | 'tm_title': tm_title, 53 | 'tm_description': tm_description, 54 | 'AU_error': AU_error, 55 | 'wrong_email': wrong_email, 56 | 'successful_payment': successful_payment, 57 | } 58 | -------------------------------------------------------------------------------- /lesson-04/payments-minimum_bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiogram import Bot, types 5 | from aiogram.utils import executor 6 | from aiogram.dispatcher import Dispatcher 7 | from aiogram.types.message import ContentType 8 | 9 | from messages import MESSAGES 10 | from config import BOT_TOKEN, PAYMENTS_PROVIDER_TOKEN, TIME_MACHINE_IMAGE_URL 11 | 12 | 13 | logging.basicConfig(format=u'%(filename)+13s [ LINE:%(lineno)-4s] %(levelname)-8s [%(asctime)s] %(message)s', 14 | level=logging.INFO) 15 | 16 | 17 | loop = asyncio.get_event_loop() 18 | bot = Bot(BOT_TOKEN, parse_mode=types.ParseMode.MARKDOWN) 19 | dp = Dispatcher(bot, loop=loop) 20 | 21 | # Setup prices 22 | PRICE = types.LabeledPrice(label='Настоящая Машина Времени', amount=4200000) 23 | 24 | 25 | @dp.message_handler(commands=['terms']) 26 | async def process_terms_command(message: types.Message): 27 | await message.reply(MESSAGES['terms'], reply=False) 28 | 29 | 30 | @dp.message_handler(commands=['buy']) 31 | async def process_buy_command(message: types.Message): 32 | if PAYMENTS_PROVIDER_TOKEN.split(':')[1] == 'TEST': 33 | await bot.send_message(message.chat.id, MESSAGES['pre_buy_demo_alert']) 34 | 35 | await bot.send_invoice(message.chat.id, 36 | title=MESSAGES['tm_title'], 37 | description=MESSAGES['tm_description'], 38 | provider_token=PAYMENTS_PROVIDER_TOKEN, 39 | currency='rub', 40 | photo_url=TIME_MACHINE_IMAGE_URL, 41 | photo_height=512, # !=0/None, иначе изображение не покажется 42 | photo_width=512, 43 | photo_size=512, 44 | is_flexible=False, # True если конечная цена зависит от способа доставки 45 | prices=[PRICE], 46 | start_parameter='time-machine-example', 47 | payload='some-invoice-payload-for-our-internal-use' 48 | ) 49 | 50 | 51 | @dp.pre_checkout_query_handler(func=lambda query: True) 52 | async def process_pre_checkout_query(pre_checkout_query: types.PreCheckoutQuery): 53 | await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True) 54 | 55 | 56 | @dp.message_handler(content_types=ContentType.SUCCESSFUL_PAYMENT) 57 | async def process_successful_payment(message: types.Message): 58 | print('successful_payment:') 59 | pmnt = message.successful_payment.to_python() 60 | for key, val in pmnt.items(): 61 | print(f'{key} = {val}') 62 | 63 | await bot.send_message( 64 | message.chat.id, 65 | MESSAGES['successful_payment'].format( 66 | total_amount=message.successful_payment.total_amount // 100, 67 | currency=message.successful_payment.currency 68 | ) 69 | ) 70 | 71 | 72 | if __name__ == '__main__': 73 | executor.start_polling(dp, loop=loop) 74 | -------------------------------------------------------------------------------- /lesson-04/payments_bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiogram import Bot, types 5 | from aiogram.utils import executor 6 | from aiogram.dispatcher import Dispatcher 7 | from aiogram.types.message import ContentType 8 | 9 | from messages import MESSAGES 10 | from config import BOT_TOKEN, PAYMENTS_PROVIDER_TOKEN, TIME_MACHINE_IMAGE_URL 11 | 12 | 13 | logging.basicConfig(format=u'%(filename)+13s [ LINE:%(lineno)-4s] %(levelname)-8s [%(asctime)s] %(message)s', 14 | level=logging.INFO) 15 | 16 | 17 | loop = asyncio.get_event_loop() 18 | bot = Bot(BOT_TOKEN, parse_mode=types.ParseMode.MARKDOWN) 19 | dp = Dispatcher(bot, loop=loop) 20 | 21 | # Setup prices 22 | PRICES = [ 23 | types.LabeledPrice(label='Настоящая Машина Времени', amount=4200000), 24 | types.LabeledPrice(label='Подарочная упаковка', amount=30000) 25 | ] 26 | 27 | # Setup shipping options 28 | 29 | TELEPORTER_SHIPPING_OPTION = types.ShippingOption( 30 | id='teleporter', 31 | title='Всемирный* телепорт' 32 | ).add(types.LabeledPrice('Телепорт', 1000000)) 33 | 34 | RUSSIAN_POST_SHIPPING_OPTION = types.ShippingOption( 35 | id='ru_post', title='Почтой России') 36 | RUSSIAN_POST_SHIPPING_OPTION.add( 37 | types.LabeledPrice( 38 | 'Деревянный ящик с амортизирующей подвеской внутри', 100000) 39 | ) 40 | RUSSIAN_POST_SHIPPING_OPTION.add( 41 | types.LabeledPrice('Срочное отправление (5-10 дней)', 500000) 42 | ) 43 | 44 | PICKUP_SHIPPING_OPTION = types.ShippingOption(id='pickup', title='Самовывоз') 45 | PICKUP_SHIPPING_OPTION.add(types.LabeledPrice('Самовывоз в Москве', 50000)) 46 | 47 | 48 | @dp.message_handler(commands=['start']) 49 | async def process_start_command(message: types.Message): 50 | await message.reply(MESSAGES['start']) 51 | 52 | 53 | @dp.message_handler(commands=['help']) 54 | async def process_help_command(message: types.Message): 55 | await message.reply(MESSAGES['help']) 56 | 57 | 58 | @dp.message_handler(commands=['terms']) 59 | async def process_terms_command(message: types.Message): 60 | await message.reply(MESSAGES['terms'], reply=False) 61 | 62 | 63 | @dp.message_handler(commands=['buy']) 64 | async def process_buy_command(message: types.Message): 65 | if PAYMENTS_PROVIDER_TOKEN.split(':')[1] == 'TEST': 66 | await bot.send_message(message.chat.id, MESSAGES['pre_buy_demo_alert']) 67 | 68 | await bot.send_invoice(message.chat.id, 69 | title=MESSAGES['tm_title'], 70 | description=MESSAGES['tm_description'], 71 | provider_token=PAYMENTS_PROVIDER_TOKEN, 72 | currency='rub', 73 | photo_url=TIME_MACHINE_IMAGE_URL, 74 | photo_height=512, # !=0/None or picture won't be shown 75 | photo_width=512, 76 | photo_size=512, 77 | need_email=True, 78 | need_phone_number=True, 79 | # need_shipping_address=True, 80 | is_flexible=True, # True If you need to set up Shipping Fee 81 | prices=PRICES, 82 | start_parameter='time-machine-example', 83 | payload='some-invoice-payload-for-our-internal-use') 84 | 85 | 86 | @dp.shipping_query_handler(func=lambda query: True) 87 | async def process_shipping_query(shipping_query: types.ShippingQuery): 88 | print('shipping_query.shipping_address') 89 | print(shipping_query.shipping_address) 90 | 91 | if shipping_query.shipping_address.country_code == 'AU': 92 | return await bot.answer_shipping_query( 93 | shipping_query.id, 94 | ok=False, 95 | error_message=MESSAGES['AU_error'] 96 | ) 97 | 98 | shipping_options = [TELEPORTER_SHIPPING_OPTION] 99 | 100 | if shipping_query.shipping_address.country_code == 'RU': 101 | shipping_options.append(RUSSIAN_POST_SHIPPING_OPTION) 102 | 103 | if shipping_query.shipping_address.city == 'Москва': 104 | shipping_options.append(PICKUP_SHIPPING_OPTION) 105 | 106 | await bot.answer_shipping_query( 107 | shipping_query.id, 108 | ok=True, 109 | shipping_options=shipping_options 110 | ) 111 | 112 | 113 | @dp.pre_checkout_query_handler(func=lambda query: True) 114 | async def process_pre_checkout_query(pre_checkout_query: types.PreCheckoutQuery): 115 | print('order_info') 116 | print(pre_checkout_query.order_info) 117 | 118 | if hasattr(pre_checkout_query.order_info, 'email') and (pre_checkout_query.order_info.email == 'vasya@pupkin.com'): 119 | return await bot.answer_pre_checkout_query( 120 | pre_checkout_query.id, 121 | ok=False, 122 | error_message=MESSAGES['wrong_email']) 123 | 124 | await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True) 125 | 126 | 127 | @dp.message_handler(content_types=ContentType.SUCCESSFUL_PAYMENT) 128 | async def process_successful_payment(message: types.Message): 129 | await bot.send_message( 130 | message.chat.id, 131 | MESSAGES['successful_payment'].format( 132 | total_amount=message.successful_payment.total_amount // 100, 133 | currency=message.successful_payment.currency 134 | ) 135 | ) 136 | 137 | 138 | if __name__ == '__main__': 139 | executor.start_polling(dp, loop=loop) 140 | -------------------------------------------------------------------------------- /lesson-05/bot.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, types 2 | from aiogram.utils import executor 3 | from aiogram.utils.markdown import text 4 | from aiogram.dispatcher import Dispatcher 5 | 6 | from config import TOKEN 7 | import keyboards as kb 8 | 9 | 10 | bot = Bot(token=TOKEN) 11 | dp = Dispatcher(bot) 12 | 13 | 14 | ## 15 | 16 | 17 | @dp.callback_query_handler(func=lambda c: c.data == 'button1') 18 | async def process_callback_button1(callback_query: types.CallbackQuery): 19 | await bot.answer_callback_query(callback_query.id) 20 | await bot.send_message(callback_query.from_user.id, 'Нажата первая кнопка!') 21 | 22 | 23 | @dp.callback_query_handler(func=lambda c: c.data and c.data.startswith('btn')) 24 | async def process_callback_kb1btn1(callback_query: types.CallbackQuery): 25 | code = callback_query.data[-1] 26 | if code.isdigit(): 27 | code = int(code) 28 | if code == 2: 29 | await bot.answer_callback_query(callback_query.id, text='Нажата вторая кнопка') 30 | elif code == 5: 31 | await bot.answer_callback_query( 32 | callback_query.id, 33 | text='Нажата кнопка с номером 5.\nА этот текст может быть длиной до 200 символов 😉', 34 | show_alert=True) 35 | else: 36 | await bot.answer_callback_query(callback_query.id) 37 | await bot.send_message(callback_query.from_user.id, f'Нажата инлайн кнопка! code={code}') 38 | 39 | 40 | ## 41 | 42 | 43 | @dp.message_handler(commands=['start']) 44 | async def process_start_command(message: types.Message): 45 | await message.reply("Привет!", reply_markup=kb.greet_kb) 46 | 47 | 48 | @dp.message_handler(commands=['hi1']) 49 | async def process_hi1_command(message: types.Message): 50 | await message.reply("Первое - изменяем размер клавиатуры", 51 | reply_markup=kb.greet_kb1) 52 | 53 | 54 | @dp.message_handler(commands=['hi2']) 55 | async def process_hi2_command(message: types.Message): 56 | await message.reply("Второе - прячем клавиатуру после одного нажатия", 57 | reply_markup=kb.greet_kb2) 58 | 59 | 60 | @dp.message_handler(commands=['hi3']) 61 | async def process_hi3_command(message: types.Message): 62 | await message.reply("Третье - добавляем больше кнопок", 63 | reply_markup=kb.markup3) 64 | 65 | 66 | @dp.message_handler(commands=['hi4']) 67 | async def process_hi4_command(message: types.Message): 68 | await message.reply("Четвертое - расставляем кнопки в ряд", 69 | reply_markup=kb.markup4) 70 | 71 | 72 | @dp.message_handler(commands=['hi5']) 73 | async def process_hi5_command(message: types.Message): 74 | await message.reply("Пятое - добавляем ряды кнопок", 75 | reply_markup=kb.markup5) 76 | 77 | 78 | @dp.message_handler(commands=['hi6']) 79 | async def process_hi6_command(message: types.Message): 80 | await message.reply("Шестое - запрашиваем контакт и геолокацию\n" 81 | "Эти две кнопки не зависят друг от друга", 82 | reply_markup=kb.markup_request) 83 | 84 | 85 | @dp.message_handler(commands=['hi7']) 86 | async def process_hi7_command(message: types.Message): 87 | await message.reply("Седьмое - все методы вместе", 88 | reply_markup=kb.markup_big) 89 | 90 | 91 | @dp.message_handler(commands=['rm']) 92 | async def process_rm_command(message: types.Message): 93 | await message.reply("Убираем шаблоны сообщений", 94 | reply_markup=kb.ReplyKeyboardRemove()) 95 | 96 | ## 97 | 98 | 99 | @dp.message_handler(commands=['1']) 100 | async def process_command_1(message: types.Message): 101 | await message.reply("Первая инлайн кнопка", 102 | reply_markup=kb.inline_kb1) 103 | 104 | 105 | @dp.message_handler(commands=['2']) 106 | async def process_command_2(message: types.Message): 107 | await message.reply("Отправляю все возможные кнопки", 108 | reply_markup=kb.inline_kb_full) 109 | 110 | help_message = text( 111 | "Это урок по клавиатурам.", 112 | "Доступные команды:\n", 113 | "/start - приветствие", 114 | "\nШаблоны клавиатур:", 115 | "/hi1 - авто размер", 116 | "/hi2 - скрыть после нажатия", 117 | "/hi3 - больше кнопок", 118 | "/hi4 - кнопки в ряд", 119 | "/hi5 - больше рядов", 120 | "/hi6 - запрос локации и номера телефона", 121 | "/hi7 - все методы" 122 | "/rm - убрать шаблоны", 123 | "\nИнлайн клавиатуры:", 124 | "/1 - первая кнопка", 125 | "/2 - сразу много кнопок", 126 | sep="\n" 127 | ) 128 | 129 | 130 | @dp.message_handler(commands=['help']) 131 | async def process_help_command(message: types.Message): 132 | await message.reply(help_message) 133 | 134 | 135 | if __name__ == '__main__': 136 | executor.start_polling(dp) 137 | -------------------------------------------------------------------------------- /lesson-05/config.py: -------------------------------------------------------------------------------- 1 | TOKEN = 'YOUR:own_private_secure-Bot_token' 2 | -------------------------------------------------------------------------------- /lesson-05/keyboards.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ReplyKeyboardRemove, \ 2 | ReplyKeyboardMarkup, KeyboardButton, \ 3 | InlineKeyboardMarkup, InlineKeyboardButton 4 | 5 | 6 | button_hi = KeyboardButton('Привет! 👋') 7 | 8 | greet_kb = ReplyKeyboardMarkup() 9 | greet_kb.add(button_hi) 10 | 11 | greet_kb1 = ReplyKeyboardMarkup(resize_keyboard=True).add(button_hi) 12 | 13 | greet_kb2 = ReplyKeyboardMarkup( 14 | resize_keyboard=True, one_time_keyboard=True 15 | ).add(button_hi) 16 | 17 | button1 = KeyboardButton('1️⃣') 18 | button2 = KeyboardButton('2️⃣') 19 | button3 = KeyboardButton('3️⃣') 20 | 21 | markup3 = ReplyKeyboardMarkup().add( 22 | button1).add(button2).add(button3) 23 | 24 | markup4 = ReplyKeyboardMarkup().row( 25 | button1, button2, button3 26 | ) 27 | 28 | markup5 = ReplyKeyboardMarkup().row( 29 | button1, button2, button3 30 | ).add(KeyboardButton('Средний ряд')) 31 | 32 | button4 = KeyboardButton('4️⃣') 33 | button5 = KeyboardButton('5️⃣') 34 | button6 = KeyboardButton('6️⃣') 35 | markup5.row(button4, button5) 36 | markup5.insert(button6) 37 | 38 | markup_request = ReplyKeyboardMarkup(resize_keyboard=True).add( 39 | KeyboardButton('Отправить свой контакт ☎️', request_contact=True) 40 | ).add( 41 | KeyboardButton('Отправить свою локацию 🗺️', request_location=True) 42 | ) 43 | 44 | markup_big = ReplyKeyboardMarkup() 45 | 46 | markup_big.add( 47 | button1, button2, button3, button4, button5, button6 48 | ) 49 | markup_big.row( 50 | button1, button2, button3, button4, button5, button6 51 | ) 52 | 53 | markup_big.row(button4, button2) 54 | markup_big.add(button3, button2) 55 | markup_big.insert(button1) 56 | markup_big.insert(button6) 57 | markup_big.insert(KeyboardButton('9️⃣')) 58 | 59 | 60 | inline_btn_1 = InlineKeyboardButton('Первая кнопка!', callback_data='button1') 61 | inline_kb1 = InlineKeyboardMarkup().add(inline_btn_1) 62 | 63 | inline_kb_full = InlineKeyboardMarkup(row_width=2).add(inline_btn_1) 64 | inline_kb_full.add(InlineKeyboardButton('Вторая кнопка', callback_data='btn2')) 65 | inline_btn_3 = InlineKeyboardButton('кнопка 3', callback_data='btn3') 66 | inline_btn_4 = InlineKeyboardButton('кнопка 4', callback_data='btn4') 67 | inline_btn_5 = InlineKeyboardButton('кнопка 5', callback_data='btn5') 68 | inline_kb_full.add(inline_btn_3, inline_btn_4, inline_btn_5) 69 | inline_kb_full.row(inline_btn_3, inline_btn_4, inline_btn_5) 70 | inline_kb_full.insert(InlineKeyboardButton("query=''", switch_inline_query='')) 71 | inline_kb_full.insert(InlineKeyboardButton("query='qwerty'", switch_inline_query='qwerty')) 72 | inline_kb_full.insert(InlineKeyboardButton("Inline в этом же чате", switch_inline_query_current_chat='wasd')) 73 | inline_kb_full.add(InlineKeyboardButton('Уроки aiogram', url='https://surik00.gitbooks.io/aiogram-lessons/content/')) 74 | --------------------------------------------------------------------------------