diff --git a/app/bot.py b/app/bot.py index fe6032e..ce3b7c9 100644 --- a/app/bot.py +++ b/app/bot.py @@ -106,6 +106,16 @@ def parse_cancellation(raw: str) -> int: return transaction_id +def parse_friend_removal(raw: str) -> int: + payload = decode_payload(raw) + if payload.get("type") != "friend_remove" or payload.get("v") != 1: + raise ValueError("Неизвестный формат данных") + friend_user_id = payload.get("friend_user_id") + if not isinstance(friend_user_id, int) or friend_user_id <= 0: + raise ValueError("Некорректный ID друга") + return friend_user_id + + class TelegramBotService: def __init__(self, settings: Settings, database: Database): self.settings = settings @@ -187,6 +197,11 @@ class TelegramBotService: if not self.database.cancel_transaction(user.id, transaction_id): raise ValueError("Запись не найдена или уже отменена") response = "Запись отменена и больше не участвует в расчётах." + elif action == "friend_remove": + friend_user_id = parse_friend_removal(raw) + if not self.database.remove_friendship(user.id, friend_user_id): + raise ValueError("Пользователь не найден в списке друзей") + response = "Пользователь удалён из друзей." else: raise ValueError("Неизвестный формат данных") except ValueError as exc: diff --git a/app/db.py b/app/db.py index feadea1..89dbba5 100644 --- a/app/db.py +++ b/app/db.py @@ -298,6 +298,18 @@ class Database: ).fetchall() return [int(row["friend_id"]) for row in rows] + def remove_friendship(self, first_user_id: int, second_user_id: int) -> bool: + if first_user_id == second_user_id: + return False + low, high = sorted((first_user_id, second_user_id)) + with closing(self.connect()) as connection: + cursor = connection.execute( + "DELETE FROM friendships WHERE user_id_low = ? AND user_id_high = ?", + (low, high), + ) + connection.commit() + return cursor.rowcount > 0 + def set_meta(self, key: str, value: str) -> None: with closing(self.connect()) as connection: connection.execute( diff --git a/static/app.js b/static/app.js index 1fd0c82..f6b2d5d 100644 --- a/static/app.js +++ b/static/app.js @@ -449,13 +449,36 @@ ? `` : `${initial}`; const percent = Number(item.saved_percent); + const canRemove = state.board === "friends" && item.user_id !== userId; return `
${index + 1} ${avatar} - ${escapeHtml(item.nickname)}${item.user_id === userId ? "Это вы" : money.format(item.balance_cents / 100)} - ${percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}% + ${escapeHtml(item.nickname)}${item.user_id === userId ? "Это вы" : ""} + + ${percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}% + ${canRemove ? `` : ""} +
`; }).join(""); + list.querySelectorAll(".friend-remove").forEach((button) => { + button.addEventListener("click", () => removeFriend(Number(button.dataset.friendUserId))); + }); + } + + function removeFriend(friendUserId) { + const send = (confirmed) => { + if (!confirmed) return; + sendTelegramPayload({ v: 1, type: "friend_remove", friend_user_id: friendUserId }); + }; + if (tg?.showConfirm && tg.isVersionAtLeast?.("6.2")) { + try { + tg.showConfirm("Удалить пользователя из друзей?", send); + return; + } catch (error) { + console.warn("Telegram confirmation failed", error); + } + } + send(window.confirm("Удалить пользователя из друзей?")); } async function shareInvite() { diff --git a/static/index.html b/static/index.html index 3dde4b8..1afef0f 100644 --- a/static/index.html +++ b/static/index.html @@ -6,9 +6,9 @@ Пожитки - + - +
diff --git a/static/styles.css b/static/styles.css index 954017a..cc9984f 100644 --- a/static/styles.css +++ b/static/styles.css @@ -200,8 +200,10 @@ h2 { margin: 0; font-size: 23px; letter-spacing: -.025em; } .leader-name { min-width: 0; } .leader-name strong { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; } .leader-name small { color: var(--muted); font-size: 10px; } +.leader-actions { display: grid; justify-items: end; gap: 4px; } .leader-percent { font-size: 15px; font-weight: 850; color: var(--accent); } .leader-percent.negative { color: var(--danger); } +.friend-remove { padding: 1px 0; border: 0; background: transparent; color: var(--danger); font-size: 10px; } .friend-hint { margin: 13px 18px; color: var(--muted); font-size: 11px; line-height: 1.5; text-align: center; } .bottom-nav { position: fixed; z-index: 10; bottom: max(12px, calc(var(--app-safe-bottom) + 8px)); left: 50%; display: flex; align-items: stretch; gap: 9px; width: min(340px, calc(100% - var(--app-safe-left) - var(--app-safe-right) - 24px)); height: 54px; transform: translateX(-50%); } diff --git a/tests/test_core.py b/tests/test_core.py index 45bca75..c8a33b1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -5,6 +5,7 @@ import pytest from app.bot import ( parse_cancellation, parse_category, + parse_friend_removal, parse_transaction, webapp_url_for_user, ) @@ -56,6 +57,13 @@ def test_friendship_is_symmetric_and_idempotent(tmp_path: Path) -> None: assert not database.add_friendship(3, 7) assert database.friend_ids(3) == [7] assert database.friend_ids(7) == [3] + assert database.remove_friendship(3, 7) + assert database.friend_ids(3) == [] + assert database.friend_ids(7) == [] + assert not database.remove_friendship(3, 7) + assert parse_friend_removal( + '{"v":1,"type":"friend_remove","friend_user_id":7}' + ) == 7 def test_filtered_leaderboard(tmp_path: Path) -> None: