Add inline examples

This commit is contained in:
Mads Marquart
2020-01-23 12:07:40 +01:00
parent 0d139cee73
commit 45a71fd1a3
10 changed files with 396 additions and 51 deletions

View File

@@ -19,11 +19,14 @@ from typing import Sequence, Iterable, Tuple, Optional, Set
@attrs_default @attrs_default
class Client: class Client:
"""A client for the Facebook Chat (Messenger). """A client for Facebook Messenger.
This contains all the methods you use to interact with Facebook. You can extend this This contains methods that are generally needed to interact with Facebook.
class, and overwrite the ``on`` methods, to provide custom event handling (mainly
useful while listening). Example:
Create a new client instance.
>>> client = fbchat.Client(session=session)
""" """
#: The session to use when making requests. #: The session to use when making requests.
@@ -39,6 +42,15 @@ class Client:
But does not include deactivated, deleted or memorialized users (logically, But does not include deactivated, deleted or memorialized users (logically,
since you can't chat with those). since you can't chat with those).
The order these are returned is arbitary.
Example:
Get the name of an arbitary user that you're currently chatting with.
>>> users = client.fetch_users()
>>> users[0].name
"A user"
""" """
data = {"viewer": self.session.user_id} data = {"viewer": self.session.user_id}
j = self.session._payload_post("/chat/user_info_all", data) j = self.session._payload_post("/chat/user_info_all", data)
@@ -54,12 +66,18 @@ class Client:
def search_for_users(self, name: str, limit: int) -> Iterable[_user.UserData]: def search_for_users(self, name: str, limit: int) -> Iterable[_user.UserData]:
"""Find and get users by their name. """Find and get users by their name.
The returned users are ordered by relevance.
Args: Args:
name: Name of the user name: Name of the user
limit: The max. amount of users to fetch limit: The max. amount of users to fetch
Returns: Example:
Users, ordered by relevance Get the full name of the first found user.
>>> (user,) = client.search_for_users("user", limit=1)
>>> user.name
"A user"
""" """
params = {"search": name, "limit": limit} params = {"search": name, "limit": limit}
(j,) = self.session._graphql_requests( (j,) = self.session._graphql_requests(
@@ -74,9 +92,18 @@ class Client:
def search_for_pages(self, name: str, limit: int) -> Iterable[_page.PageData]: def search_for_pages(self, name: str, limit: int) -> Iterable[_page.PageData]:
"""Find and get pages by their name. """Find and get pages by their name.
The returned pages are ordered by relevance.
Args: Args:
name: Name of the page name: Name of the page
limit: The max. amount of pages to fetch limit: The max. amount of pages to fetch
Example:
Get the full name of the first found page.
>>> (page,) = client.search_for_pages("page", limit=1)
>>> page.name
"A page"
""" """
params = {"search": name, "limit": limit} params = {"search": name, "limit": limit}
(j,) = self.session._graphql_requests( (j,) = self.session._graphql_requests(
@@ -91,9 +118,18 @@ class Client:
def search_for_groups(self, name: str, limit: int) -> Iterable[_group.GroupData]: def search_for_groups(self, name: str, limit: int) -> Iterable[_group.GroupData]:
"""Find and get group threads by their name. """Find and get group threads by their name.
The returned groups are ordered by relevance.
Args: Args:
name: Name of the group thread name: Name of the group thread
limit: The max. amount of groups to fetch limit: The max. amount of groups to fetch
Example:
Get the full name of the first found group.
>>> (group,) = client.search_for_groups("group", limit=1)
>>> group.name
"A group"
""" """
params = {"search": name, "limit": limit} params = {"search": name, "limit": limit}
(j,) = self.session._graphql_requests( (j,) = self.session._graphql_requests(
@@ -108,9 +144,19 @@ class Client:
def search_for_threads(self, name: str, limit: int) -> Iterable[_thread.ThreadABC]: def search_for_threads(self, name: str, limit: int) -> Iterable[_thread.ThreadABC]:
"""Find and get threads by their name. """Find and get threads by their name.
The returned threads are ordered by relevance.
Args: Args:
name: Name of the thread name: Name of the thread
limit: The max. amount of threads to fetch limit: The max. amount of threads to fetch
Example:
Search for a user, and get the full name of the first found result.
>>> (user,) = client.search_for_threads("user", limit=1)
>>> assert isinstance(user, fbchat.User)
>>> user.name
"A user"
""" """
params = {"search": name, "limit": limit} params = {"search": name, "limit": limit}
(j,) = self.session._graphql_requests( (j,) = self.session._graphql_requests(
@@ -173,16 +219,26 @@ class Client:
Intended to be used alongside `ThreadABC.search_messages` Intended to be used alongside `ThreadABC.search_messages`
Warning! If someone send a message to a thread that matches the query, while Warning! If someone send a message to a thread that matches the query, while
we're searching, some snippets will get returned twice. we're searching, some snippets will get returned twice, and some will be lost.
Not sure if we should handle it, Facebook's implementation doesn't... This is fundamentally unfixable, it's just how the endpoint is implemented.
Args: Args:
query: Text to search for query: Text to search for
limit: Max. number of threads to retrieve. If ``None``, all threads will be limit: Max. number of threads to retrieve. If ``None``, all threads will be
retrieved. retrieved
Returns: Example:
Search for messages, and print the amount of snippets in each thread.
>>> for thread, count in client.search_messages("abc", limit=3):
... print(f"{thread.id} matched the search {count} time(s)")
...
1234 matched the search 2 time(s)
2345 matched the search 1 time(s)
3456 matched the search 100 time(s)
Return:
Iterable with tuples of threads, and the total amount of matches. Iterable with tuples of threads, and the total amount of matches.
""" """
offset = 0 offset = 0
@@ -237,6 +293,13 @@ class Client:
Args: Args:
ids: Thread ids to query ids: Thread ids to query
Example:
Get data about the user with id "4".
>>> (user,) = client.fetch_thread_info(["4"])
>>> user.name
"Mark Zuckerberg"
""" """
ids = list(ids) ids = list(ids)
queries = [] queries = []
@@ -323,6 +386,16 @@ class Client:
limit: Max. number of threads to retrieve. If ``None``, all threads will be limit: Max. number of threads to retrieve. If ``None``, all threads will be
retrieved. retrieved.
location: INBOX, PENDING, ARCHIVED or OTHER location: INBOX, PENDING, ARCHIVED or OTHER
Example:
Fetch the last three threads that the user chatted with.
>>> for thread in client.fetch_threads(limit=3):
... print(f"{thread.id}: {thread.name}")
...
1234: A user
2345: A group
3456: A page
""" """
# This is measured empirically as 837, safe default chosen below # This is measured empirically as 837, safe default chosen below
MAX_BATCH_LIMIT = 100 MAX_BATCH_LIMIT = 100
@@ -395,6 +468,10 @@ class Client:
Args: Args:
image_id: The image you want to fetch image_id: The image you want to fetch
Example:
>>> client.fetch_image_url("1234")
"https://scontent-arn1-1.xx.fbcdn.net/v/t1.123-4/1_23_45_n.png?..."
Returns: Returns:
An URL where you can download the original image An URL where you can download the original image
""" """
@@ -513,7 +590,15 @@ class Client:
j = self.session._payload_post("/ajax/mercury/move_thread.php", data) j = self.session._payload_post("/ajax/mercury/move_thread.php", data)
def delete_threads(self, threads: Iterable[_thread.ThreadABC]): def delete_threads(self, threads: Iterable[_thread.ThreadABC]):
"""Delete threads.""" """Bulk delete threads.
Args:
threads: Threads to delete
Example:
>>> group = fbchat.Group(session=session, id="1234")
>>> client.delete_threads([group])
"""
data_unpin = {} data_unpin = {}
data_delete = {} data_delete = {}
for i, thread in enumerate(threads): for i, thread in enumerate(threads):
@@ -527,10 +612,15 @@ class Client:
) )
def delete_messages(self, messages: Iterable[_message.Message]): def delete_messages(self, messages: Iterable[_message.Message]):
"""Delete specified messages. """Bulk delete specified messages.
Args: Args:
messages: Messages to delete messages: Messages to delete
Example:
>>> message1 = fbchat.Message(thread=thread, id="1234")
>>> message2 = fbchat.Message(thread=thread, id="2345")
>>> client.delete_threads([message1, message2])
""" """
data = {} data = {}
for i, message in enumerate(messages): for i, message in enumerate(messages):

View File

@@ -7,7 +7,11 @@ from typing import Sequence, Iterable, Set, Mapping
@attrs_default @attrs_default
class Group(_thread.ThreadABC): class Group(_thread.ThreadABC):
"""Represents a Facebook group. Implements `ThreadABC`.""" """Represents a Facebook group. Implements `ThreadABC`.
Example:
>>> group = fbchat.Group(session=session, id="1234")
"""
#: The session to use when making requests. #: The session to use when making requests.
session = attr.ib(type=_session.Session) session = attr.ib(type=_session.Session)
@@ -22,6 +26,9 @@ class Group(_thread.ThreadABC):
Args: Args:
user_ids: One or more user IDs to add user_ids: One or more user IDs to add
Example:
>>> group.add_participants(["1234", "2345"])
""" """
data = self._to_send_data() data = self._to_send_data()
@@ -45,6 +52,9 @@ class Group(_thread.ThreadABC):
Args: Args:
user_id: User ID to remove user_id: User ID to remove
Example:
>>> group.remove_participant("1234")
""" """
data = {"uid": user_id, "tid": self.id} data = {"uid": user_id, "tid": self.id}
j = self.session._payload_post("/chat/remove_participants/", data) j = self.session._payload_post("/chat/remove_participants/", data)
@@ -62,6 +72,9 @@ class Group(_thread.ThreadABC):
Args: Args:
user_ids: One or more user IDs to set admin user_ids: One or more user IDs to set admin
Example:
>>> group.add_admins(["1234", "2345"])
""" """
self._admin_status(user_ids, True) self._admin_status(user_ids, True)
@@ -70,6 +83,9 @@ class Group(_thread.ThreadABC):
Args: Args:
user_ids: One or more user IDs to remove admin user_ids: One or more user IDs to remove admin
Example:
>>> group.remove_admins(["1234", "2345"])
""" """
self._admin_status(user_ids, False) self._admin_status(user_ids, False)
@@ -78,6 +94,9 @@ class Group(_thread.ThreadABC):
Args: Args:
title: New title title: New title
Example:
>>> group.set_title("Abc")
""" """
data = {"thread_name": title, "thread_id": self.id} data = {"thread_name": title, "thread_id": self.id}
j = self.session._payload_post("/messaging/set_thread_name/?dpr=1", data) j = self.session._payload_post("/messaging/set_thread_name/?dpr=1", data)
@@ -87,6 +106,14 @@ class Group(_thread.ThreadABC):
Args: Args:
image_id: ID of uploaded image image_id: ID of uploaded image
Example:
Upload an image, and use it as the group image.
>>> with open("image.png", "rb") as f:
... (file,) = session._upload([("image.png", f, "image/png")])
...
>>> group.set_image(file[0])
""" """
data = {"thread_image_id": image_id, "thread_id": self.id} data = {"thread_image_id": image_id, "thread_id": self.id}
j = self.session._payload_post("/messaging/set_thread_image/?dpr=1", data) j = self.session._payload_post("/messaging/set_thread_image/?dpr=1", data)
@@ -96,6 +123,9 @@ class Group(_thread.ThreadABC):
Args: Args:
require_admin_approval: True or False require_admin_approval: True or False
Example:
>>> group.set_approval_mode(False)
""" """
data = {"set_mode": int(require_admin_approval), "thread_fbid": self.id} data = {"set_mode": int(require_admin_approval), "thread_fbid": self.id}
j = self.session._payload_post("/messaging/set_approval_mode/?dpr=1", data) j = self.session._payload_post("/messaging/set_approval_mode/?dpr=1", data)
@@ -118,6 +148,9 @@ class Group(_thread.ThreadABC):
Args: Args:
user_ids: One or more user IDs to accept user_ids: One or more user IDs to accept
Example:
>>> group.accept_users(["1234", "2345"])
""" """
self._users_approval(user_ids, True) self._users_approval(user_ids, True)
@@ -126,6 +159,9 @@ class Group(_thread.ThreadABC):
Args: Args:
user_ids: One or more user IDs to deny user_ids: One or more user IDs to deny
Example:
>>> group.deny_users(["1234", "2345"])
""" """
self._users_approval(user_ids, False) self._users_approval(user_ids, False)

View File

@@ -43,7 +43,11 @@ class EmojiSize(enum.Enum):
@attrs_default @attrs_default
class Mention: class Mention:
"""Represents a ``@mention``.""" """Represents a ``@mention``.
>>> fbchat.Mention(thread_id="1234", offset=5, length=2)
Mention(thread_id="1234", offset=5, length=2)
"""
#: The thread ID the mention is pointing at #: The thread ID the mention is pointing at
thread_id = attr.ib(type=str) thread_id = attr.ib(type=str)
@@ -82,7 +86,12 @@ SENDABLE_REACTIONS = ("❤", "😍", "😆", "😮", "😢", "😠", "👍", "
@attrs_default @attrs_default
class Message: class Message:
"""Represents a Facebook message.""" """Represents a Facebook message.
Example:
>>> thread = fbchat.User(session=session, id="1234")
>>> message = fbchat.Message(thread=thread, id="mid.$XYZ")
"""
#: The thread that this message belongs to. #: The thread that this message belongs to.
thread = attr.ib(type="_thread.ThreadABC") thread = attr.ib(type="_thread.ThreadABC")
@@ -95,7 +104,11 @@ class Message:
return self.thread.session return self.thread.session
def unsend(self): def unsend(self):
"""Unsend the message (removes it for everyone).""" """Unsend the message (removes it for everyone).
Example:
>>> message.unsend()
"""
data = {"message_id": self.id} data = {"message_id": self.id}
j = self.session._payload_post("/messaging/unsend_message/?dpr=1", data) j = self.session._payload_post("/messaging/unsend_message/?dpr=1", data)
@@ -107,6 +120,9 @@ class Message:
Args: Args:
reaction: Reaction emoji to use, or if ``None``, removes reaction. reaction: Reaction emoji to use, or if ``None``, removes reaction.
Example:
>>> message.react("😍")
""" """
if reaction and reaction not in SENDABLE_REACTIONS: if reaction and reaction not in SENDABLE_REACTIONS:
raise ValueError( raise ValueError(
@@ -128,7 +144,13 @@ class Message:
_exception.handle_graphql_errors(j) _exception.handle_graphql_errors(j)
def fetch(self) -> "MessageData": def fetch(self) -> "MessageData":
"""Fetch fresh `MessageData` object.""" """Fetch fresh `MessageData` object.
Example:
>>> message = message.fetch()
>>> message.text
"The message text"
"""
message_info = self.thread._forced_fetch(self.id).get("message") message_info = self.thread._forced_fetch(self.id).get("message")
return MessageData._from_graphql(self.thread, message_info) return MessageData._from_graphql(self.thread, message_info)
@@ -139,10 +161,10 @@ class Message:
Return a tuple, with the formatted string and relevant mentions. Return a tuple, with the formatted string and relevant mentions.
>>> Message.format_mentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael")) >>> Message.format_mentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael"))
("Hey 'Peter'! My name is Michael", [<Mention 1234: offset=4 length=7>, <Mention 4321: offset=24 length=7>]) ("Hey 'Peter'! My name is Michael", [Mention(thread_id=1234, offset=4, length=7), Mention(thread_id=4321, offset=24, length=7)])
>>> Message.format_mentions("Hey {p}! My name is {}", ("1234", "Michael"), p=("4321", "Peter")) >>> Message.format_mentions("Hey {p}! My name is {}", ("1234", "Michael"), p=("4321", "Peter"))
('Hey Peter! My name is Michael', [<Mention 4321: offset=4 length=5>, <Mention 1234: offset=22 length=7>]) ('Hey Peter! My name is Michael', [Mention(thread_id=4321, offset=4, length=5), Mention(thread_id=1234, offset=22, length=7)])
""" """
result = "" result = ""
mentions = list() mentions = list()

View File

@@ -49,6 +49,9 @@ class Listener:
session: The session to use when making requests. session: The session to use when making requests.
chat_on: Whether ... chat_on: Whether ...
foreground: Whether ... foreground: Whether ...
Example:
>>> listener = fbchat.Listener.connect(session, chat_on=True, foreground=True)
""" """
mqtt = paho.mqtt.client.Client( mqtt = paho.mqtt.client.Client(
client_id="mqttwsclient", client_id="mqttwsclient",
@@ -343,6 +346,12 @@ class Listener:
Yields events when they arrive. Yields events when they arrive.
This will automatically reconnect on errors. This will automatically reconnect on errors.
Example:
Print events continually.
>>> for event in listener.listen():
... print(event)
""" """
while self._loop_once(): while self._loop_once():
if self._events: if self._events:
@@ -355,6 +364,14 @@ class Listener:
Can be called while listening, which will stop the listening loop. Can be called while listening, which will stop the listening loop.
The `Listener` object should not be used after this is called! The `Listener` object should not be used after this is called!
Example:
Stop the listener when recieving a message with the text "/stop"
>>> for event in listener.listen():
... if isinstance(event, fbchat.MessageEvent):
... if event.message.text == "/stop":
... listener.disconnect() # Almost the same "break"
""" """
self._mqtt.disconnect() self._mqtt.disconnect()

View File

@@ -6,7 +6,11 @@ from . import _session, _plan, _thread
@attrs_default @attrs_default
class Page(_thread.ThreadABC): class Page(_thread.ThreadABC):
"""Represents a Facebook page. Implements `ThreadABC`.""" """Represents a Facebook page. Implements `ThreadABC`.
Example:
>>> page = fbchat.Page(session=session, id="1234")
"""
# TODO: Implement pages properly, the implementation is lacking in a lot of places! # TODO: Implement pages properly, the implementation is lacking in a lot of places!

View File

@@ -22,7 +22,11 @@ ACONTEXT = {
@attrs_default @attrs_default
class Plan: class Plan:
"""Base model for plans.""" """Base model for plans.
Example:
>>> plan = fbchat.Plan(session=session, id="1234")
"""
#: The session to use when making requests. #: The session to use when making requests.
session = attr.ib(type=_session.Session) session = attr.ib(type=_session.Session)
@@ -30,7 +34,13 @@ class Plan:
id = attr.ib(converter=str, type=str) id = attr.ib(converter=str, type=str)
def fetch(self) -> "PlanData": def fetch(self) -> "PlanData":
"""Fetch fresh `PlanData` object.""" """Fetch fresh `PlanData` object.
Example:
>>> plan = plan.fetch()
>>> plan.title
"A plan"
"""
data = {"event_reminder_id": self.id} data = {"event_reminder_id": self.id}
j = self.session._payload_post("/ajax/eventreminder", data) j = self.session._payload_post("/ajax/eventreminder", data)
return PlanData._from_fetch(self.session, j) return PlanData._from_fetch(self.session, j)
@@ -80,7 +90,11 @@ class Plan:
j = self.session._payload_post("/ajax/eventreminder/submit", data) j = self.session._payload_post("/ajax/eventreminder/submit", data)
def delete(self): def delete(self):
"""Delete the plan.""" """Delete the plan.
Example:
>>> plan.delete()
"""
data = {"event_reminder_id": self.id, "delete": "true", "acontext": ACONTEXT} data = {"event_reminder_id": self.id, "delete": "true", "acontext": ACONTEXT}
j = self.session._payload_post("/ajax/eventreminder/submit", data) j = self.session._payload_post("/ajax/eventreminder/submit", data)
@@ -93,11 +107,19 @@ class Plan:
j = self.session._payload_post("/ajax/eventreminder/rsvp", data) j = self.session._payload_post("/ajax/eventreminder/rsvp", data)
def participate(self): def participate(self):
"""Set yourself as GOING/participating to the plan.""" """Set yourself as GOING/participating to the plan.
Example:
>>> plan.participate()
"""
return self._change_participation(True) return self._change_participation(True)
def decline(self): def decline(self):
"""Set yourself as having DECLINED the plan.""" """Set yourself as having DECLINED the plan.
Example:
>>> plan.decline()
"""
return self._change_participation(False) return self._change_participation(False)

View File

@@ -70,7 +70,15 @@ class Poll:
) )
def fetch_options(self) -> Sequence[PollOption]: def fetch_options(self) -> Sequence[PollOption]:
"""Fetch full list of `PollOption` objects on the poll.""" """Fetch all `PollOption` objects on the poll.
The result is ordered with options with the most votes first.
Example:
>>> options = poll.fetch_options()
>>> options[0].text
"An option"
"""
data = {"question_id": self.id} data = {"question_id": self.id}
j = self.session._payload_post("/ajax/mercury/get_poll_options", data) j = self.session._payload_post("/ajax/mercury/get_poll_options", data)
return [PollOption._from_graphql(m) for m in j] return [PollOption._from_graphql(m) for m in j]
@@ -83,11 +91,11 @@ class Poll:
new_options: New options to add new_options: New options to add
Example: Example:
options = poll.fetch_options() >>> options = poll.fetch_options()
# Add option >>> # Add option
poll.set_votes([o.id for o in options], new_options=["New option"]) >>> poll.set_votes([o.id for o in options], new_options=["New option"])
# Remove vote from option >>> # Remove vote from option
poll.set_votes([o.id for o in options if o.text != "Option 1"]) >>> poll.set_votes([o.id for o in options if o.text != "Option 1"])
""" """
data = {"question_id": self.id} data = {"question_id": self.id}

View File

@@ -155,10 +155,17 @@ class Session:
"""Login the user, using ``email`` and ``password``. """Login the user, using ``email`` and ``password``.
Args: Args:
email: Facebook ``email`` or ``id`` or ``phone number`` email: Facebook ``email``, ``id`` or ``phone number``
password: Facebook account password password: Facebook account password
on_2fa_callback: Function that will be called, in case a 2FA code is needed. on_2fa_callback: Function that will be called, in case a 2FA code is needed.
This should return the requested 2FA code. This should return the requested 2FA code.
Example:
>>> import getpass
>>> import fbchat
>>> session = fbchat.Session.login("<email or phone>", getpass.getpass())
>>> session.user_id
"1234"
""" """
session = session_factory() session = session_factory()
@@ -215,6 +222,9 @@ class Session:
Returns: Returns:
Whether the user is still logged in Whether the user is still logged in
Example:
>>> assert session.is_logged_in()
""" """
# Send a request to the login url, to see if we're directed to the home page # Send a request to the login url, to see if we're directed to the home page
url = "https://m.facebook.com/login.php?login_attempt=1" url = "https://m.facebook.com/login.php?login_attempt=1"
@@ -228,6 +238,9 @@ class Session:
"""Safely log out the user. """Safely log out the user.
The session object must not be used after this action has been performed! The session object must not be used after this action has been performed!
Example:
>>> session.logout()
""" """
logout_h = self._logout_h logout_h = self._logout_h
if not logout_h: if not logout_h:
@@ -285,6 +298,9 @@ class Session:
Returns: Returns:
A dictionary containing session cookies A dictionary containing session cookies
Example:
>>> cookies = session.get_cookies()
""" """
return self._session.cookies.get_dict() return self._session.cookies.get_dict()
@@ -294,6 +310,11 @@ class Session:
Args: Args:
cookies: A dictionary containing session cookies cookies: A dictionary containing session cookies
Example:
>>> cookies = session.get_cookies()
>>> # Store cookies somewhere, and then subsequently
>>> session = fbchat.Session.from_cookies(cookies)
""" """
session = session_factory() session = session_factory()
session.cookies = requests.cookies.merge_cookies(session.cookies, cookies) session.cookies = requests.cookies.merge_cookies(session.cookies, cookies)
@@ -346,7 +367,14 @@ class Session:
`files` should be a list of files that requests can upload, see `files` should be a list of files that requests can upload, see
`requests.request <https://docs.python-requests.org/en/master/api/#requests.request>`_. `requests.request <https://docs.python-requests.org/en/master/api/#requests.request>`_.
Return a list of tuples with a file's ID and mimetype. Example:
>>> with open("file.txt", "rb") as f:
... (file,) = session._upload([("file.txt", f, "text/plain")])
...
>>> file
("1234", "text/plain")
Return:
Tuples with a file's ID and mimetype.
""" """
file_dict = {"upload_{}".format(i): f for i, f in enumerate(files)} file_dict = {"upload_{}".format(i): f for i, f in enumerate(files)}

View File

@@ -84,6 +84,11 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
first: Whether to wave first or wave back first: Whether to wave first or wave back
Example:
Wave back to the thread.
>>> thread.wave(False)
""" """
data = self._to_send_data() data = self._to_send_data()
data["action_type"] = "ma-type:user-generated-message" data["action_type"] = "ma-type:user-generated-message"
@@ -109,6 +114,10 @@ class ThreadABC(metaclass=abc.ABCMeta):
files: Optional tuples, each containing an uploaded file's ID and mimetype files: Optional tuples, each containing an uploaded file's ID and mimetype
reply_to_id: Optional message to reply to reply_to_id: Optional message to reply to
Example:
>>> mention = fbchat.Mention(thread_id="1234", offset=5, length=2)
>>> thread.send_text("A message", mentions=[mention])
Returns: Returns:
The sent message The sent message
""" """
@@ -138,6 +147,9 @@ class ThreadABC(metaclass=abc.ABCMeta):
emoji: The emoji to send emoji: The emoji to send
size: The size of the emoji size: The size of the emoji
Example:
>>> thread.send_emoji("😀", size=fbchat.EmojiSize.LARGE)
Returns: Returns:
The sent message The sent message
""" """
@@ -153,6 +165,11 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
sticker_id: ID of the sticker to send sticker_id: ID of the sticker to send
Example:
Send a sticker with the id "1889713947839631"
>>> thread.send_sticker("1889713947839631")
Returns: Returns:
The sent message The sent message
""" """
@@ -175,6 +192,11 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
latitude: The location latitude latitude: The location latitude
longitude: The location longitude longitude: The location longitude
Example:
Send a location in London, United Kingdom.
>>> thread.send_location(51.5287718, -0.2416815)
""" """
self._send_location(True, latitude=latitude, longitude=longitude) self._send_location(True, latitude=latitude, longitude=longitude)
@@ -184,6 +206,11 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
latitude: The location latitude latitude: The location latitude
longitude: The location longitude longitude: The location longitude
Example:
Send a pinned location in Beijing, China.
>>> thread.send_location(39.9390731, 116.117273)
""" """
self._send_location(False, latitude=latitude, longitude=longitude) self._send_location(False, latitude=latitude, longitude=longitude)
@@ -191,6 +218,14 @@ class ThreadABC(metaclass=abc.ABCMeta):
"""Send files from file IDs to a thread. """Send files from file IDs to a thread.
`files` should be a list of tuples, with a file's ID and mimetype. `files` should be a list of tuples, with a file's ID and mimetype.
Example:
Upload and send a video to a thread.
>>> with open("video.mp4", "rb") as f:
... files = session._upload([("video.mp4", f, "video/mp4")])
>>>
>>> thread.send_files(files=files)
""" """
return self.send_text(text=None, files=files) return self.send_text(text=None, files=files)
@@ -285,11 +320,20 @@ class ThreadABC(metaclass=abc.ABCMeta):
Warning! If someone send a message to the thread that matches the query, while Warning! If someone send a message to the thread that matches the query, while
we're searching, some snippets will get returned twice. we're searching, some snippets will get returned twice.
Not sure if we should handle it, Facebook's implementation doesn't... This is fundamentally unfixable, it's just how the endpoint is implemented.
The returned message snippets are ordered by last sent.
Args: Args:
query: Text to search for query: Text to search for
limit: Max. number of message snippets to retrieve limit: Max. number of message snippets to retrieve
Example:
Fetch the latest message in the thread that matches the query.
>>> (message,) = thread.search_messages("abc", limit=1)
>>> message.text
"Some text and abc"
""" """
offset = 0 offset = 0
# The max limit is measured empirically to 420, safe default chosen below # The max limit is measured empirically to 420, safe default chosen below
@@ -330,11 +374,22 @@ class ThreadABC(metaclass=abc.ABCMeta):
] ]
def fetch_messages(self, limit: Optional[int]) -> Iterable["_message.Message"]: def fetch_messages(self, limit: Optional[int]) -> Iterable["_message.Message"]:
"""Fetch messages in a thread, with most recent messages first. """Fetch messages in a thread.
The returned messages are ordered by most recent first.
Args: Args:
limit: Max. number of threads to retrieve. If ``None``, all threads will be limit: Max. number of threads to retrieve. If ``None``, all threads will be
retrieved. retrieved.
Example:
>>> for message in thread.fetch_messages(limit=5)
... print(message.text)
...
A message
Another message
None
A fourth message
""" """
# This is measured empirically as 210 in extreme cases, fairly safe default # This is measured empirically as 210 in extreme cases, fairly safe default
# chosen below # chosen below
@@ -389,6 +444,13 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
limit: Max. number of images to retrieve. If ``None``, all images will be limit: Max. number of images to retrieve. If ``None``, all images will be
retrieved. retrieved.
Example:
>>> for image in thread.fetch_messages(limit=3)
... print(image.id)
...
1234
2345
""" """
cursor = None cursor = None
# The max limit on this request is unknown, so we set it reasonably high # The max limit on this request is unknown, so we set it reasonably high
@@ -407,6 +469,9 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
user_id: User that will have their nickname changed user_id: User that will have their nickname changed
nickname: New nickname nickname: New nickname
Example:
>>> thread.set_nickname("1234", "A nickname")
""" """
data = { data = {
"nickname": nickname, "nickname": nickname,
@@ -422,16 +487,22 @@ class ThreadABC(metaclass=abc.ABCMeta):
The new color must be one of the following:: The new color must be one of the following::
"#0084ff", "#44bec7", "#ffc300", "#fa3c4c", "#d696bb", "#6699cc", "#13cf13", "#0084ff", "#44bec7", "#ffc300", "#fa3c4c", "#d696bb", "#6699cc",
"#ff7e29", "#e68585", "#7646ff", "#20cef5", "#67b868", "#d4a88c", "#ff5ca1", "#13cf13", "#ff7e29", "#e68585", "#7646ff", "#20cef5", "#67b868",
"#a695c7", "#ff7ca8", "#1adb5b", "#f01d6a", "#ff9c19" or "#0edcde". "#d4a88c", "#ff5ca1", "#a695c7", "#ff7ca8", "#1adb5b", "#f01d6a",
"#ff9c19" or "#0edcde".
The default is "#0084ff".
This list is subject to change in the future! This list is subject to change in the future!
The default when creating a new thread is ``"#0084ff"``.
Args: Args:
color: New thread color color: New thread color
Example:
Set the thread color to "Coral Pink".
>>> thread.set_color("#e68585")
""" """
if color not in SETABLE_COLORS: if color not in SETABLE_COLORS:
raise ValueError( raise ValueError(
@@ -459,11 +530,16 @@ class ThreadABC(metaclass=abc.ABCMeta):
# _graphql.from_doc_id("1768656253222505", {"data": data}) # _graphql.from_doc_id("1768656253222505", {"data": data})
# ) # )
def set_emoji(self, emoji: str): def set_emoji(self, emoji: Optional[str]):
"""Change thread emoji. """Change thread emoji.
Args: Args:
emoji: New thread emoji emoji: New thread emoji. If ``None``, will be set to the default "LIKE" icon
Example:
Set the thread emoji to "😊".
>>> thread.set_emoji("😊")
""" """
data = {"emoji_choice": emoji, "thread_or_other_fbid": self.id} data = {"emoji_choice": emoji, "thread_or_other_fbid": self.id}
# While changing the emoji, the Facebook web client actually sends multiple # While changing the emoji, the Facebook web client actually sends multiple
@@ -477,6 +553,9 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
attachment_id: Attachment ID to forward attachment_id: Attachment ID to forward
Example:
>>> thread.forward_attachment("1234")
""" """
data = { data = {
"attachment_id": attachment_id, "attachment_id": attachment_id,
@@ -497,11 +576,19 @@ class ThreadABC(metaclass=abc.ABCMeta):
j = self.session._payload_post("/ajax/messaging/typ.php", data) j = self.session._payload_post("/ajax/messaging/typ.php", data)
def start_typing(self): def start_typing(self):
"""Set the current user to start typing in the thread.""" """Set the current user to start typing in the thread.
Example:
>>> thread.start_typing()
"""
self._set_typing(True) self._set_typing(True)
def stop_typing(self): def stop_typing(self):
"""Set the current user to stop typing in the thread.""" """Set the current user to stop typing in the thread.
Example:
>>> thread.stop_typing()
"""
self._set_typing(False) self._set_typing(False)
def create_plan( def create_plan(
@@ -518,6 +605,9 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
name: Name of the new plan name: Name of the new plan
at: When the plan is for at: When the plan is for
Example:
>>> thread.create_plan(...)
""" """
return _plan.Plan._create(self, name, at, location_name, location_id) return _plan.Plan._create(self, name, at, location_name, location_id)
@@ -529,7 +619,7 @@ class ThreadABC(metaclass=abc.ABCMeta):
options: Options and whether you want to select the option options: Options and whether you want to select the option
Example: Example:
thread.create_poll("Test poll", {"Option 1": True, "Option 2": False}) >>> thread.create_poll("Test poll", {"Option 1": True, "Option 2": False})
""" """
# We're using ordered dictionaries, because the Facebook endpoint that parses # We're using ordered dictionaries, because the Facebook endpoint that parses
# the POST parameters is badly implemented, and deals with ordering the options # the POST parameters is badly implemented, and deals with ordering the options
@@ -557,6 +647,10 @@ class ThreadABC(metaclass=abc.ABCMeta):
Args: Args:
duration: Time to mute, use ``None`` to mute forever duration: Time to mute, use ``None`` to mute forever
Example:
>>> import datetime
>>> thread.mute(datetime.timedelta(days=2))
""" """
if duration is None: if duration is None:
setting = "-1" setting = "-1"
@@ -568,7 +662,11 @@ class ThreadABC(metaclass=abc.ABCMeta):
) )
def unmute(self): def unmute(self):
"""Unmute the thread.""" """Unmute the thread.
Example:
>>> thread.unmute()
"""
return self.mute(datetime.timedelta(0)) return self.mute(datetime.timedelta(0))
def _mute_reactions(self, mode: bool): def _mute_reactions(self, mode: bool):

View File

@@ -36,7 +36,11 @@ GENDERS = {
@attrs_default @attrs_default
class User(_thread.ThreadABC): class User(_thread.ThreadABC):
"""Represents a Facebook user. Implements `ThreadABC`.""" """Represents a Facebook user. Implements `ThreadABC`.
Example:
>>> user = fbchat.User(session=session, id="1234")
"""
#: The session to use when making requests. #: The session to use when making requests.
session = attr.ib(type=_session.Session) session = attr.ib(type=_session.Session)
@@ -51,22 +55,38 @@ class User(_thread.ThreadABC):
} }
def confirm_friend_request(self): def confirm_friend_request(self):
"""Confirm a friend request, adding the user to your friend list.""" """Confirm a friend request, adding the user to your friend list.
Example:
>>> user.confirm_friend_request()
"""
data = {"to_friend": self.id, "action": "confirm"} data = {"to_friend": self.id, "action": "confirm"}
j = self.session._payload_post("/ajax/add_friend/action.php?dpr=1", data) j = self.session._payload_post("/ajax/add_friend/action.php?dpr=1", data)
def remove_friend(self): def remove_friend(self):
"""Remove the user from the client's friend list.""" """Remove the user from the client's friend list.
Example:
>>> user.remove_friend()
"""
data = {"uid": self.id} data = {"uid": self.id}
j = self.session._payload_post("/ajax/profile/removefriendconfirm.php", data) j = self.session._payload_post("/ajax/profile/removefriendconfirm.php", data)
def block(self): def block(self):
"""Block messages from the user.""" """Block messages from the user.
Example:
>>> user.block()
"""
data = {"fbid": self.id} data = {"fbid": self.id}
j = self.session._payload_post("/messaging/block_messages/?dpr=1", data) j = self.session._payload_post("/messaging/block_messages/?dpr=1", data)
def unblock(self): def unblock(self):
"""Unblock a previously blocked user.""" """Unblock a previously blocked user.
Example:
>>> user.unblock()
"""
data = {"fbid": self.id} data = {"fbid": self.id}
j = self.session._payload_post("/messaging/unblock_messages/?dpr=1", data) j = self.session._payload_post("/messaging/unblock_messages/?dpr=1", data)