From 7276e8339165a3d3dc7122f9a9ee36add944423a Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 02:20:38 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #124 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/linksplatform/Bot/issues/124 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..3ae25169 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/linksplatform/Bot/issues/124 +Your prepared branch: issue-124-3a75627d +Your prepared working directory: /tmp/gh-issue-solver-1757719232962 + +Proceed. \ No newline at end of file From 0571d996de8cca9e5bc39c2a0da03581c2e2ed96 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 02:31:08 +0300 Subject: [PATCH 2/3] Add callback buttons for top command pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement VkKeyboard utility class for creating VK inline keyboards - Add TopPaginationKeyboard specialized class for top command pagination - Add message_event handler to process callback button events - Modify top command to support pagination with callback buttons - Add send_msg_with_keyboard and send_callback_answer methods - Maintain backward compatibility for top command with number parameter - Support pagination for top, bottom, and people commands - Display 10 users per page with Previous/Next navigation buttons 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- python/__main__.py | 62 ++++++++- python/modules/__init__.py | 1 + python/modules/commands.py | 139 ++++++++++++++++++-- python/modules/keyboard_utils.py | 209 +++++++++++++++++++++++++++++++ 4 files changed, 398 insertions(+), 13 deletions(-) create mode 100644 python/modules/keyboard_utils.py diff --git a/python/__main__.py b/python/__main__.py index cdcbf7f6..5aa25c56 100644 --- a/python/__main__.py +++ b/python/__main__.py @@ -8,7 +8,7 @@ import requests from modules import ( - BetterBotBaseDataService, Commands + BetterBotBaseDataService, Commands, VkKeyboard, TopPaginationKeyboard ) from tokens import BOT_TOKEN from userbot import UserBot @@ -112,6 +112,26 @@ def message_new( except Exception as e: print(e) + def message_event( + self, + event: Dict[str, Any] + ) -> NoReturn: + """Handling callback button events. + """ + event_data = event["object"] + peer_id = event_data["peer_id"] + from_id = event_data["user_id"] + payload = event_data.get("payload", {}) + event_id = event_data["event_id"] + + # Get user for callback handling + user = self.data.get_user(from_id, self) if from_id > 0 else None + + try: + self.commands.process_callback( + payload, peer_id, from_id, event_id, user) + except Exception as e: + print(f"Error handling callback: {e}") def delete_message( self, @@ -168,6 +188,46 @@ def send_msg( message=msg, peer_id=peer_id, disable_mentions=1, random_id=0)) + def send_msg_with_keyboard( + self, + msg: str, + peer_id: int, + keyboard: str + ) -> NoReturn: + """Sends message with keyboard to chat with {peer_id}. + + :param msg: message text + :param peer_id: chat ID + :param keyboard: JSON keyboard string + """ + self.call_method( + 'messages.send', + dict( + message=msg, peer_id=peer_id, + keyboard=keyboard, + disable_mentions=1, random_id=0)) + + def send_callback_answer( + self, + event_id: str, + peer_id: int, + event_data: Dict[str, Any] = None + ) -> NoReturn: + """Send answer to callback query. + + :param event_id: Event ID from callback + :param peer_id: chat ID + :param event_data: Optional event data (for snackbar, etc.) + """ + params = { + "event_id": event_id, + "peer_id": peer_id + } + if event_data: + params["event_data"] = event_data + + self.call_method('messages.sendMessageEventAnswer', params) + def get_user_name( self, uid: int, diff --git a/python/modules/__init__.py b/python/modules/__init__.py index 6f0b661e..fd1b01e1 100644 --- a/python/modules/__init__.py +++ b/python/modules/__init__.py @@ -4,6 +4,7 @@ from .data_service import BetterBotBaseDataService from .data_builder import DataBuilder from .vk_instance import VkInstance +from .keyboard_utils import VkKeyboard, TopPaginationKeyboard from .utils import ( get_default_programming_language, contains_string, diff --git a/python/modules/commands.py b/python/modules/commands.py index 93d99817..6d976177 100644 --- a/python/modules/commands.py +++ b/python/modules/commands.py @@ -3,6 +3,7 @@ from datetime import datetime from time import time import os +import json from regex import Pattern, Match, split, match, search, IGNORECASE, sub from requests import post @@ -13,6 +14,7 @@ from .commands_builder import CommandsBuilder from .data_service import BetterBotBaseDataService from .data_builder import DataBuilder +from .keyboard_utils import TopPaginationKeyboard from .utils import ( get_default_programming_language, contains_all_strings, @@ -124,22 +126,28 @@ def top( self, reverse: bool = False ) -> NoReturn: - """Sends users top.""" + """Sends users top with pagination.""" if self.peer_id < 2e9: return maximum_users = self.matched.group("maximum_users") - maximum_users = int(maximum_users) if maximum_users else -1 - users = DataBuilder.get_users_sorted_by_karma( - self.vk_instance, self.data_service, self.peer_id) - users = [i for i in users if - (i["karma"] != 0) or - ("programming_languages" in i and len(i["programming_languages"]) > 0) - ] - self.vk_instance.send_msg( - CommandsBuilder.build_top_users( + + # If specific maximum_users is requested, use old behavior without pagination + if maximum_users: + maximum_users = int(maximum_users) + users = DataBuilder.get_users_sorted_by_karma( + self.vk_instance, self.data_service, self.peer_id) + users = [i for i in users if + (i["karma"] != 0) or + ("programming_languages" in i and len(i["programming_languages"]) > 0) + ] + message = CommandsBuilder.build_top_users( users, self.data_service, reverse, - self.karma_enabled, maximum_users), - self.peer_id) + self.karma_enabled, maximum_users) + if message: + self.vk_instance.send_msg(message, self.peer_id) + else: + # Use new paginated version + self.top_paginated(reverse=reverse, page=0) def top_langs( self, @@ -436,3 +444,110 @@ def process( if self.matched: action() return + + def process_callback( + self, + payload: Dict[str, Any], + peer_id: int, + from_id: int, + event_id: str, + user: BetterUser + ) -> NoReturn: + """Process callback button events + + :param payload: Callback payload data + :param peer_id: chat ID + :param from_id: user ID + :param event_id: callback event ID + :param user: user object + """ + self.peer_id = peer_id + self.from_id = from_id + self.karma_enabled = peer_id in config.CHATS_KARMA_WHITELIST + self.current_user = user + + if from_id < 0: + return + + try: + # Parse payload if it's a string + if isinstance(payload, str): + payload = json.loads(payload) + + action = payload.get("action") + + if action == "paginate": + self.handle_top_pagination(payload, event_id) + elif action == "page_info": + # Just acknowledge the page info button click + self.vk_instance.send_callback_answer(event_id, peer_id) + + except Exception as e: + print(f"Error processing callback: {e}") + # Send empty callback answer to acknowledge the button press + self.vk_instance.send_callback_answer(event_id, peer_id) + + def handle_top_pagination( + self, + payload: Dict[str, Any], + event_id: str + ) -> NoReturn: + """Handle pagination for top command""" + command = payload.get("command", "top") + page = payload.get("page", 0) + reverse = payload.get("reverse", False) + + # Execute the appropriate top command with pagination + if command == "top": + self.top_paginated(reverse=reverse, page=page) + elif command == "bottom": + self.top_paginated(reverse=True, page=page) + elif command == "people": + self.top_paginated(reverse=reverse, page=page) + + # Acknowledge the callback + self.vk_instance.send_callback_answer(event_id, self.peer_id) + + def top_paginated( + self, + reverse: bool = False, + page: int = 0 + ) -> NoReturn: + """Sends paginated users top with callback buttons.""" + if self.peer_id < 2e9: + return + + users = DataBuilder.get_users_sorted_by_karma( + self.vk_instance, self.data_service, self.peer_id) + users = [i for i in users if + (i["karma"] != 0) or + ("programming_languages" in i and len(i["programming_languages"]) > 0) + ] + + if reverse: + users = list(reversed(users)) + + # Calculate pagination + pagination_info = TopPaginationKeyboard.calculate_pagination(len(users)) + total_pages = pagination_info["total_pages"] + + # Ensure page is within valid range + page = max(0, min(page, total_pages - 1)) + + # Get users for current page + page_users = TopPaginationKeyboard.get_page_users(users, page) + + # Build message + command_type = "bottom" if reverse else "top" + message = CommandsBuilder.build_top_users( + page_users, self.data_service, False, + self.karma_enabled, -1) + + if message: + # Create pagination keyboard + keyboard = TopPaginationKeyboard.create_pagination_keyboard( + page, total_pages, command_type, reverse) + + self.vk_instance.send_msg_with_keyboard(message, self.peer_id, keyboard) + else: + self.vk_instance.send_msg("Пользователи не найдены.", self.peer_id) diff --git a/python/modules/keyboard_utils.py b/python/modules/keyboard_utils.py new file mode 100644 index 00000000..98e26435 --- /dev/null +++ b/python/modules/keyboard_utils.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +"""Utility module for creating VK keyboards and handling callback buttons.""" +import json +from typing import Dict, Any, Optional, List + + +class VkKeyboard: + """VK Keyboard builder for creating inline keyboards with callback buttons.""" + + def __init__(self, inline: bool = True, one_time: bool = False): + """Initialize keyboard builder. + + Args: + inline: Whether to create inline keyboard (displayed inside message) + one_time: Whether keyboard disappears after button press + """ + self.inline = inline + self.one_time = one_time + self.buttons: List[List[Dict[str, Any]]] = [] + self.current_row: List[Dict[str, Any]] = [] + + def add_callback_button(self, + label: str, + payload: Dict[str, Any], + color: str = "secondary") -> "VkKeyboard": + """Add callback button to current row. + + Args: + label: Text displayed on button + payload: Data to send with callback + color: Button color (primary, secondary, negative, positive) + + Returns: + Self for method chaining + """ + button = { + "action": { + "type": "callback", + "label": label, + "payload": json.dumps(payload) + }, + "color": color + } + self.current_row.append(button) + return self + + def add_text_button(self, + label: str, + payload: Optional[Dict[str, Any]] = None, + color: str = "secondary") -> "VkKeyboard": + """Add text button to current row. + + Args: + label: Text displayed on button and sent as message + payload: Optional data to include + color: Button color + + Returns: + Self for method chaining + """ + button = { + "action": { + "type": "text", + "label": label + }, + "color": color + } + if payload: + button["action"]["payload"] = json.dumps(payload) + self.current_row.append(button) + return self + + def row(self) -> "VkKeyboard": + """Finish current row and start new one. + + Returns: + Self for method chaining + """ + if self.current_row: + self.buttons.append(self.current_row) + self.current_row = [] + return self + + def get_keyboard(self) -> str: + """Get keyboard as JSON string for VK API. + + Returns: + JSON string representation of keyboard + """ + # Add current row if it has buttons + if self.current_row: + self.buttons.append(self.current_row) + self.current_row = [] + + keyboard = { + "inline": self.inline, + "one_time": self.one_time, + "buttons": self.buttons + } + return json.dumps(keyboard) + + @staticmethod + def get_empty_keyboard() -> str: + """Get empty keyboard to hide existing keyboard. + + Returns: + JSON string for empty keyboard + """ + return json.dumps({"buttons": []}) + + +class TopPaginationKeyboard: + """Specialized keyboard for top command pagination.""" + + USERS_PER_PAGE = 10 + + @staticmethod + def create_pagination_keyboard(current_page: int, + total_pages: int, + command_type: str = "top", + reverse: bool = False) -> str: + """Create pagination keyboard for top command. + + Args: + current_page: Current page number (0-indexed) + total_pages: Total number of pages + command_type: Type of command (top, bottom, people) + reverse: Whether results are reversed + + Returns: + JSON keyboard string + """ + keyboard = VkKeyboard(inline=True) + + # Add navigation buttons + if current_page > 0: + keyboard.add_callback_button( + "⬅️ Пред.", + { + "action": "paginate", + "command": command_type, + "page": current_page - 1, + "reverse": reverse + } + ) + + # Page indicator button (non-clickable, shows current page) + keyboard.add_callback_button( + f"{current_page + 1}/{total_pages}", + { + "action": "page_info", + "page": current_page + }, + color="primary" + ) + + if current_page < total_pages - 1: + keyboard.add_callback_button( + "След. ➡️", + { + "action": "paginate", + "command": command_type, + "page": current_page + 1, + "reverse": reverse + } + ) + + return keyboard.get_keyboard() + + @staticmethod + def calculate_pagination(total_users: int, users_per_page: int = None) -> Dict[str, int]: + """Calculate pagination parameters. + + Args: + total_users: Total number of users + users_per_page: Users per page (defaults to USERS_PER_PAGE) + + Returns: + Dictionary with pagination info + """ + if users_per_page is None: + users_per_page = TopPaginationKeyboard.USERS_PER_PAGE + + total_pages = max(1, (total_users + users_per_page - 1) // users_per_page) + + return { + "total_users": total_users, + "users_per_page": users_per_page, + "total_pages": total_pages + } + + @staticmethod + def get_page_users(users: List[Any], page: int, users_per_page: int = None) -> List[Any]: + """Get users for specific page. + + Args: + users: List of all users + page: Page number (0-indexed) + users_per_page: Users per page + + Returns: + List of users for the page + """ + if users_per_page is None: + users_per_page = TopPaginationKeyboard.USERS_PER_PAGE + + start_idx = page * users_per_page + end_idx = start_idx + users_per_page + return users[start_idx:end_idx] \ No newline at end of file From 182ebefd737e30260192727ef42a62c465aa39fc Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 02:32:23 +0300 Subject: [PATCH 3/3] Remove CLAUDE.md - Claude command completed --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 3ae25169..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/linksplatform/Bot/issues/124 -Your prepared branch: issue-124-3a75627d -Your prepared working directory: /tmp/gh-issue-solver-1757719232962 - -Proceed. \ No newline at end of file