React with count emoji
This commit is contained in:
parent
cf905a51d6
commit
2cf0f5a580
1 changed files with 96 additions and 18 deletions
|
@ -7,6 +7,7 @@ import datetime
|
||||||
import threading
|
import threading
|
||||||
from time import sleep
|
from time import sleep
|
||||||
import json
|
import json
|
||||||
|
from typing import Optional
|
||||||
import mattermost
|
import mattermost
|
||||||
import mattermost.ws
|
import mattermost.ws
|
||||||
|
|
||||||
|
@ -33,6 +34,8 @@ TOKEN = os.getenv("MM_ACCESS_TOKEN")
|
||||||
USER = os.getenv("MM_USERNAME")
|
USER = os.getenv("MM_USERNAME")
|
||||||
PASSWORD = os.getenv("MM_PASSWORD")
|
PASSWORD = os.getenv("MM_PASSWORD")
|
||||||
|
|
||||||
|
CONFIRMATION_EMOJI = True
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
since_arg_i = sys.argv.index("--since")
|
since_arg_i = sys.argv.index("--since")
|
||||||
|
@ -42,8 +45,14 @@ except ValueError:
|
||||||
if since_arg_i:
|
if since_arg_i:
|
||||||
SINCE = datetime.datetime.fromisoformat(sys.argv[since_arg_i + 1])
|
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)
|
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
|
assert PASSWORD
|
||||||
mm.login(USER, PASSWORD)
|
mm.login(USER, PASSWORD)
|
||||||
|
|
||||||
|
our_user_id = mm._my_user_id
|
||||||
|
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
# Get channel
|
# Get channel
|
||||||
|
@ -98,8 +109,8 @@ def get_username(userid):
|
||||||
##################################
|
##################################
|
||||||
# Get posts
|
# Get posts
|
||||||
posts = {}
|
posts = {}
|
||||||
def get_post(postid):
|
def get_post(postid, force_fetch=False):
|
||||||
if postid not in posts:
|
if postid not in posts or force_fetch:
|
||||||
posts[postid] = mm.get_post(postid)
|
posts[postid] = mm.get_post(postid)
|
||||||
|
|
||||||
return posts[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
|
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):
|
def award_if_appropriate(reaction):
|
||||||
if not reaction_qualifies(reaction):
|
if not reaction_qualifies(reaction):
|
||||||
return
|
return
|
||||||
|
@ -125,17 +136,28 @@ def award_if_appropriate(reaction):
|
||||||
if parse_mm_timestamp(post["create_at"]) < SINCE:
|
if parse_mm_timestamp(post["create_at"]) < SINCE:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
awardee_id = post["user_id"]
|
||||||
awarder_id = reaction["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
|
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")
|
reaction_time = parse_mm_timestamp(reaction["create_at"]).isoformat(timespec="microseconds")
|
||||||
post_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)
|
awarder = get_username(awarder_id)
|
||||||
print(f"{awardee} {post['id']} at {post_time} verified by {awarder} at {reaction_time}", flush=True)
|
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):
|
def retract_if_appropriate(reaction):
|
||||||
if not reaction_qualifies(reaction):
|
if not reaction_qualifies(reaction):
|
||||||
|
@ -145,13 +167,21 @@ def retract_if_appropriate(reaction):
|
||||||
if parse_mm_timestamp(post["create_at"]) < SINCE:
|
if parse_mm_timestamp(post["create_at"]) < SINCE:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
awardee_id = post["user_id"]
|
||||||
awarder_id = reaction["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)
|
awarder = get_username(awarder_id)
|
||||||
print(f"{awardee} {post['id']} verification removed by {awarder}", flush=True)
|
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):
|
def get_posts_for_channel(mmapi, channel_id, since, **kwargs):
|
||||||
after = None
|
after = None
|
||||||
|
@ -171,8 +201,53 @@ def get_posts_for_channel(mmapi, channel_id, since, **kwargs):
|
||||||
after = order[-1]
|
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):
|
def handle_backlog(since):
|
||||||
for post in get_posts_for_channel(mm, channel, since):
|
for post in get_posts_for_channel(mm, channel, since):
|
||||||
|
remove_reactions_from_post(post)
|
||||||
for reaction in post.get("metadata", {}).get("reactions", []):
|
for reaction in post.get("metadata", {}).get("reactions", []):
|
||||||
award_if_appropriate(reaction)
|
award_if_appropriate(reaction)
|
||||||
|
|
||||||
|
@ -192,16 +267,19 @@ def handle_live():
|
||||||
sleep(60 * 1000)
|
sleep(60 * 1000)
|
||||||
|
|
||||||
|
|
||||||
live = "--live" in sys.argv[1:]
|
if clean:
|
||||||
|
for post in get_posts_for_channel(mm, channel, SINCE):
|
||||||
|
remove_reactions_from_post(post)
|
||||||
|
|
||||||
# Note: skipping this step and updating an existing file would be dangerous: you would miss revocations that happened while not listening.
|
else:
|
||||||
handle_backlog(SINCE)
|
# 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:
|
if live:
|
||||||
print("Now watching for live posts.", file=sys.stderr)
|
print("Now watching for live posts.", file=sys.stderr)
|
||||||
handle_live()
|
handle_live()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("Use --live to keep watching new posts.", file=sys.stderr)
|
print("Use --live to keep watching new posts.", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue