Example #1
0
 def __init__(self):
     """
     Initialize the class
     """
     self._db_syncer = DBSyncer(HEADER)
     self._db_tool = DBTool()
     self._messager = HabiticaMessager(HEADER)
     super().__init__()
Example #2
0
class ListOwnedQuests(Functionality):
    """
    Respond with a list of quests owned by the party members and their owners.
    """
    def __init__(self):
        """
        Initialize the class
        """
        self._db_tool = DBTool()
        super().__init__()

    def help(self):
        return ("List all quests someone in party owns and the names of the "
                "owners.")

    @requires_party_membership
    def act(self, message):
        """
        Return a table containing quests and their owners.
        """
        partymember_uids = self._db_tool.get_party_user_ids()
        quests = {}
        for member_uid in partymember_uids:
            member_name = self._db_tool.get_loginname(member_uid)
            member_data = get_dict_from_api(
                HEADER,
                "https://habitica.com/api/v3/members/{}".format(member_uid))
            quest_counts = member_data["items"]["quests"]
            for quest_name in quest_counts:
                count = quest_counts[quest_name]
                if count == 1:
                    partymember_str = "@{}".format(member_name)
                elif count >= 1:
                    partymember_str = ("@{user} ({count})"
                                       "".format(user=member_name,
                                                 count=count))
                else:
                    continue

                if quest_name in quests:
                    quests[quest_name] = ", ".join(
                        [quests[quest_name], partymember_str])
                else:
                    quests[quest_name] = partymember_str

        content_lines = [
            "- **{}**: {}".format(quest, quests[quest]) for quest in quests
        ]
        return "\n".join(content_lines)
Example #3
0
def db_tool_fx(db_connection_fx):
    """
    Yield an operator for a test database.
    """
    # on some machines, @mark.usefixtures wasn't sufficient to prevent errors
    # pylint: disable=unused-argument
    yield DBTool()
Example #4
0
    def wrapper(self, message):
        partymember_uids = DBTool().get_party_user_ids()

        if message.from_id not in partymember_uids:
            # pylint: disable=protected-access
            self._logger.debug("Unauthorized %s request from %s",
                               message.content.strip().split()[0],
                               message.from_id)
            return ("This command is usable only by people within the "
                    "party. No messages sent.")
        return act_function(self, message)
