Skip to content

Commit eebdf9c

Browse files
konardclaude
andcommitted
Implement friend recommendations based on shared programming languages
This commit addresses issue #60 by adding a new "friends" command that recommends users based on the number of shared programming languages. Features: - New command pattern accepting "друзья", "friends", "рекомендации", or "recommendations" - Algorithm that finds users with common programming languages - Sorting by number of shared languages (descending) then by karma (descending) - Optional parameter to limit the number of recommendations (default: 10) - Comprehensive message formatting with shared languages display - Message length truncation for VK API limits Files modified: - python/patterns.py: Added FRIENDS_RECOMMENDATIONS pattern - python/modules/commands.py: Implemented friends_recommendations method - python/modules/commands_builder.py: Added build_friends_recommendations method - python/__main__.py: Registered the new command - python/tests.py: Added unit tests for the new functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 7d927b7 commit eebdf9c

File tree

6 files changed

+248
-0
lines changed

6 files changed

+248
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test script for the friend recommendations feature.
4+
This script creates mock data to test the recommendation algorithm logic.
5+
"""
6+
import sys
7+
import os
8+
9+
# Add the parent directory to the path so we can import the modules
10+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
11+
12+
def test_shared_languages_calculation():
13+
"""Test the shared languages calculation logic."""
14+
print("Testing shared languages calculation...")
15+
16+
# Test case 1: Basic overlap
17+
user1_langs = ["Python", "JavaScript", "Go"]
18+
user2_langs = ["Python", "Java", "C++"]
19+
shared = set(user1_langs) & set(user2_langs)
20+
assert len(shared) == 1 and "Python" in shared, f"Expected 1 shared language (Python), got {shared}"
21+
22+
# Test case 2: No overlap
23+
user3_langs = ["Rust", "Haskell"]
24+
user4_langs = ["PHP", "Ruby"]
25+
shared = set(user3_langs) & set(user4_langs)
26+
assert len(shared) == 0, f"Expected no shared languages, got {shared}"
27+
28+
# Test case 3: Full overlap
29+
user5_langs = ["Python", "JavaScript"]
30+
user6_langs = ["Python", "JavaScript"]
31+
shared = set(user5_langs) & set(user6_langs)
32+
assert len(shared) == 2, f"Expected 2 shared languages, got {shared}"
33+
34+
print("✅ All shared languages calculation tests passed!")
35+
36+
def test_recommendation_sorting():
37+
"""Test the recommendation sorting logic."""
38+
print("Testing recommendation sorting...")
39+
40+
# Mock recommendation data
41+
recommendations = [
42+
{'shared_count': 1, 'karma': 50, 'name': 'User1'},
43+
{'shared_count': 3, 'karma': 10, 'name': 'User2'},
44+
{'shared_count': 2, 'karma': 100, 'name': 'User3'},
45+
{'shared_count': 3, 'karma': 20, 'name': 'User4'},
46+
]
47+
48+
# Sort by shared_count (descending), then by karma (descending)
49+
recommendations.sort(key=lambda x: (-x['shared_count'], -x['karma']))
50+
51+
# Expected order: User4 (3,20), User2 (3,10), User3 (2,100), User1 (1,50)
52+
expected_names = ['User4', 'User2', 'User3', 'User1']
53+
actual_names = [rec['name'] for rec in recommendations]
54+
55+
assert actual_names == expected_names, f"Expected {expected_names}, got {actual_names}"
56+
57+
print("✅ Recommendation sorting test passed!")
58+
59+
def test_message_formatting():
60+
"""Test basic message formatting logic."""
61+
print("Testing message formatting...")
62+
63+
user_languages = ["Python", "JavaScript"]
64+
user_languages_str = ", ".join(sorted(user_languages))
65+
66+
expected = "JavaScript, Python"
67+
assert user_languages_str == expected, f"Expected '{expected}', got '{user_languages_str}'"
68+
69+
# Test shared languages formatting
70+
shared_languages = ["Python", "Go"]
71+
shared_languages_str = ", ".join(shared_languages)
72+
expected_shared = "Python, Go"
73+
assert shared_languages_str == expected_shared, f"Expected '{expected_shared}', got '{shared_languages_str}'"
74+
75+
print("✅ Message formatting test passed!")
76+
77+
def main():
78+
"""Run all tests."""
79+
print("🧪 Testing Friend Recommendations Feature")
80+
print("=" * 50)
81+
82+
try:
83+
test_shared_languages_calculation()
84+
test_recommendation_sorting()
85+
test_message_formatting()
86+
87+
print("=" * 50)
88+
print("✅ All tests passed! The friend recommendations feature should work correctly.")
89+
return 0
90+
91+
except AssertionError as e:
92+
print(f"❌ Test failed: {e}")
93+
return 1
94+
except Exception as e:
95+
print(f"❌ Unexpected error: {e}")
96+
return 1
97+
98+
if __name__ == "__main__":
99+
sys.exit(main())

python/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def __init__(
6060
(patterns.PEOPLE_LANGUAGES, self.commands.top_langs),
6161
(patterns.BOTTOM_LANGUAGES,
6262
lambda: self.commands.top_langs(True)),
63+
(patterns.FRIENDS_RECOMMENDATIONS, self.commands.friends_recommendations),
6364
(patterns.WHAT_IS, self.commands.what_is),
6465
(patterns.WHAT_MEAN, self.commands.what_is),
6566
(patterns.APPLY_KARMA, self.commands.apply_karma),

python/modules/commands.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,71 @@ def top_langs(
161161
self.vk_instance.send_msg(built, self.peer_id)
162162
return
163163

164+
def friends_recommendations(self) -> NoReturn:
165+
"""Sends friend recommendations based on shared programming languages."""
166+
if self.peer_id < 2e9:
167+
return
168+
maximum_users = self.matched.group("maximum_users")
169+
maximum_users = int(maximum_users) if maximum_users else 10
170+
171+
# Get current user's programming languages
172+
current_user_languages = self.data_service.get_user_sorted_programming_languages(self.current_user)
173+
if not current_user_languages:
174+
self.vk_instance.send_msg(
175+
"Сначала добавьте свои языки программирования (например: += Python).",
176+
self.peer_id
177+
)
178+
return
179+
180+
# Get all users with their programming languages
181+
all_users = DataBuilder.get_users_sorted_by_karma(
182+
self.vk_instance, self.data_service, self.peer_id)
183+
184+
# Calculate compatibility scores for each user
185+
recommendations = []
186+
current_user_id = self.current_user.uid
187+
188+
for user_data in all_users:
189+
user_id = self.data_service.get_user_property(user_data, "uid")
190+
# Skip current user
191+
if user_id == current_user_id:
192+
continue
193+
194+
user_languages = self.data_service.get_user_property(user_data, "programming_languages")
195+
if not user_languages or not isinstance(user_languages, list):
196+
continue
197+
198+
# Calculate number of shared languages
199+
shared_languages = set(current_user_languages) & set(user_languages)
200+
if shared_languages:
201+
recommendations.append({
202+
'user_data': user_data,
203+
'shared_count': len(shared_languages),
204+
'shared_languages': sorted(list(shared_languages))
205+
})
206+
207+
# Sort by number of shared languages (descending), then by karma
208+
recommendations.sort(key=lambda x: (
209+
-x['shared_count'],
210+
-self.data_service.get_user_property(x['user_data'], 'karma')
211+
))
212+
213+
# Limit results
214+
recommendations = recommendations[:maximum_users]
215+
216+
if not recommendations:
217+
self.vk_instance.send_msg(
218+
"Пока не найдено пользователей с общими языками программирования.",
219+
self.peer_id
220+
)
221+
return
222+
223+
self.vk_instance.send_msg(
224+
CommandsBuilder.build_friends_recommendations(
225+
recommendations, self.data_service, current_user_languages),
226+
self.peer_id
227+
)
228+
164229
def apply_karma(self) -> NoReturn:
165230
"""Changes user karma."""
166231
if self.peer_id < 2e9 or not self.karma_enabled or not self.matched or self.is_bot_selected:

python/modules/commands_builder.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,57 @@ def build_karma_change(
185185
return ("Карма изменена: [id%s|%s] [%s]->[%s]. Голосовали: (%s)" %
186186
(selected_user_karma_change + (", ".join([f"@id{voter}" for voter in voters]),)))
187187
return None
188+
189+
@staticmethod
190+
def build_friends_recommendations(
191+
recommendations: List[dict],
192+
data: BetterBotBaseDataService,
193+
user_languages: List[str]
194+
) -> str:
195+
"""Builds friends recommendations message based on shared programming languages.
196+
197+
Arguments:
198+
- {recommendations} - list of recommendation dicts with user_data, shared_count, shared_languages
199+
- {data} - data service
200+
- {user_languages} - current user's programming languages
201+
"""
202+
if not recommendations:
203+
return "Пока не найдено пользователей с общими языками программирования."
204+
205+
user_languages_str = ", ".join(sorted(user_languages))
206+
message_lines = [f"🤝 Рекомендации друзей (ваши языки: {user_languages_str}):\n"]
207+
208+
for i, rec in enumerate(recommendations, 1):
209+
user_data = rec['user_data']
210+
shared_count = rec['shared_count']
211+
shared_languages = rec['shared_languages']
212+
213+
user_id = data.get_user_property(user_data, 'uid')
214+
user_name = data.get_user_property(user_data, 'name')
215+
karma = DataBuilder.build_karma(user_data, data)
216+
github_profile = DataBuilder.build_github_profile(user_data, data, prefix=" - ", default="")
217+
218+
shared_languages_str = ", ".join(shared_languages)
219+
220+
# Format: "1. [id123|Name] (karma) - github.com/user - общие: Python, Java (2)"
221+
line = (f"{i}. [id{user_id}|{user_name}] ({karma}){github_profile} - "
222+
f"общие: {shared_languages_str} ({shared_count})")
223+
message_lines.append(line)
224+
225+
# Check total message length and truncate if needed
226+
full_message = '\n'.join(message_lines)
227+
if len(full_message) > 4000: # Leave some margin for VK's 4096 character limit
228+
# Truncate the recommendations and add notice
229+
truncated_lines = message_lines[:1] # Keep header
230+
current_length = len(message_lines[0])
231+
232+
for line in message_lines[1:]:
233+
if current_length + len(line) + 50 > 4000: # 50 chars for truncation notice
234+
truncated_lines.append("... (список сокращён)")
235+
break
236+
truncated_lines.append(line)
237+
current_length += len(line) + 1 # +1 for newline
238+
239+
full_message = '\n'.join(truncated_lines)
240+
241+
return full_message

python/patterns.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
r'\A\s*(люди|народ|people)\s*(?P<languages>(' + DEFAULT_LANGUAGES +
5555
r')(\s+(' + DEFAULT_LANGUAGES + r'))*)\s*\Z', IGNORECASE)
5656

57+
FRIENDS_RECOMMENDATIONS = recompile(
58+
r'\A\s*(друзья|friends|рекомендации|recommendations)\s*(?P<maximum_users>\d+)?\s*\Z', IGNORECASE)
59+
5760
WHAT_IS = recompile(
5861
r'\A\s*(what is|что такое|що таке)\s+(?P<question>[\S\s]+?)\??\s*\Z', IGNORECASE)
5962

python/tests.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,32 @@ def test_apply_karma_change(
222222
self.commands.apply_karma_change('-', 6)
223223
self.commands.karma_message()
224224

225+
@ordered
226+
def test_friends_recommendations(
227+
self
228+
) -> NoReturn:
229+
"""Test friend recommendations feature."""
230+
# Set up test data - create user 3 with some shared languages
231+
user_3 = db.get_or_create_user(3, None)
232+
user_3.programming_languages = ['Python', 'Java']
233+
user_3.karma = 50
234+
db.save_user(user_3)
235+
236+
# Set current user (user 2) to have some languages
237+
self.commands.current_user = db.get_user(2)
238+
self.commands.current_user.programming_languages = ['Python', 'JavaScript']
239+
db.save_user(self.commands.current_user)
240+
241+
# Test the friends recommendations command
242+
self.commands.msg = 'friends'
243+
self.commands.match_command(patterns.FRIENDS_RECOMMENDATIONS)
244+
self.commands.friends_recommendations()
245+
246+
# Test with maximum users limit
247+
self.commands.msg = 'friends 5'
248+
self.commands.match_command(patterns.FRIENDS_RECOMMENDATIONS)
249+
self.commands.friends_recommendations()
250+
225251

226252
if __name__ == '__main__':
227253
db = BetterBotBaseDataService("test_db")

0 commit comments

Comments
 (0)