Compare commits

..

6 Commits

Author SHA1 Message Date
Mads Marquart
f64c487a2d Bump version: 2.0.0a1 -> 2.0.0a2 2020-03-11 15:45:02 +01:00
Mads Marquart
11534604fe Remove user agent randomization
Caused problems with logging in, and didn't really help on anything
2020-03-11 15:44:34 +01:00
Mads Marquart
9990952fa6 Add Connect and Disconnect events 2020-03-11 15:27:00 +01:00
Mads Marquart
7ee7361646 Clean up event parsing 2020-03-11 15:10:25 +01:00
Mads Marquart
89c6af516c Fix various documentation mistakes 2020-03-11 15:00:50 +01:00
Mads Marquart
c27f599e37 Fix type specifiers in models 2020-03-11 14:43:28 +01:00
23 changed files with 201 additions and 158 deletions

View File

@@ -4,5 +4,8 @@ Exceptions
.. autoexception:: FacebookError()
.. autoexception:: HTTPError()
.. autoexception:: ParseError()
.. autoexception:: NotLoggedIn()
.. autoexception:: ExternalError()
.. autoexception:: GraphQLError()
.. autoexception:: InvalidParameters()
.. autoexception:: PleaseRefresh()

View File

@@ -1,3 +1,4 @@
.. highlight:: sh
.. See README.rst for explanation of these markers
.. include:: ../README.rst

View File

@@ -46,7 +46,7 @@ A thread basically just means "something I can chat with", but more precisely, i
- The conversation between you and a single Facebook user (`User`)
- The conversation between you and a Facebook Page (`Page`)
You can get your own user ID with `Session.user.id`.
You can get your own user ID from `Session.user` with ``session.user.id``.
Getting the ID of a specific group thread is fairly trivial, you only need to login to `<https://www.messenger.com/>`_, click on the group you want to find the ID of, and then read the id from the address bar.
The URL will look something like this: ``https://www.messenger.com/t/1234567890``, where ``1234567890`` would be the ID of the group.
@@ -111,19 +111,19 @@ Some functionality, like adding users to a `Group`, or blocking a `User`, logica
With that out of the way, let's see some examples!
The simplest way of interracting with a thread is by sending a message::
The simplest way of interacting with a thread is by sending a message::
# Send a message to the user
message = user.send_text("test message")
There are many types of messages you can send, see the full API documentation for more.
Notice how we held on to the sent message? The return type i a `Message` instance, so you can interract with it afterwards::
Notice how we held on to the sent message? The return type i a `Message` instance, so you can interact with it afterwards::
# React to the message with the 😍 emoji
message.react("😍")
Besides sending messages, you can also interract with threads in other ways. An example is to change the thread color::
Besides sending messages, you can also interact with threads in other ways. An example is to change the thread color::
# Will change the thread color to the default blue
thread.set_color("#0084ff")

View File

