예제 #1
0
 def __init__(self):
     """
     Initialize the class
     """
     self._db_syncer = DBSyncer(HEADER)
     self._db_tool = DBTool()
     self._messager = HabiticaMessager(HEADER)
     super().__init__()
예제 #2
0
파일: birthdays.py 프로젝트: bgigous/habot
    def send_birthday_reminder(self, recipient_uid, sync=True):
        """
        Send a message telling whether any party member is having a birthday.

        :recipient_uid: The UID of the Habitician to whom the PM is sent
        :sync: True if database should be synced before sending message
        """
        if sync:
            db_syncer = DBSyncer(self._header)
            db_syncer.update_partymember_data()
        message = self.birthday_reminder_message()
        messager = HabiticaMessager(self._header)
        messager.send_private_message(recipient_uid, message)
예제 #3
0
파일: react.py 프로젝트: bgigous/habot
def react_to_message(message):
    """
    Perform whatever actions the given Message requires and send a response
    """
    logger = habot.logger.get_logger()

    if ignorable(message.content):
        HabiticaMessager.set_reaction_pending(message, False)
        logger.debug("Message %s doesn' need a reaction", message.content)
        return

    commands = {
        "list-birthdays": ListBirthdays,
        "send-winner-message": SendWinnerMessage,
        "create-next-sharing-weekend": CreateNextSharingWeekend,
        "award-latest-winner": AwardWinner,
        "ping": Ping,
        "add-task": AddTask,
        "quest-reminders": SendQuestReminders,
        "party-newsletter": SendPartyNewsletter,
        "owned-quests": ListOwnedQuests,
        "update-party-description": UpdatePartyDescription,
    }
    first_word = message.content.strip().split()[0]
    logger.debug("Got message starting with %s", first_word)
    if first_word in commands:
        try:
            functionality = commands[first_word]()
            response = functionality.act(message)
        except:  # noqa: E722  pylint: disable=bare-except
            logger.error(
                "A problem was encountered during reacting to "
                "message. See stack trace.",
                exc_info=True)
            response = ("Something unexpected happened while handling command "
                        "`{}`. Contact @Antonbury for "
                        "help.".format(first_word))
    else:
        command_list = [
            "`{}`: {}".format(command, commands[command]().help())
            for command in commands
        ]
        response = ("Command `{}` not recognized.\n\n".format(first_word) +
                    "I am a bot: not a real human user. If I am misbehaving " +
                    "or you need assistance, please contact @Antonbury.\n\n" +
                    "Available commands:\n\n" + "\n\n".join(command_list))

    HabiticaMessager(HEADER).send_private_message(message.from_id, response)
    HabiticaMessager.set_reaction_pending(message, False)
예제 #4
0
def main():
    """
    Run the scheduled operations repeatedly.

    All exceptions raised from scheduled tasks are logged, and if possible,
    a report is sent to the admin. If there are too many consecutive errors,
    all operations are ceased.
    """
    consecutive_errors = 0

    while True:
        try:
            schedule.run_pending()
            consecutive_errors = 0
        except Exception:  # pylint: disable=broad-except
            get_logger().exception("A problem was encountered during a "
                                   "scheduled task. See stack trace. ",
                                   exc_info=True)
            consecutive_errors += 1
            report = ("A problem was encountered:\n"
                      "```{}```\n"
                      "Consecutive error count: {}"
                      "".format(traceback.format_exc(), consecutive_errors))
            try:
                HabiticaMessager(HEADER).send_private_message(
                    conf.ADMIN_UID,
                    report)
            except CommunicationFailedException:
                get_logger().exception("Could not send the error report. ",
                                       exc_info=True)
            if consecutive_errors > conf.MAX_CONSECUTIVE_FAILS:
                get_logger().info("Shutting down due to too many failures.")
                return
        time.sleep(2)
예제 #5
0
def sharing_winner_message():
    """
    Send a message announcing the sharing weekend winner.
    """
    winner_message_creator = SendWinnerMessage()
    HabiticaMessager(HEADER).send_private_message(
        conf.ADMIN_UID,
        winner_message_creator.act("send scheduled sharing weekend winner msg")
        )
예제 #6
0
def fetch_messages():
    """
    Fetch messages using Habitica API
    """
    messager = HabiticaMessager(HEADER)
    messager.get_private_messages()
    messager.get_party_messages()
예제 #7
0
def handle_sharing_weekend():
    """
    Does the work of the weekly routine of ending and creating a challenge.
    """
    challenge_ender = AwardWinner()
    winner_message = challenge_ender.act("end challenge", scheduled_run=True)

    challenge_creator = CreateNextSharingWeekend()
    end_message = challenge_creator.act("create challenge", scheduled_run=True)

    HabiticaMessager(HEADER).send_group_message(
        "party", "\n\n".join([winner_message, end_message])
        )
예제 #8
0
def bday():
    """
    Send birthday messages.

    A message is sent to the admin regardless of whether anyone is celebrating
    their birthday or not, but if someone is actually celebrating today, a
    message is sent to the party also.
    """
    bday_reminder = BirthdayReminder(HEADER)
    bday_reminder.send_birthday_reminder(conf.ADMIN_UID, sync=True)

    birthday_revellers = bday_reminder.birthdays_today()
    if not birthday_revellers:
        return
    message = bday_reminder.birthday_reminder_message()
    HabiticaMessager(HEADER).send_group_message("party", message)
예제 #9
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))
예제 #10
0
파일: conftest.py 프로젝트: bgigous/habot
def test_messager(header_fx):
    """
    Create a HabiticaMessager for testing purposes.
    """
    return HabiticaMessager(header_fx)
예제 #11
0
파일: newsletter.py 프로젝트: bgigous/habot
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))
                )