From 2cf0f5a5809bacdf121eb7382209046401aaf921 Mon Sep 17 00:00:00 2001 From: Midgard Date: Sun, 6 Dec 2020 14:28:52 +0100 Subject: [PATCH] React with count emoji --- read_mattermost.py | 114 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 18 deletions(-) diff --git a/read_mattermost.py b/read_mattermost.py index 8c3f787..12ea4b1 100755 --- a/read_mattermost.py +++ b/read_mattermost.py @@ -7,6 +7,7 @@ import datetime import threading from time import sleep import json +from typing import Optional import mattermost import mattermost.ws @@ -33,6 +34,8 @@ TOKEN = os.getenv("MM_ACCESS_TOKEN") USER = os.getenv("MM_USERNAME") PASSWORD = os.getenv("MM_PASSWORD") +CONFIRMATION_EMOJI = True + try: since_arg_i = sys.argv.index("--since") @@ -42,8 +45,14 @@ except ValueError: if since_arg_i: SINCE = datetime.datetime.fromisoformat(sys.argv[since_arg_i + 1]) +clean = "--clean" in sys.argv[1:] +live = "--live" in sys.argv[1:] +if "--no-confirm" in sys.argv[1:]: + CONFIRMATION_EMOJI = None -if sys.stdout.isatty(): + + +if not clean and sys.stdout.isatty(): print("To use this data, redirect stdout to a file and use table.py on it.", file=sys.stderr) @@ -64,6 +73,8 @@ else: assert PASSWORD mm.login(USER, PASSWORD) +our_user_id = mm._my_user_id + ################################## # Get channel @@ -98,8 +109,8 @@ def get_username(userid): ################################## # Get posts posts = {} -def get_post(postid): - if postid not in posts: +def get_post(postid, force_fetch=False): + if postid not in posts or force_fetch: posts[postid] = mm.get_post(postid) return posts[postid] @@ -116,7 +127,7 @@ def reaction_qualifies(reaction): return reaction["emoji_name"] == EMOJI_NAME and reaction["user_id"] in tagger_ids -awarded = defaultdict(set) +awarded = {} # awarded[awardee][post_id]: set of verifiers def award_if_appropriate(reaction): if not reaction_qualifies(reaction): return @@ -125,17 +136,28 @@ def award_if_appropriate(reaction): if parse_mm_timestamp(post["create_at"]) < SINCE: return + awardee_id = post["user_id"] awarder_id = reaction["user_id"] - if awarder_id in awarded[post["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[post["id"]].add(awarder_id) + + 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(post["user_id"]) + 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) + update_confirmation(post["id"]) + def retract_if_appropriate(reaction): if not reaction_qualifies(reaction): @@ -145,13 +167,21 @@ def retract_if_appropriate(reaction): if parse_mm_timestamp(post["create_at"]) < SINCE: return + awardee_id = post["user_id"] awarder_id = reaction["user_id"] - awarded[post["id"]].discard(awarder_id) + awarded[awardee_id][post["id"]].discard(awarder_id) - awardee = get_username(post["user_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"]) + def get_posts_for_channel(mmapi, channel_id, since, **kwargs): after = None @@ -171,8 +201,53 @@ def get_posts_for_channel(mmapi, channel_id, since, **kwargs): after = order[-1] +CONFIRMATION_EMOJI_NAMES = "one,two,three,four,five,six,seven,eight,nine,keycap_ten,asterisk".split(",") +def confirmation_emoji_name(count): + if count < 0: + return "exclamation" + try: + return CONFIRMATION_EMOJI_NAMES[count - 1] + except IndexError: + return CONFIRMATION_EMOJI_NAMES[-1] + + +def persevere(f, backoff=1): + while True: + try: + f() + return + except Exception as e: + print(e, file=sys.stderr) + print(f"Trying again in {backoff} second(s)", file=sys.stderr) + sleep(backoff) + + +def update_confirmation(post_id): + if not CONFIRMATION_EMOJI: + return + + post = get_post(post_id, force_fetch=True) + remove_reactions_from_post(post) + + new_count = len(awarded.get(post["user_id"], [])) + + if new_count > 0: + persevere(lambda: mm.create_reaction(our_user_id, post_id, confirmation_emoji_name(new_count))) + + +def remove_reactions_from_post(post): + for reaction in post.get("metadata", {}).get("reactions", []): + if reaction["user_id"] == our_user_id: + persevere(lambda: remove_reaction(post["id"], reaction["emoji_name"])) + + +def remove_reaction(post_id, emoji_name): + mm._delete(f"/v4/users/me/posts/{post_id}/reactions/{emoji_name}") + + def handle_backlog(since): for post in get_posts_for_channel(mm, channel, since): + remove_reactions_from_post(post) for reaction in post.get("metadata", {}).get("reactions", []): award_if_appropriate(reaction) @@ -192,17 +267,20 @@ def handle_live(): sleep(60 * 1000) -live = "--live" in sys.argv[1:] - -# Note: skipping this step and updating an existing file would be dangerous: you would miss revocations that happened while not listening. -handle_backlog(SINCE) - -if live: - print("Now watching for live posts.", file=sys.stderr) - handle_live() +if clean: + for post in get_posts_for_channel(mm, channel, SINCE): + remove_reactions_from_post(post) else: - print("Use --live to keep watching new posts.", file=sys.stderr) + # Note: skipping this step and updating an existing file would be dangerous: you would miss revocations that happened while not listening. + handle_backlog(SINCE) + + if live: + print("Now watching for live posts.", file=sys.stderr) + handle_live() + + else: + print("Use --live to keep watching new posts.", file=sys.stderr) # Logout