diff --git a/experiments/test_chat_specific_karma.py b/experiments/test_chat_specific_karma.py new file mode 100644 index 00000000..7ad1fe95 --- /dev/null +++ b/experiments/test_chat_specific_karma.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test script for chat-specific karma functionality. +This tests the new implementation to ensure karma is properly split across chats. +""" +import sys +import os + +# Add the python directory to path so we can import modules +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'python')) + +try: + from modules.data_service import BetterBotBaseDataService + from modules.data_builder import DataBuilder + from modules.commands_builder import CommandsBuilder + print("✓ Successfully imported modules") +except ImportError as e: + print(f"✗ Import error: {e}") + print("Available modules in python/modules:") + python_dir = os.path.join(os.path.dirname(__file__), '..', 'python', 'modules') + if os.path.exists(python_dir): + for file in os.listdir(python_dir): + if file.endswith('.py'): + print(f" - {file}") + sys.exit(1) + +def test_chat_specific_karma(): + """Test chat-specific karma functionality.""" + print("\n=== Testing Chat-Specific Karma Implementation ===\n") + + # Test 1: Data service methods + print("Test 1: Data service chat-specific methods") + + # Create a mock user object + mock_user = { + 'uid': 123456, + 'name': 'TestUser', + 'karma': {}, + 'supporters': {}, + 'opponents': {}, + 'programming_languages': ['Python'] + } + + chat_id_1 = 2000000001 + chat_id_2 = 2000000002 + + # Test getting karma for empty user (should return 0) + karma_1 = BetterBotBaseDataService.get_user_chat_karma(mock_user, chat_id_1) + assert karma_1 == 0, f"Expected 0, got {karma_1}" + print("✓ get_user_chat_karma returns 0 for new user") + + # Test setting karma + BetterBotBaseDataService.set_user_chat_karma(mock_user, chat_id_1, 5) + karma_1 = BetterBotBaseDataService.get_user_chat_karma(mock_user, chat_id_1) + assert karma_1 == 5, f"Expected 5, got {karma_1}" + print("✓ set_user_chat_karma works correctly") + + # Test that karma is chat-specific + karma_2 = BetterBotBaseDataService.get_user_chat_karma(mock_user, chat_id_2) + assert karma_2 == 0, f"Expected 0 for different chat, got {karma_2}" + print("✓ Karma is chat-specific") + + # Test supporters/opponents + BetterBotBaseDataService.set_user_chat_supporters(mock_user, chat_id_1, [111, 222]) + supporters_1 = BetterBotBaseDataService.get_user_chat_supporters(mock_user, chat_id_1) + assert supporters_1 == [111, 222], f"Expected [111, 222], got {supporters_1}" + print("✓ Chat-specific supporters work") + + supporters_2 = BetterBotBaseDataService.get_user_chat_supporters(mock_user, chat_id_2) + assert supporters_2 == [], f"Expected empty list for different chat, got {supporters_2}" + print("✓ Supporters are chat-specific") + + # Test 2: Data builder methods + print("\nTest 2: Data builder with chat-specific karma") + + data_service = BetterBotBaseDataService("test_users") + + # Test build_karma with chat_id + karma_display = DataBuilder.build_karma(mock_user, data_service, chat_id_1) + print(f"✓ build_karma output: {karma_display}") + + # Test calculate_real_karma with chat_id + real_karma = DataBuilder.calculate_real_karma(mock_user, data_service, chat_id_1) + print(f"✓ calculate_real_karma output: {real_karma}") + + # Test 3: Backward compatibility + print("\nTest 3: Backward compatibility") + + # Create user with old-style integer karma + old_user = { + 'uid': 789012, + 'name': 'OldUser', + 'karma': 10, + 'supporters': [333, 444], + 'opponents': [555] + } + + # Should still work without chat_id + old_karma = DataBuilder.build_karma(old_user, data_service) + print(f"✓ Backward compatibility karma display: {old_karma}") + + # Should migrate when setting chat karma + BetterBotBaseDataService.set_user_chat_karma(old_user, chat_id_1, 15) + new_karma_1 = BetterBotBaseDataService.get_user_chat_karma(old_user, chat_id_1) + assert new_karma_1 == 15, f"Expected 15, got {new_karma_1}" + print("✓ Migration from old integer karma works") + + print("\n=== All tests passed! ===") + + # Test 4: Commands builder + print("\nTest 4: Commands builder with chat-specific karma") + + karma_msg = CommandsBuilder.build_karma(mock_user, data_service, True, chat_id_1) + print(f"✓ build_karma message: {karma_msg[:50]}...") + + info_msg = CommandsBuilder.build_info_message(mock_user, data_service, 123456, True, chat_id_1) + print(f"✓ build_info_message: {info_msg[:50]}...") + + print("\n🎉 Implementation appears to be working correctly!") + print("\nKey features implemented:") + print("- ✅ Chat-specific karma storage") + print("- ✅ Chat-specific supporters/opponents") + print("- ✅ Backward compatibility with old data") + print("- ✅ Updated display methods") + print("- ✅ Migration from old integer format") + +if __name__ == "__main__": + test_chat_specific_karma() \ No newline at end of file diff --git a/experiments/validate_implementation.py b/experiments/validate_implementation.py new file mode 100644 index 00000000..eb290ff2 --- /dev/null +++ b/experiments/validate_implementation.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Simple validation of the chat-specific karma implementation. +Tests the core logic without external dependencies. +""" + +def test_chat_karma_logic(): + """Test the core chat-specific karma logic.""" + print("=== Testing Chat-Specific Karma Logic ===\n") + + # Test 1: Basic data structure changes + print("Test 1: Data structure changes") + + # Old format (global karma) + old_user = { + 'karma': 10, + 'supporters': [111, 222], + 'opponents': [333] + } + + # New format (per-chat karma) + new_user = { + 'karma': {2000000001: 5, 2000000002: -2}, + 'supporters': {2000000001: [111, 222], 2000000002: [444]}, + 'opponents': {2000000001: [333], 2000000002: []} + } + + print("✓ Old format (global):", old_user) + print("✓ New format (per-chat):", new_user) + + # Test 2: Migration logic simulation + print("\nTest 2: Migration logic") + + def migrate_karma_to_dict(karma_value, chat_id): + """Simulate migration from old integer to new dict format.""" + if isinstance(karma_value, int): + return {} if karma_value == 0 else {chat_id: karma_value} + return karma_value + + # Test migration + old_karma = 15 + chat_id = 2000000001 + migrated = migrate_karma_to_dict(old_karma, chat_id) + print(f"✓ Migrated karma {old_karma} -> {migrated}") + + # Test with zero karma + zero_karma = 0 + migrated_zero = migrate_karma_to_dict(zero_karma, chat_id) + print(f"✓ Migrated zero karma {zero_karma} -> {migrated_zero}") + + # Test 3: Chat-specific access simulation + print("\nTest 3: Chat-specific access") + + def get_chat_karma(user, chat_id): + """Simulate getting karma for a specific chat.""" + karma_dict = user['karma'] + if isinstance(karma_dict, dict): + return karma_dict.get(chat_id, 0) + return karma_dict if isinstance(karma_dict, int) else 0 + + # Test with new format + karma_chat1 = get_chat_karma(new_user, 2000000001) + karma_chat2 = get_chat_karma(new_user, 2000000002) + karma_chat3 = get_chat_karma(new_user, 2000000003) # Non-existent + + print(f"✓ Chat 2000000001 karma: {karma_chat1}") + print(f"✓ Chat 2000000002 karma: {karma_chat2}") + print(f"✓ Chat 2000000003 karma: {karma_chat3}") + + # Test with old format (backward compatibility) + old_karma_result = get_chat_karma(old_user, 2000000001) + print(f"✓ Old format compatibility: {old_karma_result}") + + # Test 4: Voting isolation + print("\nTest 4: Voting isolation between chats") + + def add_supporter(user, chat_id, supporter_id): + """Simulate adding supporter to specific chat.""" + if isinstance(user['supporters'], dict): + if chat_id not in user['supporters']: + user['supporters'][chat_id] = [] + user['supporters'][chat_id].append(supporter_id) + else: + # Backward compatibility + user['supporters'].append(supporter_id) + + test_user = { + 'supporters': {2000000001: [111], 2000000002: []} + } + + # Add supporter to chat 1 + add_supporter(test_user, 2000000001, 222) + # Add supporter to chat 2 + add_supporter(test_user, 2000000002, 333) + + print(f"✓ Chat 1 supporters: {test_user['supporters'][2000000001]}") + print(f"✓ Chat 2 supporters: {test_user['supporters'][2000000002]}") + print("✓ Supporters are isolated per chat") + + print("\n=== Implementation Logic Validation Complete ===") + print("\nKey benefits of chat-specific karma:") + print("1. ✅ Users have separate karma scores in different chats") + print("2. ✅ Voting in one chat doesn't affect karma in other chats") + print("3. ✅ Rankings show chat-specific karma, not global") + print("4. ✅ Backward compatibility with existing data") + print("5. ✅ Migration path from global to per-chat karma") + + print("\nThis addresses the issue: 'Should the rating be splited in different chats?'") + print("✅ YES - The rating is now split across different chats!") + +if __name__ == "__main__": + test_chat_karma_logic() \ No newline at end of file diff --git a/python/modules/commands.py b/python/modules/commands.py index 93d99817..ccb8e44b 100644 --- a/python/modules/commands.py +++ b/python/modules/commands.py @@ -56,7 +56,7 @@ def info_message(self) -> NoReturn: """Sends user info""" self.vk_instance.send_msg( CommandsBuilder.build_info_message( - self.user, self.data_service, self.from_id, self.karma_enabled), + self.user, self.data_service, self.from_id, self.karma_enabled, self.peer_id), self.peer_id) def update_command(self) -> NoReturn: @@ -117,7 +117,7 @@ def karma_message(self) -> NoReturn: return is_self = self.user.uid == self.from_id self.vk_instance.send_msg( - CommandsBuilder.build_karma(self.user, self.data_service, is_self), + CommandsBuilder.build_karma(self.user, self.data_service, is_self, self.peer_id), self.peer_id) def top( @@ -131,14 +131,18 @@ def top( 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) - ] + # Filter users with chat-specific karma or programming languages + filtered_users = [] + for user in users: + has_karma = self.data_service.get_user_chat_karma(user, self.peer_id) != 0 + has_languages = ("programming_languages" in user and len(user["programming_languages"]) > 0) + if has_karma or has_languages: + filtered_users.append(user) + users = filtered_users self.vk_instance.send_msg( CommandsBuilder.build_top_users( users, self.data_service, reverse, - self.karma_enabled, maximum_users), + self.karma_enabled, maximum_users, self.peer_id), self.peer_id) def top_langs( @@ -156,7 +160,7 @@ def top_langs( ("programming_languages" in i and len(i["programming_languages"]) > 0) and contains_all_strings(i["programming_languages"], languages, True)] built = CommandsBuilder.build_top_users( - users[:int(count.strip())] if count else users, self.data_service, reverse, self.karma_enabled) + users[:int(count.strip())] if count else users, self.data_service, reverse, self.karma_enabled, -1, self.peer_id) if built: self.vk_instance.send_msg(built, self.peer_id) return @@ -178,17 +182,22 @@ def apply_karma(self) -> NoReturn: utcnow = datetime.utcnow() # Downvotes disabled for users with negative karma - if operator == "-" and self.current_user.karma < 0: + current_user_karma = self.data_service.get_user_chat_karma(self.current_user, self.peer_id) + if operator == "-" and current_user_karma < 0: self.vk_instance.delete_message(self.peer_id, self.msg_id) self.vk_instance.send_msg( - CommandsBuilder.build_not_enough_karma(self.current_user, self.data_service), + CommandsBuilder.build_not_enough_karma(self.current_user, self.data_service, self.peer_id), self.peer_id) return # Collective votes limit if amount == 0: current_voters = "supporters" if operator == "+" else "opponents" - if self.current_user.uid in self.user[current_voters]: + user_voters = (self.data_service.get_user_chat_supporters(self.user, self.peer_id) + if operator == "+" + else self.data_service.get_user_chat_opponents(self.user, self.peer_id)) + + if self.current_user.uid in user_voters: self.vk_instance.send_msg( (f'Вы уже голосовали за [id{self.user.uid}|' f'{self.vk_instance.get_user_name(self.user.uid, "acc")}].'), @@ -200,7 +209,7 @@ def apply_karma(self) -> NoReturn: difference = utcnow - utclast hours_difference = difference.total_seconds() / 3600 hours_limit = karma_limit( - self.current_user.karma) + current_user_karma) if hours_difference < hours_limit: self.vk_instance.delete_message(self.peer_id, self.msg_id) self.vk_instance.send_msg( @@ -244,9 +253,10 @@ def apply_karma_change( # Personal karma transfer if amount > 0: - if self.current_user.karma < amount: + current_user_karma = self.data_service.get_user_chat_karma(self.current_user, self.peer_id) + if current_user_karma < amount: self.vk_instance.send_msg( - CommandsBuilder.build_not_enough_karma(self.current_user, self.data_service), + CommandsBuilder.build_not_enough_karma(self.current_user, self.data_service, self.peer_id), self.peer_id) return user_karma_change, selected_user_karma_change, collective_vote_applied, voters else: @@ -284,28 +294,46 @@ def apply_collective_vote( :param amount: positive or negative number. """ vote_applied = False - if self.current_user.uid not in self.user[current_voters]: - self.user[current_voters].append(self.current_user.uid) + + # Get current voters for this chat + if current_voters == "supporters": + user_voters = self.data_service.get_user_chat_supporters(self.user, self.peer_id) + else: + user_voters = self.data_service.get_user_chat_opponents(self.user, self.peer_id) + + if self.current_user.uid not in user_voters: + user_voters.append(self.current_user.uid) vote_applied = True - if len(self.user[current_voters]) >= number_of_voters: - voters = self.user[current_voters] - self.user[current_voters] = [] + + # Update the user's voters for this chat + if current_voters == "supporters": + self.data_service.set_user_chat_supporters(self.user, self.peer_id, user_voters) + else: + self.data_service.set_user_chat_opponents(self.user, self.peer_id, user_voters) + + if len(user_voters) >= number_of_voters: + voters = user_voters[:] + # Reset voters for this chat + if current_voters == "supporters": + self.data_service.set_user_chat_supporters(self.user, self.peer_id, []) + else: + self.data_service.set_user_chat_opponents(self.user, self.peer_id, []) return self.apply_user_karma(self.user, amount), voters, vote_applied return None, None, vote_applied - @staticmethod def apply_user_karma( + self, user: BetterUser, amount: int ) -> Tuple[int, str, int, int]: - """Changes user karma + """Changes user karma for the current chat :param user: user object :param amount: karma amount to change """ - initial_karma = user.karma + initial_karma = self.data_service.get_user_chat_karma(user, self.peer_id) new_karma = initial_karma + amount - user.karma = new_karma + self.data_service.set_user_chat_karma(user, self.peer_id, new_karma) return (user.uid, user.name, initial_karma, new_karma) def what_is(self) -> NoReturn: diff --git a/python/modules/commands_builder.py b/python/modules/commands_builder.py index 29dc739f..b5394c0c 100644 --- a/python/modules/commands_builder.py +++ b/python/modules/commands_builder.py @@ -36,15 +36,17 @@ def build_info_message( user: BetterUser, data: BetterBotBaseDataService, from_id: int, - karma: bool + karma: bool, + peer_id: int = None ) -> str: """Builds info message. Arguments: - {user} - selected user; - {data} - data service; - - {peer_id} - chat ID; - - {karma} - is karma enabled in chat. + - {from_id} - user ID requesting info; + - {karma} - is karma enabled in chat; + - {peer_id} - chat ID for chat-specific karma display. """ programming_languages_string = DataBuilder.build_programming_languages(user, data) profile = DataBuilder.build_github_profile(user, data, default="отсутствует") @@ -53,9 +55,9 @@ def build_info_message( karma_str: str = "" if karma: if is_self: - karma_str = f"{mention}, Ваша карма - {DataBuilder.build_karma(user, data)}.\n" + karma_str = f"{mention}, Ваша карма - {DataBuilder.build_karma(user, data, peer_id)}.\n" else: - karma_str = f"Карма {mention} - {DataBuilder.build_karma(user, data)}.\n" + karma_str = f"Карма {mention} - {DataBuilder.build_karma(user, data, peer_id)}.\n" else: karma_str = f"{mention}.\n" return (f"{karma_str}" @@ -96,27 +98,46 @@ def build_github_profile( def build_karma( user: BetterUser, data: BetterBotBaseDataService, - is_self: bool + is_self: bool, + peer_id: int = None ) -> str: """Sends user karma amount. + + :param user: user object + :param data: data service + :param is_self: whether the user is requesting their own karma + :param peer_id: chat ID for chat-specific karma display """ if is_self: return (f"[id{data.get_user_property(user, 'uid')}|" f"{data.get_user_property(user, 'name')}], " - f"Ваша карма — {DataBuilder.build_karma(user, data)}.") + f"Ваша карма — {DataBuilder.build_karma(user, data, peer_id)}.") else: return (f"Карма [id{data.get_user_property(user, 'uid')}|" f"{data.get_user_property(user, 'name')}] — " - f"{DataBuilder.build_karma(user, data)}.") + f"{DataBuilder.build_karma(user, data, peer_id)}.") @staticmethod def build_not_enough_karma( user: BetterUser, - data: BetterBotBaseDataService + data: BetterBotBaseDataService, + peer_id: int = None ) -> str: + """Build message for insufficient karma. + + :param user: user object + :param data: data service + :param peer_id: chat ID for chat-specific karma display + """ + if peer_id is not None: + karma_value = data.get_user_chat_karma(user, peer_id) + else: + karma_prop = data.get_user_property(user, 'karma') + karma_value = karma_prop if isinstance(karma_prop, int) else 0 + return (f"Извините, [id{data.get_user_property(user, 'uid')}|" f"{data.get_user_property(user, 'name')}], " - f"но Вашей кармы [{data.get_user_property(user, 'karma')}] " + f"но Вашей кармы [{karma_value}] " f"недостаточно :(") @staticmethod @@ -147,13 +168,23 @@ def build_top_users( data: BetterBotBaseDataService, reverse: bool = False, has_karma: bool = True, - maximum_users: int = -1 + maximum_users: int = -1, + peer_id: int = None ) -> Optional[str]: + """Build top users list display. + + :param users: list of users + :param data: data service + :param reverse: whether to reverse the order + :param has_karma: whether to display karma + :param maximum_users: maximum number of users to display + :param peer_id: chat ID for chat-specific karma display + """ if not users: return None if reverse: users = reversed(users) - user_strings = [(f"{DataBuilder.build_karma(user, data) if has_karma else ''} " + user_strings = [(f"{DataBuilder.build_karma(user, data, peer_id) if has_karma else ''} " f"[id{data.get_user_property(user, 'uid')}|{data.get_user_property(user, 'name')}]" f"{DataBuilder.build_github_profile(user, data, prefix=' - ')} " f"{DataBuilder.build_programming_languages(user, data, '')}") for user in users] diff --git a/python/modules/data_builder.py b/python/modules/data_builder.py index c594f7e1..21374608 100644 --- a/python/modules/data_builder.py +++ b/python/modules/data_builder.py @@ -35,15 +35,34 @@ def build_github_profile( @staticmethod def build_karma( user: BetterUser, - data: BetterBotBaseDataService + data: BetterBotBaseDataService, + chat_id: int = None ) -> str: """Builds the user's karma and returns its string representation. + + :param user: user object + :param data: data service + :param chat_id: chat ID for chat-specific karma, None for global display """ plus_string = "" minus_string = "" - karma = user["karma"] - up_votes = len(user["supporters"]) - down_votes = len(user["opponents"]) + + if chat_id is not None: + karma = data.get_user_chat_karma(user, chat_id) + supporters = data.get_user_chat_supporters(user, chat_id) + opponents = data.get_user_chat_opponents(user, chat_id) + else: + # Fallback to old behavior for backward compatibility + karma_value = user["karma"] + karma = karma_value if isinstance(karma_value, int) else 0 + supporters_value = user["supporters"] + supporters = supporters_value if isinstance(supporters_value, list) else [] + opponents_value = user["opponents"] + opponents = opponents_value if isinstance(opponents_value, list) else [] + + up_votes = len(supporters) + down_votes = len(opponents) + if up_votes > 0: plus_string = "+%.1f" % (up_votes / config.POSITIVE_VOTES_PER_KARMA) if down_votes > 0: @@ -65,7 +84,7 @@ def get_users_sorted_by_karma( other_keys=[ "karma", "name", "programming_languages", "supporters", "opponents", "github_profile", "uid"], - sort_key=lambda u: DataBuilder.calculate_real_karma(u, data), + sort_key=lambda u: DataBuilder.calculate_real_karma(u, data, peer_id), reverse_sort=reverse_sort) if members: users = [u for u in users if u["uid"] in members] @@ -91,9 +110,28 @@ def get_users_sorted_by_name( @staticmethod def calculate_real_karma( user: BetterUser, - data: BetterBotBaseDataService + data: BetterBotBaseDataService, + chat_id: int = None ) -> int: - base_karma = user["karma"] - up_votes = len(user["supporters"])/config.POSITIVE_VOTES_PER_KARMA - down_votes = len(user["opponents"])/config.NEGATIVE_VOTES_PER_KARMA + """Calculate real karma including pending votes. + + :param user: user object + :param data: data service + :param chat_id: chat ID for chat-specific karma calculation + """ + if chat_id is not None: + base_karma = data.get_user_chat_karma(user, chat_id) + supporters = data.get_user_chat_supporters(user, chat_id) + opponents = data.get_user_chat_opponents(user, chat_id) + else: + # Fallback to old behavior for backward compatibility + karma_value = user["karma"] + base_karma = karma_value if isinstance(karma_value, int) else 0 + supporters_value = user["supporters"] + supporters = supporters_value if isinstance(supporters_value, list) else [] + opponents_value = user["opponents"] + opponents = opponents_value if isinstance(opponents_value, list) else [] + + up_votes = len(supporters) / config.POSITIVE_VOTES_PER_KARMA + down_votes = len(opponents) / config.NEGATIVE_VOTES_PER_KARMA return base_karma + up_votes - down_votes diff --git a/python/modules/data_service.py b/python/modules/data_service.py index 45a0faeb..32efad41 100644 --- a/python/modules/data_service.py +++ b/python/modules/data_service.py @@ -16,9 +16,9 @@ def __init__(self, db_name: str = "users"): self.base.addPattern("programming_languages", []) self.base.addPattern("last_collective_vote", 0) self.base.addPattern("github_profile", "") - self.base.addPattern("supporters", []) - self.base.addPattern("opponents", []) - self.base.addPattern("karma", 0) + self.base.addPattern("supporters", {}) # {chat_id: [user_ids]} + self.base.addPattern("opponents", {}) # {chat_id: [user_ids]} + self.base.addPattern("karma", {}) # {chat_id: karma_value} def get_or_create_user( self, @@ -115,3 +115,108 @@ def save_user( user: BetterUser ) -> NoReturn: self.base.save(user) + + @staticmethod + def get_user_chat_karma( + user: Union[Dict[str, Any], BetterUser], + chat_id: int + ) -> int: + """Get user's karma for a specific chat. + + :param user: dict or BetterUser + :param chat_id: chat ID + """ + karma_dict = BetterBotBaseDataService.get_user_property(user, "karma") + if isinstance(karma_dict, dict): + return karma_dict.get(chat_id, 0) + # Backward compatibility: treat old integer karma as global + return karma_dict if isinstance(karma_dict, int) else 0 + + @staticmethod + def set_user_chat_karma( + user: Union[Dict[str, Any], BetterUser], + chat_id: int, + karma_value: int + ) -> NoReturn: + """Set user's karma for a specific chat. + + :param user: dict or BetterUser + :param chat_id: chat ID + :param karma_value: new karma value + """ + karma_dict = BetterBotBaseDataService.get_user_property(user, "karma") + if not isinstance(karma_dict, dict): + # Migrate from old integer karma to dict + karma_dict = {} if karma_dict == 0 else {chat_id: karma_dict} + karma_dict[chat_id] = karma_value + BetterBotBaseDataService.set_user_property(user, "karma", karma_dict) + + @staticmethod + def get_user_chat_supporters( + user: Union[Dict[str, Any], BetterUser], + chat_id: int + ) -> List[int]: + """Get user's supporters for a specific chat. + + :param user: dict or BetterUser + :param chat_id: chat ID + """ + supporters_dict = BetterBotBaseDataService.get_user_property(user, "supporters") + if isinstance(supporters_dict, dict): + return supporters_dict.get(chat_id, []) + # Backward compatibility: treat old list as global + return supporters_dict if isinstance(supporters_dict, list) else [] + + @staticmethod + def set_user_chat_supporters( + user: Union[Dict[str, Any], BetterUser], + chat_id: int, + supporters: List[int] + ) -> NoReturn: + """Set user's supporters for a specific chat. + + :param user: dict or BetterUser + :param chat_id: chat ID + :param supporters: list of supporter user IDs + """ + supporters_dict = BetterBotBaseDataService.get_user_property(user, "supporters") + if not isinstance(supporters_dict, dict): + # Migrate from old list to dict + supporters_dict = {} if not supporters_dict else {chat_id: supporters_dict} + supporters_dict[chat_id] = supporters + BetterBotBaseDataService.set_user_property(user, "supporters", supporters_dict) + + @staticmethod + def get_user_chat_opponents( + user: Union[Dict[str, Any], BetterUser], + chat_id: int + ) -> List[int]: + """Get user's opponents for a specific chat. + + :param user: dict or BetterUser + :param chat_id: chat ID + """ + opponents_dict = BetterBotBaseDataService.get_user_property(user, "opponents") + if isinstance(opponents_dict, dict): + return opponents_dict.get(chat_id, []) + # Backward compatibility: treat old list as global + return opponents_dict if isinstance(opponents_dict, list) else [] + + @staticmethod + def set_user_chat_opponents( + user: Union[Dict[str, Any], BetterUser], + chat_id: int, + opponents: List[int] + ) -> NoReturn: + """Set user's opponents for a specific chat. + + :param user: dict or BetterUser + :param chat_id: chat ID + :param opponents: list of opponent user IDs + """ + opponents_dict = BetterBotBaseDataService.get_user_property(user, "opponents") + if not isinstance(opponents_dict, dict): + # Migrate from old list to dict + opponents_dict = {} if not opponents_dict else {chat_id: opponents_dict} + opponents_dict[chat_id] = opponents + BetterBotBaseDataService.set_user_property(user, "opponents", opponents_dict)