Example #5
0
class SendQuestReminders(Functionality):
    """
    Send out quest reminders.
    """

    # In this class, responses to user contain backticks which have to be
    # escaped for Habitica. Thus this warning is disabled to avoid them being
    # flagged as possibly erroneous.
    # pylint: disable=anomalous-backslash-in-string

    def __init__(self):
        """
        Initialize the class
        """
        self._db_syncer = DBSyncer(HEADER)
        self._db_tool = DBTool()
        self._messager = HabiticaMessager(HEADER)
        super().__init__()

    def help(self):
        """
        Provide instructions for the reminder command.
        """
        # pylint: disable=no-self-use
        return ("Send out quest reminders to the people in the given quest "
                "queue. The quest queue must be given inside a code block "
                "with each quest on its own line. Each quest line starts with "
                "the name of the quest, followed by a semicolon (;) and a "
                "comma-separated list of quest owner Habitica login names."
                "\n\n"
                "Reminders are sent for all except the first quest in the "
                "given queue. The first quest name is only used for telling "
                "the owner(s) of the second quest after which quest they "
                "should send the invite."
                "\n\n"
                "Each user is sent a separate message for each quest. Thus, "
                "if one user owns copies of more than one quest in the queue, "
                "they will receive more than one message."
                "\n\n"
                "For example the following message is a valid quest reminder:"
                "\n"
                "````\n"
                "quest-reminders\n"
                "```\n"
                "Lunar Battle: Part 1; @FirstInQueue\n"
                "Unicorn; @SomePartyMember\n"
                "Brazen Beetle Battle; @OtherGuy, @Questgoer9000\n"
                "```\n"
                "````\n"
                "and will result in quest reminder being sent out to "
                "`@SomePartyMember` for unicorn quest and to `@OtherGuy` "
                "and `@QuestGoer9000` for the beetle. Note that as mentioned, "
                "@FirstInQueue gets no reminder of their quest."
                "")

    def act(self, message):
        """
        Send reminders for quests in the message body.

        The body is expected to consist of a code block (enclosed by three
        backticks ``` on each side) containing one line for each quest
        for which a reminder is to be sent. The earliest quests are assumed
        to be in order from earliest in the queue to the last.

        Each line must begin with a identifier of the quest (this can be
        anything, e.g. the name of the quest) followed by a semicolon and a
        comma-separeted list of Habitica login names of the partymembers who
        should be reminded of this quest. For example
        Questname; @user1, @user2, @user3
        is a valid line.
        """
        self._db_syncer.update_partymember_data()
        content = self._command_body(message)

        try:
            self._validate(content)
        except ValidationError as err:
            return ("A problem was encountered when reading the quest list: {}"
                    "\n\n"
                    "No messages were sent.".format(str(err)))

        reminder_data = content.split("```")[1]
        reminder_lines = reminder_data.strip().split("\n")
        previous_quest = reminder_lines[0].split(";")[0]
        sent_reminders = 0
        for line in reminder_lines[1:]:
            if line.strip():
                parts = line.split(";")
                quest_name = parts[0].strip()
                users = [
                    name.strip().lstrip("@") for name in parts[1].split(",")
                ]
                for user in users:
                    self._send_reminder(quest_name, user, len(users),
                                        previous_quest)
                    sent_reminders += 1
                previous_quest = quest_name

        return "Sent out {} quest reminders.".format(sent_reminders)

    def _validate(self, command_body):
        """
        Ensure that the command body looks sensible.

        Make sure that
          - the command body contains exactly one code block
          - each non-empty line in the code block contains exactly one
            semicolon
          - there is content both before and after the semicolon
          - all names in the list of quest owners start with an '@'

        If the command is deemed faulty, a ValidationError is raised.
        """
        parts = command_body.split("```")
        if not len(parts) == 3:
            raise ValidationError(
                "The list of reminders to be sent must be given inside "
                "a code block (surrounded by three backticks i"
                "\`\`\`). A code block was not found in "  # noqa: W605
                "the message.")

        reminder_data = parts[1]
        first_line = True

        for line in reminder_data.split("\n"):

            if not line.strip():
                continue

            parts = line.split(";")
            if len(parts) != 2:
                raise ValidationError(
                    "Each line in the quest queue must be divided into "
                    "two parts by a semicolon (;), the first part "
                    "containing the name of the quest and the latter "
                    "holding the names of the participants. Line `{}` "
                    "did not match this format.".format(line))

            if not parts[0].strip():
                raise ValidationError(
                    "Problem in line `{}`: quest name cannot be empty."
                    "".format(line))

            if not first_line:
                if not parts[1].strip():
                    raise ValidationError("No quest owners listed for quest {}"
                                          "".format(parts[0].strip()))

                for owner_str in parts[1].split(","):
                    owner_name = owner_str.strip().lstrip("@")
                    if not owner_name:
                        raise ValidationError(
                            "Malformed quest owner list for quest {}"
                            "".format(line))
                    try:
                        self._db_tool.get_user_id(owner_name)
                    except ValueError as err:
                        raise ValidationError("User @{} not found in the party"
                                              "".format(owner_name)) from err
            first_line = False
        self._logger.debug("Quest data successfully validated")

    def _send_reminder(self, quest_name, user_name, n_users, previous_quest):
        """
        Send out a reminder about given quest to given user.

        :quest_name: Name of the quest
        :user_name: Habitica login name for the recipient
        :n_users: Total number of users receiving this reminder
        :previous_quest: Name of the quest after which the user should send out
                         the invitation to their quest
        """
        recipient_uid = self._db_tool.get_user_id(user_name)
        message = self._message(quest_name, n_users, previous_quest)
        self._logger.debug("Sending a quest reminder for %s to %s (%s)",
                           quest_name, user_name, recipient_uid)
        self._messager.send_private_message(recipient_uid, message)

    def _message(self, quest_name, n_users, previous_quest):
        """
        Return a reminder message for the parameters.

        :quest_name: Name of the quest
        :n_users: Total number of users receiving this reminder
        :previous_quest: Name of the quest after which the user should send out
                         the invitation to their quest
        """
        # pylint: disable=no-self-use
        if n_users > 2:
            who = "You (and {} others)".format(n_users - 1)
        elif n_users == 2:
            who = "You (and one other partymember)"
        else:
            who = "You"
        return ("{who} have a quest coming up in the queue: {quest_name}! "
                "It comes after {previous_quest}, so when you notice that "
                "{previous_quest} has ended, please send out the invite for "
                "{quest_name}.".format(who=who,
                                       quest_name=quest_name,
                                       previous_quest=previous_quest))
