Merge pull request #472 from carpedm20/use-datetime

Use datetime/timedelta objects
This commit is contained in:
Mads Marquart
2019-09-08 18:41:41 +02:00
committed by GitHub
9 changed files with 257 additions and 186 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import attr import attr
from . import _util
from ._attachment import Attachment from ._attachment import Attachment
@@ -36,7 +37,7 @@ class AudioAttachment(Attachment):
filename = attr.ib(None) filename = attr.ib(None)
#: URL of the audio file #: URL of the audio file
url = attr.ib(None) url = attr.ib(None)
#: Duration of the audio clip in milliseconds #: Duration of the audio clip as a timedelta
duration = attr.ib(None) duration = attr.ib(None)
#: Audio type #: Audio type
audio_type = attr.ib(None) audio_type = attr.ib(None)
@@ -49,7 +50,7 @@ class AudioAttachment(Attachment):
return cls( return cls(
filename=data.get("filename"), filename=data.get("filename"),
url=data.get("playable_url"), url=data.get("playable_url"),
duration=data.get("playable_duration_in_ms"), duration=_util.millis_to_timedelta(data.get("playable_duration_in_ms")),
audio_type=data.get("audio_type"), audio_type=data.get("audio_type"),
) )
@@ -175,7 +176,7 @@ class VideoAttachment(Attachment):
width = attr.ib(None) width = attr.ib(None)
#: Height of original video #: Height of original video
height = attr.ib(None) height = attr.ib(None)
#: Length of video in milliseconds #: Length of video as a timedelta
duration = attr.ib(None) duration = attr.ib(None)
#: URL to very compressed preview video #: URL to very compressed preview video
preview_url = attr.ib(None) preview_url = attr.ib(None)
@@ -243,7 +244,7 @@ class VideoAttachment(Attachment):
return cls( return cls(
width=data.get("original_dimensions", {}).get("width"), width=data.get("original_dimensions", {}).get("width"),
height=data.get("original_dimensions", {}).get("height"), height=data.get("original_dimensions", {}).get("height"),
duration=data.get("playable_duration_in_ms"), duration=_util.millis_to_timedelta(data.get("playable_duration_in_ms")),
preview_url=data.get("playable_url"), preview_url=data.get("playable_url"),
small_image=data.get("chat_image"), small_image=data.get("chat_image"),
medium_image=data.get("inbox_image"), medium_image=data.get("inbox_image"),
@@ -255,7 +256,7 @@ class VideoAttachment(Attachment):
def _from_subattachment(cls, data): def _from_subattachment(cls, data):
media = data["media"] media = data["media"]
return cls( return cls(
duration=media.get("playable_duration_in_ms"), duration=_util.millis_to_timedelta(media.get("playable_duration_in_ms")),
preview_url=media.get("playable_url"), preview_url=media.get("playable_url"),
medium_image=media.get("image"), medium_image=media.get("image"),
uid=data["target"].get("video_id"), uid=data["target"].get("video_id"),

View File

@@ -1,5 +1,5 @@
import attr import attr
from . import _plan from . import _util, _plan
from ._thread import ThreadType, Thread from ._thread import ThreadType, Thread
@@ -63,11 +63,11 @@ class Group(Thread):
if data.get("image") is None: if data.get("image") is None:
data["image"] = {} data["image"] = {}
c_info = cls._parse_customization_info(data) c_info = cls._parse_customization_info(data)
last_message_timestamp = None last_active = None
if "last_message" in data: if "last_message" in data:
last_message_timestamp = data["last_message"]["nodes"][0][ last_active = _util.millis_to_datetime(
"timestamp_precise" int(data["last_message"]["nodes"][0]["timestamp_precise"])
] )
plan = None plan = None
if data.get("event_reminders") and data["event_reminders"].get("nodes"): if data.get("event_reminders") and data["event_reminders"].get("nodes"):
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
@@ -97,7 +97,7 @@ class Group(Thread):
photo=data["image"].get("uri"), photo=data["image"].get("uri"),
name=data.get("name"), name=data.get("name"),
message_count=data.get("messages_count"), message_count=data.get("messages_count"),
last_message_timestamp=last_message_timestamp, last_active=last_active,
plan=plan, plan=plan,
) )

View File

@@ -59,14 +59,14 @@ class LiveLocationAttachment(LocationAttachment):
#: Name of the location #: Name of the location
name = attr.ib(None) name = attr.ib(None)
#: Timestamp when live location expires #: Datetime when live location expires
expiration_time = attr.ib(None) expires_at = attr.ib(None)
#: True if live location is expired #: True if live location is expired
is_expired = attr.ib(None) is_expired = attr.ib(None)
def __init__(self, name=None, expiration_time=None, is_expired=None, **kwargs): def __init__(self, name=None, expires_at=None, is_expired=None, **kwargs):
super(LiveLocationAttachment, self).__init__(**kwargs) super(LiveLocationAttachment, self).__init__(**kwargs)
self.expiration_time = expiration_time self.expires_at = expires_at
self.is_expired = is_expired self.is_expired = is_expired
@classmethod @classmethod
@@ -80,7 +80,7 @@ class LiveLocationAttachment(LocationAttachment):
if not data.get("stopReason") if not data.get("stopReason")
else None, else None,
name=data.get("locationTitle"), name=data.get("locationTitle"),
expiration_time=data["expirationTime"], expires_at=_util.millis_to_datetime(data["expirationTime"]),
is_expired=bool(data.get("stopReason")), is_expired=bool(data.get("stopReason")),
) )
@@ -96,7 +96,7 @@ class LiveLocationAttachment(LocationAttachment):
if target.get("coordinate") if target.get("coordinate")
else None, else None,
name=data["title_with_entities"]["text"], name=data["title_with_entities"]["text"],
expiration_time=target.get("expiration_time"), expires_at=_util.millis_to_datetime(target.get("expiration_time")),
is_expired=target.get("is_expired"), is_expired=target.get("is_expired"),
) )
media = data.get("media") media = data.get("media")

