def __init__(self): """ Initialize the class """ self._db_syncer = DBSyncer(HEADER) self._db_tool = DBTool() self._messager = HabiticaMessager(HEADER) super().__init__()
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)
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()
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)
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 __init__(self): """ Initialize the class """ self._db_tool = DBTool() super().__init__()
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)) )