@@ -74,6 +74,8 @@ from ._events import (
Event,
UnknownEvent,
ThreadEvent,
Connect,
Disconnect,
# _client_payload
ReactionEvent,
UserStatusEvent,
@@ -115,7 +117,7 @@ from ._listen import Listener
from ._client import Client
__version__ = "2.0.0a1"
__version__ = "2.0.0a2"
__all__ = ("Session", "Listener", "Client")

View File

@@ -33,10 +33,10 @@ class Client:
But does not include deactivated, deleted or memorialized users (logically,
since you can't chat with those).
The order these are returned is arbitary.
The order these are returned is arbitrary.
Example:
Get the name of an arbitary user that you're currently chatting with.
Get the name of an arbitrary user that you're currently chatting with.
>>> users = client.fetch_users()
>>> users[0].name
@@ -211,7 +211,7 @@ class Client:
Warning! If someone send a message to a thread that matches the query, while
we're searching, some snippets will get returned twice, and some will be lost.
This is fundamentally unfixable, it's just how the endpoint is implemented.
This is fundamentally not fixable, it's just how the endpoint is implemented.
Args:
query: Text to search for

View File

@@ -24,8 +24,7 @@ class Typing(ThreadEvent):
return cls(author=author, thread=author, status=status)
@classmethod
def _parse(cls, session, data):
# TODO: Rename this method
def _parse_thread_typing(cls, session, data):
author = _threads.User(session=session, id=str(data["sender_fbid"]))
thread = _threads.Group(session=session, id=str(data["thread"]))
status = data["state"] == 1
@@ -68,6 +67,25 @@ class Presence(Event):
return cls(statuses=statuses, full=data["list_type"] == "full")
@attrs_event
class Connect(Event):
"""The client was connected to Facebook.
This is not guaranteed to be triggered the same amount of times `Disconnect`!
"""
@attrs_event
class Disconnect(Event):
"""The client lost the connection to Facebook.
This is not guaranteed to be triggered the same amount of times `Connect`!
"""
#: The reason / error string for the disconnect
reason = attr.ib(type=str)
def parse_events(session, topic, data):
# See Mqtt._configure_connect_options for information about these topics
try:
@@ -90,7 +108,7 @@ def parse_events(session, topic, data):
) from e
elif topic == "/thread_typing":
yield Typing._parse(session, data)
yield Typing._parse_thread_typing(session, data)
elif topic == "/orca_typing_notifications":
yield Typing._parse_orca(session, data)

View File

@@ -1,5 +1,4 @@
import attr
import abc
from .._common import kw_only
from .. import _exception, _util, _threads
@@ -10,14 +9,9 @@ attrs_event = attr.s(slots=True, kw_only=kw_only, frozen=True)
@attrs_event
class Event(metaclass=abc.ABCMeta):
class Event:
"""Base class for all events."""
@classmethod
@abc.abstractmethod
def _parse(cls, session, data):
raise NotImplementedError
@staticmethod
def _get_thread(session, data):
# TODO: Handle pages? Is it even possible?
@@ -60,3 +54,9 @@ class ThreadEvent(Event):
thread = cls._get_thread(session, metadata)
at = _util.millis_to_datetime(int(metadata["timestamp"]))
return author, thread, at
@classmethod
def _parse_fetch(cls, session, data):
author = _threads.User(session=session, id=data["message_sender"]["id"])
at = _util.millis_to_datetime(int(data["timestamp_precise"]))
return author, at

View File

@@ -1,7 +1,7 @@
import attr
import requests
from typing import Any
from typing import Any, Optional
# Not frozen, since that doesn't work in PyPy
@attr.s(slots=True, auto_exc=True)
@@ -20,7 +20,7 @@ class HTTPError(FacebookError):
"""Base class for errors with the HTTP(s) connection to Facebook."""
#: The returned HTTP status code, if relevant
status_code = attr.ib(None, type=int)
status_code = attr.ib(None, type=Optional[int])
def __str__(self):
if not self.status_code:
@@ -58,7 +58,7 @@ class ExternalError(FacebookError):
#: The error message that Facebook returned (Possibly in the user's own language)
description = attr.ib(type=str)
#: The error code that Facebook returned
code = attr.ib(None, type=int)
code = attr.ib(None, type=Optional[int])
def __str__(self):
if self.code:
@@ -73,7 +73,7 @@ class GraphQLError(ExternalError):
# TODO: Handle multiple errors
#: Query debug information
debug_info = attr.ib(None, type=str)
debug_info = attr.ib(None, type=Optional[str])
def __str__(self):
if self.debug_info:

View File

@@ -5,7 +5,7 @@ import requests
from ._common import log, kw_only
from . import _util, _exception, _session, _graphql, _events
from typing import Iterable, Optional, Mapping
from typing import Iterable, Optional, Mapping, List
HOST = "edge-chat.facebook.com"
@@ -118,7 +118,7 @@ class Listener:
_mqtt = attr.ib(factory=mqtt_factory, type=paho.mqtt.client.Client)
_sync_token = attr.ib(None, type=Optional[str])
_sequence_id = attr.ib(None, type=Optional[int])
_tmp_events = attr.ib(factory=list, type=Iterable[_events.Event])
_tmp_events = attr.ib(factory=list, type=List[_events.Event])
def __attrs_post_init__(self):
# Configure callbacks
@@ -152,6 +152,7 @@ class Listener:
"The MQTT listener was disconnected for too long,"
" events may have been lost"
)
# TODO: Find a way to tell the user that they may now be missing events
self._sync_token = None
self._sequence_id = None
return False
@@ -282,11 +283,12 @@ class Listener:
path="/chat?sid={}".format(session_id), headers=headers
)
def _reconnect(self):
def _reconnect(self) -> bool:
# Try reconnecting
self._configure_connect_options()
try:
self._mqtt.reconnect()
return True
except (
# Taken from .loop_forever
paho.mqtt.client.socket.error,
@@ -296,6 +298,7 @@ class Listener:
log.debug("MQTT reconnection failed: %s", e)
# Wait before reconnecting
self._mqtt._reconnect_wait()
return False
def listen(self) -> Iterable[_events.Event]:
"""Run the listening loop continually.
@@ -315,12 +318,10 @@ class Listener:
self._sequence_id = fetch_sequence_id(self.session)
# Make sure we're connected
while True:
# Beware, internal API, may have to change this to something more stable!
if self._mqtt._state == paho.mqtt.client.mqtt_cs_connect_async:
self._reconnect()
else:
break
while not self._reconnect():
pass
yield _events.Connect()
while True:
rc = self._mqtt.loop(timeout=1.0)
@@ -339,18 +340,23 @@ class Listener:
if rc != paho.mqtt.client.MQTT_ERR_SUCCESS:
# If known/expected error
if rc == paho.mqtt.client.MQTT_ERR_CONN_LOST:
log.warning("Connection lost, retrying")
yield _events.Disconnect(reason="Connection lost, retrying")
elif rc == paho.mqtt.client.MQTT_ERR_NOMEM:
# This error is wrongly classified
# See https://github.com/eclipse/paho.mqtt.python/issues/340
log.warning("Connection error, retrying")
yield _events.Disconnect(reason="Connection error, retrying")
elif rc == paho.mqtt.client.MQTT_ERR_CONN_REFUSED:
raise _exception.NotLoggedIn("MQTT connection refused")
else:
err = paho.mqtt.client.error_string(rc)
log.error("MQTT Error: %s", err)
reason = "MQTT Error: {}, retrying".format(err)
yield _events.Disconnect(reason=reason)
self._reconnect()
while not self._reconnect():
pass
yield _events.Connect()
if self._tmp_events:
yield from self._tmp_events
@@ -364,7 +370,7 @@ class Listener:
The `Listener` object should not be used after this is called!
Example:
Stop the listener when recieving a message with the text "/stop"
Stop the listener when receiving a message with the text "/stop"
>>> for event in listener.listen():
... if isinstance(event, fbchat.MessageEvent):
@@ -374,7 +380,7 @@ class Listener:
self._mqtt.disconnect()
def set_foreground(self, value: bool) -> None:
"""Set the `foreground` value while listening."""
"""Set the ``foreground`` value while listening."""
# TODO: Document what this actually does!
payload = _util.json_minimal({"foreground": value})
info = self._mqtt.publish("/foreground_state", payload=payload, qos=1)
@@ -383,7 +389,7 @@ class Listener:
# info.wait_for_publish()
def set_chat_on(self, value: bool) -> None:
"""Set the `chat_on` value while listening."""
"""Set the ``chat_on`` value while listening."""
# TODO: Document what this actually does!
# TODO: Is this the right request to make?
data = {"make_user_available_when_in_foreground": value}

View File

@@ -3,7 +3,7 @@ from . import Image
from .._common import attrs_default
from .. import _util
from typing import Sequence
from typing import Optional, Sequence
@attrs_default
@@ -11,7 +11,7 @@ class Attachment:
"""Represents a Facebook attachment."""
#: The attachment ID
id = attr.ib(None, type=str)
id = attr.ib(None, type=Optional[str])
@attrs_default
@@ -24,21 +24,21 @@ class ShareAttachment(Attachment):
"""Represents a shared item (e.g. URL) attachment."""
#: ID of the author of the shared post
author = attr.ib(None, type=str)
author = attr.ib(None, type=Optional[str])
#: Target URL
url = attr.ib(None, type=str)
url = attr.ib(None, type=Optional[str])
#: Original URL if Facebook redirects the URL
original_url = attr.ib(None, type=str)
original_url = attr.ib(None, type=Optional[str])
#: Title of the attachment
title = attr.ib(None, type=str)
title = attr.ib(None, type=Optional[str])
#: Description of the attachment
description = attr.ib(None, type=str)
description = attr.ib(None, type=Optional[str])
#: Name of the source
source = attr.ib(None, type=str)
source = attr.ib(None, type=Optional[str])
#: The attached image
image = attr.ib(None, type=Image)
image = attr.ib(None, type=Optional[Image])
#: URL of the original image if Facebook uses ``safe_image``
original_image_url = attr.ib(None, type=str)
original_image_url = attr.ib(None, type=Optional[str])
#: List of additional attachments
attachments = attr.ib(factory=list, type=Sequence[Attachment])

View File

@@ -4,6 +4,8 @@ import enum
from .._common import attrs_default
from .. import _util
from typing import Optional
class ThreadLocation(enum.Enum):
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
@@ -21,11 +23,11 @@ class ThreadLocation(enum.Enum):
@attrs_default
class ActiveStatus:
#: Whether the user is active now
active = attr.ib(None, type=bool)
#: Datetime when the user was last active
last_active = attr.ib(None, type=datetime.datetime)
active = attr.ib(type=bool)
#: When the user was last active
last_active = attr.ib(None, type=Optional[datetime.datetime])
#: Whether the user is playing Messenger game now
in_game = attr.ib(None, type=bool)
in_game = attr.ib(None, type=Optional[bool])
@classmethod
def _from_orca_presence(cls, data):
@@ -42,9 +44,9 @@ class Image:
#: URL to the image
url = attr.ib(type=str)
#: Width of the image
width = attr.ib(None, type=int)
width = attr.ib(None, type=Optional[int])
#: Height of the image
height = attr.ib(None, type=int)
height = attr.ib(None, type=Optional[int])
@classmethod
def _from_uri(cls, data):

View File

@@ -4,7 +4,7 @@ from . import Image, Attachment
from .._common import attrs_default
from .. import _util
from typing import Set
from typing import Set, Optional
@attrs_default
@@ -12,13 +12,13 @@ class FileAttachment(Attachment):
"""Represents a file that has been sent as a Facebook attachment."""
#: URL where you can download the file
url = attr.ib(None, type=str)
url = attr.ib(None, type=Optional[str])
#: Size of the file in bytes
size = attr.ib(None, type=int)
size = attr.ib(None, type=Optional[int])
#: Name of the file
name = attr.ib(None, type=str)
name = attr.ib(None, type=Optional[str])
#: Whether Facebook determines that this file may be harmful
is_malicious = attr.ib(None, type=bool)
is_malicious = attr.ib(None, type=Optional[bool])
@classmethod
def _from_graphql(cls, data, size=None):
@@ -36,13 +36,13 @@ class AudioAttachment(Attachment):
"""Represents an audio file that has been sent as a Facebook attachment."""
#: Name of the file
filename = attr.ib(None, type=str)
filename = attr.ib(None, type=Optional[str])
#: URL of the audio file
url = attr.ib(None, type=str)
#: Duration of the audio clip as a timedelta
duration = attr.ib(None, type=datetime.timedelta)
url = attr.ib(None, type=Optional[str])
#: Duration of the audio clip
duration = attr.ib(None, type=Optional[datetime.timedelta])
#: Audio type
audio_type = attr.ib(None, type=str)
audio_type = attr.ib(None, type=Optional[str])
@classmethod
def _from_graphql(cls, data):
@@ -63,13 +63,13 @@ class ImageAttachment(Attachment):
"""
#: The extension of the original image (e.g. ``png``)
original_extension = attr.ib(None, type=str)
original_extension = attr.ib(None, type=Optional[str])
#: Width of original image
width = attr.ib(None, converter=lambda x: None if x is None else int(x), type=int)
width = attr.ib(None, converter=_util.int_or_none, type=Optional[int])
#: Height of original image
height = attr.ib(None, converter=lambda x: None if x is None else int(x), type=int)
height = attr.ib(None, converter=_util.int_or_none, type=Optional[int])
#: Whether the image is animated
is_animated = attr.ib(None, type=bool)
is_animated = attr.ib(None, type=Optional[bool])
#: A set, containing variously sized / various types of previews of the image
previews = attr.ib(factory=set, type=Set[Image])
@@ -113,15 +113,15 @@ class VideoAttachment(Attachment):
"""Represents a video that has been sent as a Facebook attachment."""
#: Size of the original video in bytes
size = attr.ib(None, type=int)
size = attr.ib(None, type=Optional[int])
#: Width of original video
width = attr.ib(None, type=int)
width = attr.ib(None, type=Optional[int])
#: Height of original video
height = attr.ib(None, type=int)
#: Length of video as a timedelta
duration = attr.ib(None, type=datetime.timedelta)
height = attr.ib(None, type=Optional[int])
#: Length of video
duration = attr.ib(None, type=Optional[datetime.timedelta])
#: URL to very compressed preview video
preview_url = attr.ib(None, type=str)
preview_url = attr.ib(None, type=Optional[str])
#: A set, containing variously sized previews of the video
previews = attr.ib(factory=set, type=Set[Image])

View File

@@ -1,8 +1,11 @@
import attr
import datetime
from . import Image, Attachment
from .._common import attrs_default
from .. import _util, _exception
from typing import Optional
@attrs_default
class LocationAttachment(Attachment):
@@ -12,15 +15,15 @@ class LocationAttachment(Attachment):
"""
#: Latitude of the location
latitude = attr.ib(None, type=float)
latitude = attr.ib(None, type=Optional[float])
#: Longitude of the location
longitude = attr.ib(None, type=float)
longitude = attr.ib(None, type=Optional[float])
#: Image showing the map of the location
image = attr.ib(None, type=Image)
image = attr.ib(None, type=Optional[Image])
#: URL to Bing maps with the location
url = attr.ib(None, type=str)
url = attr.ib(None, type=Optional[str])
# Address of the location
address = attr.ib(None, type=str)
address = attr.ib(None, type=Optional[str])
@classmethod
def _from_graphql(cls, data):
@@ -51,11 +54,11 @@ class LiveLocationAttachment(LocationAttachment):
"""Represents a live user location."""
#: Name of the location
name = attr.ib(None)
#: Datetime when live location expires
expires_at = attr.ib(None)
name = attr.ib(None, type=Optional[str])
#: When live location expires
expires_at = attr.ib(None, type=Optional[datetime.datetime])
#: True if live location is expired
is_expired = attr.ib(None)
is_expired = attr.ib(None, type=Optional[bool])
@classmethod
def _from_pull(cls, data):

View File

@@ -224,7 +224,7 @@ class MessageSnippet(Message):
#: ID of the sender
author = attr.ib(type=str)
#: Datetime of when the message was sent
#: When the message was sent
created_at = attr.ib(type=datetime.datetime)
#: The actual message
text = attr.ib(type=str)
@@ -252,34 +252,34 @@ class MessageData(Message):
#: ID of the sender
author = attr.ib(type=str)
#: Datetime of when the message was sent
#: When the message was sent
created_at = attr.ib(type=datetime.datetime)
#: The actual message
text = attr.ib(None, type=str)
text = attr.ib(None, type=Optional[str])
#: A list of `Mention` objects
mentions = attr.ib(factory=list, type=Sequence[Mention])
#: Size of a sent emoji
emoji_size = attr.ib(None, type=EmojiSize)
emoji_size = attr.ib(None, type=Optional[EmojiSize])
#: Whether the message is read
is_read = attr.ib(None, type=bool)
#: A list of people IDs who read the message, works only with `Client.fetch_thread_messages`
is_read = attr.ib(None, type=Optional[bool])
#: People IDs who read the message, only works with `ThreadABC.fetch_messages`
read_by = attr.ib(factory=list, type=bool)
#: A dictionary with user's IDs as keys, and their reaction as values
reactions = attr.ib(factory=dict, type=Mapping[str, str])
#: A `Sticker`
sticker = attr.ib(None, type=_sticker.Sticker)
sticker = attr.ib(None, type=Optional[_sticker.Sticker])
#: A list of attachments
attachments = attr.ib(factory=list, type=Sequence[_attachment.Attachment])
#: A list of `QuickReply`
quick_replies = attr.ib(factory=list, type=Sequence[_quick_reply.QuickReply])
#: Whether the message is unsent (deleted for everyone)
unsent = attr.ib(False, type=bool)
unsent = attr.ib(False, type=Optional[bool])
#: Message ID you want to reply to
reply_to_id = attr.ib(None, type=str)
reply_to_id = attr.ib(None, type=Optional[str])
#: Replied message
replied_to = attr.ib(None, type="MessageData")
replied_to = attr.ib(None, type=Optional["MessageData"])
#: Whether the message was forwarded
forwarded = attr.ib(False, type=bool)
forwarded = attr.ib(False, type=Optional[bool])
@staticmethod
def _get_forwarded_from_tags(tags):

View File

@@ -4,7 +4,7 @@ import enum
from .._common import attrs_default
from .. import _exception, _util, _session
from typing import Mapping, Sequence
from typing import Mapping, Sequence, Optional
class GuestStatus(enum.Enum):
@@ -132,13 +132,13 @@ class PlanData(Plan):
#: Plan title
title = attr.ib(type=str)
#: Plan location name
location = attr.ib(None, converter=lambda x: x or "", type=str)
location = attr.ib(None, converter=lambda x: x or "", type=Optional[str])
#: Plan location ID
location_id = attr.ib(None, converter=lambda x: x or "", type=str)
location_id = attr.ib(None, converter=lambda x: x or "", type=Optional[str])
#: ID of the plan creator
author_id = attr.ib(None, type=str)
author_id = attr.ib(None, type=Optional[str])
#: `User` ids mapped to their `GuestStatus`
guests = attr.ib(None, type=Mapping[str, GuestStatus])
guests = attr.ib(None, type=Optional[Mapping[str, GuestStatus]])
@property
def going(self) -> Sequence[str]:

View File

@@ -2,7 +2,7 @@ import attr
from . import Attachment
from .._common import attrs_default
from typing import Any
from typing import Any, Optional
@attrs_default
@@ -24,9 +24,9 @@ class QuickReplyText(QuickReply):
"""Represents a text quick reply."""
#: Title of the quick reply
title = attr.ib(None, type=str)
#: URL of the quick reply image (optional)
image_url = attr.ib(None, type=str)
title = attr.ib(None, type=Optional[str])
#: URL of the quick reply image
image_url = attr.ib(None, type=Optional[str])
#: Type of the quick reply
_type = "text"
@@ -43,8 +43,8 @@ class QuickReplyLocation(QuickReply):
class QuickReplyPhoneNumber(QuickReply):
"""Represents a phone number quick reply (Doesn't work on mobile)."""
#: URL of the quick reply image (optional)
image_url = attr.ib(None, type=str)
#: URL of the quick reply image
image_url = attr.ib(None, type=Optional[str])
#: Type of the quick reply
_type = "user_phone_number"
@@ -53,8 +53,8 @@ class QuickReplyPhoneNumber(QuickReply):
class QuickReplyEmail(QuickReply):
"""Represents an email quick reply (Doesn't work on mobile)."""
#: URL of the quick reply image (optional)
image_url = attr.ib(None, type=str)
#: URL of the quick reply image
image_url = attr.ib(None, type=Optional[str])
#: Type of the quick reply
_type = "user_email"

View File

@@ -2,34 +2,36 @@ import attr
from . import Image, Attachment
from .._common import attrs_default
from typing import Optional
@attrs_default
class Sticker(Attachment):
"""Represents a Facebook sticker that has been sent to a thread as an attachment."""
#: The sticker-pack's ID
pack = attr.ib(None, type=str)
pack = attr.ib(None, type=Optional[str])
#: Whether the sticker is animated
is_animated = attr.ib(False, type=bool)
# If the sticker is animated, the following should be present
#: URL to a medium spritemap
medium_sprite_image = attr.ib(None, type=str)
medium_sprite_image = attr.ib(None, type=Optional[str])
#: URL to a large spritemap
large_sprite_image = attr.ib(None, type=str)
large_sprite_image = attr.ib(None, type=Optional[str])
#: The amount of frames present in the spritemap pr. row
frames_per_row = attr.ib(None, type=int)
frames_per_row = attr.ib(None, type=Optional[int])
#: The amount of frames present in the spritemap pr. column
frames_per_col = attr.ib(None, type=int)
frames_per_col = attr.ib(None, type=Optional[int])
#: The total amount of frames in the spritemap
frame_count = attr.ib(None, type=int)
frame_count = attr.ib(None, type=Optional[int])
#: The frame rate the spritemap is intended to be played in
frame_rate = attr.ib(None, type=int)
frame_rate = attr.ib(None, type=Optional[int])
#: The sticker's image
image = attr.ib(None, type=Image)
image = attr.ib(None, type=Optional[Image])
#: The sticker's label/name
label = attr.ib(None, type=str)
label = attr.ib(None, type=Optional[str])
@classmethod
def _from_graphql(cls, data):

View File

@@ -56,10 +56,14 @@ def find_input_fields(html: str):
def session_factory() -> requests.Session:
from . import __version__
session = requests.session()
session.headers["Referer"] = "https://www.facebook.com"
# TODO: Deprecate setting the user agent manually
session.headers["User-Agent"] = random.choice(_util.USER_AGENTS)
# We won't try to set a fake user agent to mask our presence!
# Facebook allows us access anyhow, and it makes our motives clearer:
# We're not trying to cheat Facebook, we simply want to access their service
session.headers["User-Agent"] = "fbchat/{}".format(__version__)
return session
@@ -157,7 +161,7 @@ class Session:
_session = attr.ib(factory=session_factory, type=requests.Session)
_counter = attr.ib(0, type=int)
_client_id = attr.ib(factory=client_id_factory, type=str)
_logout_h = attr.ib(None, type=str)
_logout_h = attr.ib(None, type=Optional[str])
@property
def user(self):

View File

@@ -319,7 +319,7 @@ class ThreadABC(metaclass=abc.ABCMeta):
Warning! If someone send a message to the thread that matches the query, while
we're searching, some snippets will get returned twice.
This is fundamentally unfixable, it's just how the endpoint is implemented.
This is fundamentally not fixable, it's just how the endpoint is implemented.
The returned message snippets are ordered by last sent first.

View File

@@ -4,7 +4,8 @@ from ._abc import ThreadABC
from . import _user
from .._common import attrs_default
from .. import _util, _session, _graphql, _models
from typing import Sequence, Iterable, Set, Mapping
from typing import Sequence, Iterable, Set, Mapping, Optional
@attrs_default
@@ -179,31 +180,31 @@ class GroupData(Group):
"""
#: The group's picture
photo = attr.ib(None, type="_models.Image")
photo = attr.ib(None, type=Optional["_models.Image"])
#: The name of the group
name = attr.ib(None, type=str)
name = attr.ib(None, type=Optional[str])
#: When the group was last active / when the last message was sent
last_active = attr.ib(None, type=datetime.datetime)
last_active = attr.ib(None, type=Optional[datetime.datetime])
#: Number of messages in the group
message_count = attr.ib(None, type=int)
message_count = attr.ib(None, type=Optional[int])
#: Set `Plan`
plan = attr.ib(None, type="_models.PlanData")
plan = attr.ib(None, type=Optional["_models.PlanData"])
#: The group thread's participant user ids
participants = attr.ib(factory=set, type=Set[str])
#: A dictionary, containing user nicknames mapped to their IDs
nicknames = attr.ib(factory=dict, type=Mapping[str, str])
#: The groups's message color
color = attr.ib(None, type=str)
color = attr.ib(None, type=Optional[str])
#: The groups's default emoji
emoji = attr.ib(None, type=str)
emoji = attr.ib(None, type=Optional[str])
# User ids of thread admins
admins = attr.ib(factory=set, type=Set[str])
# True if users need approval to join
approval_mode = attr.ib(None, type=bool)
approval_mode = attr.ib(None, type=Optional[bool])
# Set containing user IDs requesting to join
approval_requests = attr.ib(factory=set, type=Set[str])
# Link for joining group
join_link = attr.ib(None, type=str)
join_link = attr.ib(None, type=Optional[str])
@classmethod
def _from_graphql(cls, session, data):

View File

@@ -4,6 +4,8 @@ from ._abc import ThreadABC
from .._common import attrs_default
from .. import _session, _models
from typing import Optional
@attrs_default
class Page(ThreadABC):
@@ -39,21 +41,21 @@ class PageData(Page):
#: The name of the page
name = attr.ib(type=str)
#: When the thread was last active / when the last message was sent
last_active = attr.ib(None, type=datetime.datetime)
last_active = attr.ib(None, type=Optional[datetime.datetime])
#: Number of messages in the thread
message_count = attr.ib(None, type=int)
message_count = attr.ib(None, type=Optional[int])
#: Set `Plan`
plan = attr.ib(None, type="_models.PlanData")
plan = attr.ib(None, type=Optional["_models.PlanData"])
#: The page's custom URL
url = attr.ib(None, type=str)
url = attr.ib(None, type=Optional[str])
#: The name of the page's location city
city = attr.ib(None, type=str)
city = attr.ib(None, type=Optional[str])
#: Amount of likes the page has
likes = attr.ib(None, type=int)
likes = attr.ib(None, type=Optional[int])
#: Some extra information about the page
sub_title = attr.ib(None, type=str)
sub_title = attr.ib(None, type=Optional[str])
#: The page's category
category = attr.ib(None, type=str)
category = attr.ib(None, type=Optional[str])
@classmethod
def _from_graphql(cls, session, data):

View File

@@ -4,6 +4,8 @@ from ._abc import ThreadABC
from .._common import log, attrs_default
from .. import _util, _session, _models
from typing import Optional
GENDERS = {
# For standard requests
@@ -111,27 +113,27 @@ class UserData(User):
#: The users first name
first_name = attr.ib(type=str)
#: The users last name
last_name = attr.ib(None, type=str)
#: Datetime when the thread was last active / when the last message was sent
last_active = attr.ib(None, type=datetime.datetime)
last_name = attr.ib(None, type=Optional[str])
#: When the thread was last active / when the last message was sent
last_active = attr.ib(None, type=Optional[datetime.datetime])
#: Number of messages in the thread
message_count = attr.ib(None, type=int)
message_count = attr.ib(None, type=Optional[int])
#: Set `Plan`
plan = attr.ib(None, type="_models.PlanData")
plan = attr.ib(None, type=Optional["_models.PlanData"])
#: The profile URL. ``None`` for Messenger-only users
url = attr.ib(None, type=str)
url = attr.ib(None, type=Optional[str])
#: The user's gender
gender = attr.ib(None, type=str)
gender = attr.ib(None, type=Optional[str])
#: From 0 to 1. How close the client is to the user
affinity = attr.ib(None, type=float)
affinity = attr.ib(None, type=Optional[float])
#: The user's nickname
nickname = attr.ib(None, type=str)
nickname = attr.ib(None, type=Optional[str])
#: The clients nickname, as seen by the user
own_nickname = attr.ib(None, type=str)
own_nickname = attr.ib(None, type=Optional[str])
#: The message color
color = attr.ib(None, type=str)
color = attr.ib(None, type=Optional[str])
#: The default emoji
emoji = attr.ib(None, type=str)
emoji = attr.ib(None, type=Optional[str])
@staticmethod
def _get_other_user(data):

View File

@@ -9,15 +9,12 @@ from . import _exception
from typing import Iterable, Optional, Any, Mapping, Sequence
#: Default list of user agents
USER_AGENTS = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/601.1.10 (KHTML, like Gecko) Version/8.0.5 Safari/601.1.10",
"Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
]
def int_or_none(inp: Any) -> Optional[int]:
try:
return int(inp)
except Exception:
return None
def get_limits(limit: Optional[int], max_limit: int) -> Iterable[int]: