Skip to content

Commit c40d095

Browse files
konardclaude
andcommitted
Add local karma functionality for chat-specific karma tracking
This commit implements local karma feature that allows users to have separate karma values for each chat, in addition to their global karma. Key features implemented: - Local karma storage per chat in user profiles - Local karma commands: 'local karma', 'local +/-', 'local top/bottom' - Chat-specific karma voting and leaderboards - Comprehensive test coverage for local karma functionality - Updated help messages and documentation with local karma commands - Support for Russian and English command aliases The local karma system works independently from global karma: - Users can have different karma values in different chats - Local karma voting follows same rules as global karma (time limits, negative karma restrictions) - Local karma is displayed separately from global karma - Local leaderboards show only participants from current chat All existing global karma functionality remains unchanged and fully compatible. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 9b48421 commit c40d095

File tree

8 files changed

+388
-0
lines changed

8 files changed

+388
-0
lines changed

python/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,25 @@
1717
| ✔️ | top | Вывести информацию о участниках беседы в порядке уменьшения кармы. |
1818
| ✔️ | top [ЯЗЫКИ] | Вывести информацию о участниках беседы с указанными языками в порядке уменьшения кармы. |
1919
| ✔️ | top [ЧИСЛО] | Вывести информацию об указанном числе участников беседы в порядке уменьшения кармы. |
20+
| ✔️ | local top | Вывести информацию о участниках беседы в порядке уменьшения локальной кармы в текущем чате. |
21+
| ✔️ | local top [ЧИСЛО] | Вывести информацию об указанном числе участников беседы в порядке уменьшения локальной кармы в текущем чате. |
2022
| ✔️ | bottom | Вывести информацию о участниках беседы в порядке увеличения кармы. |
2123
| ✔️ | bottom [ЯЗЫКИ] | Вывести информацию о участниках беседы с указанными языками в порядке увеличения кармы. |
2224
| ✔️ | bottom [ЧИСЛО] | Вывести информацию об указанном числе участников беседы беседы в порядке увеличения кармы. |
25+
| ✔️ | local bottom | Вывести информацию о участниках беседы в порядке увеличения локальной кармы в текущем чате. |
26+
| ✔️ | local bottom [ЧИСЛО] | Вывести информацию об указанном числе участников беседы в порядке увеличения локальной кармы в текущем чате. |
2327
| ✔️ | karma | Вывод своей кармы или кармы участника беседы из пересланного сообщения. |
28+
| ✔️ | local karma | Вывод своей локальной кармы в текущем чате или локальной кармы участника беседы из пересланного сообщения. |
2429
|| info | Вывести общую информацию (карма (только для бесед с кармой), добавленные языки, ссылка на профиль github) о себе или участнике беседы из пересланного сообщения. |
2530
|| update | Обновить информацию о вас (имя). Эта команда так же выводит информацию о вас как это делает команда info. |
2631
| ✔️ | + | Проголосовать за повышение кармы участника беседы из пересланного сообщения. |
2732
| ✔️ | - | Проголосовать за понижение кармы участника беседы из пересланного сообщения. |
2833
| ✔️ | +[ЧИСЛО] | Повысить карму участника беседы из пересланного сообщения на указанное число, потратив свою. |
2934
| ✔️ | -[ЧИСЛО] | Понизить карму участника беседы из пересланного сообщения на указанное число, потратив свою. |
35+
| ✔️ | local + | Повысить локальную карму участника беседы из пересланного сообщения в текущем чате. |
36+
| ✔️ | local - | Понизить локальную карму участника беседы из пересланного сообщения в текущем чате. |
37+
| ✔️ | local +[ЧИСЛО] | Повысить локальную карму участника беседы из пересланного сообщения на указанное число в текущем чате. |
38+
| ✔️ | local -[ЧИСЛО] | Понизить локальную карму участника беседы из пересланного сообщения на указанное число в текущем чате. |
3039
|| += [ЯЗЫК] | Добавить язык программирования в свой профиль. |
3140
|| -= [ЯЗЫК] | Убрать язык программирования из своего профиля. |
3241
|| += [ССЫЛКА] | Добавить ссылку на профиль github в свой профиль. |
@@ -42,6 +51,9 @@
4251
| top | топ | верх |
4352
| bottom | дно | низ |
4453
| karma | карма|
54+
| local karma | локальная карма | местная карма | лкарма |
55+
| local top | локальный топ | местный топ | лтоп |
56+
| local bottom | локальный низ | местный низ | лдно |
4557
| info | инфо |
4658
| update | обновить |
4759
| what is | что такое |

