def __init__(self): """ Initialize the class """ self._db_syncer = DBSyncer(HEADER) self._db_tool = DBTool() self._messager = HabiticaMessager(HEADER) super().__init__()
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)
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)
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)
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") )
def fetch_messages(): """ Fetch messages using Habitica API """ messager = HabiticaMessager(HEADER) messager.get_private_messages() messager.get_party_messages()
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]) )
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)
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))
def test_messager(header_fx): """ Create a HabiticaMessager for testing purposes. """ return HabiticaMessager(header_fx)
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)) )