View File

@@ -68,8 +68,8 @@ class Message:
uid = attr.ib(None, init=False) uid = attr.ib(None, init=False)
#: ID of the sender #: ID of the sender
author = attr.ib(None, init=False) author = attr.ib(None, init=False)
#: Timestamp of when the message was sent #: Datetime of when the message was sent
timestamp = attr.ib(None, init=False) created_at = attr.ib(None, init=False)
#: Whether the message is read #: Whether the message is read
is_read = attr.ib(None, init=False) is_read = attr.ib(None, init=False)
#: A list of people IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages` #: A list of people IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages`
@@ -220,7 +220,7 @@ class Message:
rtn.forwarded = cls._get_forwarded_from_tags(tags) rtn.forwarded = cls._get_forwarded_from_tags(tags)
rtn.uid = str(data["message_id"]) rtn.uid = str(data["message_id"])
rtn.author = str(data["message_sender"]["id"]) rtn.author = str(data["message_sender"]["id"])
rtn.timestamp = data.get("timestamp_precise") rtn.created_at = _util.millis_to_datetime(int(data.get("timestamp_precise")))
rtn.unsent = False rtn.unsent = False
if data.get("unread") is not None: if data.get("unread") is not None:
rtn.is_read = not data["unread"] rtn.is_read = not data["unread"]
@@ -271,7 +271,7 @@ class Message:
rtn.forwarded = cls._get_forwarded_from_tags(tags) rtn.forwarded = cls._get_forwarded_from_tags(tags)
rtn.uid = metadata.get("messageId") rtn.uid = metadata.get("messageId")
rtn.author = str(metadata.get("actorFbId")) rtn.author = str(metadata.get("actorFbId"))
rtn.timestamp = metadata.get("timestamp") rtn.created_at = _util.millis_to_datetime(metadata.get("timestamp"))
rtn.unsent = False rtn.unsent = False
if data.get("data", {}).get("platform_xmd"): if data.get("data", {}).get("platform_xmd"):
quick_replies = json.loads(data["data"]["platform_xmd"]).get( quick_replies = json.loads(data["data"]["platform_xmd"]).get(
@@ -307,11 +307,11 @@ class Message:
return rtn return rtn
@classmethod @classmethod
def _from_pull(cls, data, mid=None, tags=None, author=None, timestamp=None): def _from_pull(cls, data, mid=None, tags=None, author=None, created_at=None):
rtn = cls(text=data.get("body")) rtn = cls(text=data.get("body"))
rtn.uid = mid rtn.uid = mid
rtn.author = author rtn.author = author
rtn.timestamp = timestamp rtn.created_at = created_at
if data.get("data") and data["data"].get("prng"): if data.get("data") and data["data"].get("prng"):
try: try:

View File

@@ -1,6 +1,7 @@
import attr import attr
import json import json
from ._core import Enum from ._core import Enum
from . import _util
class GuestStatus(Enum): class GuestStatus(Enum):
@@ -15,8 +16,8 @@ class Plan:
#: ID of the plan #: ID of the plan
uid = attr.ib(None, init=False) uid = attr.ib(None, init=False)
#: Plan time (timestamp), only precise down to the minute #: Plan time (datetime), only precise down to the minute
time = attr.ib(converter=int) time = attr.ib()
#: Plan title #: Plan title
title = attr.ib() title = attr.ib()
#: Plan location name #: Plan location name
@@ -58,7 +59,7 @@ class Plan:
@classmethod @classmethod
def _from_pull(cls, data): def _from_pull(cls, data):
rtn = cls( rtn = cls(
time=data.get("event_time"), time=_util.seconds_to_datetime(int(data.get("event_time"))),
title=data.get("event_title"), title=data.get("event_title"),
location=data.get("event_location_name"), location=data.get("event_location_name"),
location_id=data.get("event_location_id"), location_id=data.get("event_location_id"),
@@ -74,7 +75,7 @@ class Plan:
@classmethod @classmethod
def _from_fetch(cls, data): def _from_fetch(cls, data):
rtn = cls( rtn = cls(
time=data.get("event_time"), time=_util.seconds_to_datetime(data.get("event_time")),
title=data.get("title"), title=data.get("title"),
location=data.get("location_name"), location=data.get("location_name"),
location_id=str(data["location_id"]) if data.get("location_id") else None, location_id=str(data["location_id"]) if data.get("location_id") else None,
@@ -87,7 +88,7 @@ class Plan:
@classmethod @classmethod
def _from_graphql(cls, data): def _from_graphql(cls, data):
rtn = cls( rtn = cls(
time=data.get("time"), time=_util.seconds_to_datetime(data.get("time")),
title=data.get("event_title"), title=data.get("event_title"),
location=data.get("location_name"), location=data.get("location_name"),
) )

View File

@@ -81,8 +81,8 @@ class Thread:
photo = attr.ib(None) photo = attr.ib(None)
#: The name of the thread #: The name of the thread
name = attr.ib(None) name = attr.ib(None)
#: Timestamp of last message #: Datetime when the thread was last active / when the last message was sent
last_message_timestamp = attr.ib(None) last_active = attr.ib(None)
#: Number of messages in the thread #: Number of messages in the thread
message_count = attr.ib(None) message_count = attr.ib(None)
#: Set :class:`Plan` #: Set :class:`Plan`
@@ -94,7 +94,7 @@ class Thread:
uid, uid,
photo=None, photo=None,
name=None, name=None,
last_message_timestamp=None, last_active=None,
message_count=None, message_count=None,
plan=None, plan=None,
): ):
@@ -102,7 +102,7 @@ class Thread:
self.type = _type self.type = _type
self.photo = photo self.photo = photo
self.name = name self.name = name
self.last_message_timestamp = last_message_timestamp self.last_active = last_active
self.message_count = message_count self.message_count = message_count
self.plan = plan self.plan = plan

View File

@@ -1,6 +1,6 @@
import attr import attr
from ._core import Enum from ._core import Enum
from . import _plan from . import _util, _plan
from ._thread import ThreadType, Thread from ._thread import ThreadType, Thread
@@ -131,11 +131,11 @@ class User(Thread):
user = next( user = next(
p for p in participants if p["id"] == data["thread_key"]["other_user_id"] p for p in participants if p["id"] == data["thread_key"]["other_user_id"]
) )
last_message_timestamp = None last_active = None
if "last_message" in data: if "last_message" in data:
last_message_timestamp = data["last_message"]["nodes"][0][ last_active = _util.millis_to_datetime(
"timestamp_precise" int(data["last_message"]["nodes"][0]["timestamp_precise"])
] )
first_name = user.get("short_name") first_name = user.get("short_name")
if first_name is None: if first_name is None:
@@ -162,7 +162,7 @@ class User(Thread):
own_nickname=c_info.get("own_nickname"), own_nickname=c_info.get("own_nickname"),
photo=user["big_image_src"].get("uri"), photo=user["big_image_src"].get("uri"),
message_count=data.get("messages_count"), message_count=data.get("messages_count"),
last_message_timestamp=last_message_timestamp, last_active=last_active,
plan=plan, plan=plan,
) )
@@ -183,7 +183,7 @@ class User(Thread):
class ActiveStatus: class ActiveStatus:
#: Whether the user is active now #: Whether the user is active now
active = attr.ib(None) active = attr.ib(None)
#: Timestamp when the user was last active #: Datetime when the user was last active
last_active = attr.ib(None) last_active = attr.ib(None)
#: Whether the user is playing Messenger game now #: Whether the user is playing Messenger game now
in_game = attr.ib(None) in_game = attr.ib(None)
@@ -192,7 +192,7 @@ class ActiveStatus:
def _from_chatproxy_presence(cls, id_, data): def _from_chatproxy_presence(cls, id_, data):
return cls( return cls(
active=data["p"] in [2, 3] if "p" in data else None, active=data["p"] in [2, 3] if "p" in data else None,
last_active=data.get("lat"), last_active=_util.millis_to_datetime(data.get("lat")),
in_game=int(id_) in data.get("gamers", {}), in_game=int(id_) in data.get("gamers", {}),
) )
@@ -200,6 +200,6 @@ class ActiveStatus:
def _from_buddylist_overlay(cls, data, in_game=None): def _from_buddylist_overlay(cls, data, in_game=None):
return cls( return cls(
active=data["a"] in [2, 3] if "a" in data else None, active=data["a"] in [2, 3] if "a" in data else None,
last_active=data.get("la"), last_active=_util.millis_to_datetime(data.get("la")),
in_game=None, in_game=None,
) )

View File

@@ -1,3 +1,4 @@
import datetime
import json import json
import time import time
import random import random
@@ -233,3 +234,57 @@ def prefix_url(url):
if url.startswith("/"): if url.startswith("/"):
return "https://www.facebook.com" + url return "https://www.facebook.com" + url
return url return url
def seconds_to_datetime(timestamp_in_seconds):
"""Convert an UTC timestamp to a timezone-aware datetime object."""
# `.utcfromtimestamp` will return a "naive" datetime object, which is why we use the
# following:
return datetime.datetime.fromtimestamp(
timestamp_in_seconds, tz=datetime.timezone.utc
)
def millis_to_datetime(timestamp_in_milliseconds):
"""Convert an UTC timestamp, in milliseconds, to a timezone-aware datetime."""
return seconds_to_datetime(timestamp_in_milliseconds / 1000)
def datetime_to_seconds(dt):
"""Convert a datetime to an UTC timestamp.
Naive datetime objects are presumed to represent time in the system timezone.
The returned seconds will be rounded to the nearest whole number.
"""
# We could've implemented some fancy "convert naive timezones to UTC" logic, but
# it's not really worth the effort.
return round(dt.timestamp())
def datetime_to_millis(dt):
"""Convert a datetime to an UTC timestamp, in milliseconds.
Naive datetime objects are presumed to represent time in the system timezone.
The returned milliseconds will be rounded to the nearest whole number.
"""
return round(dt.timestamp() * 1000)
def seconds_to_timedelta(seconds):
"""Convert seconds to a timedelta."""
return datetime.timedelta(seconds=seconds)
def millis_to_timedelta(milliseconds):
"""Convert a duration (in milliseconds) to a timedelta object."""
return datetime.timedelta(milliseconds=milliseconds)
def timedelta_to_seconds(td):
"""Convert a timedelta to seconds.
The returned seconds will be rounded to the nearest whole number.
"""
return round(td.total_seconds())