python/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,22 @@ def __init__(
5252
(patterns.REMOVE_GITHUB_PROFILE,
5353
lambda: self.commands.change_github_profile(False)),
5454
(patterns.KARMA, self.commands.karma_message),
55+
(patterns.LOCAL_KARMA, self.commands.local_karma_message),
5556
(patterns.TOP, self.commands.top),
57+
(patterns.LOCAL_TOP, self.commands.local_top),
5658
(patterns.PEOPLE, self.commands.top),
5759
(patterns.BOTTOM,
5860
lambda: self.commands.top(True)),
61+
(patterns.LOCAL_BOTTOM,
62+
lambda: self.commands.local_top(True)),
5963
(patterns.TOP_LANGUAGES, self.commands.top_langs),
6064
(patterns.PEOPLE_LANGUAGES, self.commands.top_langs),
6165
(patterns.BOTTOM_LANGUAGES,
6266
lambda: self.commands.top_langs(True)),
6367
(patterns.WHAT_IS, self.commands.what_is),
6468
(patterns.WHAT_MEAN, self.commands.what_is),
6569
(patterns.APPLY_KARMA, self.commands.apply_karma),
70+
(patterns.APPLY_LOCAL_KARMA, self.commands.apply_local_karma),
6671
(patterns.GITHUB_COPILOT, self.commands.github_copilot)
6772
)
6873

