diff --git a/experiments/test_friend_recommendations.py b/experiments/test_friend_recommendations.py new file mode 100644 index 00000000..7dfcf8ed --- /dev/null +++ b/experiments/test_friend_recommendations.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +Test script for the friend recommendations feature. +This script creates mock data to test the recommendation algorithm logic. +""" +import sys +import os + +# Add the parent directory to the path so we can import the modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def test_shared_languages_calculation(): + """Test the shared languages calculation logic.""" + print("Testing shared languages calculation...") + + # Test case 1: Basic overlap + user1_langs = ["Python", "JavaScript", "Go"] + user2_langs = ["Python", "Java", "C++"] + shared = set(user1_langs) & set(user2_langs) + assert len(shared) == 1 and "Python" in shared, f"Expected 1 shared language (Python), got {shared}" + + # Test case 2: No overlap + user3_langs = ["Rust", "Haskell"] + user4_langs = ["PHP", "Ruby"] + shared = set(user3_langs) & set(user4_langs) + assert len(shared) == 0, f"Expected no shared languages, got {shared}" + + # Test case 3: Full overlap + user5_langs = ["Python", "JavaScript"] + user6_langs = ["Python", "JavaScript"] + shared = set(user5_langs) & set(user6_langs) + assert len(shared) == 2, f"Expected 2 shared languages, got {shared}" + + print("✅ All shared languages calculation tests passed!") + +def test_recommendation_sorting(): + """Test the recommendation sorting logic.""" + print("Testing recommendation sorting...") + + # Mock recommendation data + recommendations = [ + {'shared_count': 1, 'karma': 50, 'name': 'User1'}, + {'shared_count': 3, 'karma': 10, 'name': 'User2'}, + {'shared_count': 2, 'karma': 100, 'name': 'User3'}, + {'shared_count': 3, 'karma': 20, 'name': 'User4'}, + ] + + # Sort by shared_count (descending), then by karma (descending) + recommendations.sort(key=lambda x: (-x['shared_count'], -x['karma'])) + + # Expected order: User4 (3,20), User2 (3,10), User3 (2,100), User1 (1,50) + expected_names = ['User4', 'User2', 'User3', 'User1'] + actual_names = [rec['name'] for rec in recommendations] + + assert actual_names == expected_names, f"Expected {expected_names}, got {actual_names}" + + print("✅ Recommendation sorting test passed!") + +def test_message_formatting(): + """Test basic message formatting logic.""" + print("Testing message formatting...") + + user_languages = ["Python", "JavaScript"] + user_languages_str = ", ".join(sorted(user_languages)) + + expected = "JavaScript, Python" + assert user_languages_str == expected, f"Expected '{expected}', got '{user_languages_str}'" + + # Test shared languages formatting + shared_languages = ["Python", "Go"] + shared_languages_str = ", ".join(shared_languages) + expected_shared = "Python, Go" + assert shared_languages_str == expected_shared, f"Expected '{expected_shared}', got '{shared_languages_str}'" + + print("✅ Message formatting test passed!") + +def main(): + """Run all tests.""" + print("🧪 Testing Friend Recommendations Feature") + print("=" * 50) + + try: + test_shared_languages_calculation() + test_recommendation_sorting() + test_message_formatting() + + print("=" * 50) + print("✅ All tests passed! The friend recommendations feature should work correctly.") + return 0 + + except AssertionError as e: + print(f"❌ Test failed: {e}") + return 1 + except Exception as e: + print(f"❌ Unexpected error: {e}") + return 1 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/python/__main__.py b/python/__main__.py index cdcbf7f6..7c6bc2fa 100644 --- a/python/__main__.py +++ b/python/__main__.py @@ -60,6 +60,7 @@ def __init__( (patterns.PEOPLE_LANGUAGES, self.commands.top_langs), (patterns.BOTTOM_LANGUAGES, lambda: self.commands.top_langs(True)), + (patterns.FRIENDS_RECOMMENDATIONS, self.commands.friends_recommendations), (patterns.WHAT_IS, self.commands.what_is), (patterns.WHAT_MEAN, self.commands.what_is), (patterns.APPLY_KARMA, self.commands.apply_karma), diff --git a/python/modules/commands.py b/python/modules/commands.py index 93d99817..e09041ea 100644 --- a/python/modules/commands.py +++ b/python/modules/commands.py @@ -161,6 +161,71 @@ def top_langs( self.vk_instance.send_msg(built, self.peer_id) return + def friends_recommendations(self) -> NoReturn: + """Sends friend recommendations based on shared programming languages.""" + if self.peer_id < 2e9: + return + maximum_users = self.matched.group("maximum_users") + maximum_users = int(maximum_users) if maximum_users else 10 + + # Get current user's programming languages + current_user_languages = self.data_service.get_user_sorted_programming_languages(self.current_user) + if not current_user_languages: + self.vk_instance.send_msg( + "Сначала добавьте свои языки программирования (например: += Python).", + self.peer_id + ) + return + + # Get all users with their programming languages + all_users = DataBuilder.get_users_sorted_by_karma( + self.vk_instance, self.data_service, self.peer_id) + + # Calculate compatibility scores for each user + recommendations = [] + current_user_id = self.current_user.uid + + for user_data in all_users: + user_id = self.data_service.get_user_property(user_data, "uid") + # Skip current user + if user_id == current_user_id: + continue + + user_languages = self.data_service.get_user_property(user_data, "programming_languages") + if not user_languages or not isinstance(user_languages, list): + continue + + # Calculate number of shared languages + shared_languages = set(current_user_languages) & set(user_languages) + if shared_languages: + recommendations.append({ + 'user_data': user_data, + 'shared_count': len(shared_languages), + 'shared_languages': sorted(list(shared_languages)) + }) + + # Sort by number of shared languages (descending), then by karma + recommendations.sort(key=lambda x: ( + -x['shared_count'], + -self.data_service.get_user_property(x['user_data'], 'karma') + )) + + # Limit results + recommendations = recommendations[:maximum_users] + + if not recommendations: + self.vk_instance.send_msg( + "Пока не найдено пользователей с общими языками программирования.", + self.peer_id + ) + return + + self.vk_instance.send_msg( + CommandsBuilder.build_friends_recommendations( + recommendations, self.data_service, current_user_languages), + self.peer_id + ) + def apply_karma(self) -> NoReturn: """Changes user karma.""" if self.peer_id < 2e9 or not self.karma_enabled or not self.matched or self.is_bot_selected: diff --git a/python/modules/commands_builder.py b/python/modules/commands_builder.py index 29dc739f..d4d68ef8 100644 --- a/python/modules/commands_builder.py +++ b/python/modules/commands_builder.py @@ -185,3 +185,57 @@ def build_karma_change( return ("Карма изменена: [id%s|%s] [%s]->[%s]. Голосовали: (%s)" % (selected_user_karma_change + (", ".join([f"@id{voter}" for voter in voters]),))) return None + + @staticmethod + def build_friends_recommendations( + recommendations: List[dict], + data: BetterBotBaseDataService, + user_languages: List[str] + ) -> str: + """Builds friends recommendations message based on shared programming languages. + + Arguments: + - {recommendations} - list of recommendation dicts with user_data, shared_count, shared_languages + - {data} - data service + - {user_languages} - current user's programming languages + """ + if not recommendations: + return "Пока не найдено пользователей с общими языками программирования." + + user_languages_str = ", ".join(sorted(user_languages)) + message_lines = [f"🤝 Рекомендации друзей (ваши языки: {user_languages_str}):\n"] + + for i, rec in enumerate(recommendations, 1): + user_data = rec['user_data'] + shared_count = rec['shared_count'] + shared_languages = rec['shared_languages'] + + user_id = data.get_user_property(user_data, 'uid') + user_name = data.get_user_property(user_data, 'name') + karma = DataBuilder.build_karma(user_data, data) + github_profile = DataBuilder.build_github_profile(user_data, data, prefix=" - ", default="") + + shared_languages_str = ", ".join(shared_languages) + + # Format: "1. [id123|Name] (karma) - github.com/user - общие: Python, Java (2)" + line = (f"{i}. [id{user_id}|{user_name}] ({karma}){github_profile} - " + f"общие: {shared_languages_str} ({shared_count})") + message_lines.append(line) + + # Check total message length and truncate if needed + full_message = '\n'.join(message_lines) + if len(full_message) > 4000: # Leave some margin for VK's 4096 character limit + # Truncate the recommendations and add notice + truncated_lines = message_lines[:1] # Keep header + current_length = len(message_lines[0]) + + for line in message_lines[1:]: + if current_length + len(line) + 50 > 4000: # 50 chars for truncation notice + truncated_lines.append("... (список сокращён)") + break + truncated_lines.append(line) + current_length += len(line) + 1 # +1 for newline + + full_message = '\n'.join(truncated_lines) + + return full_message diff --git a/python/patterns.py b/python/patterns.py index 1834c72c..1b38985b 100644 --- a/python/patterns.py +++ b/python/patterns.py @@ -54,6 +54,9 @@ r'\A\s*(люди|народ|people)\s*(?P(' + DEFAULT_LANGUAGES + r')(\s+(' + DEFAULT_LANGUAGES + r'))*)\s*\Z', IGNORECASE) +FRIENDS_RECOMMENDATIONS = recompile( + r'\A\s*(друзья|friends|рекомендации|recommendations)\s*(?P\d+)?\s*\Z', IGNORECASE) + WHAT_IS = recompile( r'\A\s*(what is|что такое|що таке)\s+(?P[\S\s]+?)\??\s*\Z', IGNORECASE) diff --git a/python/tests.py b/python/tests.py index 4be4241e..18f9db12 100644 --- a/python/tests.py +++ b/python/tests.py @@ -222,6 +222,32 @@ def test_apply_karma_change( self.commands.apply_karma_change('-', 6) self.commands.karma_message() + @ordered + def test_friends_recommendations( + self + ) -> NoReturn: + """Test friend recommendations feature.""" + # Set up test data - create user 3 with some shared languages + user_3 = db.get_or_create_user(3, None) + user_3.programming_languages = ['Python', 'Java'] + user_3.karma = 50 + db.save_user(user_3) + + # Set current user (user 2) to have some languages + self.commands.current_user = db.get_user(2) + self.commands.current_user.programming_languages = ['Python', 'JavaScript'] + db.save_user(self.commands.current_user) + + # Test the friends recommendations command + self.commands.msg = 'friends' + self.commands.match_command(patterns.FRIENDS_RECOMMENDATIONS) + self.commands.friends_recommendations() + + # Test with maximum users limit + self.commands.msg = 'friends 5' + self.commands.match_command(patterns.FRIENDS_RECOMMENDATIONS) + self.commands.friends_recommendations() + if __name__ == '__main__': db = BetterBotBaseDataService("test_db")