Example #6
0
 def __init__(self):
     """
     Initialize the class
     """
     self._db_tool = DBTool()
     super().__init__()
Example #7
0
class SendPartyNewsletter(Functionality):
    """
    Send a message to all party members.
    """

    def __init__(self):
        """
        Initialize the class
        """
        self._db_syncer = DBSyncer(HEADER)
        self._db_tool = DBTool()
        self._messager = HabiticaMessager(HEADER)
        super().__init__()

    def help(self):
        """
        Return a help string.
        """
        example_content = (
                "# Important News!\n"
                "There's something very interesting going on and you should "
                "know about it. That's why you are receiving this newsletter. "
                "Please read it carefully :blush:\n\n"
                "Another paragraph with something **real** important here!"
                )
        return ("Send an identical message to all party members."
                "\n\n"
                "For example the following command:\n"
                "```\n"
                "party-newsletter"
                "\n\n"
                "{example_content}\n"
                "```\n"
                "will send the following message to all party members:\n"
                "{example_result}"
                "".format(
                    example_content=example_content,
                    example_result=self._format_newsletter(example_content,
                                                           "YourUsername"))
                )

    @requires_party_membership
    def act(self, message):
        """
        Send out a newsletter to all party members.

        The bot does not send the message to itself. The command is only usable
        from within the party: if an external user requests sending a
        newsletter, they get an error message instead.

        The requestor gets a list of users to whom the newsletter was sent.
        """
        self._db_syncer.update_partymember_data()
        content = self._command_body(message).strip()
        partymember_uids = self._db_tool.get_party_user_ids()

        if message.from_id not in partymember_uids:
            self._logger.debug("Unauthorized newsletter request from %s",
                               message.from_id)
            return ("This command is usable only by people within the "
                    "party. No messages sent.")

        message = self._format_newsletter(
                content, self._db_tool.get_loginname(message.from_id))

        self._logger.debug("Going to send out the following party newsletter:"
                           "\n%s", message)
        recipients = []
        for uid in partymember_uids:
            if uid == HEADER["x-api-user"]:
                continue
            self._messager.send_private_message(uid, message)
            recipients.append(self._db_tool.get_loginname(uid))
            self._logger.debug("Sent out a newsletter to %s", recipients[-1])

        recipient_list_str = "\n".join(["- @{}".format(name)
                                        for name in recipients])
        self._logger.debug("A newsletter sent to %d party members",
                           len(recipients))
        return ("Sent the given newsletter to the following users:\n"
                "{}".format(recipient_list_str))

    def _format_newsletter(self, message, sender_name):
        """
        Return the given message with a standard footer appended.

        The footer tells who originally sent the newsletter and urges people to
        contact the admin if the bot is misbehaving.
        """
        return ("{message}"
                "\n\n---\n\n"
                "This is a party newsletter written by @{user} and "
                "brought you by the party bot. If you suspect you should "
                "not have received this message, please contact "
                "@{admin}."
                "".format(message=message,
                          user=sender_name,
                          admin=self._db_tool.get_loginname(conf.ADMIN_UID))
                )