-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstate_machine_bot.py
331 lines (304 loc) · 18.1 KB
/
state_machine_bot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
"""
Принцип работы FSM (машины состояний) конечный автомат
"""
from aiogram import Bot, Dispatcher, F
from aiogram.filters import Command, CommandStart, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import default_state, State, StatesGroup
from aiogram.fsm.storage.redis import RedisStorage, Redis
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
InlineKeyboardMarkup, Message, PhotoSize)
BOT_TOKEN = 'YOUR_BOT_TOKEN'
# Инициализируем Redis
redis = Redis(host='localhost')
# Инициализируем хранилище (создвем экземпляр класса MemoryStorage)
storage = RedisStorage(redis=redis)
bot = Bot(token=BOT_TOKEN)
dp = Dispatcher(storage=storage)
# Создаем "Базу данных" пользователей
user_dict: dict[int, dict[str, str | int | bool]] = {}
# Создаем класс, наследуемый от StatesGroup, для группы состояний нашей FSM.
class FSMFillForm(StatesGroup):
# создаем экземпляры класса State, последовательно
# перечисляя возможные состояния, в которых будет находиться
# бот в разные моменты взаимодействия с пользователем.
fill_name = State() # Состояние ожидания ввода имени
fill_age = State() # Состояние ожидания ввода возраста
fill_gender = State() # Состояние ожидания выбора пола
upload_photo = State() # Состояние ожидания загрузки фото
fill_education = State() # Состояние ожидания выбора образования
fill_wish_news = State() # Состояние ожидания выбора получать ли новости
# Этот хэндлер будет срабатывать на команду /start вне состояний
# и предлагать перейти к заполнению анкеты, отправив команду /fillform
@dp.message(CommandStart(), StateFilter(default_state))
async def process_start_command(message: Message):
await message.answer(
text='Этот бот демонстрирует работу FSM\n\n'
'Чтобы перейти к заполнению анкеты - '
'отправьте команду /fillform'
)
# Этот хэндлер будет срабатывать на команду /cancel
# в состоянии по умолчанию и сообщать, что эта команда работает
# внутри машины состояний
@dp.message(Command(commands='cancel'), StateFilter(default_state))
async def process_cancel_command(message: Message):
await message.answer(
text='Отменять нечего. Вы вне машины состояний\n\n'
'Чтобы перейти к заполнению анкеты - '
'отправьте команду /fiilform'
)
# Этот хэндлер будет срабатывать на команду /cancel в любых
# состояниях кроме состояния по умолчанию, и отключать машину состояний
@dp.message(Command(commands='cancel'), ~StateFilter(default_state))
async def process_cancel_command_state(message: Message, state: FSMContext):
await message.answer(
text='Вы вышли из машины состояний\n\n'
'Чтобы снова перейти к заполнению анкеты - '
'отправьте команду /fiilform'
)
# Сбрасываем состояние и очищаем данные, полученные внутри состояний
await state.clear()
# Этот хэндлер будет срабатывать на команду /fillform
# и переводить бота в состояние ожидания ввода имени
@dp.message(Command(commands='fillform'), StateFilter(default_state))
async def process_fillform_command(message: Message, state: FSMContext):
await message.answer(text='Пожалуйста, введите ваше имя')
# Устанавливаем состояние ожидания ввода имени
await state.set_state(FSMFillForm.fill_name)
# Этот хэндлер будет срабатывать, если введено корректное имя
# и переводить в состояние ожидания ввода возраста
@dp.message(StateFilter(FSMFillForm.fill_name), F.text.isalpha())
async def process_name_sent(message: Message, state: FSMContext):
# Сохраняем введенное имя в хранилище по ключу "name"
await state.update_data(name=message.text)
await message.answer(text='Спасибо!\n\nА теперь введите ваш возраст')
# Устанавливаем состояние ожидания ввода возраста
await state.set_state(FSMFillForm.fill_age)
# Этот хэндлер будет срабатывать, если во время ввода имени
# будет введено что-то неккоректное
@dp.message(StateFilter(FSMFillForm.fill_name))
async def warning_not_name(message: Message):
await message.answer(
text='То, что вы отправили не похоже на имя\n\n'
'Пожалуйста, введите ваше имя\n\n'
'Если вы хотите прервать заполнение анкеты - '
'отправьте команду /cancel'
)
# Этот хэндлер будет срабатывать, если введен корректный возраст
# и переводить в состояние выбора пола
@dp.message(StateFilter(FSMFillForm.fill_age),
lambda x: x.text.isdigit() and 4 <= int(x.text) <= 120)
async def process_age_sent(message: Message, state: FSMContext):
# Сохраняем возраст в хранилище по ключу "age"
await state.update_data(age=message.text)
# Создаем объекты инлайн-кнопок
male_button = InlineKeyboardButton(
text='Мужской 👨⚕️',
callback_data='male'
)
female_button = InlineKeyboardButton(
text='Женский 👩⚕️',
callback_data='female'
)
undefined_button = InlineKeyboardButton(
text='🤷♂️ Пока не ясно',
callback_data='undifined_gender'
)
# Добавляем кнопки в клавиатуру(две в одном ряду и одну в другом)
keyboard: list[list[InlineKeyboardButton]] = [
[male_button, female_button],
[undefined_button]
]
# Создаем объект инлайн-клавиатуры
markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
# Отправляем пользователю сообщение с клавиатурой
await message.answer(
text='Спасибо!\n\nУкажите выш пол',
reply_markup=markup
)
# Устанавливаем состояние ожидания выбора пола
await state.set_state(FSMFillForm.fill_gender)
# Этот хэндлер будет срабатывать, если во время ввода возраста
# будет введено что-то неккоректное
@dp.message(StateFilter(FSMFillForm.fill_age))
async def warning_not_age(message: Message):
await message.answer(
text='Возраст должен быть целым числом от 4 до 120\n\n'
'Попробуйте еще раз\n\nЕсли вы хотите прервать '
'заполнение анкеты - отправьте команду /cancel'
)
# Этот хэндлер будет срабатывать при нажатие кнопки
# выборе пола и переводить в состояние отправки фото
@dp.callback_query(StateFilter(FSMFillForm.fill_gender),
F.data.in_(['male', 'female', 'undefinned_gender']))
async def process_gender_press(callback: CallbackQuery, state: FSMContext):
# Сохраняем пол (callback.data нажатой кнопки) в хранилище,
# по ключу 'gender'
await state.update_data(gender=callback.data)
# Удаляем сообщение с кнопками, потому что следующий этап - загрузка фото,
# чтобы у пользователя не было желания тыкать кнопки
await callback.message.delete()
await callback.message.answer(
text='Спасибо! А теперь, пожалуйста, загрузите ваше фото'
)
# Устанавливаем состояние ожидания загрузки фото
await state.set_state(FSMFillForm.upload_photo)
# Этот хэндлер будет срабатывать, если во время выбора пола
# будет введено/отправлено что-то неккоректное
@dp.message(StateFilter(FSMFillForm.fill_gender))
async def warning_not_gender(message: Message):
await message.answer(
text='Пожалуйста, пользуйтесь кнопка '
'при выборе пола\n\nЕсли вы хотите прервать '
'заполнение анкеты - отправьте команду /cancel'
)
# Этот хэндлер будет срабатывать, если отправлено фото
# и переводить в состояние выбора образования
@dp.message(StateFilter(FSMFillForm.upload_photo),
F.photo[-1].as_('largest_photo'))
async def process_photo_sent(
message: Message,
state: FSMContext,
largest_photo: PhotoSize
):
# Сохраняем данные фото (file_unique_id и file_id) в хранилище
# по ключам "photo_unique_id" и "photo_id"
await state.update_data(
photo_unique_id = largest_photo.file_unique_id,
photo_id = largest_photo.file_id
)
# Создаем объекты инлайн-кнопок
secondary_button = InlineKeyboardButton(
text='Среднее',
callback_data='secondary'
)
higher_button = InlineKeyboardButton(
text='Высшее',
callback_data='higher'
)
no_edu_button = InlineKeyboardButton(
text='Нету',
callback_data='no_edu'
)
# Добавляем кнопки в клавиатуру (две в одном ряду и третью в другом)
keyboard: list[list[InlineKeyboardButton]] = [
[secondary_button, higher_button],
[no_edu_button]
]
# Создаем объект инлайн-клавиатуры
markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
# Отправляем пользователю сообщение с клавиатурой
await message.answer(
text='Спасибо!\n\nУкажите ваше образование',
reply_markup=markup
)
# Устанавливаем состояние ожидания выбора образования
await state.set_state(FSMFillForm.fill_education)
# Этот хэндлер будет срабатывать, если во время отправки фото
# будет введено/отправлено что-то неккоректное
@dp.message(StateFilter(FSMFillForm.upload_photo))
async def warning_not_photo(message: Message):
await message.answer(
text='Пожалуйста, на этом шаге отправьте '
'ваше фото\n\nЕсли вы хотите прервать '
'заполнение анкеты - введите команду /cancel'
)
# Этот хэндлер будет срабатывать, если выбрано образование
# и переводить в состояние согласия получать новости
@dp.callback_query(StateFilter(FSMFillForm.fill_education),
F.data.in_(['secondary', 'higher', 'no_edu']))
async def process_education_press(callback: CallbackQuery, state: FSMContext):
# Сохраняем данные об образовании по ключу "education"
await state.update_data(education=callback.data)
# Создаем объекты инлайн-кнопок
yes_news_button = InlineKeyboardButton(
text='Да',
callback_data='yes_news'
)
no_news_button = InlineKeyboardButton(
text='Нет, спасибо',
callback_data='no_news'
)
# Добавляем кнопки в клавиатуру в один ряд
keyboard: list[list[InlineKeyboardButton]] = [
[yes_news_button, no_news_button]
]
# Создаем объект инлайн-клавиатуры
markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
# Редактируем предыдущее сообщение с кнопками, отправляя
# новый текст и новую клавиатуру
await callback.message.edit_text(
text='Спасибо!\n\nОстался последний шаг.\n'
'Хотели бы вы получать новости?',
reply_markup=markup
)
# Устанавливаем состояние ожидания выбора получать новости или нет
await state.set_state(FSMFillForm.fill_wish_news)
# Этот хэндлер будет срабатывать, если во время выбора образования
# будет введено/отправлено что-то неккоректное
@dp.message(StateFilter(FSMFillForm.fill_education))
async def warning_not_education(message: Message):
await message.answer(
text='Пожалуйста, пользуйтесь кнопками при выборе образования\n\n'
'Если вы хотите прервать заполнение анкеты - '
'отправьте команду /cancel'
)
# Этот хэндлер будет срабатывать на сообщение получать или не получать новости
# и выводить из машины состояний
@dp.callback_query(StateFilter(FSMFillForm.fill_wish_news),
F.data.in_(['yes_news', 'no_news']))
async def process_wish_news_press(callback: CallbackQuery, state: FSMContext):
# сохраняем данные о получении новостей по ключу "wish_news"
await state.update_data(wish_news=callback.data == 'yes_news')
# добавляем в "базу данных" анкету пользователя
# по ключу id пользователя
user_dict[callback.from_user.id] = await state.get_data()
# завершаем машину состояний
await state.clear()
# отправляем в чат сообщение о выходе из машины состояний
await callback.message.edit_text(
text='Спасибо! Ваши данные сохранены!\n\n'
'Вы вышли из машины состояний'
)
# Отправляем в чат сообщение с предложением посмотреть свою анкету
await callback.message.answer(
text='Чтобы посмотреть данные вашей '
'анкеты - отправьте команду /showdata'
)
# Этот хэндлер будет срабатывать, усли во время согласия на получение
# новостей будет введено/отправлено что-то неккоректное
@dp.message(StateFilter(FSMFillForm.fill_wish_news))
async def warning_not_wish(message: Message):
await message.answer(
text='Пожалуйста, воспользуйтесь кнопками\n\n'
'Если вы хотите прервать заполнение анкеты - '
'отправьте команду /cancel'
)
# Этот хэндлер будет срабатывать на отправку команды /showdata
# и отправлять в чат данные анкеты, либо сообщение об отсутствии данных
@dp.message(Command(commands='showdata'), StateFilter(default_state))
async def process_showdata_command(message: Message):
# отправляем пользователю анкету, если она есть в "базе данных"
if message.from_user.id in user_dict:
await message.answer_photo(
photo=user_dict[message.from_user.id]['photo_id'],
caption=f'Имя: {user_dict[message.from_user.id]["name"]}\n'
f'Возраст: {user_dict[message.from_user.id]["age"]}\n'
f'Пол: {user_dict[message.from_user.id]["gender"]}\n'
f'Образование: {user_dict[message.from_user.id]["education"]}\n'
f'Получать новости: {user_dict[message.from_user.id]["wish_news"]}'
)
else:
await message.answer(
text='Вы еще не заполняли анкету. Чтобы приступить - '
'отправьте команду /fillform'
)
# Этот хэндлер будет срабатывать на любые сообщения, кроме тех
# для которых есть отдельные хэндлеры, вне состояний
@dp.message(StateFilter(default_state))
async def send_echo(message: Message):
await message.answer(
text='Извините! Моя твоя не понимать...'
)
if __name__ == '__main__':
dp.run_polling(bot)