* 28 June 2015: **Promotion to Lieutenant Commander** for PotW * 21 May 2015: **Contribution towards Lieutenant Commander** for DELPHI entry * 26 April 2015: **Promoted to Lieutenant** for PotW win * 29 March 2015: **Promoted to Lieutenant j.g.** for PotW win * 4 January 2015: **Promoted to Ensign** for PotW win ## Section that ought to be ignored. powt * Post of the week for blub """,(7, 2, 9)], [ """ """, None], [""" #### Service Record * 11 September 2016: **Post of the Week** * 28 December 2014: **Promoted to Lieutenant j.g.** for Post of the Week * 11 March 2013: **Commissioned at Ensign.** * Post of the week for this invalid line of a continued service record after a commmission. """, None] ] for testcase in testcases: ret = RankUtils.analyzeServiceRecord(testcase[0].split("\n")) print("Got \"{}\", expected \"{}\"".format(ret, testcase[1]))
def step2(self, state, input, subreddit): """Post of the Week Step 2 - Calculate Promotions""" if len(input) < 1 or len(input) > 2: state["error"] = "Your comment has a line number other than two or one. Please keep to the aforementioned syntax." return state if not input[0].lower().startswith("potw "): state["error"] = "Your comments first line must start with 'POTW '" return state potwIndices = input[0][len("POTW "):].split(" ") ecIndices = [] if len(input) == 2: if not input[1].lower().startswith("ec "): state["error"] = "Your comments second line must start with 'EC '" return state ecIndices = input[1][len("EC "):].split(" ") # important: user-facing indices are in [1,n+1], move them back to [0,n], also convert them to numbers try: potwIndices = [int(x)-1 for x in potwIndices] ecIndices = [int(x)-1 for x in ecIndices] # remove duplicates potwIndices = list(set(potwIndices)) ecIndices = list(set(ecIndices)) if any([x < 0 or x >= len(state["voteEntries"]) for x in potwIndices]): raise ValueError("") if any([x < 0 or x >= len(state["voteEntries"]) for x in ecIndices]): raise ValueError("") except: return {"error": "Some of the indices you provided were not numbers, or not within the required range."} if len(set(ecIndices) ^ set(potwIndices)) != len(ecIndices) + len(potwIndices): return {"error": "No post can win BOTH Exemplary Contribution and Post of the Week."} # Assemble the winners and candidates for the CPO position cpoCandidates = [] # list of (user, current_flair_css) winnersInfo = [] for idx,voteEntry in enumerate(state["voteEntries"]): # Do not check for winning-state in a misguided attempt at optimization here, need to go through it all to be # able to see if anyone's rank entitles them to a CPO promotion. winnerInfo = RedditUtils.findFirstLinkedObject(voteEntry.body, subreddit, self.r) if winnerInfo is None: state["error"] = "Couldn't find the thread/comment referenced by the vote entry {}\n\n".format(idx) return state linkText, linkURL, contributionObject = winnerInfo # get the user from the vote comment match = re.search(r'\/u\/(.*?)\s', voteEntry.body, re.IGNORECASE) if match is None: state["error"] = "Couldn't find the username from the vote comment for '{}'\n\n".format(voteEntry.body) return state # ensure that they are both the same username = match.group(1).strip() if contributionObject.author is None or username.lower() != contributionObject.author.name.lower(): state["error"] = ("Username in the voting thread doesn't match the username of the nominated object." + " Username: {}, Nominated Object: {}").format(username, idx) return state winnerUser = contributionObject.author # returns dict {"flair_css_class":?, "flair_text":?, "user":?} winnerFlair = self.r.get_flair(subreddit, winnerUser) winnerCurrentRank = RankUtils.getRankLevelFromFlair(winnerFlair["flair_css_class"]) # cpo possible at all? if winnerCurrentRank in RankUtils.cpoStepRanks: cpoCandidates.append((winnerUser, winnerFlair["flair_css_class"] if winnerFlair["flair_css_class"] is not None else "")) logging.info("CPO Candidate: " + winnerUser.name) if not idx in potwIndices+ecIndices: continue logging.info("{} - {} - {}".format(winnerUser.name, winnerCurrentRank, RankUtils.rankLevels[winnerCurrentRank]["name"])) winnersInfo.append({"user": winnerUser, "currentRank": winnerCurrentRank, "promotionKind": "potw" if idx in potwIndices else "ec", "comment": voteEntry, "text": winnerInfo[0]}) notes = "" message = "" finalWinnerInfo = [] # now that we have all the winners, lets see who gets promoted to what for wi in winnersInfo: # calculate the promotion of that person, to that end get his service # record username = wi["user"].name careerPotW = 0 careerDelphi = 0 total = 0 serviceRecord = self.r.get_wiki_page(subreddit, username) # check if the record exists. This combersome way currently seems to be # the only working one pageExists = False try: serviceRecord.content_md pageExists = True except: pageExists = False temporaryNoPromotion = False if serviceRecord is None or pageExists is False: if wi["currentRank"] >= RankUtils.serviceRecordThresholdRank: # Ensign ought to have a page ... notes += "* Person /u/{} ought to have a service record, but hasn't gotten one. Setting them to not promoted temporarily, **please review and, if applicable, change this**.\n".format(username) temporaryNoPromotion = True else: # split into lines lines = serviceRecord.content_md.split("\n") ret = RankUtils.analyzeServiceRecord(lines) if ret is None: notes += "* Couldn't read the [service record](https://reddit.com/r/DaystromInstitute/wiki/{}) of user /u/{}. Setting them to not promoted temporarily, **please review and, if applicable, change this**.\n".format(username, username) temporaryNoPromotion = True else: careerPotW, careerDelphi, total = ret logging.info("Analyzed the service record of /u/{}. They have {} PotW, {} DELPHI totalling {}".format(username, careerPotW, careerDelphi, total)) # check the next promotion total += 1 # if they were promoted careerPotW += 1 resDict = {"serviceRecord": serviceRecord, "user": wi["user"], "currentRank": RankUtils.rankLevels[wi["currentRank"]], "promotionKind": wi["promotionKind"], "comment": wi["comment"], "text": wi["text"]} res = None newRank = wi["currentRank"] if temporaryNoPromotion: res = (False, "") elif wi["currentRank"] < 3: # crewman, cpo and citizen get autopromoted to ensign on PotW res = (True, "") newRank = RankUtils.stepFinishRank # ensign elif wi["currentRank"] < len(RankUtils.rankLevels) - 1: nextRank = RankUtils.rankLevels[wi["currentRank"] + 1] res = nextRank["advanceFunction"](total, careerPotW, careerDelphi) newRank = newRank + 1 else: res = (False, "") resDict["willBePromoted"] = res[0] resDict["promoteAftermessage"] = res[1] resDict["promoteTo"] = RankUtils.rankLevels[newRank] finalWinnerInfo.append(resDict) # format this for the message message += "\n\n" message += formatFinalWinnerInfo(finalWinnerInfo) message += "\nPlease note: \n\n" message += notes message += "\n" + InfoMessages.step2 self.messageHandler.dispatchOutput(message) # put it into the state state["finalWinnerInfo"] = finalWinnerInfo state["cpoCandidates"] = cpoCandidates state["reminders"].append(notes) return state
def step3(self, state, input, subreddit): """PotW Step 3 - Re-interpret promotions.""" r = self.r while True: message = "" removeIdxs = [] # remove those only after doing the other changes so that the indices dont get mixed up for line in input: if not line.strip(): continue parts = line.split(")") num = 0 try: num = int(parts[0]) except: message += "* Couldn't understand entry: " + line + "\n" continue num-=1 # CONVERT TO INTERNAL REPRESENTATION if num >= len(state["finalWinnerInfo"]) or num < 0: message += "* The entry id has to be one of the numbers in the last message in: " + line + "\n" continue if len(parts) < 2: message += "* The entry here lacks a command verb: " + line + "\n" continue # get the verb cmd = parts[1].lower().strip() if cmd.startswith("remove"): removeIdxs.append(num) elif cmd.startswith("change "): rankNameLower = cmd[len("change "):].lower().strip() newRank = RankUtils.getRankByName(rankNameLower) if newRank is None: message += "Couldn't find the rank " + \ rankNameLower + " in [{}]".format(num) continue state["finalWinnerInfo"][num]["promoteTo"] = newRank if state["finalWinnerInfo"][num]["promoteTo"] != state["finalWinnerInfo"][num]["currentRank"]: state["finalWinnerInfo"][num]["willBePromoted"] = True else: state["finalWinnerInfo"][num]["willBePromoted"] = False logging.info("Changed to newRank.") else: message += "* Warning: Did not understand the command '{}'".format(cmd) removeIdxs = sorted(removeIdxs, reverse=True) for idx in removeIdxs: del state["finalWinnerInfo"][idx] logging.info("Removed {}".format(idx)) message += "\n\n" message += formatFinalWinnerInfo(state["finalWinnerInfo"]) message += "\n\n" message += InfoMessages.promotionsCorrected self.messageHandler.dispatchOutput(message) message = "" inpt = self.waitForNewInstructions() if inpt is not None and len(inpt) > 0 and inpt[0].lower().startswith("okay"): break input = inpt message="Notes (If there's anything in this section, please heed the message):" # get the nominations nominationsThread = state["nominationsThread"] nominations = [] for comment in nominationsThread.comments: if not comment.is_root: message += "* Ignored [comment]({}) because it was not a root comment in the nominations thread.\n\n".format(comment.permalink) continue if comment.author is None or comment.author.name is None: message += "* Ignored deleted [comment]({}) in the nominations thread.\n\n".format(comment.permalink) continue if comment.banned_by is not None: message += "* Ignored removed [comment]({}) in the nominations thread.\n\n".format(comment.permalink) continue winnerInfo = RedditUtils.findFirstLinkedObject(comment.body, subreddit, r) if winnerInfo is None: message += "* Couldn't find the thread/comment referenced by the [nomination entry]({}). **Please handle this manually LATER. DON'T Forget.\n\n** ".format( comment.permalink) continue if winnerInfo[2].author is None: message += "* The comment linked to by this [nomination entry]({}) appears to have been deleted. **Please handle this manually LATER. DON'T Forget.\n\n** ".format( comment.permalink) continue # get the user from the nomination comment username = "" match = re.search(r'\/u\/(.*?)\s', comment.body, re.IGNORECASE) if match is None: message += ("* The user in [this]({}) nomination comment didn't specify a username. Better double-check" + " that they at least got the permalink right and didn't accidentially link the thread. " + "If they did mess up, remove the entry from the bots nominations in the next step and add it to the vote thread after the bot has finished. **DON'T Forget**\n\n** ").format(comment.permalink) username = winnerInfo[2].author.name else: username = match.group(1).strip() # ensure that they are both the same - reddit usernames are case # insensitive if username.lower() != winnerInfo[2].author.name.lower(): message += "This [nomination entry]{}'s link doesn't match the user they nominated. **Please handle this manually LATER (after the bot has finished). DON'T Forget.\n\n** ".format(comment.permalink) continue nominations.append({"name": winnerInfo[2].author.name, "reason": winnerInfo[ 0], "permalink": winnerInfo[1]}) state["nominations"] = nominations state["messageCarryover"] = message return state
def handleComment(comment, r): # check if this triggers the bot lowerBody = comment.body.lower() if not (any(x in lowerBody for x in keywordsM5) and any(x in lowerBody for x in keywordsNominate)): return logging.info("Handling comment {}".format(comment.permalink)) # don't reply to our own stuff if comment.author is not None and comment.author.name == NominationsSettings.m5Username: return # don't handle things twice if comment.replies is not None: for cmt in comment.replies: if cmt.author is not None and cmt.author.name == NominationsSettings.m5Username: logging.info("Didn't act on {} because there was already a m-5 reply.") return # get the parent thread thread = comment.submission if thread is None: return if thread.title.lower().find("now nominate posts of the week") != -1: logging.info("Ignored comment in nomination bot announcement") return # check if the nomination was part of a quote (at least one of both must be outside of a quote) bodyLines = comment.body.lower().split("\n") # split by paragraphs for line in bodyLines: line = line.strip() outsideNominate = False for phrase in keywordsNominate: for line in bodyLines: if phrase in line and not line.startswith(">"): # check for negation to be extra certain lineChanged = line.replace("'", "") occurrence = lineChanged.find(phrase) if occurrence > 9: if lineChanged[occurrence-9:occurrence].startswith("shouldnt "): continue if occurrence > 5: if lineChanged[occurrence-5:occurrence].startswith("dont "): continue if lineChanged[occurrence-5:occurrence].startswith("wont "): continue if occurrence > 4: if lineChanged[occurrence-4:occurrence].startswith("not "): continue outsideNominate = True outsideM5 = False for phrase in keywordsM5: for line in bodyLines: if phrase in line and not line.startswith(">"): outsideM5 = True if not (outsideM5 and outsideNominate): logging.info("Turned out to be in quote.") return # check that the nominations keywords are in the same sentence additionalChecks = ["'{}'", '"{}"', "({})", "{{{}}}", "[{}]"] sentenceSplitRegex = "[.!;]" # this and thee resulting idx are used later sentences = re.split(sentenceSplitRegex, lowerBody) nominateSentences = [] for phrase in keywordsNominate: sentenceIdx = 0; for line in sentences: if phrase in line and not any(x.format(phrase) in line for x in additionalChecks): nominateSentences.append(sentenceIdx) sentenceIdx += 1 m5Sentences = [] for phrase in keywordsM5: sentenceIdx = 0; for line in sentences: if phrase in line and not any(x.format(phrase) in line for x in additionalChecks): m5Sentences.append(sentenceIdx) sentenceIdx += 1 found = False whichSentence = 0 for i in nominateSentences: if i in m5Sentences: found = True whichSentence = i break if not found: logging.info("Keywords weren't in the same sentence") return # check if the thread is flaired meta if thread.title is not None and thread.title.lower().startswith("meta"): return "You may not nominate anything for PotW in Meta threads." # so what's the nominated item? nominated = comment.submission if comment.is_root else r.get_info(thing_id=comment.parent_id) # either its a submission or a comment, but that doesn't matter at this stage, both have the author-property set # keep this in mind in the code below and only access properties that both share without further checks if nominated is None or nominated.author is None: return "The comment you want to nominate appears to have been deleted." # we don't want deleted comments to be nominated for PotW # check authorship if nominated.author.name == comment.author.name and not NominationsSettings.m5Subreddit == "DaystromFlairTest": # for testing. return "You may not nominate your own content for PotW." # don't nominate M-5 stuff if nominated.author.name == "M-5": # check if there is chaining if "This unit is not susceptible to flattery. You may not nominate its comments for Post of the Week.".lower().strip() in nominated.body.strip().lower(): logging.info("Ignore chained comments.") return return "This unit is not susceptible to flattery. You may not nominate its comments for Post of the Week." # check if the nomination was removed by a mod if nominated.banned_by is not None: return "You may not nominate a comment that has been removed by a moderator." # don't nominate distinguished comment if nominated.distinguished == "moderator": return "You can't nominate comments made by moderators in an official capacity for PotW." # try to get a link to the current nomination thread nominationsThread = None sidebarText = r.get_wiki_page(NominationsSettings.m5Subreddit, 'config/sidebar').content_md # cheaper than regex idx = sidebarText.find(nominateSidebarPhrase) if idx == 0: # no nominations link in the sidebar, what now if sidebarText.find("CLOSED]") != -1: return "Sorry, but the current Post of the Week is being processed right now. Please wait an hour or so and try again. Thanks for understanding!" # no nominations link, no closed vote, this is getting sad logging.info("Just couldn't find the damn nominations thread. -.-") return else: linkStart = idx + len(nominateSidebarPhrase) endIdx = sidebarText.find(")", linkStart) if endIdx == -1: logging.info("Just couldn't find the damn nominations thread. -.- (endIdx)") return link = sidebarText[linkStart:endIdx] nominationsThreadIdMatch = re.search(r'\/([0-9A-Z]{5,8})(\/[^\/]*\/?)?$', link, re.IGNORECASE) if nominationsThreadIdMatch is None: logging.info("Just couldn't find the damn nominations thread. -.- (Regex)") return nominationsThreadId = nominationsThreadIdMatch.group(1) subreddit = r.get_subreddit(NominationsSettings.m5Subreddit) if subreddit is None: logging.info("Subreddit is none.") return nominationsThread = r.get_submission(submission_id=nominationsThreadId) if nominationsThread is None: logging.info("No nominations thread at the end.") return if nominationsThread.subreddit.display_name != NominationsSettings.m5Subreddit: logging.info("Somehow we ended up in the wrong subreddit. " + nominationsThreadId + " " + nominationsThread.subreddit.display_name) return # now that we have a nomination thread cnt = checkNominationsThread(nominated, nominationsThread) if cnt is not None: return cnt; # if that had objections, pass them on # safety check, was the bot active here before? if comment.replies is not None: for c in comment.replies: if c is not None and c.author is not None and c.author.name == NominationsSettings.m5Username: logging.info("We were here before. " + c.permalink) return # Cool, an actual nomination that passed all our tests logging.info('Nominating: /u/'+nominated.author.name+" for [this]("+nominated.permalink+")" + " | Source: " + comment.permalink + " by " + comment.author.name) # can we find out a good text for the nomination? nominationReason = None possibleReason = re.split(sentenceSplitRegex, comment.body)[whichSentence] possibleReson2 = possibleReason possibleReason = possibleReason.strip() if possibleReason.lower().startswith("this "): # gracefully work around decent grammar possibleReason = possibleReason[len("this "):] if "with context" in possibleReason: possibleReason = possibleReason[:possibleReason.find("with context")] idx2 = possibleReason.lower().find("for ") if idx2 != -1: # gracefully work around decent grammar possibleReason = possibleReason[idx2+len("for "):] possibleReasonLower = possibleReason.lower().strip() if not(possibleReasonLower.startswith("post") or possibleReasonLower.startswith("potw")): if len(possibleReason) < 500: # bare-bones spam protection nominationReason = possibleReason if nominationReason is not None and nominationReason.startswith("its "): nominationReason = "their "+nominationReason[4:] contextLevelNumber, contextLevelName = getContextLevel(possibleReson2) # overwrite user-supplied reason if isThread(nominated): nominationReason = nominated.title # if it's an user supplied reason, check for closing " if nominationReason is not None and nominationReason.startswith('"') and not nominationReason.endswith('"'): nominationReason = nominationReason + '"' # get the rank of the nominated person rankText = "" try: flairInfo = r.get_flair(r.get_subreddit(NominationsSettings.m5Subreddit), nominated.author) rank = RankUtils.rankLevels[RankUtils.getRankLevelFromFlair(flairInfo["flair_css_class"])] # -1 arrayoutoufbounds handled by the exception block anyways if flairInfo["flair_text"] != rank["name"] and flairInfo["flair_text"] is not None: rankText = flairInfo["flair_text"] else: rankText = rank["abbreviated"] except: logging.info("Couldn't get flair for " + nominated.author.name + " " + str(sys.exc_info())) permalink = nominated.permalink if contextLevelNumber is not None: permalink += "?context=" + str(contextLevelNumber) nominationsCommentText = "{} /u/{} for [{}]({}).".format(rankText, nominated.author.name, nominationReason if nominationReason is not None else "this", permalink) logging.info(nominationsCommentText) ntComment = nominationsThread.add_comment(nominationsCommentText) ntComment.distinguish() atComment = comment.reply("Nominated [this]({}) {} by {} /u/{} for you. It will be voted on next week. {}".format(permalink, "post" if isThread(nominated) else "comment", rankText, nominated.author.name, addendum)) atComment.distinguish() return