smart-bot-factory 0.3.2__py3-none-any.whl → 0.3.4__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 +932 -0
- smart_bot_factory/admin/admin_logic.py +59 -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 +218 -4
- smart_bot_factory/core/states.py +12 -0
- smart_bot_factory/creation/bot_builder.py +54 -2
- smart_bot_factory/handlers/handlers.py +10 -3
- smart_bot_factory/integrations/supabase_client.py +422 -37
- smart_bot_factory/utm_link_generator.py +13 -3
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.4.dist-info}/METADATA +3 -1
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.4.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.4.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.4.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.2.dist-info → smart_bot_factory-0.3.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,6 +20,47 @@ 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
|
+
# Очищаем временные файлы если это создание события
|
|
33
|
+
if current_state and current_state.startswith('AdminStates:create_event'):
|
|
34
|
+
from .admin_events import cleanup_temp_files
|
|
35
|
+
await cleanup_temp_files(state)
|
|
36
|
+
|
|
37
|
+
# Очищаем state
|
|
38
|
+
await state.clear()
|
|
39
|
+
|
|
40
|
+
if current_state:
|
|
41
|
+
logger.info(f"State очищен для пользователя {message.from_user.id}: {current_state}")
|
|
42
|
+
|
|
43
|
+
# Если это админ, возвращаем в админ режим
|
|
44
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
45
|
+
await state.set_state(AdminStates.admin_mode)
|
|
46
|
+
await message.answer(
|
|
47
|
+
"✅ Текущее действие отменено\n"
|
|
48
|
+
"Вы вернулись в админ режим\n\n"
|
|
49
|
+
"Используйте /admin для просмотра доступных команд",
|
|
50
|
+
parse_mode='Markdown'
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
await message.answer(
|
|
54
|
+
"✅ Текущее действие отменено\n\n"
|
|
55
|
+
"Используйте /start для начала работы",
|
|
56
|
+
parse_mode='Markdown'
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
await message.answer(
|
|
60
|
+
"ℹ️ Нет активных действий для отмены",
|
|
61
|
+
parse_mode='Markdown'
|
|
62
|
+
)
|
|
63
|
+
|
|
23
64
|
async def admin_start_handler(message: Message, state: FSMContext):
|
|
24
65
|
"""Обработчик /start для админов в режиме администратора"""
|
|
25
66
|
from ..handlers.handlers import get_global_var
|
|
@@ -43,16 +84,22 @@ async def admin_start_handler(message: Message, state: FSMContext):
|
|
|
43
84
|
|
|
44
85
|
Доступные команды:
|
|
45
86
|
• `/стат` - статистика воронки
|
|
46
|
-
• `/история
|
|
47
|
-
• `/чат
|
|
87
|
+
• `/история user_id` - история пользователя
|
|
88
|
+
• `/чат user_id` - начать диалог
|
|
48
89
|
• `/чаты` - активные диалоги
|
|
49
90
|
• `/стоп` - завершить диалог
|
|
50
91
|
• `/админ` - переключить режим
|
|
92
|
+
• `/отмена` - отменить текущее действие
|
|
93
|
+
|
|
94
|
+
📅 **Управление событиями:**
|
|
95
|
+
• `/создать_событие` - создать новое событие
|
|
96
|
+
• `/список_событий` - список активных событий
|
|
97
|
+
• `/удалить_событие название` - отменить событие
|
|
51
98
|
"""
|
|
52
99
|
|
|
53
100
|
await message.answer(welcome_text, reply_markup=keyboard, parse_mode='Markdown')
|
|
54
101
|
|
|
55
|
-
@admin_router.message(Command("стат"))
|
|
102
|
+
@admin_router.message(Command(commands=["стат", "stats"]))
|
|
56
103
|
async def admin_stats_handler(message: Message, state: FSMContext):
|
|
57
104
|
"""Статистика воронки"""
|
|
58
105
|
from ..handlers.handlers import get_global_var
|
|
@@ -73,13 +120,13 @@ async def admin_stats_handler(message: Message, state: FSMContext):
|
|
|
73
120
|
|
|
74
121
|
full_text = f"{funnel_text}\n\n{events_text}"
|
|
75
122
|
|
|
76
|
-
await message.answer(full_text
|
|
123
|
+
await message.answer(full_text)
|
|
77
124
|
|
|
78
125
|
except Exception as e:
|
|
79
126
|
logger.error(f"Ошибка получения статистики: {e}")
|
|
80
127
|
await message.answer("❌ Ошибка получения статистики")
|
|
81
128
|
|
|
82
|
-
@admin_router.message(Command("история"))
|
|
129
|
+
@admin_router.message(Command(commands=["история", "history"]))
|
|
83
130
|
async def admin_history_handler(message: Message, state: FSMContext):
|
|
84
131
|
"""История пользователя"""
|
|
85
132
|
from ..handlers.handlers import get_global_var
|
|
@@ -115,7 +162,7 @@ async def admin_history_handler(message: Message, state: FSMContext):
|
|
|
115
162
|
logger.error(f"Ошибка получения истории: {e}")
|
|
116
163
|
await message.answer("❌ Ошибка получения истории")
|
|
117
164
|
|
|
118
|
-
@admin_router.message(Command("чат"))
|
|
165
|
+
@admin_router.message(Command(commands=["чат", "chat"]))
|
|
119
166
|
async def admin_chat_handler(message: Message, state: FSMContext):
|
|
120
167
|
"""Начать диалог с пользователем"""
|
|
121
168
|
from ..handlers.handlers import get_global_var
|
|
@@ -170,7 +217,7 @@ async def admin_chat_handler(message: Message, state: FSMContext):
|
|
|
170
217
|
logger.error(f"❌ Ошибка начала диалога: {e}")
|
|
171
218
|
await message.answer("❌ Ошибка начала диалога")
|
|
172
219
|
|
|
173
|
-
@admin_router.message(Command("чаты"))
|
|
220
|
+
@admin_router.message(Command(commands=["чаты", "chats"]))
|
|
174
221
|
async def admin_active_chats_command(message: Message, state: FSMContext):
|
|
175
222
|
"""Показать активные диалоги админов"""
|
|
176
223
|
from ..handlers.handlers import get_global_var
|
|
@@ -191,7 +238,7 @@ async def admin_active_chats_command(message: Message, state: FSMContext):
|
|
|
191
238
|
logger.error(f"Ошибка получения активных чатов: {e}")
|
|
192
239
|
await message.answer("❌ Ошибка получения активных диалогов")
|
|
193
240
|
|
|
194
|
-
@admin_router.message(Command("стоп"))
|
|
241
|
+
@admin_router.message(Command(commands=["стоп", "stop"]))
|
|
195
242
|
async def admin_stop_handler(message: Message, state: FSMContext):
|
|
196
243
|
"""Завершить диалог"""
|
|
197
244
|
from ..handlers.handlers import get_global_var
|
|
@@ -230,7 +277,7 @@ async def admin_stop_handler(message: Message, state: FSMContext):
|
|
|
230
277
|
logger.error(f"Ошибка завершения диалога: {e}")
|
|
231
278
|
await message.answer("❌ Ошибка завершения диалога")
|
|
232
279
|
|
|
233
|
-
@admin_router.message(Command("админ"))
|
|
280
|
+
@admin_router.message(Command(commands=["админ", "admin"]))
|
|
234
281
|
async def admin_toggle_handler(message: Message, state: FSMContext):
|
|
235
282
|
"""Переключение режима админа"""
|
|
236
283
|
from ..handlers.handlers import get_global_var
|
|
@@ -391,7 +438,7 @@ async def admin_callback_handler(callback: CallbackQuery, state: FSMContext):
|
|
|
391
438
|
logger.error(f"Ошибка обработки callback {data}: {e}")
|
|
392
439
|
await callback.answer("Ошибка")
|
|
393
440
|
|
|
394
|
-
@admin_router.message(StateFilter(AdminStates.admin_mode, AdminStates.in_conversation))
|
|
441
|
+
@admin_router.message(StateFilter(AdminStates.admin_mode, AdminStates.in_conversation), F.text, lambda message: not message.text.startswith('/'))
|
|
395
442
|
async def admin_message_handler(message: Message, state: FSMContext):
|
|
396
443
|
"""Обработчик сообщений админов"""
|
|
397
444
|
from ..handlers.handlers import get_global_var
|
|
@@ -417,8 +464,8 @@ async def admin_message_handler(message: Message, state: FSMContext):
|
|
|
417
464
|
|
|
418
465
|
Доступные команды:
|
|
419
466
|
• `/стат` - статистика воронки
|
|
420
|
-
• `/история
|
|
421
|
-
• `/чат
|
|
467
|
+
• `/история user_id` - история пользователя
|
|
468
|
+
• `/чат user_id` - начать диалог
|
|
422
469
|
• `/стоп` - завершить диалог
|
|
423
470
|
• `/админ` - переключить режим
|
|
424
471
|
|
|
@@ -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
|