diff --git a/read_mattermost.py b/read_mattermost.py index 12ea4b1..2e64457 100755 --- a/read_mattermost.py +++ b/read_mattermost.py @@ -7,7 +7,7 @@ import datetime import threading from time import sleep import json -from typing import Optional +from typing import Optional, Mapping, Set import mattermost import mattermost.ws @@ -15,6 +15,7 @@ SERVER = "mattermost.zeus.gent" TEAM_NAME = "zeus" CHAN_NAME = "pannenkoeken" EMOJI_NAME = "pancakes" +DOUBLE_EMOJI_NAME = "own_pancakes" TAGGERS = [ # Board "flynn", @@ -34,6 +35,7 @@ TOKEN = os.getenv("MM_ACCESS_TOKEN") USER = os.getenv("MM_USERNAME") PASSWORD = os.getenv("MM_PASSWORD") +# Set to False to disable reacting with an emoji for the count CONFIRMATION_EMOJI = True @@ -48,7 +50,7 @@ if since_arg_i: clean = "--clean" in sys.argv[1:] live = "--live" in sys.argv[1:] if "--no-confirm" in sys.argv[1:]: - CONFIRMATION_EMOJI = None + CONFIRMATION_EMOJI = False @@ -124,12 +126,46 @@ def to_mm_timestamp(dt): def reaction_qualifies(reaction): - return reaction["emoji_name"] == EMOJI_NAME and reaction["user_id"] in tagger_ids + if reaction["user_id"] not in tagger_ids: + return 0 + if reaction["emoji_name"] == EMOJI_NAME: + return 1 + if reaction["emoji_name"] == DOUBLE_EMOJI_NAME: + return 2 + return 0 -awarded = {} # awarded[awardee][post_id]: set of verifiers -def award_if_appropriate(reaction): - if not reaction_qualifies(reaction): +def post_score(awardee_id, post_id, awarder_id): + return max(awarded[awardee_id][post_id][awarder_id], default=0) + + +def emit_change_line(post, awardee_id, awarder_id, prev_score, score): + if score == prev_score: + return + + awardee = get_username(awardee_id) + awarder = get_username(awarder_id) + + post_time = parse_mm_timestamp(post["create_at"]).isoformat(timespec="microseconds") + + if score == 0: + message = f"{awarder} retracted their verification" + elif prev_score == 0: + message = f"{awarder} verified with score {score}" + else: + message = f"{awarder} updated their verification's score from {prev_score} to {score}" + + print(f"{awardee} {post['id']} at {post_time}: {message}", flush=True) + + +# awarded[awardee][post_id][verifier]: set of values +awarded: Mapping[str, Mapping[str, Mapping[str, Set[int]]]] = \ + defaultdict(lambda: defaultdict(lambda: defaultdict(set))) + + +def process_change(reaction, action): + value = reaction_qualifies(reaction) + if value == 0: return post = get_post(reaction["post_id"]) @@ -139,48 +175,25 @@ def award_if_appropriate(reaction): awardee_id = post["user_id"] awarder_id = reaction["user_id"] - if awardee_id not in awarded: - awarded[awardee_id] = {} - if post["id"] not in awarded[awardee_id]: - awarded[awardee_id][post["id"]] = set() - - if awarder_id in awarded[awardee_id][post["id"]]: - # We already knew that this user verified this post - return - - awarded[awardee_id][post["id"]].add(awarder_id) - - reaction_time = parse_mm_timestamp(reaction["create_at"]).isoformat(timespec="microseconds") - post_time = parse_mm_timestamp(reaction["create_at"]).isoformat(timespec="microseconds") - awardee = get_username(awardee_id) - awarder = get_username(awarder_id) - print(f"{awardee} {post['id']} at {post_time} verified by {awarder} at {reaction_time}", flush=True) + prev_score = post_score(awardee_id, post["id"], awarder_id) + action(awarded[awardee_id][post["id"]][awarder_id], value) + score = post_score(awardee_id, post["id"], awarder_id) + emit_change_line(post, awardee_id, awarder_id, prev_score, score) update_confirmation(post["id"]) +def award_if_appropriate(reaction): + process_change( + reaction, + lambda values_set, value: values_set.add(value) + ) + def retract_if_appropriate(reaction): - if not reaction_qualifies(reaction): - return - - post = get_post(reaction["post_id"]) - if parse_mm_timestamp(post["create_at"]) < SINCE: - return - - awardee_id = post["user_id"] - awarder_id = reaction["user_id"] - awarded[awardee_id][post["id"]].discard(awarder_id) - - if not awarded[awardee_id][post["id"]]: - del awarded[awardee_id][post["id"]] - if not awarded[awardee_id]: - del awarded[awardee_id] - - awardee = get_username(awardee_id) - awarder = get_username(awarder_id) - print(f"{awardee} {post['id']} verification removed by {awarder}", flush=True) - - update_confirmation(post["id"]) + process_change( + reaction, + lambda values_set, value: values_set.discard(value) + ) def get_posts_for_channel(mmapi, channel_id, since, **kwargs): @@ -222,6 +235,19 @@ def persevere(f, backoff=1): sleep(backoff) +# awarded[awardee][post_id][verifier]: set of values +def count_verifications(user_id): + return sum( + max( + ( + max(values, default=0) for values in post_verifications.values() + ), + default=0 + ) + for post_verifications in awarded[user_id].values() + ) + + def update_confirmation(post_id): if not CONFIRMATION_EMOJI: return @@ -229,7 +255,7 @@ def update_confirmation(post_id): post = get_post(post_id, force_fetch=True) remove_reactions_from_post(post) - new_count = len(awarded.get(post["user_id"], [])) + new_count = count_verifications(post["user_id"]) if new_count > 0: persevere(lambda: mm.create_reaction(our_user_id, post_id, confirmation_emoji_name(new_count))) @@ -268,8 +294,8 @@ def handle_live(): if clean: - for post in get_posts_for_channel(mm, channel, SINCE): - remove_reactions_from_post(post) + for _post in get_posts_for_channel(mm, channel, SINCE): + remove_reactions_from_post(_post) else: # Note: skipping this step and updating an existing file would be dangerous: you would miss revocations that happened while not listening.