Ejemplo n.º 1
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.º 2
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