def populate_subreddit_lists(cfg: Config) -> None: """ Gets the list of subreddits to monitor and loads it into memory. :return: None. """ cfg.subreddits_to_check = get_wiki_page("subreddits", cfg).splitlines() cfg.subreddits_to_check = clean_list(cfg.subreddits_to_check) log.debug( f"Created list of subreddits from wiki: {cfg.subreddits_to_check}") for line in get_wiki_page("subreddits/upvote-filtered", cfg).splitlines(): if "," in line: sub, threshold = line.split(",") cfg.upvote_filter_subs[sub] = int(threshold) log.debug( f"Retrieved subreddits subject to the upvote filter: {cfg.upvote_filter_subs}" ) cfg.subreddits_domain_filter_bypass = clean_list( get_wiki_page("subreddits/domain-filter-bypass", cfg).splitlines()) log.debug( f"Retrieved subreddits that bypass the domain filter: {cfg.subreddits_domain_filter_bypass}" ) cfg.no_link_header_subs = clean_list( get_wiki_page("subreddits/no-link-header", cfg).splitlines()) log.debug( f"Retrieved subreddits subject to the upvote filter: {cfg.no_link_header_subs}" )
def populate_formatting(cfg: Config) -> None: """ Grabs the contents of the three wiki pages that contain the formatting examples and stores them in the cfg object. :return: None. """ cfg.audio_formatting = get_wiki_page("formats/audio", cfg) cfg.video_formatting = get_wiki_page("formats/video", cfg) cfg.image_formatting = get_wiki_page("format/images", cfg)
def populate_domain_lists(cfg): """ Loads the approved content domains into the config object from the wiki page. :return: None. """ cfg.video_domains = [] cfg.image_domains = [] cfg.audio_domains = [] domains = get_wiki_page('domains', cfg) domains = ''.join(domains.splitlines()).split('---') for domainset in domains: domain_list = domainset[domainset.index('['):].strip('[]').split(', ') current_domain_list = [] if domainset.startswith('video'): current_domain_list = cfg.video_domains elif domainset.startswith('audio'): current_domain_list = cfg.audio_domains elif domainset.startswith('images'): current_domain_list = cfg.image_domains current_domain_list += domain_list # [current_domain_list.append(x) for x in domain_list] logging.debug(f'Domain list populated: {current_domain_list}')
def populate_formatting(cfg): """ Grabs the contents of the three wiki pages that contain the formatting examples and stores them in the cfg object. :return: None. """ # zero out everything so we can reinitialize later cfg.audio_formatting = '' cfg.video_formatting = '' cfg.image_formatting = '' cfg.other_formatting = '' cfg.audio_formatting = get_wiki_page('format/audio', cfg) cfg.video_formatting = get_wiki_page('format/video', cfg) cfg.image_formatting = get_wiki_page('format/images', cfg) cfg.other_formatting = get_wiki_page('format/other', cfg)
def populate_subreddit_lists(cfg): """ Gets the list of subreddits to monitor and loads it into memory. :return: None. """ cfg.subreddits_to_check = [] cfg.upvote_filter_subs = {} cfg.no_link_header_subs = [] cfg.subreddits_to_check = get_wiki_page('subreddits', cfg).splitlines() cfg.subreddits_to_check = clean_list(cfg.subreddits_to_check) logging.debug( f'Created list of subreddits from wiki: {cfg.subreddits_to_check}') for line in get_wiki_page('subreddits/upvote-filtered', cfg).splitlines(): if ',' in line: sub, threshold = line.split(',') cfg.upvote_filter_subs[sub] = int(threshold) logging.debug(f'Retrieved subreddits subject to the upvote filter: ' f'{cfg.upvote_filter_subs} ') cfg.subreddits_domain_filter_bypass = get_wiki_page( 'subreddits/domain-filter-bypass', cfg).split('\r\n') cfg.subreddits_domain_filter_bypass = clean_list( cfg.subreddits_domain_filter_bypass) logging.debug(f'Retrieved subreddits that bypass the domain filter: ' f'{cfg.subreddits_domain_filter_bypass} ') cfg.no_link_header_subs = get_wiki_page('subreddits/no-link-header', cfg).split('\r\n') cfg.no_link_header_subs = clean_list(cfg.no_link_header_subs) logging.debug(f'Retrieved subreddits subject to the upvote filter: ' f'{cfg.no_link_header_subs} ') lines = get_wiki_page('subreddits/archive-time', cfg).splitlines() cfg.archive_time_default = int(lines[0]) cfg.archive_time_subreddits = {} for line in lines[1:]: if ',' in line: sub, time = line.split(',') cfg.archive_time_subreddits[sub.lower()] = int(time)
def process_coc(username: str, context: str, blossom_submission: Dict, cfg: Config) -> Tuple: """ Process the acceptation of the CoC by the specified user. :param username: The name of the user accepting the CoC :param context: The context of the reply, to use as a link :param blossom_submission: The corresponding Submission in Blossom :param cfg: Config of tor """ user_response = cfg.blossom.get_user(username=username) if user_response.status == BlossomStatus.ok: # The status codes of accepting the CoC are not checked because they are already # caught by getting the user. response = cfg.blossom.accept_coc(username=username) new_acceptance = response.status == BlossomStatus.ok if new_acceptance: emote = random.choice(MODCHAT_EMOTES) user_url = i18n["urls"]["reddit_url"].format(f"/u/{username}") post_url = i18n["urls"]["reddit_url"].format(context) send_to_modchat( f"<{user_url}|u/{username}> has just " f"<{post_url}|accepted the CoC!> {emote}", cfg, channel="CAZN8J078", ) return process_claim(username, blossom_submission, cfg, first_time=new_acceptance) elif user_response.status == BlossomStatus.not_found: cfg.blossom.create_user(username=username) return ( i18n["responses"]["general"]["coc_not_accepted"].format( get_wiki_page("codeofconduct", cfg)), None, ) else: return process_claim(username, blossom_submission, cfg)
def process_unclaim(username: str, blossom_submission: Dict, submission: Submission, cfg: Config) -> Tuple: """ Process an unclaim request. Note that this function also checks whether a post should be removed and does so when required. :param username: The name of the user unclaiming the submission :param blossom_submission: The relevant Submission of Blossom :param submission: The relevant Submission in Reddit :param cfg: Config of tor """ response = cfg.blossom.unclaim(submission_id=blossom_submission["id"], username=username) return_flair = None unclaim_messages = i18n["responses"]["unclaim"] if response.status == BlossomStatus.ok: message = unclaim_messages["success"] return_flair = flair.unclaimed removed, reported = remove_if_required(submission, blossom_submission["id"], cfg) if removed: # Select the message based on whether the post was reported or not. message = unclaim_messages["success_with_report" if reported else "success_without_report"] elif response.status == BlossomStatus.not_found: message = i18n["responses"]["general"]["coc_not_accepted"].format( get_wiki_page("codeofconduct", cfg)) cfg.blossom.create_user(username) elif response.status == BlossomStatus.other_user: message = unclaim_messages["claimed_other_user"] elif response.status == BlossomStatus.already_completed: message = unclaim_messages["post_already_completed"] elif response.status == BlossomStatus.blacklisted: message = i18n["responses"]["general"]["blacklisted"] else: message = unclaim_messages["still_unclaimed"] return message, return_flair
def populate_domain_lists(cfg: Config) -> None: """ Loads the approved content domains into the config object from the wiki page. :return: None. """ domain_string = get_wiki_page("domains", cfg) domains = "".join(domain_string.splitlines()).split("---") for domainset in domains: domain_list = domainset[domainset.index("["):].strip("[]").split(", ") current_domain_list = [] if domainset.startswith("video"): current_domain_list = cfg.video_domains elif domainset.startswith("audio"): current_domain_list = cfg.audio_domains elif domainset.startswith("images"): current_domain_list = cfg.image_domains current_domain_list += domain_list # [current_domain_list.append(x) for x in domain_list] log.debug(f"Domain list populated: {current_domain_list}")
def populate_header(cfg: Config) -> None: cfg.header = get_wiki_page("format/header", cfg)
def populate_gifs(cfg: Config) -> None: cfg.no_gifs = get_wiki_page("usefulgifs/no", cfg).splitlines()
def process_claim(username: str, blossom_submission: Dict, cfg: Config, first_time=False) -> Tuple: """ Process a claim request. This function sends a reply depending on the response from Blossom and creates an user when this is the first time a user uses the bot. :param username: Name of the user claiming the submission :param blossom_submission: The relevant submission in Blossom :param cfg: Config of tor :param first_time: Whether this is the first time a user claims something """ coc_not_accepted = i18n["responses"]["general"]["coc_not_accepted"] response = cfg.blossom.claim(submission_id=blossom_submission["id"], username=username) return_flair = None if response.status == BlossomStatus.ok: # A random tip to append to the response random_tip = i18n["tips"]["message"].format( tip_message=random.choice(i18n["tips"]["collection"])) message = (i18n["responses"]["claim"] ["first_claim_success" if first_time else "success"] + "\n\n" + random_tip) return_flair = flair.in_progress log.info( f'Claim on Submission {blossom_submission["tor_url"]} by {username} successful.' ) elif response.status == BlossomStatus.coc_not_accepted: message = coc_not_accepted.format(get_wiki_page("codeofconduct", cfg)) elif response.status == BlossomStatus.not_found: message = coc_not_accepted.format(get_wiki_page("codeofconduct", cfg)) cfg.blossom.create_user(username=username) elif response.status == BlossomStatus.blacklisted: message = i18n["responses"]["general"]["blacklisted"] elif response.status == BlossomStatus.already_claimed: claimed_by = response.data["username"] if claimed_by == username: # This user already got the submission message = i18n["responses"]["claim"]["already_claimed_by_self"] else: # The submission was claimed by someone else message = i18n["responses"]["claim"][ "already_claimed_by_someone"].format(claimed_by=claimed_by) elif response.status == BlossomStatus.too_many_claims: claimed_links = [submission["tor_url"] for submission in response.data] message = i18n["responses"]["claim"]["too_many_claims"].format( links="\n".join(f"- {link}" for link in claimed_links), ) else: message = i18n["responses"]["general"]["oops"] return message, return_flair
def process_done( user: Redditor, blossom_submission: Dict, comment: Comment, cfg: Config, override=False, alt_text_trigger=False, ) -> Tuple: """ Handles comments where the user claims to have completed a post. This function sends a reply to the user depending on the responses received from Blossom. :param user: The user claiming his transcription is done :param blossom_submission: The relevant submission in Blossom :param comment: The comment of the user, used to retrieve the user's flair :param cfg: the global config object. :param override: whether the validation check should be skipped :param alt_text_trigger: whether there is an alternative to "done" that has triggered this function. """ return_flair = None done_messages = i18n["responses"]["done"] # This is explicitly missing the format call that adds the code of # conduct text because if we populate it here, we will fetch the wiki # page on _every single `done`_ and that's just silly. Only populate # it if it's necessary. coc_not_accepted = i18n["responses"]["general"]["coc_not_accepted"] blossom_user = cfg.blossom.get_user(username=user.name) if blossom_user.status != BlossomStatus.ok: # If we don't know who the volunteer is, then we don't have a record of # them and they need to go through the code of conduct process. return ( coc_not_accepted.format(get_wiki_page("codeofconduct", cfg)), return_flair, ) if not blossom_user.data["accepted_coc"]: # If the volunteer in question hasn't accepted the code of conduct, # eject early and return. Although the `create_transcription` endpoint # returns a code of conduct check, we only hit it when we create a # transcription, which requires that they wrote something. If a volunteer # just writes `done` without putting a transcription down, it will hit # this edge case. return ( coc_not_accepted.format(get_wiki_page("codeofconduct", cfg)), return_flair, ) transcription, is_visible = get_transcription(blossom_submission["url"], user, cfg) message = done_messages["cannot_find_transcript"] # default message if not transcription: # When the user replies `done` quickly after posting the transcription, # it might not be available on Reddit yet. Wait a bit and try again. time.sleep(1) transcription, is_visible = get_transcription( blossom_submission["url"], user, cfg) if transcription and not override: # Try to detect common formatting errors formatting_errors = check_for_formatting_issues(transcription.body) if len(formatting_errors) > 0: # Formatting issues found. Reject the `done` and ask the # volunteer to fix them. issues = ", ".join([error.value for error in formatting_errors]) # TODO: Re-evaluate if this is necessary # This is more of a temporary thing to see how the # volunteers react to the bot. send_to_modchat( i18n["mod"]["formatting_issues"].format( author=user.name, issues=issues, link=f"https://reddit.com{comment.context}", ), cfg, "formatting-issues", ) message = get_formatting_issue_message(formatting_errors) return message, return_flair if transcription: cfg.blossom.create_transcription( transcription.id, transcription.body, i18n["urls"]["reddit_url"].format(str(transcription.permalink)), transcription.author.name, blossom_submission["id"], not is_visible, ) if transcription or override: # because we can enter this state with or without a transcription, it # makes sense to have this as a separate block. done_response = cfg.blossom.done(blossom_submission["id"], user.name, override) # Note that both the not_found and coc_not_accepted status are already # caught in the previous lines of code, hence these are not checked again. if done_response.status == BlossomStatus.ok: return_flair = flair.completed set_user_flair(user, comment, cfg) log.info( f'Done on Submission {blossom_submission["tor_url"]} by {user.name}' f" successful.") message = done_messages["completed_transcript"] transcription_count = blossom_user.data["gamma"] + 1 if check_promotion(transcription_count): additional_message = generate_promotion_message( transcription_count) message = f"{message}\n\n{additional_message}" if alt_text_trigger: message = f"I think you meant `done`, so here we go!\n\n{message}" elif done_response.status == BlossomStatus.already_completed: message = done_messages["already_completed"] elif done_response.status == BlossomStatus.missing_prerequisite: message = done_messages["not_claimed_by_user"] elif done_response.status == BlossomStatus.blacklisted: message = i18n["responses"]["general"]["blacklisted"] return message, return_flair
def process_claim(post, cfg, first_time=False): """ Handles comment replies containing the word 'claim' and routes based on a basic decision tree. :param post: The Comment object containing the claim. :param cfg: the global config dict. :return: None. """ top_parent = get_parent_post_id(post, cfg.r) already_claimed = i18n['responses']['claim']['already_claimed'] claim_already_complete = i18n['responses']['claim']['already_complete'] please_accept_coc = i18n['responses']['general']['coc_not_accepted'] if first_time: claim_success = i18n['responses']['claim']['first_claim_success'] else: claim_success = i18n['responses']['claim']['success'] # WAIT! Do we actually own this post? if top_parent.author.name not in __BOT_NAMES__: logging.debug('Received `claim` on post we do not own. Ignoring.') return try: if not coc_accepted(post, cfg): # do not cache this page. We want to get it every time. post.reply( _(please_accept_coc.format(get_wiki_page('codeofconduct', cfg)))) return # this can be either '' or None depending on how the API is feeling # today if top_parent.link_flair_text in ['', None]: # There exists the very small possibility that the post was # malformed and doesn't actually have flair on it. In that case, # let's set something so the next part doesn't crash. flair_post(top_parent, flair.unclaimed) if flair.unclaimed in top_parent.link_flair_text: # need to get that "Summoned - Unclaimed" in there too post.reply(_(claim_success)) flair_post(top_parent, flair.in_progress) logging.info( f'Claim on ID {top_parent.fullname} by {post.author} successful' ) # can't claim something that's already claimed elif top_parent.link_flair_text == flair.in_progress: post.reply(_(already_claimed)) elif top_parent.link_flair_text == flair.completed: post.reply(_(claim_already_complete)) except praw.exceptions.APIException as e: if e.error_type == 'DELETED_COMMENT': logging.info( f'Comment attempting to claim ID {top_parent.fullname} has ' f'been deleted. Back up for grabs! ') return raise # Re-raise exception if not
def populate_header(cfg): cfg.header = '' cfg.header = get_wiki_page('format/header', cfg)
def populate_gifs(cfg): # zero it out so we can load more cfg.no_gifs = [] cfg.no_gifs = get_wiki_page('usefulgifs/no', cfg).split('\r\n')