users: allow profile without maintainer

parent 950d64a6
......@@ -4,13 +4,15 @@ from database.func import DB
def menu_kb(user_id: int):
user = DB.user.get(user_id)
user_rules = DB.user.get_roles(user_id)
kb = Keyboard()
kb.add(Button("Профиль"))
kb.row()
kb.add(Button("Отслеживание"))
kb.add(Button("Ошибки"))
if user and user.maintainer:
kb.row()
kb.add(Button("Отслеживание"))
kb.add(Button("Ошибки"))
kb.row()
if "news" in user_rules:
kb.add(Button("Новости"))
......
......@@ -9,18 +9,22 @@ profile_kb = (
.add(InlineButton("Настройки", callback_data="profile/settings"))
).get_markup()
profile_settings_kb = (
InlineKeyboard()
.add(InlineButton("Рассылки и уведомления", callback_data="profile/mailing"))
.row()
.add(InlineButton("Отслеживаемые пакеты", callback_data="profile/settings/packages"))
.row()
.add(InlineButton("Сменить сопровождающего", callback_data="profile/settings/maintainer"))
.row()
.add(InlineButton("Сменить репозиторий", callback_data="profile/settings/branch"))
.row()
.add(InlineButton("Закрыть", callback_data="profile/close"))
).get_markup()
def profile_settings_kb(has_maintainer: bool = True):
maintainer_label = "Сменить сопровождающего" if has_maintainer else "Выбрать сопровождающего"
kb = InlineKeyboard()
kb.add(InlineButton("Рассылки и уведомления", callback_data="profile/mailing"))
kb.row()
kb.add(InlineButton("Отслеживаемые пакеты", callback_data="profile/settings/packages"))
kb.row()
kb.add(InlineButton(maintainer_label, callback_data="profile/settings/maintainer"))
kb.row()
if has_maintainer:
kb.add(InlineButton("Выйти", callback_data="profile/settings/logout"))
kb.row()
kb.add(InlineButton("Сменить репозиторий", callback_data="profile/settings/branch"))
kb.row()
kb.add(InlineButton("Закрыть", callback_data="profile/close"))
return kb.get_markup()
def profile_settings_branch_kb():
......@@ -44,14 +48,19 @@ INSTANT_TYPES = [
]
def mailing_settings_kb(schedule, enabled: list[str]):
def mailing_settings_kb(schedule, enabled: list[str], has_maintainer: bool = True):
kb = InlineKeyboard()
kb.add(InlineButton("Дни недели", callback_data="profile/mailing/days"))
kb.add(InlineButton("Время", callback_data="profile/mailing/set-time"))
kb.row()
for type_id, label in SCHEDULED_TYPES:
scheduled_types = SCHEDULED_TYPES if has_maintainer else [
item for item in SCHEDULED_TYPES if item[0] == "pkg_watch"
]
instant_types = INSTANT_TYPES if has_maintainer else []
for type_id, label in scheduled_types:
status = "вкл" if type_id in enabled else "выкл"
kb.add(InlineButton(
f"{label}: {status}",
......@@ -59,7 +68,7 @@ def mailing_settings_kb(schedule, enabled: list[str]):
))
kb.row()
for type_id, label in INSTANT_TYPES:
for type_id, label in instant_types:
status = "вкл" if type_id in enabled else "выкл"
kb.add(InlineButton(
f"{label}: {status}",
......
......@@ -3,6 +3,12 @@ from telegrinder import Button, Keyboard
from config import DEFAUIL_BRANCHES
def maintainer_kb():
kb = Keyboard(one_time_keyboard=True)
kb.add(Button("Пропустить"))
return kb.get_markup()
def default_branch_kb():
kb = Keyboard(one_time_keyboard=True)
for branch in DEFAUIL_BRANCHES:
......
......@@ -60,7 +60,7 @@ class UserMethod:
def add(
cls,
user_id: int,
maintainer: Maintainer,
maintainer: Maintainer | None,
default_branch: str = "sisyphus",
):
"""создание записи пользователя"""
......@@ -132,7 +132,7 @@ class UserMethod:
return False
@classmethod
def change_maintainer(cls, user_id: int, maintainer: Maintainer):
def change_maintainer(cls, user_id: int, maintainer: Maintainer | None):
user = cls.get(user_id)
if user is None:
return False
......
......@@ -36,9 +36,10 @@ class User(BaseModel):
"""модель пользователя"""
user_id = IntegerField() # id пользователя
maintainer: Maintainer = ForeignKeyField( # сопровождающий
maintainer: Maintainer | None = ForeignKeyField( # сопровождающий
Maintainer,
to_field="nickname"
to_field="nickname",
null=True,
)
default_branch = TextField( # Репозитоий по умолчанию
default="sisyphus"
......
......@@ -27,7 +27,8 @@ async def alrtrepo_users(m: Message):
users_data = DB.user.get_all()
user_message = ""
for user in users_data:
user_message += f"{user.user_id} | {user.maintainer.nickname} | {user.default_branch}\n"
maintainer = user.maintainer.nickname if user.maintainer else "-"
user_message += f"{user.user_id} | {maintainer} | {user.default_branch}\n"
await m.answer(user_message)
......@@ -43,10 +44,11 @@ async def alrtrepo_users(m: Message, user_id: int):
user = (await tg_api.get_chat(chat_id=user_id)).unwrap()
username = user.username.unwrap_or_none()
username = f"(@{username})\n" if username else "\n"
maintainer = db_user.maintainer.nickname if db_user.maintainer else "-"
await m.answer(
f"{user.first_name.unwrap_or("")} {user.last_name.unwrap_or("")} {username}"
f" Сопровождающий: {db_user.maintainer.nickname}\n"
f" Сопровождающий: {maintainer}\n"
f" Репозиторий: {db_user.default_branch}\n"
f" Роли: {", ".join(user_roles) or "пользователь"}\n"
)
......@@ -70,7 +72,12 @@ async def alrtrepo_users(
if not branch:
branch = "sisyphus"
if DB.user.add(user_id, maintainer, branch):
maintainer_obj = DB.maintainer.get(maintainer)
if not maintainer_obj:
await m.answer("Сопровождающий не найден")
return
if DB.user.add(user_id, maintainer_obj, branch):
await m.answer("Пользователь добавлен")
......
......@@ -24,9 +24,12 @@ async def ftbfs_handler(m: Message, user: User | None, _maintainer: str | None =
return
else:
if user:
if user and user.maintainer:
maintainer = user.maintainer.nickname
else:
await m.answer(
"Команда требует сопровождающего. Укажите ник в команде или выберите сопровождающего в профиле."
)
return
ftbfs_data = await altrepo.parser.packages.ftbfs()
......
......@@ -78,7 +78,9 @@ async def help_handler(m: Message) -> None:
await m.answer(
f"{_bold("Основные:")}\n"
" /profile — профиль сопровождающего\n"
" /profile — профиль\n"
" /login — выбрать сопровождающего\n"
" /logout — удалить сопровождающего\n"
" /help — помощь\n"
" /altrepo_info — информация о боте\n\n"
......@@ -103,4 +105,3 @@ async def help_handler(m: Message) -> None:
" /news_range дата_от дата_до — новости за период\n"
" /pulse [дней] — пульс Сизифа (по умолчанию 7)\n"
)
......@@ -25,10 +25,20 @@ DESCRIPTIONS = {
"task_events": "Изменения статуса тасков текущего сопровождающего.",
}
MAINTAINER_MAILINGS = {"watch", "bugs", "task_events"}
async def _drop_pending_input(user_id: int) -> None:
try:
await wm.drop(MESSAGE_FROM_USER, user_id)
except LookupError:
pass
def _mailing_text_and_markup(user):
schedule = DB.schedule.get(user)
enabled = DB.mailing.get_enabled(user)
has_maintainer = user.maintainer is not None
text = _bold("Рассылки:\n\n")
......@@ -39,19 +49,28 @@ def _mailing_text_and_markup(user):
else:
text += "Расписание: не настроено\n\n"
for type_id, label in profile_keyboards.SCHEDULED_TYPES:
scheduled_types = profile_keyboards.SCHEDULED_TYPES if has_maintainer else [
item for item in profile_keyboards.SCHEDULED_TYPES if item[0] not in MAINTAINER_MAILINGS
]
instant_types = profile_keyboards.INSTANT_TYPES if has_maintainer else []
for type_id, label in scheduled_types:
status = "вкл" if type_id in enabled else "выкл"
text += _bold(f"{label}: {status}\n")
text += f"{DESCRIPTIONS[type_id]}\n\n"
text += _bold("Уведомления:\n\n")
if instant_types:
text += _bold("Уведомления:\n\n")
for type_id, label in profile_keyboards.INSTANT_TYPES:
for type_id, label in instant_types:
status = "вкл" if type_id in enabled else "выкл"
text += _bold(f"{label}: {status}\n")
text += f"{DESCRIPTIONS[type_id]}\n\n"
markup = profile_keyboards.mailing_settings_kb(schedule, enabled)
if not has_maintainer:
text += "Рассылки по сопровождающему появятся после выбора сопровождающего в профиле.\n"
markup = profile_keyboards.mailing_settings_kb(schedule, enabled, has_maintainer)
return text, markup
......@@ -61,6 +80,17 @@ async def profile_handler(m: Message, user: User | None) -> None:
if user is None:
return
if not user.maintainer:
roles = DB.user.get_roles(m.from_user.id)
await m.answer(
f"{_bold("Профиль:\n\n")}"
"Сопровождающий: не выбран\n"
f"Репозиторий: {user.default_branch}\n\n"
f"Роли в боте: {", ".join(roles) or "пользователь"}",
reply_markup=profile_keyboards.profile_kb
)
return
nickname = user.maintainer.nickname
maintainer_data, watch, bugs_data, branches_data, ftbfs_data = await asyncio.gather(
......@@ -104,9 +134,13 @@ async def profile_handler(m: Message, user: User | None) -> None:
@dp.callback_query(PayloadEqRule("profile/settings"))
async def settings_handler(cb: CallbackQuery) -> None:
await _drop_pending_input(cb.from_user.id)
user = DB.user.get(cb.from_user.id)
await cb.edit_text(
"Настройки",
reply_markup=profile_keyboards.profile_settings_kb
reply_markup=profile_keyboards.profile_settings_kb(
user is not None and user.maintainer is not None
)
)
await cb.answer()
......@@ -117,37 +151,93 @@ async def close_handler(cb: CallbackQuery) -> None:
await cb.answer()
@dp.callback_query(PayloadEqRule("profile/settings/maintainer"))
async def maintainer_handler(cb: CallbackQuery) -> None:
async def _login(chat_id: int, user_id: int, ctx_api, edit_cb: CallbackQuery | None = None) -> None:
cancel = cancel_kb("profile/settings")
await cb.edit_text("Введите никнейм сопровождающего:", reply_markup=cancel)
if edit_cb:
await edit_cb.edit_text("Введите никнейм сопровождающего:", reply_markup=cancel)
else:
await ctx_api.send_message(
chat_id=chat_id,
text="Введите никнейм сопровождающего:",
reply_markup=cancel,
)
try:
while True:
msg, _ = await wm.wait(MESSAGE_FROM_USER, cb.from_user.id, release=HasText())
msg, _ = await wm.wait(MESSAGE_FROM_USER, user_id, release=HasText())
maintainer = msg.text.unwrap().lower()
await msg.delete()
maintainer_obj = DB.maintainer.get(maintainer)
if maintainer_obj:
DB.user.change_maintainer(cb.from_user.id, maintainer_obj)
await cb.edit_text(
f"Сопровождающий: {maintainer}",
reply_markup=profile_keyboards.profile_settings_kb
)
DB.user.change_maintainer(user_id, maintainer_obj)
text = f"Сопровождающий: {maintainer}"
markup = profile_keyboards.profile_settings_kb(True)
if edit_cb:
await edit_cb.edit_text(text, reply_markup=markup)
else:
await ctx_api.send_message(chat_id=chat_id, text=text, reply_markup=markup)
break
else:
await cb.edit_text(
text = (
f"Сопровождающий {maintainer} не найден.\n"
"Введите никнейм сопровождающего:",
reply_markup=cancel,
"Введите никнейм сопровождающего:"
)
if edit_cb:
await edit_cb.edit_text(text, reply_markup=cancel)
else:
await ctx_api.send_message(chat_id=chat_id, text=text, reply_markup=cancel)
except (asyncio.CancelledError, LookupError):
return
@dp.callback_query(PayloadEqRule("profile/settings/maintainer"))
async def maintainer_handler(cb: CallbackQuery) -> None:
await _login(cb.chat_id.unwrap(), cb.from_user.id, cb.ctx_api, cb)
await cb.answer()
@dp.callback_query(PayloadEqRule("profile/settings/logout"))
async def logout_callback_handler(cb: CallbackQuery) -> None:
user = DB.user.get(cb.from_user.id)
if not user:
return
DB.user.change_maintainer(cb.from_user.id, None)
for mailing_type in MAINTAINER_MAILINGS:
await scheduler.disable_mailing(user, mailing_type)
await cb.edit_text(
"Сопровождающий удалён.",
reply_markup=profile_keyboards.profile_settings_kb(False),
)
await cb.answer()
@dp.message(Command("login"), IsPrivate())
async def login_handler(m: Message, user: User | None) -> None:
if user is None:
await m.answer("Сначала запустите /start.")
return
await _login(m.chat_id, m.from_user.id, m.ctx_api)
@dp.message(Command("logout"), IsPrivate())
async def logout_handler(m: Message, user: User | None) -> None:
if user is None:
await m.answer("Сначала запустите /start.")
return
if not user.maintainer:
await m.answer("Сопровождающий не выбран.")
return
DB.user.change_maintainer(m.from_user.id, None)
for mailing_type in MAINTAINER_MAILINGS:
await scheduler.disable_mailing(user, mailing_type)
await m.answer("Сопровождающий удалён.")
@dp.callback_query(PayloadEqRule("profile/settings/branch"))
async def branch_handler(cb: CallbackQuery) -> None:
await cb.edit_text(
......@@ -159,9 +249,12 @@ async def branch_handler(cb: CallbackQuery) -> None:
@dp.callback_query(PayloadMarkupRule("profile/settings/branch/<branch>"))
async def branch_select_handler(cb: CallbackQuery, branch: str) -> None:
DB.user.change_default_branch(cb.from_user.id, branch)
user = DB.user.get(cb.from_user.id)
await cb.edit_text(
f"Репозиторий: {branch}",
reply_markup=profile_keyboards.profile_settings_kb
reply_markup=profile_keyboards.profile_settings_kb(
user is not None and user.maintainer is not None
)
)
await cb.answer()
......@@ -170,6 +263,7 @@ async def branch_select_handler(cb: CallbackQuery, branch: str) -> None:
@dp.callback_query(PayloadEqRule("profile/mailing"))
async def mailing_handler(cb: CallbackQuery) -> None:
await _drop_pending_input(cb.from_user.id)
user = DB.user.get(cb.from_user.id)
if not user:
return
......@@ -249,6 +343,10 @@ async def mailing_toggle_handler(cb: CallbackQuery, mailing_type: str) -> None:
if not user:
return
if mailing_type in MAINTAINER_MAILINGS and not user.maintainer:
await cb.answer("Сначала выберите сопровождающего.", show_alert=True)
return
if DB.mailing.is_enabled(user, mailing_type):
await scheduler.disable_mailing(user, mailing_type)
else:
......@@ -263,6 +361,7 @@ async def mailing_toggle_handler(cb: CallbackQuery, mailing_type: str) -> None:
@dp.callback_query(PayloadEqRule("profile/settings/packages"))
async def user_packages_handler(cb: CallbackQuery) -> None:
await _drop_pending_input(cb.from_user.id)
uid = cb.from_user.id
packages = DB.watch_list.get_list(uid)
......
......@@ -20,11 +20,16 @@ async def start_handler(m: Message, user: User | None) -> None:
if user is None:
await m.answer(
"Введите никнейм сопровождающего:"
"Введите никнейм сопровождающего или пропустите этот шаг:",
reply_markup=start_keyboards.maintainer_kb(),
)
maintainer = None
while True:
msg, _ = await wm.wait(MESSAGE_FROM_USER, m.from_user.id, release=HasText())
_maintainer = msg.text.unwrap().lower()
if _maintainer in ["пропустить", "skip", "/skip"]:
await m.answer("Сопровождающий не выбран.")
break
maintainer = DB.maintainer.get(_maintainer)
if maintainer:
await m.answer(
......@@ -34,7 +39,8 @@ async def start_handler(m: Message, user: User | None) -> None:
else:
await m.answer(
f"Сопровождающий {_maintainer} не найден.\n"
"Введите никнейм сопровождающего:"
"Введите никнейм сопровождающего или пропустите этот шаг:",
reply_markup=start_keyboards.maintainer_kb(),
)
await m.answer(
"Выберите репозиторий по умолчанию.",
......
......@@ -45,10 +45,12 @@ async def tasks_handler(m: Message, user: User | None, maintainer: str | None =
maintainer_obj = DB.maintainer.get(maintainer.lower())
if not maintainer_obj:
return await m.answer("Сопровождающий не найден.")
elif user:
elif user and user.maintainer:
maintainer_obj = user.maintainer
else:
return
return await m.answer(
"Команда требует сопровождающего. Укажите ник в команде или выберите сопровождающего в профиле."
)
try:
tasks_data = await altrepo.api.task.progress.find_tasks(
......
......@@ -55,6 +55,44 @@ def _migrate_scheduled_tasks():
logger.info("Migration complete, old table dropped")
def _migrate_nullable_user_maintainer():
if not db.table_exists("users"):
return
columns = db.execute_sql("PRAGMA table_info(users)").fetchall()
maintainer_column = next((col for col in columns if col[1] == "maintainer_id"), None)
if maintainer_column is None or not maintainer_column[3]:
return
logger.info("Migrating users.maintainer_id to nullable")
db.execute_sql("PRAGMA foreign_keys=off")
try:
db.execute_sql(
"""
CREATE TABLE users_new (
id INTEGER NOT NULL PRIMARY KEY,
user_id INTEGER NOT NULL,
maintainer_id TEXT,
default_branch TEXT NOT NULL,
roles TEXT NOT NULL,
FOREIGN KEY (maintainer_id) REFERENCES maintainers (nickname)
)
"""
)
db.execute_sql(
"""
INSERT INTO users_new (id, user_id, maintainer_id, default_branch, roles)
SELECT id, user_id, maintainer_id, default_branch, roles FROM users
"""
)
db.execute_sql("DROP TABLE users")
db.execute_sql("ALTER TABLE users_new RENAME TO users")
db.execute_sql("CREATE INDEX user_maintainer_id ON users (maintainer_id)")
finally:
db.execute_sql("PRAGMA foreign_keys=on")
logger.info("Migration complete, users.maintainer_id is nullable")
@bot.loop_wrapper.lifespan.on_startup
async def startup():
db.create_tables(
......@@ -69,6 +107,7 @@ async def startup():
]
)
_migrate_nullable_user_maintainer()
_migrate_scheduled_tasks()
logger.info("initializing ALTRepo")
......@@ -89,6 +128,8 @@ async def startup():
BotCommand("pkg_watch", "Обновления отслеживаемых пакетов"),
BotCommand("ftbfs", "Ошибки пересборки"),
BotCommand("statistics", "Статистика репозитория"),
BotCommand("login", "Выбрать сопровождающего"),
BotCommand("logout", "Удалить сопровождающего"),
BotCommand("altrepo_info", "Информация о боте"),
BotCommand("help", "Справка"),
]
......
......@@ -17,6 +17,8 @@ MAILING_HANDLERS = {
),
}
MAINTAINER_MAILINGS = {"watch", "bugs"}
class CustomScheduler:
def __init__(self):
......@@ -116,6 +118,8 @@ class CustomScheduler:
if not user:
return
for mailing_type in DB.mailing.get_enabled(user):
if mailing_type in MAINTAINER_MAILINGS and not user.maintainer:
continue
handler = MAILING_HANDLERS.get(mailing_type)
if handler:
await handler(user)
......
......@@ -15,7 +15,10 @@ async def bugs(
maintainer: str | None = None
) -> None:
chat_id = chat_id or user.user_id
if chat_id is None:
if user is None:
return
chat_id = user.user_id
resolved = await resolve_maintainer(maintainer, user, chat_id)
if not resolved:
......
......@@ -18,11 +18,15 @@ async def resolve_maintainer(
if maintainer_arg:
maintainer = DB.maintainer.get(maintainer_arg.lower())
if not maintainer:
await tg_api.send_message(chat_id, "Сопровождающий не найден.")
await tg_api.send_message(chat_id=chat_id, text="Сопровождающий не найден.")
return None
return maintainer
elif user:
elif user and user.maintainer:
return user.maintainer
await tg_api.send_message(
chat_id=chat_id,
text="Команда требует сопровождающего. Укажите ник в команде или выберите сопровождающего в профиле.",
)
return None
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment