def process_claim(post, config): """ Handles comment replies containing the word 'claim' and routes based on a basic decision tree. :param post: The Comment object containing the claim. :param config: the global config dict. :return: None. """ top_parent = get_parent_post_id(post, config.r) # WAIT! Do we actually own this post? if top_parent.author.name != 'transcribersofreddit': logging.debug('Received `claim` on post we do not own. Ignoring.') return try: if not coc_accepted(post, config): # do not cache this page. We want to get it every time. post.reply( _( please_accept_coc.format( get_wiki_page('codeofconduct', config)))) 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 request_transcription(post: PostSummary, content_type: str, content_format: str, cfg: Config) -> None: """Request a transcription by posting the provided post to our subreddit.""" title = i18n["posts"]["discovered_submit_title"].format( sub=str(post["subreddit"]), type=content_type.title(), title=truncate_title(cleanup_post_title(str(post["title"]))), ) permalink = i18n["urls"]["reddit_url"].format(str(post["permalink"])) submission = cfg.tor.submit(title=title, url=permalink) intro = i18n["posts"]["rules_comment"].format( post_type=content_type, formatting=content_format, header=cfg.header, ) submission.reply(_(intro)) flair_post(submission, flair.unclaimed) create_blossom_submission(post, submission, cfg)
def set_meta_flair_on_other_posts(r, tor, config): """ Loops through the 10 newest posts on ToR and sets the flair to 'Meta' for any post that is not authored by the bot or any of the moderators. :param r: Active reddit object :param tor: The Subreddit object for ToR. :param config: the active config object. :return: None. """ for post in tor.new(limit=10): if (post.author != r.redditor('transcribersofreddit') and post.author not in config.tor_mods and post.link_flair_text != flair.meta): logging.info('Flairing post {} by author {} with Meta.'.format( post.fullname, post.author)) flair_post(post, flair.meta)
def process_claim(post, config): """ Handles comment replies containing the word 'claim' and routes based on a basic decision tree. :param post: The Comment object containing the claim. :param config: the global config dict. :return: None. """ top_parent = get_parent_post_id(post, config.r) # WAIT! Do we actually own this post? if top_parent.author.name != 'transcribersofreddit': logging.debug('Received `claim` on post we do not own. Ignoring.') return if not coc_accepted(post, config): # do not cache this page. We want to get it every time. post.reply(_( please_accept_coc.format(get_wiki_page('codeofconduct', config.tor)) )) return if top_parent.link_flair_text is 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( 'Claim on ID {} by {} successful'.format( top_parent.fullname, post.author ) ) # 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))
def process_post(new_post, tor, config): """ After a valid post has been discovered, this handles the formatting and posting of those calls as workable jobs to ToR. :param new_post: Submission object that needs to be posted. :param tor: TranscribersOfReddit subreddit instance. :param config: the config object. :return: None. """ subreddit = new_post.subreddit if subreddit.display_name in config.upvote_filter_subs: # ignore posts if they don't meet the threshold for karma and the sub # is in our list of upvoted filtered ones if new_post.ups < config.upvote_filter_subs[subreddit.display_name]: return if not is_valid(new_post.fullname, config): logging.debug(id_already_handled_in_db.format(new_post.fullname)) return if new_post.archived: return if new_post.author is None: # we don't want to handle deleted posts, that's just silly return logging.info('Posting call for transcription on ID {} posted by {}'.format( new_post.fullname, new_post.author.name)) if new_post.domain in config.image_domains: content_type = 'image' content_format = config.image_formatting elif new_post.domain in config.audio_domains: content_type = 'audio' content_format = config.audio_formatting elif new_post.domain in config.video_domains: if 'youtu' in new_post.domain: if not valid_youtube_video(new_post.url): return if get_yt_transcript(new_post.url): new_post.reply(_(yt_already_has_transcripts)) add_complete_post_id(new_post.fullname, config) logging.info('Found YouTube video, {}, with good transcripts.' ''.format(get_yt_video_id(new_post.url))) return content_type = 'video' content_format = config.video_formatting else: # how could we get here without fulfilling one of the above # criteria? Just remember: the users will find a way. content_type = 'Unknown' content_format = 'Formatting? I think something went wrong here...' # noinspection PyBroadException try: result = tor.submit(title=discovered_submit_title.format( sub=new_post.subreddit.display_name, type=content_type.title(), title=new_post.title), url=reddit_url.format(new_post.permalink)) result.reply( _( rules_comment.format(post_type=content_type, formatting=content_format, header=config.header))) flair_post(result, flair.unclaimed) add_complete_post_id(new_post.fullname, config) config.redis.incr('total_posted', amount=1) if config.OCR and content_type == 'image': # hook for OCR bot; in order to avoid race conditions, we add the # key / value pair that the bot isn't looking for before adding # to the set that it's monitoring. config.redis.set(new_post.fullname, result.fullname) config.redis.rpush('ocr_ids', new_post.fullname) config.redis.incr('total_new', amount=1) # I need to figure out what errors can happen here except Exception as e: logging.error('{} - unable to post content.\n' 'ID: {id}\n' 'Title: {title}\n' 'Subreddit: {sub}'.format( e, id=new_post.fullname, title=new_post.title, sub=new_post.subreddit.display_name))
alt_text_trigger=alt_text, ) elif "!override" in r_body: message, flair = process_override(reply.author, blossom_submission, reply.parent_id, cfg) elif "!debug" in r_body: message, flair = process_debug(reply.author, blossom_submission, cfg) else: # If we made it this far, it's something we can't process automatically forward_to_slack(reply, cfg) if message: send_reddit_reply(reply, message) if flair: flair_post(reply.submission, flair) except (ClientException, AttributeError) as e: # the only way we should hit this is if somebody comments and then # deletes their comment before the bot finished processing. It's # uncommon, but common enough that this is necessary. log.warning(e) log.warning(f"Unable to process comment {reply.submission.shortlink} " f"by {reply.author}") @beeline.traced(name="process_mention") def process_mention(mention: Comment) -> None: """ Handles username mentions and handles the formatting and posting of those calls as workable jobs to ToR.
def process_done(post, config, override=False): """ Handles comments where the user says they've completed a post. Also includes a basic decision tree to enable verification of the posts to try and make sure they actually posted a transcription. :param post: the Comment object which contains the string 'done'. :param config: the global config object. :param override: A parameter that can only come from process_override() and skips the validation check. :return: None. """ top_parent = get_parent_post_id(post, config.r) # WAIT! Do we actually own this post? if top_parent.author.name != 'transcribersofreddit': logging.info('Received `done` on post we do not own. Ignoring.') return try: if flair.unclaimed in top_parent.link_flair_text: post.reply(_(done_still_unclaimed)) elif top_parent.link_flair_text == flair.in_progress: if not override and not verified_posted_transcript(post, config): # we need to double-check these things to keep people # from gaming the system logging.info( f'Post {top_parent.fullname} does not appear to have a ' f'post by claimant {post.author}. Hrm... ' ) # noinspection PyUnresolvedReferences try: post.reply(_(done_cannot_find_transcript)) except praw.exceptions.ClientException as e: # We've run into an issue where someone has commented and # then deleted the comment between when the bot pulls mail # and when it processes comments. This should catch that. # Possibly should look into subclassing praw.Comment.reply # to include some basic error handling of this so that # we can fix it throughout the application. logging.warning(e) return # Control flow: # If we have an override, we end up here to complete. # If there is no override, we go into the validation above. # If the validation fails, post the apology and return. # If the validation succeeds, come down here. if override: logging.info('Moderator override starting!') # noinspection PyUnresolvedReferences try: post.reply(_(done_completed_transcript)) update_user_flair(post, config) logging.info( f'Post {top_parent.fullname} completed by {post.author}!' ) except praw.exceptions.ClientException: # If the butt deleted their comment and we're already this # far into validation, just mark it as done. Clearly they # already passed. logging.info( f'Attempted to mark post {top_parent.fullname} ' f'as done... hit ClientException.' ) flair_post(top_parent, flair.completed) config.redis.incr('total_completed', amount=1) except praw.exceptions.APIException as e: if e.error_type == 'DELETED_COMMENT': logging.info( f'Comment attempting to mark ID {top_parent.fullname} ' f'as done has been deleted' ) return raise # Re-raise exception if not
def process_mention(mention, config): """ Handles username mentions and handles the formatting and posting of those calls as workable jobs to ToR. :param mention: the Comment object containing the username mention. :param config: the global config dict :return: None. """ # We have to do this entire parent / parent_permalink thing twice because # the method for calling a permalink changes for each object. Laaaame. if not mention.is_root: # this comment is in reply to something. Let's grab a comment object. parent = config.r.comment(id=clean_id(mention.parent_id)) parent_permalink = parent.permalink() # a comment does not have a title attribute. Let's fake one by giving # it something to work with. parent.title = 'Unknown Content' else: # this is a post. parent = config.r.submission(id=clean_id(mention.link_id)) parent_permalink = parent.permalink # format that sucker so it looks right in the template. parent.title = '"' + parent.title + '"' # Ignore requests made by the OP of content or the OP of the submission if mention.author == parent.author: logging.info('Ignoring mention by OP u/{} on ID {}'.format( mention.author, mention.parent_id)) return logging.info('Posting call for transcription on ID {}'.format( mention.parent_id)) if is_valid(parent.fullname, config): # we're only doing this if we haven't seen this one before. # noinspection PyBroadException try: result = config.tor.submit(title=summoned_submit_title.format( sub=mention.subreddit.display_name, commentorpost=parent.__class__.__name__.lower(), title=parent.title), url=reddit_url.format(parent_permalink)) result.reply( _(rules_comment_unknown_format.format(header=config.header))) result.reply( _( summoned_by_comment.format( reddit_url.format( config.r.comment(clean_id( mention.fullname)).permalink())))) flair_post(result, flair.summoned_unclaimed) logging.debug( 'Posting success message in response to caller, u/{}'.format( mention.author)) mention.reply( _('The transcribers have been summoned! Please be patient ' 'and we\'ll be along as quickly as we can.')) add_complete_post_id(parent.fullname, config) # I need to figure out what errors can happen here except Exception as e: logging.error( '{} - Posting failure message in response to caller, ' 'u/{}'.format(e, mention.author)) mention.reply(_(something_went_wrong))
def process_post(new_post, config): """ After a valid post has been discovered, this handles the formatting and posting of those calls as workable jobs to ToR. :param new_post: Submission object that needs to be posted. :param config: the config object. :return: None. """ if new_post['subreddit'] in config.upvote_filter_subs: # ignore posts if they don't meet the threshold for karma and the sub # is in our list of upvoted filtered ones if new_post['ups'] < config.upvote_filter_subs[new_post['subreddit']]: return if not is_valid(new_post['name'], config): logging.debug(id_already_handled_in_db.format(new_post['name'])) return if new_post['archived']: return if new_post['author'] is None: # we don't want to handle deleted posts, that's just silly return logging.info( f'Posting call for transcription on ID {new_post["name"]} posted by ' f'{new_post["author"]}') if new_post['domain'] in config.image_domains: content_type = 'image' content_format = config.image_formatting elif new_post['domain'] in config.audio_domains: content_type = 'audio' content_format = config.audio_formatting elif new_post['domain'] in config.video_domains: if 'youtu' in new_post['domain']: if not valid_youtube_video(new_post['url']): add_complete_post_id(new_post['name'], config) return if get_yt_transcript(new_post['url']): np = config.r.submission(id=new_post['name']) np.reply(_(yt_already_has_transcripts)) add_complete_post_id(new_post['name'], config) logging.info( f'Found YouTube video, {get_yt_video_id(new_post["url"])},' f' with good transcripts.') return content_type = 'video' content_format = config.video_formatting else: # This means we pulled from a subreddit bypassing the filters. content_type = 'Other' content_format = config.other_formatting # Truncate a post title if it exceeds 250 characters, so the added # formatting still fits in Reddit's 300 char limit for post titles post_title = new_post['title'] max_title_length = 250 if len(post_title) > max_title_length: post_title = post_title[:max_title_length - 3] + '...' # noinspection PyBroadException try: result = config.tor.submit( title=discovered_submit_title.format(sub=new_post['subreddit'], type=content_type.title(), title=post_title), url=reddit_url.format(new_post['permalink'])) result.reply( _( rules_comment.format(post_type=content_type, formatting=content_format, header=config.header))) flair_post(result, flair.unclaimed) add_complete_post_id(new_post['name'], config) config.redis.incr('total_posted', amount=1) if config.OCR and content_type == 'image': # hook for OCR bot; in order to avoid race conditions, we add the # key / value pair that the bot isn't looking for before adding # to the set that it's monitoring. config.redis.set(new_post['name'], result.fullname) config.redis.rpush('ocr_ids', new_post['name']) config.redis.incr('total_new', amount=1) # The only errors that happen here are on Reddit's side -- pretty much # exclusively 503s and 403s that arbitrarily resolve themselves. A missed # post or two is not the end of the world. except Exception as e: logging.error( f'{e} - unable to post content.\nID: {new_post["name"]}\n ' f'Title: {new_post["title"]}\n Subreddit: ' f'{new_post["subreddit"]}')
def process_unclaim(post, cfg): # Sometimes people need to unclaim things. Usually this happens because of # an issue with the post itself, like it's been locked or deleted. Either # way, we should probably be able to handle it. # Process: # If the post has been reported, then remove it. No checks, just do it. # If the post has not been reported, attempt to load the linked post. # If the linked post is still up, then reset the flair on ToR's side # and reply to the user. # If the linked post has been taken down or deleted, then remove the post # on ToR's side and reply to the user. top_parent = post.submission unclaim_failure_post_already_completed = i18n['responses']['unclaim'][ 'post_already_completed'] unclaim_still_unclaimed = i18n['responses']['unclaim']['still_unclaimed'] unclaim_success = i18n['responses']['unclaim']['success'] unclaim_success_with_report = i18n['responses']['unclaim'][ 'success_with_report'] unclaim_success_without_report = i18n['responses']['unclaim'][ 'success_without_report'] # WAIT! Do we actually own this post? if top_parent.author.name not in __BOT_NAMES__: logging.info('Received `unclaim` on post we do not own. Ignoring.') return if flair.unclaimed in top_parent.link_flair_text: post.reply(_(unclaim_still_unclaimed)) return for item in top_parent.user_reports: if (reports.original_post_deleted_or_locked in item[0] or reports.post_violates_rules in item[0]): top_parent.mod.remove() send_to_modchat( 'Removed the following reported post in response to an ' '`unclaim`: {}'.format(top_parent.shortlink), cfg, channel='removed_posts') post.reply(_(unclaim_success_with_report)) return # Okay, so they commented with unclaim, but they didn't report it. # Time to check to see if they should have. linked_resource = cfg.r.submission(top_parent.id_from_url(top_parent.url)) if is_removed(linked_resource): top_parent.mod.remove() send_to_modchat( 'Received `unclaim` on an unreported post, but it looks like it ' 'was removed on the parent sub. I removed ours here: {}' ''.format(top_parent.shortlink), cfg, channel='removed_posts') post.reply(_(unclaim_success_without_report)) return # Finally, if none of the other options apply, we'll reset the flair and # continue on as normal. if top_parent.link_flair_text == flair.completed: post.reply(_(unclaim_failure_post_already_completed)) return if top_parent.link_flair_text == flair.in_progress: flair_post(top_parent, flair.unclaimed) post.reply(_(unclaim_success)) return