Ejemplo n.º 1
0
* 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]))
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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