def _fetch_all_ids(self, url, pagelimit): """ Return all user IDs returned by url, even from multiple pages. If not all users fit into one page returned by Habitica, a new query is run for the next page of users until all have been found. :url: Habitica API url for the interesting query :pagelimit: Maximum number of returned items per request. """ user_ids = [] last_id = None current_url = url while True: if last_id: current_url = "{}?lastId={}".format(url, last_id) data = utils.get_dict_from_api(self._header, current_url) for user in data: user_ids.append(user["id"]) if len(data) < pagelimit: break else: last_id = data[len(data) - 1]["id"] return user_ids
def _party_id(self): """ Return the ID of the party user is currently in. """ user_data = get_dict_from_api(self._header, "https://habitica.com/api/v3/user") return user_data["party"]["_id"]
def join_quest(self): """ If there's an unjoined quest, join it. :return: True if a quest was joined. """ self._logger.debug("Checking if a quest can be joined.") questdata = get_dict_from_api( self._header, "https://habitica.com/api/v3/groups/party")["quest"] self._logger.debug("Quest information: %s", questdata) if ("key" in questdata and not questdata["active"] and (self._header["x-api-user"] not in questdata["members"] or not questdata["members"][self._header["x-api-user"]])): self._logger.debug("New quest found") try: habrequest.post( "https://habitica.com/api/v3/groups/party/quests/accept", headers=self._header) # pylint: disable=invalid-name except requests.exceptions.HTTPError as e: self._logger.error("Quest joining failed: %s", str(e)) # pylint: disable=raise-missing-from raise CommunicationFailedException(str(e)) self._logger.info("Joined quest %s", questdata["key"]) return True return False
def party_description(self): """ Return the description of the party """ data = utils.get_dict_from_api( self._header, "https://habitica.com/api/v3/groups/party") return data["description"]
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 get_private_messages(self): """ Fetch private messages using Habitica API. If there are new messages, they are written to the database and returned. No paging is implemented: all new messages are assumed to fit into the returned data from the API. """ try: message_data = get_dict_from_api( self._header, "https://habitica.com/api/v3/inbox/messages") except requests.exceptions.HTTPError as err: raise CommunicationFailedException(err.response) from err messages = [None] * len(message_data) for i, message_dict in zip(range(len(message_data)), message_data): if message_dict["sent"]: recipient = message_dict["uuid"] sender = message_dict["ownerId"] else: recipient = message_dict["ownerId"] sender = message_dict["uuid"] messages[i] = PrivateMessage(sender, recipient, timestamp=timestamp_to_datetime( message_dict["timestamp"]), content=message_dict["text"], message_id=message_dict["id"]) self._logger.debug("Fetched %d messages from Habitica API", len(messages)) self.add_PMs_to_db(messages)
def get_party_messages(self): """ Fetches party messages and stores them into the database. Both system messages (e.g. boss damage) and chat messages (sent by habiticians) are stored. """ message_data = get_dict_from_api( self._header, "https://habitica.com/api/v3/groups/party/chat") messages = [None] * len(message_data) for i, message_dict in zip(range(len(message_data)), message_data): if "user" in message_dict: messages[i] = ChatMessage( message_dict["uuid"], message_dict["groupId"], content=message_dict["text"], message_id=message_dict["id"], timestamp=datetime.utcfromtimestamp( # Habitica saves party chat message times as unix time # with three extra digits for milliseconds (no # decimal separator) message_dict["timestamp"] / 1000), likers=self._marker_list(message_dict["likes"]), flags=self._marker_list(message_dict["flags"])) else: messages[i] = SystemMessage( message_dict["groupId"], datetime.utcfromtimestamp( # Habitica saves party chat message times as unix time # with three extra digits for milliseconds (no # decimal separator) message_dict["timestamp"] / 1000), content=message_dict["text"], message_id=message_dict["id"], likers=self._marker_list(message_dict["likes"]), info=message_dict["info"]) self._logger.debug("Fetched %d messages from Habitica API", len(messages)) new_messages = 0 for message in messages: if isinstance(message, SystemMessage): new = self._write_system_message_to_db(message) elif isinstance(message, ChatMessage): new = self._write_chat_message_to_db(message) else: raise ValueError("Unexpected message type received from API") new_messages += 1 if new else 0 self._logger.debug( "%d new chat/system messages written to the " "database", new_messages)
def __init__(self, header, challenge_id): """ Create a class for a challenge. :header: Header to use with API :challenge_id: The ID of the represented challenge """ self.id = challenge_id # pylint: disable=invalid-name self._header = header self._full_data = utils.get_dict_from_api( header, "https://habitica.com/api/v3/challenges/{}".format(challenge_id)) self._party_tool = PartyTool(header) self._participants = None self._completers = None
def _get_tasks(self, task_type=None): """ Return a list of tasks. If task_type is given, only tasks of that type are returned. :task_type: None (for all tasks) or a Habitica task type ("habit", "daily", or "todo) """ if task_type not in ["habit", "daily", "todo", None]: raise ValueError("Task type {} not supported".format(task_type)) url = "https://habitica.com/api/v3/tasks/user" tasks = get_dict_from_api(self._header, url) if task_type is None: return tasks matching_tasks = [task for task in tasks if task["type"] == task_type] return matching_tasks
def newest_matching_challenge(self, must_haves, no_gos): """ Return the newest challenge with a name that fits the given criteria. The challenge is chosen based on its title containing all strings in the given must_haves iterable and none that are in no_gos. If there are multiple matching challenges, the one that has the most recent "created at" time is returned. :must_haves: iterable of strings that must be present in the name :no_gos: iterable of strings that must not be present in the name :returns: A dict representing the newest matching challenge """ challenges = utils.get_dict_from_api( self._header, "https://habitica.com/api/v3/challenges/groups/party") matching_challenge = None for challenge in challenges: name = challenge["name"] name_ok = True for substring in must_haves: if substring not in name: name_ok = False for substring in no_gos: if substring in name: name_ok = False if name_ok: if not matching_challenge: matching_challenge = challenge else: old_created = utils.timestamp_to_datetime( matching_challenge["createdAt"]) new_created = utils.timestamp_to_datetime( challenge["createdAt"]) if new_created > old_created: matching_challenge = challenge return matching_challenge
def __init__(self, user_id, header=None, profile_data=None): """ Initialize the class with data from API/dict. If header is provided, data is fetched from the api, otherwise given profile_data is used. :user_id: User ID for the represented Habitica user :header: HTTP header for accessing Habitica API :profile_data: Dict containing the following data: id: UID for the user (str) displayname: Display name (str) loginname: Login name (str) birthday: Habitica birthday (datetime) last_login: Last time the user logged in (datetime) """ if header: profile_data = utils.get_dict_from_api( header, "https://habitica.com/api/v3/members/{}".format(user_id)) self.id = profile_data["_id"] # pylint: disable=invalid-name self.displayname = profile_data["profile"]["name"] self.login_name = profile_data["auth"]["local"]["username"] self.habitica_birthday = datetime.strptime( profile_data["auth"]["timestamps"]["created"], "%Y-%m-%dT%H:%M:%S.%fZ") self.last_login = datetime.strptime( profile_data["auth"]["timestamps"]["loggedin"], "%Y-%m-%dT%H:%M:%S.%fZ") elif profile_data: self.id = profile_data["id"] self.displayname = profile_data["displayname"] self.login_name = profile_data["loginname"] self.habitica_birthday = profile_data["birthday"] self.last_login = profile_data["last_login"] else: raise AttributeError("Either header or profile_data must be " "provided for initializing a Member.")
def eligible_winners(self, challenge_id, user_ids): """ Return a list of eligible challenge winners. Here, anyone who has completed all todo type tasks is eligible: habits or dailies are not inspected. :challenge_id: ID of challenge for which eligibility is assessed. :user_ids: A list of IDs for users whose eligibility is to be tested. :returns: A Member object. """ eligible_winners = [] for user_id in user_ids: progress_dict = utils.get_dict_from_api( self._header, "https://habitica.com/api/v3/challenges/{}/members/{}" "".format(challenge_id, user_id)) eligible = True for task in progress_dict["tasks"]: if task["type"] == "todo" and not task["completed"]: eligible = False if eligible: eligible_winners.append(Member(user_id, header=self._header)) return eligible_winners
def _get_user_data(self): """ Return the full user data dict. """ url = "https://habitica.com/api/v3/user" return get_dict_from_api(self._header, url)