smart-bot-factory 0.3.2__py3-none-any.whl → 0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/admin/__init__.py +2 -0
- smart_bot_factory/admin/admin_events.py +757 -0
- smart_bot_factory/admin/admin_logic.py +54 -12
- smart_bot_factory/aiogram_calendar/__init__.py +6 -0
- smart_bot_factory/aiogram_calendar/common.py +70 -0
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +197 -0
- smart_bot_factory/aiogram_calendar/schemas.py +78 -0
- smart_bot_factory/aiogram_calendar/simple_calendar.py +180 -0
- smart_bot_factory/analytics/analytics_manager.py +42 -5
- smart_bot_factory/cli.py +1 -1
- smart_bot_factory/core/bot_utils.py +17 -16
- smart_bot_factory/core/decorators.py +215 -4
- smart_bot_factory/core/states.py +12 -0
- smart_bot_factory/creation/bot_builder.py +54 -2
- smart_bot_factory/handlers/handlers.py +2 -1
- smart_bot_factory/integrations/supabase_client.py +413 -37
- smart_bot_factory/utm_link_generator.py +13 -3
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/METADATA +2 -1
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/RECORD +22 -18
- smart_bot_factory/table/database_structure.sql +0 -57
- smart_bot_factory/table/schema.sql +0 -1094
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,6 +20,42 @@ def setup_admin_handlers(dp):
|
|
|
20
20
|
"""Настройка админских обработчиков"""
|
|
21
21
|
dp.include_router(admin_router)
|
|
22
22
|
|
|
23
|
+
@admin_router.message(Command(commands=["отмена", "cancel"]))
|
|
24
|
+
async def cancel_handler(message: Message, state: FSMContext):
|
|
25
|
+
"""Отмена текущего действия и очистка state"""
|
|
26
|
+
from ..handlers.handlers import get_global_var
|
|
27
|
+
admin_manager = get_global_var('admin_manager')
|
|
28
|
+
|
|
29
|
+
# Получаем текущий state
|
|
30
|
+
current_state = await state.get_state()
|
|
31
|
+
|
|
32
|
+
# Очищаем state
|
|
33
|
+
await state.clear()
|
|
34
|
+
|
|
35
|
+
if current_state:
|
|
36
|
+
logger.info(f"State очищен для пользователя {message.from_user.id}: {current_state}")
|
|
37
|
+
|
|
38
|
+
# Если это админ, возвращаем в админ режим
|
|
39
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
40
|
+
await state.set_state(AdminStates.admin_mode)
|
|
41
|
+
await message.answer(
|
|
42
|
+
"✅ Текущее действие отменено\n"
|
|
43
|
+
"Вы вернулись в админ режим\n\n"
|
|
44
|
+
"Используйте /admin для просмотра доступных команд",
|
|
45
|
+
parse_mode='Markdown'
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
await message.answer(
|
|
49
|
+
"✅ Текущее действие отменено\n\n"
|
|
50
|
+
"Используйте /start для начала работы",
|
|
51
|
+
parse_mode='Markdown'
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
await message.answer(
|
|
55
|
+
"ℹ️ Нет активных действий для отмены",
|
|
56
|
+
parse_mode='Markdown'
|
|
57
|
+
)
|
|
58
|
+
|
|
23
59
|
async def admin_start_handler(message: Message, state: FSMContext):
|
|
24
60
|
"""Обработчик /start для админов в режиме администратора"""
|
|
25
61
|
from ..handlers.handlers import get_global_var
|
|
@@ -43,16 +79,22 @@ async def admin_start_handler(message: Message, state: FSMContext):
|
|
|
43
79
|
|
|
44
80
|
Доступные команды:
|
|
45
81
|
• `/стат` - статистика воронки
|
|
46
|
-
• `/история
|
|
47
|
-
• `/чат
|
|
82
|
+
• `/история user_id` - история пользователя
|
|
83
|
+
• `/чат user_id` - начать диалог
|
|
48
84
|
• `/чаты` - активные диалоги
|
|
49
85
|
• `/стоп` - завершить диалог
|
|
50
86
|
• `/админ` - переключить режим
|
|
87
|
+
• `/отмена` - отменить текущее действие
|
|
88
|
+
|
|
89
|
+
📅 **Управление событиями:**
|
|
90
|
+
• `/создать_событие` - создать новое событие
|
|
91
|
+
• `/список_событий` - список активных событий
|
|
92
|
+
• `/удалить_событие название` - отменить событие
|
|
51
93
|
"""
|
|
52
94
|
|
|
53
95
|
await message.answer(welcome_text, reply_markup=keyboard, parse_mode='Markdown')
|
|
54
96
|
|
|
55
|
-
@admin_router.message(Command("стат"))
|
|
97
|
+
@admin_router.message(Command(commands=["стат", "stats"]))
|
|
56
98
|
async def admin_stats_handler(message: Message, state: FSMContext):
|
|
57
99
|
"""Статистика воронки"""
|
|
58
100
|
from ..handlers.handlers import get_global_var
|
|
@@ -73,13 +115,13 @@ async def admin_stats_handler(message: Message, state: FSMContext):
|
|
|
73
115
|
|
|
74
116
|
full_text = f"{funnel_text}\n\n{events_text}"
|
|
75
117
|
|
|
76
|
-
await message.answer(full_text
|
|
118
|
+
await message.answer(full_text)
|
|
77
119
|
|
|
78
120
|
except Exception as e:
|
|
79
121
|
logger.error(f"Ошибка получения статистики: {e}")
|
|
80
122
|
await message.answer("❌ Ошибка получения статистики")
|
|
81
123
|
|
|
82
|
-
@admin_router.message(Command("история"))
|
|
124
|
+
@admin_router.message(Command(commands=["история", "history"]))
|
|
83
125
|
async def admin_history_handler(message: Message, state: FSMContext):
|
|
84
126
|
"""История пользователя"""
|
|
85
127
|
from ..handlers.handlers import get_global_var
|
|
@@ -115,7 +157,7 @@ async def admin_history_handler(message: Message, state: FSMContext):
|
|
|
115
157
|
logger.error(f"Ошибка получения истории: {e}")
|
|
116
158
|
await message.answer("❌ Ошибка получения истории")
|
|
117
159
|
|
|
118
|
-
@admin_router.message(Command("чат"))
|
|
160
|
+
@admin_router.message(Command(commands=["чат", "chat"]))
|
|
119
161
|
async def admin_chat_handler(message: Message, state: FSMContext):
|
|
120
162
|
"""Начать диалог с пользователем"""
|
|
121
163
|
from ..handlers.handlers import get_global_var
|
|
@@ -170,7 +212,7 @@ async def admin_chat_handler(message: Message, state: FSMContext):
|
|
|
170
212
|
logger.error(f"❌ Ошибка начала диалога: {e}")
|
|
171
213
|
await message.answer("❌ Ошибка начала диалога")
|
|
172
214
|
|
|
173
|
-
@admin_router.message(Command("чаты"))
|
|
215
|
+
@admin_router.message(Command(commands=["чаты", "chats"]))
|
|
174
216
|
async def admin_active_chats_command(message: Message, state: FSMContext):
|
|
175
217
|
"""Показать активные диалоги админов"""
|
|
176
218
|
from ..handlers.handlers import get_global_var
|
|
@@ -191,7 +233,7 @@ async def admin_active_chats_command(message: Message, state: FSMContext):
|
|
|
191
233
|
logger.error(f"Ошибка получения активных чатов: {e}")
|
|
192
234
|
await message.answer("❌ Ошибка получения активных диалогов")
|
|
193
235
|
|
|
194
|
-
@admin_router.message(Command("стоп"))
|
|
236
|
+
@admin_router.message(Command(commands=["стоп", "stop"]))
|
|
195
237
|
async def admin_stop_handler(message: Message, state: FSMContext):
|
|
196
238
|
"""Завершить диалог"""
|
|
197
239
|
from ..handlers.handlers import get_global_var
|
|
@@ -230,7 +272,7 @@ async def admin_stop_handler(message: Message, state: FSMContext):
|
|
|
230
272
|
logger.error(f"Ошибка завершения диалога: {e}")
|
|
231
273
|
await message.answer("❌ Ошибка завершения диалога")
|
|
232
274
|
|
|
233
|
-
@admin_router.message(Command("админ"))
|
|
275
|
+
@admin_router.message(Command(commands=["админ", "admin"]))
|
|
234
276
|
async def admin_toggle_handler(message: Message, state: FSMContext):
|
|
235
277
|
"""Переключение режима админа"""
|
|
236
278
|
from ..handlers.handlers import get_global_var
|
|
@@ -391,7 +433,7 @@ async def admin_callback_handler(callback: CallbackQuery, state: FSMContext):
|
|
|
391
433
|
logger.error(f"Ошибка обработки callback {data}: {e}")
|
|
392
434
|
await callback.answer("Ошибка")
|
|
393
435
|
|
|
394
|
-
@admin_router.message(StateFilter(AdminStates.admin_mode, AdminStates.in_conversation))
|
|
436
|
+
@admin_router.message(StateFilter(AdminStates.admin_mode, AdminStates.in_conversation), F.text, lambda message: not message.text.startswith('/'))
|
|
395
437
|
async def admin_message_handler(message: Message, state: FSMContext):
|
|
396
438
|
"""Обработчик сообщений админов"""
|
|
397
439
|
from ..handlers.handlers import get_global_var
|
|
@@ -417,8 +459,8 @@ async def admin_message_handler(message: Message, state: FSMContext):
|
|
|
417
459
|
|
|
418
460
|
Доступные команды:
|
|
419
461
|
• `/стат` - статистика воронки
|
|
420
|
-
• `/история
|
|
421
|
-
• `/чат
|
|
462
|
+
• `/история user_id` - история пользователя
|
|
463
|
+
• `/чат user_id` - начать диалог
|
|
422
464
|
• `/стоп` - завершить диалог
|
|
423
465
|
• `/админ` - переключить режим
|
|
424
466
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .common import get_user_locale
|
|
2
|
+
from .simple_calendar import SimpleCalendar
|
|
3
|
+
from .dialog_calendar import DialogCalendar
|
|
4
|
+
from .schemas import SimpleCalendarCallback, DialogCalendarCallback, CalendarLabels
|
|
5
|
+
|
|
6
|
+
__all__ = ['SimpleCalendar', 'DialogCalendar', 'SimpleCalendarCallback', 'DialogCalendarCallback', 'CalendarLabels', 'get_user_locale']
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import calendar
|
|
2
|
+
import locale
|
|
3
|
+
|
|
4
|
+
from aiogram.types import User
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from .schemas import CalendarLabels
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def get_user_locale(from_user: User) -> str:
|
|
11
|
+
"Returns user locale in format en_US, accepts User instance from Message, CallbackData etc"
|
|
12
|
+
loc = from_user.language_code
|
|
13
|
+
return locale.locale_alias[loc].split(".")[0]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GenericCalendar:
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
locale: str = None,
|
|
21
|
+
cancel_btn: str = None,
|
|
22
|
+
today_btn: str = None,
|
|
23
|
+
show_alerts: bool = False
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Pass labels if you need to have alternative language of buttons
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
locale (str): Locale calendar must have captions in (in format uk_UA), if None - default English will be used
|
|
29
|
+
cancel_btn (str): label for button Cancel to cancel date input
|
|
30
|
+
today_btn (str): label for button Today to set calendar back to todays date
|
|
31
|
+
show_alerts (bool): defines how the date range error would shown (defaults to False)
|
|
32
|
+
"""
|
|
33
|
+
self._labels = CalendarLabels()
|
|
34
|
+
if locale:
|
|
35
|
+
# getting month names and days of week in specified locale
|
|
36
|
+
with calendar.different_locale(locale):
|
|
37
|
+
self._labels.days_of_week = list(calendar.day_abbr)
|
|
38
|
+
self._labels.months = calendar.month_abbr[1:]
|
|
39
|
+
|
|
40
|
+
if cancel_btn:
|
|
41
|
+
self._labels.cancel_caption = cancel_btn
|
|
42
|
+
if today_btn:
|
|
43
|
+
self._labels.today_caption = today_btn
|
|
44
|
+
|
|
45
|
+
self.min_date = None
|
|
46
|
+
self.max_date = None
|
|
47
|
+
self.show_alerts = show_alerts
|
|
48
|
+
|
|
49
|
+
def set_dates_range(self, min_date: datetime, max_date: datetime):
|
|
50
|
+
"""Sets range of minimum & maximum dates"""
|
|
51
|
+
self.min_date = min_date
|
|
52
|
+
self.max_date = max_date
|
|
53
|
+
|
|
54
|
+
async def process_day_select(self, data, query):
|
|
55
|
+
"""Checks selected date is in allowed range of dates"""
|
|
56
|
+
date = datetime(int(data.year), int(data.month), int(data.day))
|
|
57
|
+
if self.min_date and self.min_date > date:
|
|
58
|
+
await query.answer(
|
|
59
|
+
f'The date have to be later {self.min_date.strftime("%d/%m/%Y")}',
|
|
60
|
+
show_alert=self.show_alerts
|
|
61
|
+
)
|
|
62
|
+
return False, None
|
|
63
|
+
elif self.max_date and self.max_date < date:
|
|
64
|
+
await query.answer(
|
|
65
|
+
f'The date have to be before {self.max_date.strftime("%d/%m/%Y")}',
|
|
66
|
+
show_alert=self.show_alerts
|
|
67
|
+
)
|
|
68
|
+
return False, None
|
|
69
|
+
await query.message.delete_reply_markup() # removing inline keyboard
|
|
70
|
+
return True, date
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import calendar
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
|
5
|
+
from aiogram.types import CallbackQuery
|
|
6
|
+
|
|
7
|
+
from .schemas import DialogCalendarCallback, DialogCalAct, highlight, superscript
|
|
8
|
+
from .common import GenericCalendar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DialogCalendar(GenericCalendar):
|
|
12
|
+
|
|
13
|
+
ignore_callback = DialogCalendarCallback(act=DialogCalAct.ignore).pack() # placeholder for no answer buttons
|
|
14
|
+
|
|
15
|
+
async def _get_month_kb(self, year: int):
|
|
16
|
+
"""Creates an inline keyboard with months for specified year"""
|
|
17
|
+
|
|
18
|
+
today = datetime.now()
|
|
19
|
+
now_month, now_year = today.month, today.year
|
|
20
|
+
now_year = today.year
|
|
21
|
+
|
|
22
|
+
kb = []
|
|
23
|
+
# first row with year button
|
|
24
|
+
years_row = []
|
|
25
|
+
years_row.append(
|
|
26
|
+
InlineKeyboardButton(
|
|
27
|
+
text=self._labels.cancel_caption,
|
|
28
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.cancel, year=year, month=1, day=1).pack()
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
years_row.append(InlineKeyboardButton(
|
|
32
|
+
text=str(year) if year != today.year else highlight(year),
|
|
33
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.start, year=year, month=-1, day=-1).pack()
|
|
34
|
+
))
|
|
35
|
+
years_row.append(InlineKeyboardButton(text=" ", callback_data=self.ignore_callback))
|
|
36
|
+
kb.append(years_row)
|
|
37
|
+
# two rows with 6 months buttons
|
|
38
|
+
month6_row = []
|
|
39
|
+
|
|
40
|
+
def highlight_month():
|
|
41
|
+
month_str = self._labels.months[month - 1]
|
|
42
|
+
if now_month == month and now_year == year:
|
|
43
|
+
return highlight(month_str)
|
|
44
|
+
return month_str
|
|
45
|
+
|
|
46
|
+
for month in range(1, 7):
|
|
47
|
+
month6_row.append(InlineKeyboardButton(
|
|
48
|
+
text=highlight_month(),
|
|
49
|
+
callback_data=DialogCalendarCallback(
|
|
50
|
+
act=DialogCalAct.set_m, year=year, month=month, day=-1
|
|
51
|
+
).pack()
|
|
52
|
+
))
|
|
53
|
+
month12_row = []
|
|
54
|
+
|
|
55
|
+
for month in range(7, 13):
|
|
56
|
+
month12_row.append(InlineKeyboardButton(
|
|
57
|
+
text=highlight_month(),
|
|
58
|
+
callback_data=DialogCalendarCallback(
|
|
59
|
+
act=DialogCalAct.set_m, year=year, month=month, day=-1
|
|
60
|
+
).pack()
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
kb.append(month6_row)
|
|
64
|
+
kb.append(month12_row)
|
|
65
|
+
return InlineKeyboardMarkup(row_width=6, inline_keyboard=kb)
|
|
66
|
+
|
|
67
|
+
async def _get_days_kb(self, year: int, month: int):
|
|
68
|
+
"""Creates an inline keyboard with calendar days of month for specified year and month"""
|
|
69
|
+
|
|
70
|
+
today = datetime.now()
|
|
71
|
+
now_weekday = self._labels.days_of_week[today.weekday()]
|
|
72
|
+
now_month, now_year, now_day = today.month, today.year, today.day
|
|
73
|
+
|
|
74
|
+
def highlight_month():
|
|
75
|
+
month_str = self._labels.months[month - 1]
|
|
76
|
+
if now_month == month and now_year == year:
|
|
77
|
+
return highlight(month_str)
|
|
78
|
+
return month_str
|
|
79
|
+
|
|
80
|
+
def highlight_weekday():
|
|
81
|
+
if now_month == month and now_year == year and now_weekday == weekday:
|
|
82
|
+
return highlight(weekday)
|
|
83
|
+
return weekday
|
|
84
|
+
|
|
85
|
+
def format_day_string():
|
|
86
|
+
date_to_check = datetime(year, month, day)
|
|
87
|
+
if self.min_date and date_to_check < self.min_date:
|
|
88
|
+
return superscript(str(day))
|
|
89
|
+
elif self.max_date and date_to_check > self.max_date:
|
|
90
|
+
return superscript(str(day))
|
|
91
|
+
return str(day)
|
|
92
|
+
|
|
93
|
+
def highlight_day():
|
|
94
|
+
day_string = format_day_string()
|
|
95
|
+
if now_month == month and now_year == year and now_day == day:
|
|
96
|
+
return highlight(day_string)
|
|
97
|
+
return day_string
|
|
98
|
+
|
|
99
|
+
kb = []
|
|
100
|
+
nav_row = []
|
|
101
|
+
nav_row.append(
|
|
102
|
+
InlineKeyboardButton(
|
|
103
|
+
text=self._labels.cancel_caption,
|
|
104
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.cancel, year=year, month=1, day=1).pack()
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
nav_row.append(InlineKeyboardButton(
|
|
108
|
+
text=str(year) if year != now_year else highlight(year),
|
|
109
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.start, year=year, month=-1, day=-1).pack()
|
|
110
|
+
))
|
|
111
|
+
nav_row.append(InlineKeyboardButton(
|
|
112
|
+
text=highlight_month(),
|
|
113
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.set_y, year=year, month=-1, day=-1).pack()
|
|
114
|
+
))
|
|
115
|
+
kb.append(nav_row)
|
|
116
|
+
|
|
117
|
+
week_days_labels_row = []
|
|
118
|
+
for weekday in self._labels.days_of_week:
|
|
119
|
+
week_days_labels_row.append(InlineKeyboardButton(
|
|
120
|
+
text=highlight_weekday(), callback_data=self.ignore_callback))
|
|
121
|
+
kb.append(week_days_labels_row)
|
|
122
|
+
|
|
123
|
+
month_calendar = calendar.monthcalendar(year, month)
|
|
124
|
+
|
|
125
|
+
for week in month_calendar:
|
|
126
|
+
days_row = []
|
|
127
|
+
for day in week:
|
|
128
|
+
if day == 0:
|
|
129
|
+
days_row.append(InlineKeyboardButton(text=" ", callback_data=self.ignore_callback))
|
|
130
|
+
continue
|
|
131
|
+
days_row.append(InlineKeyboardButton(
|
|
132
|
+
text=highlight_day(),
|
|
133
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.day, year=year, month=month, day=day).pack()
|
|
134
|
+
))
|
|
135
|
+
kb.append(days_row)
|
|
136
|
+
return InlineKeyboardMarkup(row_width=7, inline_keyboard=kb)
|
|
137
|
+
|
|
138
|
+
async def start_calendar(
|
|
139
|
+
self,
|
|
140
|
+
year: int = datetime.now().year,
|
|
141
|
+
month: int = None
|
|
142
|
+
) -> InlineKeyboardMarkup:
|
|
143
|
+
today = datetime.now()
|
|
144
|
+
now_year = today.year
|
|
145
|
+
|
|
146
|
+
if month:
|
|
147
|
+
return await self._get_days_kb(year, month)
|
|
148
|
+
kb = []
|
|
149
|
+
# inline_kb = InlineKeyboardMarkup(row_width=5)
|
|
150
|
+
# first row - years
|
|
151
|
+
years_row = []
|
|
152
|
+
for value in range(year - 2, year + 3):
|
|
153
|
+
years_row.append(InlineKeyboardButton(
|
|
154
|
+
text=str(value) if value != now_year else highlight(value),
|
|
155
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.set_y, year=value, month=-1, day=-1).pack()
|
|
156
|
+
))
|
|
157
|
+
kb.append(years_row)
|
|
158
|
+
# nav buttons
|
|
159
|
+
nav_row = []
|
|
160
|
+
nav_row.append(InlineKeyboardButton(
|
|
161
|
+
text='<<',
|
|
162
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.prev_y, year=year, month=-1, day=-1).pack()
|
|
163
|
+
))
|
|
164
|
+
nav_row.append(InlineKeyboardButton(
|
|
165
|
+
text=self._labels.cancel_caption,
|
|
166
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.cancel, year=year, month=1, day=1).pack()
|
|
167
|
+
))
|
|
168
|
+
nav_row.append(InlineKeyboardButton(
|
|
169
|
+
text='>>',
|
|
170
|
+
callback_data=DialogCalendarCallback(act=DialogCalAct.next_y, year=year, month=1, day=1).pack()
|
|
171
|
+
))
|
|
172
|
+
kb.append(nav_row)
|
|
173
|
+
return InlineKeyboardMarkup(row_width=5, inline_keyboard=kb)
|
|
174
|
+
|
|
175
|
+
async def process_selection(self, query: CallbackQuery, data: DialogCalendarCallback) -> tuple:
|
|
176
|
+
return_data = (False, None)
|
|
177
|
+
if data.act == DialogCalAct.ignore:
|
|
178
|
+
await query.answer(cache_time=60)
|
|
179
|
+
if data.act == DialogCalAct.set_y:
|
|
180
|
+
await query.message.edit_reply_markup(reply_markup=await self._get_month_kb(int(data.year)))
|
|
181
|
+
if data.act == DialogCalAct.prev_y:
|
|
182
|
+
new_year = int(data.year) - 5
|
|
183
|
+
await query.message.edit_reply_markup(reply_markup=await self.start_calendar(year=new_year))
|
|
184
|
+
if data.act == DialogCalAct.next_y:
|
|
185
|
+
new_year = int(data.year) + 5
|
|
186
|
+
await query.message.edit_reply_markup(reply_markup=await self.start_calendar(year=new_year))
|
|
187
|
+
if data.act == DialogCalAct.start:
|
|
188
|
+
await query.message.edit_reply_markup(reply_markup=await self.start_calendar(int(data.year)))
|
|
189
|
+
if data.act == DialogCalAct.set_m:
|
|
190
|
+
await query.message.edit_reply_markup(reply_markup=await self._get_days_kb(int(data.year), int(data.month)))
|
|
191
|
+
if data.act == DialogCalAct.day:
|
|
192
|
+
|
|
193
|
+
return await self.process_day_select(data, query)
|
|
194
|
+
|
|
195
|
+
if data.act == DialogCalAct.cancel:
|
|
196
|
+
await query.message.delete_reply_markup()
|
|
197
|
+
return return_data
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, conlist, Field
|
|
5
|
+
|
|
6
|
+
from aiogram.filters.callback_data import CallbackData
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SimpleCalAct(str, Enum):
|
|
10
|
+
ignore = 'IGNORE'
|
|
11
|
+
prev_y = 'PREV-YEAR'
|
|
12
|
+
next_y = 'NEXT-YEAR'
|
|
13
|
+
prev_m = 'PREV-MONTH'
|
|
14
|
+
next_m = 'NEXT-MONTH'
|
|
15
|
+
cancel = 'CANCEL'
|
|
16
|
+
today = 'TODAY'
|
|
17
|
+
day = 'DAY'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DialogCalAct(str, Enum):
|
|
21
|
+
ignore = 'IGNORE'
|
|
22
|
+
set_y = 'SET-YEAR'
|
|
23
|
+
set_m = 'SET-MONTH'
|
|
24
|
+
prev_y = 'PREV-YEAR'
|
|
25
|
+
next_y = 'NEXT-YEAR'
|
|
26
|
+
cancel = 'CANCEL'
|
|
27
|
+
start = 'START'
|
|
28
|
+
day = 'SET-DAY'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CalendarCallback(CallbackData, prefix="calendar"):
|
|
32
|
+
act: str
|
|
33
|
+
year: Optional[int] = None
|
|
34
|
+
month: Optional[int] = None
|
|
35
|
+
day: Optional[int] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SimpleCalendarCallback(CalendarCallback, prefix="simple_calendar"):
|
|
39
|
+
act: SimpleCalAct
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DialogCalendarCallback(CalendarCallback, prefix="dialog_calendar"):
|
|
43
|
+
act: DialogCalAct
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CalendarLabels(BaseModel):
|
|
47
|
+
"Schema to pass labels for calendar. Can be used to put in different languages"
|
|
48
|
+
days_of_week: conlist(str, max_length=7, min_length=7) = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
|
49
|
+
months: conlist(str, max_length=12, min_length=12) = [
|
|
50
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
|
51
|
+
]
|
|
52
|
+
cancel_caption: str = Field(default='Cancel', description='Caprion for Cancel button')
|
|
53
|
+
today_caption: str = Field(default='Today', description='Caprion for Cancel button')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
HIGHLIGHT_FORMAT = "[{}]"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def highlight(text):
|
|
60
|
+
return HIGHLIGHT_FORMAT.format(text)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def superscript(text):
|
|
64
|
+
normal = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-=()"
|
|
65
|
+
super_s = "ᴬᴮᶜᴰᴱᶠᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾQᴿˢᵀᵁⱽᵂˣʸᶻᵃᵇᶜᵈᵉᶠᵍʰᶦʲᵏˡᵐⁿᵒᵖ۹ʳˢᵗᵘᵛʷˣʸᶻ⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾"
|
|
66
|
+
output = ''
|
|
67
|
+
for i in text:
|
|
68
|
+
output += (super_s[normal.index(i)] if i in normal else i)
|
|
69
|
+
return output
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def subscript(text):
|
|
73
|
+
normal = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-=()"
|
|
74
|
+
sub_s = "ₐ₈CDₑբGₕᵢⱼₖₗₘₙₒₚQᵣₛₜᵤᵥwₓᵧZₐ♭꜀ᑯₑբ₉ₕᵢⱼₖₗₘₙₒₚ૧ᵣₛₜᵤᵥwₓᵧ₂₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎"
|
|
75
|
+
output = ''
|
|
76
|
+
for i in text:
|
|
77
|
+
output += (sub_s[normal.index(i)] if i in normal else i)
|
|
78
|
+
return output
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import calendar
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
|
|
4
|
+
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
|
5
|
+
from aiogram.types import CallbackQuery
|
|
6
|
+
|
|
7
|
+
from .schemas import SimpleCalendarCallback, SimpleCalAct, highlight, superscript
|
|
8
|
+
from .common import GenericCalendar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SimpleCalendar(GenericCalendar):
|
|
12
|
+
|
|
13
|
+
ignore_callback = SimpleCalendarCallback(act=SimpleCalAct.ignore).pack() # placeholder for no answer buttons
|
|
14
|
+
|
|
15
|
+
async def start_calendar(
|
|
16
|
+
self,
|
|
17
|
+
year: int = datetime.now().year,
|
|
18
|
+
month: int = datetime.now().month
|
|
19
|
+
) -> InlineKeyboardMarkup:
|
|
20
|
+
"""
|
|
21
|
+
Creates an inline keyboard with the provided year and month
|
|
22
|
+
:param int year: Year to use in the calendar, if None the current year is used.
|
|
23
|
+
:param int month: Month to use in the calendar, if None the current month is used.
|
|
24
|
+
:return: Returns InlineKeyboardMarkup object with the calendar.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
today = datetime.now()
|
|
28
|
+
now_weekday = self._labels.days_of_week[today.weekday()]
|
|
29
|
+
now_month, now_year, now_day = today.month, today.year, today.day
|
|
30
|
+
|
|
31
|
+
def highlight_month():
|
|
32
|
+
month_str = self._labels.months[month - 1]
|
|
33
|
+
if now_month == month and now_year == year:
|
|
34
|
+
return highlight(month_str)
|
|
35
|
+
return month_str
|
|
36
|
+
|
|
37
|
+
def highlight_weekday():
|
|
38
|
+
if now_month == month and now_year == year and now_weekday == weekday:
|
|
39
|
+
return highlight(weekday)
|
|
40
|
+
return weekday
|
|
41
|
+
|
|
42
|
+
def format_day_string():
|
|
43
|
+
date_to_check = datetime(year, month, day)
|
|
44
|
+
if self.min_date and date_to_check < self.min_date:
|
|
45
|
+
return superscript(str(day))
|
|
46
|
+
elif self.max_date and date_to_check > self.max_date:
|
|
47
|
+
return superscript(str(day))
|
|
48
|
+
return str(day)
|
|
49
|
+
|
|
50
|
+
def highlight_day():
|
|
51
|
+
day_string = format_day_string()
|
|
52
|
+
if now_month == month and now_year == year and now_day == day:
|
|
53
|
+
return highlight(day_string)
|
|
54
|
+
return day_string
|
|
55
|
+
|
|
56
|
+
# building a calendar keyboard
|
|
57
|
+
kb = []
|
|
58
|
+
|
|
59
|
+
# inline_kb = InlineKeyboardMarkup(row_width=7)
|
|
60
|
+
# First row - Year
|
|
61
|
+
years_row = []
|
|
62
|
+
years_row.append(InlineKeyboardButton(
|
|
63
|
+
text="<<",
|
|
64
|
+
callback_data=SimpleCalendarCallback(act=SimpleCalAct.prev_y, year=year, month=month, day=1).pack()
|
|
65
|
+
))
|
|
66
|
+
years_row.append(InlineKeyboardButton(
|
|
67
|
+
text=str(year) if year != now_year else highlight(year),
|
|
68
|
+
callback_data=self.ignore_callback
|
|
69
|
+
))
|
|
70
|
+
years_row.append(InlineKeyboardButton(
|
|
71
|
+
text=">>",
|
|
72
|
+
callback_data=SimpleCalendarCallback(act=SimpleCalAct.next_y, year=year, month=month, day=1).pack()
|
|
73
|
+
))
|
|
74
|
+
kb.append(years_row)
|
|
75
|
+
|
|
76
|
+
# Month nav Buttons
|
|
77
|
+
month_row = []
|
|
78
|
+
month_row.append(InlineKeyboardButton(
|
|
79
|
+
text="<",
|
|
80
|
+
callback_data=SimpleCalendarCallback(act=SimpleCalAct.prev_m, year=year, month=month, day=1).pack()
|
|
81
|
+
))
|
|
82
|
+
month_row.append(InlineKeyboardButton(
|
|
83
|
+
text=highlight_month(),
|
|
84
|
+
callback_data=self.ignore_callback
|
|
85
|
+
))
|
|
86
|
+
month_row.append(InlineKeyboardButton(
|
|
87
|
+
text=">",
|
|
88
|
+
callback_data=SimpleCalendarCallback(act=SimpleCalAct.next_m, year=year, month=month, day=1).pack()
|
|
89
|
+
))
|
|
90
|
+
kb.append(month_row)
|
|
91
|
+
|
|
92
|
+
# Week Days
|
|
93
|
+
week_days_labels_row = []
|
|
94
|
+
for weekday in self._labels.days_of_week:
|
|
95
|
+
week_days_labels_row.append(
|
|
96
|
+
InlineKeyboardButton(text=highlight_weekday(), callback_data=self.ignore_callback)
|
|
97
|
+
)
|
|
98
|
+
kb.append(week_days_labels_row)
|
|
99
|
+
|
|
100
|
+
# Calendar rows - Days of month
|
|
101
|
+
month_calendar = calendar.monthcalendar(year, month)
|
|
102
|
+
|
|
103
|
+
for week in month_calendar:
|
|
104
|
+
days_row = []
|
|
105
|
+
for day in week:
|
|
106
|
+
if day == 0:
|
|
107
|
+
days_row.append(InlineKeyboardButton(text=" ", callback_data=self.ignore_callback))
|
|
108
|
+
continue
|
|
109
|
+
days_row.append(InlineKeyboardButton(
|
|
110
|
+
text=highlight_day(),
|
|
111
|
+
callback_data=SimpleCalendarCallback(act=SimpleCalAct.day, year=year, month=month, day=day).pack()
|
|
112
|
+
))
|
|
113
|
+
kb.append(days_row)
|
|
114
|
+
|
|
115
|
+
# nav today & cancel button
|
|
116
|
+
cancel_row = []
|
|
117
|
+
cancel_row.append(InlineKeyboardButton(
|
|
118
|
+
text=self._labels.cancel_caption,
|
|
119
|
+
callback_data=SimpleCalendarCallback(act=SimpleCalAct.cancel, year=year, month=month, day=day).pack()
|
|
120
|
+
))
|
|
121
|
+
cancel_row.append(InlineKeyboardButton(text=" ", callback_data=self.ignore_callback))
|
|
122
|
+
cancel_row.append(InlineKeyboardButton(
|
|
123
|
+
text=self._labels.today_caption,
|
|
124
|
+
callback_data=SimpleCalendarCallback(act=SimpleCalAct.today, year=year, month=month, day=day).pack()
|
|
125
|
+
))
|
|
126
|
+
kb.append(cancel_row)
|
|
127
|
+
return InlineKeyboardMarkup(row_width=7, inline_keyboard=kb)
|
|
128
|
+
|
|
129
|
+
async def _update_calendar(self, query: CallbackQuery, with_date: datetime):
|
|
130
|
+
await query.message.edit_reply_markup(
|
|
131
|
+
reply_markup=await self.start_calendar(int(with_date.year), int(with_date.month))
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def process_selection(self, query: CallbackQuery, data: SimpleCalendarCallback) -> tuple:
|
|
135
|
+
"""
|
|
136
|
+
Process the callback_query. This method generates a new calendar if forward or
|
|
137
|
+
backward is pressed. This method should be called inside a CallbackQueryHandler.
|
|
138
|
+
:param query: callback_query, as provided by the CallbackQueryHandler
|
|
139
|
+
:param data: callback_data, dictionary, set by calendar_callback
|
|
140
|
+
:return: Returns a tuple (Boolean,datetime), indicating if a date is selected
|
|
141
|
+
and returning the date if so.
|
|
142
|
+
"""
|
|
143
|
+
return_data = (False, None)
|
|
144
|
+
|
|
145
|
+
# processing empty buttons, answering with no action
|
|
146
|
+
if data.act == SimpleCalAct.ignore:
|
|
147
|
+
await query.answer(cache_time=60)
|
|
148
|
+
return return_data
|
|
149
|
+
|
|
150
|
+
temp_date = datetime(int(data.year), int(data.month), 1)
|
|
151
|
+
|
|
152
|
+
# user picked a day button, return date
|
|
153
|
+
if data.act == SimpleCalAct.day:
|
|
154
|
+
return await self.process_day_select(data, query)
|
|
155
|
+
|
|
156
|
+
# user navigates to previous year, editing message with new calendar
|
|
157
|
+
if data.act == SimpleCalAct.prev_y:
|
|
158
|
+
prev_date = datetime(int(data.year) - 1, int(data.month), 1)
|
|
159
|
+
await self._update_calendar(query, prev_date)
|
|
160
|
+
# user navigates to next year, editing message with new calendar
|
|
161
|
+
if data.act == SimpleCalAct.next_y:
|
|
162
|
+
next_date = datetime(int(data.year) + 1, int(data.month), 1)
|
|
163
|
+
await self._update_calendar(query, next_date)
|
|
164
|
+
# user navigates to previous month, editing message with new calendar
|
|
165
|
+
if data.act == SimpleCalAct.prev_m:
|
|
166
|
+
prev_date = temp_date - timedelta(days=1)
|
|
167
|
+
await self._update_calendar(query, prev_date)
|
|
168
|
+
# user navigates to next month, editing message with new calendar
|
|
169
|
+
if data.act == SimpleCalAct.next_m:
|
|
170
|
+
next_date = temp_date + timedelta(days=31)
|
|
171
|
+
await self._update_calendar(query, next_date)
|
|
172
|
+
if data.act == SimpleCalAct.today:
|
|
173
|
+
# Возвращаем сегодняшнюю дату
|
|
174
|
+
await query.answer()
|
|
175
|
+
return (True, datetime.now())
|
|
176
|
+
if data.act == SimpleCalAct.cancel:
|
|
177
|
+
await query.message.delete_reply_markup()
|
|
178
|
+
return ('cancel', None)
|
|
179
|
+
# at some point user clicks DAY button, returning date
|
|
180
|
+
return return_data
|