Add type hints, and clean up Client a bit

This commit is contained in:
Mads Marquart
2020-01-22 01:43:04 +01:00
parent 701fe8ffc8
commit 2644aa9b7a
19 changed files with 339 additions and 346 deletions

View File

@@ -11,14 +11,10 @@ from . import (
_page,
_group,
_thread,
_message,
)
from ._thread import ThreadLocation
from ._user import User, UserData
from ._group import Group, GroupData
from ._page import Page, PageData
from typing import Sequence, Iterable, Tuple, Optional
from typing import Sequence, Iterable, Tuple, Optional, Set
@attrs_default
@@ -68,7 +64,7 @@ class Client:
limit: The max. amount of users to fetch
Returns:
list: `User` objects, ordered by relevance
Users, ordered by relevance
"""
params = {"search": name, "limit": limit}
(j,) = self.session._graphql_requests(
@@ -76,7 +72,7 @@ class Client:
)
return (
UserData._from_graphql(self.session, node)
_user.UserData._from_graphql(self.session, node)
for node in j[name]["users"]["nodes"]
)
@@ -93,7 +89,7 @@ class Client:
)
return (
PageData._from_graphql(self.session, node)
_page.PageData._from_graphql(self.session, node)
for node in j[name]["pages"]["nodes"]
)
@@ -110,7 +106,7 @@ class Client:
)
return (
GroupData._from_graphql(self.session, node)
_group.GroupData._from_graphql(self.session, node)
for node in j["viewer"]["groups"]["nodes"]
)
@@ -128,12 +124,12 @@ class Client:
for node in j[name]["threads"]["nodes"]:
if node["__typename"] == "User":
yield UserData._from_graphql(self.session, node)
yield _user.UserData._from_graphql(self.session, node)
elif node["__typename"] == "MessageThread":
# MessageThread => Group thread
yield GroupData._from_graphql(self.session, node)
yield _group.GroupData._from_graphql(self.session, node)
elif node["__typename"] == "Page":
yield PageData._from_graphql(self.session, node)
yield _page.PageData._from_graphql(self.session, node)
elif node["__typename"] == "Group":
# We don't handle Facebook "Groups"
pass
@@ -152,7 +148,7 @@ class Client:
for node in j["graphql_payload"]["message_threads"]:
type_ = node["thread_type"]
if type_ == "GROUP":
thread = Group(
thread = _group.Group(
session=self.session, id=node["thread_key"]["thread_fbid"]
)
elif type_ == "ONE_TO_ONE":
@@ -160,9 +156,9 @@ class Client:
session=self.session, id=node["thread_key"]["other_user_id"]
)
# if True: # TODO: This check!
# thread = UserData._from_graphql(self.session, node)
# thread = _user.UserData._from_graphql(self.session, node)
# else:
# thread = PageData._from_graphql(self.session, node)
# thread = _page.PageData._from_graphql(self.session, node)
else:
thread = None
log.warning("Unknown thread type %s, data: %s", type_, node)
@@ -238,20 +234,18 @@ class Client:
log.debug(entries)
return entries
def fetch_thread_info(self, *thread_ids):
def fetch_thread_info(self, ids: Iterable[str]) -> Iterable[_thread.ThreadABC]:
"""Fetch threads' info from IDs, unordered.
Warning:
Sends two requests if users or pages are present, to fetch all available info!
Args:
thread_ids: One or more thread ID(s) to query
Returns:
dict: `Thread` objects, labeled by their ID
ids: Thread ids to query
"""
ids = list(ids)
queries = []
for thread_id in thread_ids:
for thread_id in ids:
params = {
"id": thread_id,
"message_limit": 0,
@@ -267,7 +261,7 @@ class Client:
if entry.get("message_thread") is None:
# If you don't have an existing thread with this person, attempt to retrieve user data anyways
j[i]["message_thread"] = {
"thread_key": {"other_user_id": thread_ids[i]},
"thread_key": {"other_user_id": ids[i]},
"thread_type": "ONE_TO_ONE",
}
@@ -280,12 +274,11 @@ class Client:
if len(pages_and_user_ids) != 0:
pages_and_users = self._fetch_info(*pages_and_user_ids)
rtn = {}
for i, entry in enumerate(j):
entry = entry["message_thread"]
if entry.get("thread_type") == "GROUP":
_id = entry["thread_key"]["thread_fbid"]
rtn[_id] = GroupData._from_graphql(self.session, entry)
yield _group.GroupData._from_graphql(self.session, entry)
elif entry.get("thread_type") == "ONE_TO_ONE":
_id = entry["thread_key"]["other_user_id"]
if pages_and_users.get(_id) is None:
@@ -294,14 +287,12 @@ class Client:
)
entry.update(pages_and_users[_id])
if "first_name" in entry:
rtn[_id] = UserData._from_graphql(self.session, entry)
yield _user.UserData._from_graphql(self.session, entry)
else:
rtn[_id] = PageData._from_graphql(self.session, entry)
yield _page.PageData._from_graphql(self.session, entry)
else:
raise _exception.ParseError("Unknown thread type", data=entry)
return rtn
def _fetch_threads(self, limit, before, folders):
params = {
"limit": limit,
@@ -318,16 +309,18 @@ class Client:
for node in j["viewer"]["message_threads"]["nodes"]:
_type = node.get("thread_type")
if _type == "GROUP":
rtn.append(GroupData._from_graphql(self.session, node))
rtn.append(_group.GroupData._from_graphql(self.session, node))
elif _type == "ONE_TO_ONE":
rtn.append(UserData._from_thread_fetch(self.session, node))
rtn.append(_user.UserData._from_thread_fetch(self.session, node))
else:
rtn.append(None)
log.warning("Unknown thread type: %s, data: %s", _type, node)
return rtn
def fetch_threads(
self, limit: Optional[int], location: ThreadLocation = ThreadLocation.INBOX,
self,
limit: Optional[int],
location: _thread.ThreadLocation = _thread.ThreadLocation.INBOX,
) -> Iterable[_thread.ThreadABC]:
"""Fetch the client's thread list.
@@ -340,7 +333,7 @@ class Client:
MAX_BATCH_LIMIT = 100
# TODO: Clean this up after implementing support for more threads types
seen_ids = set()
seen_ids = set() # type: Set[str]
before = None
for limit in _util.get_limits(limit, MAX_BATCH_LIMIT):
threads = self._fetch_threads(limit, before, [location.value])
@@ -361,11 +354,11 @@ class Client:
if not before:
raise ValueError("Too many unknown threads.")
def fetch_unread(self):
def fetch_unread(self) -> Sequence[_thread.ThreadABC]:
"""Fetch unread threads.
Returns:
list: List of unread thread ids
Warning:
This is not finished, and the API may change at any point!
"""
form = {
"folders[0]": "inbox",
@@ -376,27 +369,39 @@ class Client:
j = self.session._payload_post("/ajax/mercury/unread_threads.php", form)
result = j["unread_thread_fbids"][0]
return result["thread_fbids"] + result["other_user_fbids"]
# TODO: Parse Pages?
return [
_group.Group(session=self.session, id=id_) for id_ in result["thread_fbids"]
] + [
_user.User(session=self.session, id=id_)
for id_ in result["other_user_fbids"]
]
def fetch_unseen(self):
def fetch_unseen(self) -> Sequence[_thread.ThreadABC]:
"""Fetch unseen / new threads.
Returns:
list: List of unseen thread ids
Warning:
This is not finished, and the API may change at any point!
"""
j = self.session._payload_post("/mercury/unseen_thread_ids/", {})
result = j["unseen_thread_fbids"][0]
return result["thread_fbids"] + result["other_user_fbids"]
# TODO: Parse Pages?
return [
_group.Group(session=self.session, id=id_) for id_ in result["thread_fbids"]
] + [
_user.User(session=self.session, id=id_)
for id_ in result["other_user_fbids"]
]
def fetch_image_url(self, image_id):
def fetch_image_url(self, image_id: str) -> str:
"""Fetch URL to download the original image from an image attachment ID.
Args:
image_id (str): The image you want to fetch
image_id: The image you want to fetch
Returns:
str: An URL where you can download the original image
An URL where you can download the original image
"""
image_id = str(image_id)
data = {"photo_id": str(image_id)}
@@ -414,77 +419,67 @@ class Client:
)
return j["viewer"]
def get_phone_numbers(self):
"""Fetch list of user's phone numbers.
Returns:
list: List of phone numbers
"""
def get_phone_numbers(self) -> Sequence[str]:
"""Fetch the user's phone numbers."""
data = self._get_private_data()
return [
j["phone_number"]["universal_number"] for j in data["user"]["all_phones"]
]
def get_emails(self):
"""Fetch list of user's emails.
Returns:
list: List of emails
"""
def get_emails(self) -> Sequence[str]:
"""Fetch the user's emails."""
data = self._get_private_data()
return [j["display_email"] for j in data["all_emails"]]
def mark_as_delivered(self, thread_id, message_id):
def mark_as_delivered(self, message: _message.Message):
"""Mark a message as delivered.
Warning:
This is not finished, and the API may change at any point!
Args:
thread_id: User/Group ID to which the message belongs. See :ref:`intro_threads`
message_id: Message ID to set as delivered. See :ref:`intro_threads`
message: The message to set as delivered
"""
data = {
"message_ids[0]": message_id,
"thread_ids[%s][0]" % thread_id: message_id,
"message_ids[0]": message.id,
"thread_ids[%s][0]" % message.thread.id: message.id,
}
j = self.session._payload_post("/ajax/mercury/delivery_receipts.php", data)
return True
def _read_status(self, read, thread_ids, timestamp=None):
thread_ids = _util.require_list(thread_ids)
def _read_status(self, read, threads, at):
data = {
"watermarkTimestamp": _util.datetime_to_millis(timestamp)
if timestamp
else _util.now(),
"watermarkTimestamp": _util.datetime_to_millis(at),
"shouldSendReadReceipt": "true",
}
for thread_id in thread_ids:
data["ids[{}]".format(thread_id)] = "true" if read else "false"
for threads in threads:
data["ids[{}]".format(thread.id)] = "true" if read else "false"
j = self.session._payload_post("/ajax/mercury/change_read_status.php", data)
def mark_as_read(self, thread_ids=None, timestamp=None):
def mark_as_read(self, threads: Iterable[_thread.ThreadABC], at: datetime.datetime):
"""Mark threads as read.
All messages inside the specified threads will be marked as read.
Args:
thread_ids: User/Group IDs to set as read. See :ref:`intro_threads`
timestamp: Timestamp (as a Datetime) to signal the read cursor at, default is the current time
threads: Threads to set as read
at: Timestamp to signal the read cursor at
"""
self._read_status(True, thread_ids, timestamp)
return self._read_status(True, threads, at)
def mark_as_unread(self, thread_ids=None, timestamp=None):
def mark_as_unread(
self, threads: Iterable[_thread.ThreadABC], at: datetime.datetime
):
"""Mark threads as unread.
All messages inside the specified threads will be marked as unread.
Args:
thread_ids: User/Group IDs to set as unread. See :ref:`intro_threads`
timestamp: Timestamp (as a Datetime) to signal the read cursor at, default is the current time
threads: Threads to set as unread
at: Timestam to signal the read cursor at
"""
self._read_status(False, thread_ids, timestamp)
return self._read_status(False, threads, at)
def mark_as_seen(self):
"""
@@ -495,24 +490,24 @@ class Client:
"/ajax/mercury/mark_seen.php", {"seen_timestamp": _util.now()}
)
def move_threads(self, location, thread_ids):
def move_threads(
self, location: _thread.ThreadLocation, threads: Iterable[_thread.ThreadABC]
):
"""Move threads to specified location.
Args:
location (ThreadLocation): INBOX, PENDING, ARCHIVED or OTHER
thread_ids: Thread IDs to move. See :ref:`intro_threads`
location: INBOX, PENDING, ARCHIVED or OTHER
threads: Threads to move
"""
thread_ids = _util.require_list(thread_ids)
if location == _thread.ThreadLocation.PENDING:
location = _thread.ThreadLocation.OTHER
if location == ThreadLocation.PENDING:
location = ThreadLocation.OTHER
if location == ThreadLocation.ARCHIVED:
data_archive = dict()
data_unpin = dict()
for thread_id in thread_ids:
data_archive["ids[{}]".format(thread_id)] = "true"
data_unpin["ids[{}]".format(thread_id)] = "false"
if location == _thread.ThreadLocation.ARCHIVED:
data_archive = {}
data_unpin = {}
for thread in threads:
data_archive["ids[{}]".format(thread.id)] = "true"
data_unpin["ids[{}]".format(thread.id)] = "false"
j_archive = self.session._payload_post(
"/ajax/mercury/change_archived_status.php?dpr=1", data_archive
)
@@ -520,42 +515,32 @@ class Client:
"/ajax/mercury/change_pinned_status.php?dpr=1", data_unpin
)
else:
data = dict()
for i, thread_id in enumerate(thread_ids):
data["{}[{}]".format(location.name.lower(), i)] = thread_id
data = {}
for i, thread in enumerate(threads):
data["{}[{}]".format(location.name.lower(), i)] = thread.id
j = self.session._payload_post("/ajax/mercury/move_thread.php", data)
return True
def delete_threads(self, thread_ids):
"""Delete threads.
Args:
thread_ids: Thread IDs to delete. See :ref:`intro_threads`
"""
thread_ids = _util.require_list(thread_ids)
data_unpin = dict()
data_delete = dict()
for i, thread_id in enumerate(thread_ids):
data_unpin["ids[{}]".format(thread_id)] = "false"
data_delete["ids[{}]".format(i)] = thread_id
def delete_threads(self, threads: Iterable[_thread.ThreadABC]):
"""Delete threads."""
data_unpin = {}
data_delete = {}
for i, thread in enumerate(threads):
data_unpin["ids[{}]".format(thread.id)] = "false"
data_delete["ids[{}]".format(i)] = thread.id
j_unpin = self.session._payload_post(
"/ajax/mercury/change_pinned_status.php?dpr=1", data_unpin
)
j_delete = self.session._payload_post(
"/ajax/mercury/delete_thread.php?dpr=1", data_delete
)
return True
def delete_messages(self, message_ids):
def delete_messages(self, messages: Iterable[_message.Message]):
"""Delete specified messages.
Args:
message_ids: Message IDs to delete
messages: Messages to delete
"""
message_ids = _util.require_list(message_ids)
data = dict()
for i, message_id in enumerate(message_ids):
data["message_ids[{}]".format(i)] = message_id
data = {}
for i, message in enumerate(messages):
data["message_ids[{}]".format(i)] = message.id
j = self.session._payload_post("/ajax/mercury/delete_messages.php?dpr=1", data)
return True