python/modules/commands.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,102 @@ def apply_user_karma(
308308
user.karma = new_karma
309309
return (user.uid, user.name, initial_karma, new_karma)
310310

311+
def local_karma_message(self) -> NoReturn:
312+
"""Shows user's local karma for current chat."""
313+
if self.peer_id < 2e9 and not self.karma_enabled:
314+
return
315+
is_self = self.user.uid == self.from_id
316+
self.vk_instance.send_msg(
317+
CommandsBuilder.build_local_karma(self.user, self.data_service, is_self, self.peer_id),
318+
self.peer_id)
319+
320+
def apply_local_karma(self) -> NoReturn:
321+
"""Changes user local karma for current chat."""
322+
if self.peer_id < 2e9 or not self.karma_enabled or not self.matched or self.is_bot_selected:
323+
return
324+
325+
operator = self.matched.group("operator")
326+
amount_string = self.matched.group("amount")
327+
amount = int(amount_string) if amount_string else 1
328+
329+
if amount > 10:
330+
return
331+
332+
selected_user_id = self.matched.group("selectedUserId")
333+
selected_user = self.data_service.get_user(
334+
int(selected_user_id), self.vk_instance) if selected_user_id else self.user
335+
336+
if selected_user.uid == self.from_id:
337+
return
338+
339+
if selected_user.uid == config.BOT_GROUP_ID:
340+
self.is_bot_selected = True
341+
return
342+
343+
if operator == "-":
344+
# Downvotes disabled for users with negative local karma
345+
if self.data_service.get_local_karma(self.current_user, self.peer_id) < 0:
346+
self.vk_instance.send_msg(
347+
CommandsBuilder.build_not_enough_local_karma(self.current_user, self.data_service, self.peer_id),
348+
self.peer_id)
349+
return
350+
351+
current_time = datetime.now()
352+
last_vote_time = self.current_user.last_collective_vote
353+
354+
if last_vote_time > 0:
355+
time_diff = (current_time - datetime.fromtimestamp(last_vote_time)).total_seconds() / 3600
356+
hours_limit = karma_limit(
357+
self.data_service.get_local_karma(self.current_user, self.peer_id))
358+
359+
if time_diff < hours_limit:
360+
return
361+
362+
# Apply local karma change
363+
local_karma_change = self.apply_user_local_karma(selected_user, amount if operator == "+" else -amount)
364+
365+
if local_karma_change:
366+
self.current_user.last_collective_vote = current_time.timestamp()
367+
self.data_service.save_user(self.current_user)
368+
self.data_service.save_user(selected_user)
369+
self.vk_instance.send_msg(
370+
CommandsBuilder.build_local_karma_change(local_karma_change, self.peer_id),
371+
self.peer_id)
372+
373+
def apply_user_local_karma(
374+
self,
375+
user: BetterUser,
376+
amount: int
377+
) -> Optional[Tuple[int, str, int, int]]:
378+
"""Changes user local karma for current chat
379+
380+
:param user: user object
381+
:param amount: karma amount to change
382+
:return: tuple of (user_id, username, initial_karma, new_karma) or None
383+
"""
384+
initial_karma = self.data_service.get_local_karma(user, self.peer_id)
385+
new_karma = initial_karma + amount
386+
self.data_service.set_local_karma(user, self.peer_id, new_karma)
387+
return (user.uid, user.name, initial_karma, new_karma)
388+
389+
def local_top(self, reverse: bool = False) -> NoReturn:
390+
"""Sends users local karma top for current chat."""
391+
if self.peer_id < 2e9:
392+
return
393+
maximum_users = self.matched.group("maximum_users")
394+
maximum_users = int(maximum_users) if maximum_users else -1
395+
users = DataBuilder.get_users_sorted_by_local_karma(
396+
self.vk_instance, self.data_service, self.peer_id, self.peer_id)
397+
users = [user for user in users if user["local_karma"] != 0 or not reverse]
398+
if maximum_users != -1:
399+
users = users[:maximum_users]
400+
if reverse:
401+
users.reverse()
402+
self.vk_instance.send_msg(
403+
CommandsBuilder.build_local_top_users(
404+
users, self.data_service, reverse, self.karma_enabled, maximum_users),
405+
self.peer_id)
406+
311407
def what_is(self) -> NoReturn:
312408
"""Search on wikipedia and sends if available"""
313409
question = self.matched.groups()

python/modules/commands_builder.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ def build_help_message(
2626
elif peer_id > 2e9:
2727
if karma:
2828
return ("Вы находитесь в беседе с включённой кармой.\n"
29+
"Доступные команды для локальной кармы:\n"
30+
"• 'local karma' / 'локальная карма' - показать локальную карму\n"
31+
"• 'local +/-' - изменить локальную карму пользователя\n"
32+
"• 'local top' / 'локальный топ' - топ по локальной карме\n"
33+
"• 'local bottom' / 'локальный низ' - низ по локальной карме\n"
2934
f"Документация — {documentation_link}")
3035
else:
3136
return (f"Вы находитесь в беседе (#{peer_id}) с выключенной кармой.\n"
@@ -185,3 +190,76 @@ def build_karma_change(
185190
return ("Карма изменена: [id%s|%s] [%s]->[%s]. Голосовали: (%s)" %
186191
(selected_user_karma_change + (", ".join([f"@id{voter}" for voter in voters]),)))
187192
return None
193+
194+
@staticmethod
195+
def build_local_karma(
196+
user: BetterUser,
197+
data: BetterBotBaseDataService,
198+
is_self: bool,
199+
chat_id: int
200+
) -> str:
201+
"""Sends user local karma amount for specific chat.
202+
"""
203+
if is_self:
204+
return (f"Ваша локальная карма в этом чате — "
205+
f"{DataBuilder.build_local_karma(user, data, chat_id)}.")
206+
else:
207+
mention = f"[id{user.uid}|{user.name}]"
208+
return (f"Локальная карма {mention} в этом чате — "
209+
f"{DataBuilder.build_local_karma(user, data, chat_id)}.")
210+
211+
@staticmethod
212+
def build_not_enough_local_karma(
213+
user: BetterUser,
214+
data: BetterBotBaseDataService,
215+
chat_id: int
216+
) -> str:
217+
"""Builds message about insufficient local karma."""
218+
return (f"Вы не можете минусовать карму, "
219+
f"но Вашей локальной кармы [{data.get_local_karma(user, chat_id)}] "
220+
f"в этом чате недостаточно.")
221+
222+
@staticmethod
223+
def build_local_karma_change(
224+
local_karma_change: Tuple[int, str, int, int],
225+
chat_id: int
226+
) -> str:
227+
"""Builds local karma changing message."""
228+
return ("Локальная карма в этом чате изменена: [id%s|%s] [%s]->[%s]." %
229+
local_karma_change)
230+
231+
@staticmethod
232+
def build_local_top_users(
233+
users: List[Dict[str, Any]],
234+
data: BetterBotBaseDataService,
235+
reverse: bool = False,
236+
has_karma: bool = True,
237+
maximum_users: int = -1
238+
) -> Optional[str]:
239+
"""Builds local karma top users list."""
240+
if not users:
241+
return None
242+
if reverse:
243+
users = list(reversed(users))
244+
user_strings = []
245+
for user in users:
246+
karma_str = f"[{user['local_karma']}]" if has_karma else ""
247+
user_string = (f"{karma_str} "
248+
f"[id{user['uid']}|{user['name']}]"
249+
f"{DataBuilder.build_github_profile_from_dict(user, ' - ')}"
250+
f"{DataBuilder.build_programming_languages_from_dict(user, '')}")
251+
user_strings.append(user_string)
252+
253+
total_symbols = 0
254+
i = 0
255+
for user_string in user_strings:
256+
user_string_length = len(user_string)
257+
if (total_symbols + user_string_length + 2) >= 4096: # Maximum message size for VK API (messages.send)
258+
user_strings = user_strings[:i]
259+
break
260+
else:
261+
total_symbols += user_string_length + 2
262+
i += 1
263+
if maximum_users > 0:
264+
return '\n'.join(user_strings[:maximum_users])
265+
return '\n'.join(user_strings)

python/modules/data_builder.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,66 @@ def calculate_real_karma(
9797
up_votes = len(user["supporters"])/config.POSITIVE_VOTES_PER_KARMA
9898
down_votes = len(user["opponents"])/config.NEGATIVE_VOTES_PER_KARMA
9999
return base_karma + up_votes - down_votes
100+
101+
@staticmethod
102+
def build_local_karma(
103+
user: BetterUser,
104+
data: BetterBotBaseDataService,
105+
chat_id: int
106+
) -> str:
107+
"""Builds the user's local karma for specific chat and returns its string representation.
108+
"""
109+
local_karma = data.get_local_karma(user, chat_id)
110+
plus_string = ""
111+
minus_string = ""
112+
up_votes = len(user["supporters"])
113+
down_votes = len(user["opponents"])
114+
if up_votes > 0:
115+
plus_string = "+%.1f" % (up_votes / config.POSITIVE_VOTES_PER_KARMA)
116+
if down_votes > 0:
117+
minus_string = "-%.1f" % (down_votes / config.NEGATIVE_VOTES_PER_KARMA)
118+
if up_votes > 0 or down_votes > 0:
119+
return f"[{local_karma}][{plus_string}{minus_string}]"
120+
else:
121+
return f"[{local_karma}]"
122+
123+
@staticmethod
124+
def get_users_sorted_by_local_karma(
125+
vk_instance: Vk,
126+
data: BetterBotBaseDataService,
127+
peer_id: int,
128+
chat_id: int
129+
) -> List[Dict[str, Any]]:
130+
"""Returns users from the chat sorted by local karma."""
131+
members = vk_instance.get_members_ids(peer_id)
132+
users = []
133+
for member_id in members:
134+
user = data.get_user(member_id)
135+
local_karma = data.get_local_karma(user, chat_id)
136+
users.append({
137+
"uid": member_id,
138+
"local_karma": local_karma,
139+
"name": user.name,
140+
"programming_languages": user.programming_languages,
141+
"github_profile": user.github_profile
142+
})
143+
return sorted(users, key=lambda u: u["local_karma"], reverse=True)
144+
145+
@staticmethod
146+
def build_programming_languages_from_dict(
147+
user_dict: Dict[str, Any],
148+
default: str = "отсутствуют"
149+
) -> str:
150+
"""Builds programming languages from user dictionary."""
151+
languages = user_dict.get("programming_languages", [])
152+
languages = languages if isinstance(languages, list) else []
153+
return ", ".join(sorted(languages)) if len(languages) > 0 else default
154+
155+
@staticmethod
156+
def build_github_profile_from_dict(
157+
user_dict: Dict[str, Any],
158+
prefix: str = ""
159+
) -> str:
160+
"""Builds github profile from user dictionary."""
161+
profile = user_dict.get("github_profile", "")
162+
return f"{prefix}github.com/{profile}" if profile else ""

python/modules/data_service.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def __init__(self, db_name: str = "users"):
1919
self.base.addPattern("supporters", [])
2020
self.base.addPattern("opponents", [])
2121
self.base.addPattern("karma", 0)
22+
self.base.addPattern("local_karma", {})
2223

2324
def get_or_create_user(
2425
self,
@@ -115,3 +116,35 @@ def save_user(
115116
user: BetterUser
116117
) -> NoReturn:
117118
self.base.save(user)
119+
120+
@staticmethod
121+
def get_local_karma(
122+
user: Union[Dict[str, Any], BetterUser],
123+
chat_id: int
124+
) -> int:
125+
"""Get user's karma for specific chat.
126+
127+
:param user: dict or BetterUser
128+
:param chat_id: chat identifier
129+
:return: karma value for the chat
130+
"""
131+
local_karma_dict = BetterBotBaseDataService.get_user_property(user, "local_karma")
132+
return local_karma_dict.get(str(chat_id), 0)
133+
134+
@staticmethod
135+
def set_local_karma(
136+
user: Union[Dict[str, Any], BetterUser],
137+
chat_id: int,
138+
karma_value: int
139+
) -> NoReturn:
140+
"""Set user's karma for specific chat.
141+
142+
:param user: dict or BetterUser
143+
:param chat_id: chat identifier
144+
:param karma_value: new karma value
145+
"""
146+
local_karma_dict = BetterBotBaseDataService.get_user_property(user, "local_karma")
147+
if local_karma_dict is None:
148+
local_karma_dict = {}
149+
local_karma_dict[str(chat_id)] = karma_value
150+
BetterBotBaseDataService.set_user_property(user, "local_karma", local_karma_dict)

0 commit comments

Comments
 (0)