From 0383d613e60bb65ee40e185d10dacb73f1f6b0f7 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 18:12:37 +0100 Subject: [PATCH 01/29] Move ActiveStatus pull parsing into the model --- fbchat/_client.py | 52 +++++++++++------------------------------------ fbchat/_user.py | 16 +++++++++++++++ 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index a4a363d..fa4fb2c 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -3199,53 +3199,25 @@ class Client(object): # Chat timestamp elif mtype == "chatproxy-presence": - buddylist = dict() - for _id in m.get("buddyList", {}): - payload = m["buddyList"][_id] + statuses = dict() + for id_, data in m.get("buddyList", {}).items(): + statuses[id_] = ActiveStatus._from_chatproxy_presence(id_, data) + self._buddylist[id_] = statuses[id_] - last_active = payload.get("lat") - active = payload.get("p") in [2, 3] - in_game = int(_id) in m.get("gamers", {}) - - buddylist[_id] = last_active - - if self._buddylist.get(_id): - self._buddylist[_id].last_active = last_active - self._buddylist[_id].active = active - self._buddylist[_id].in_game = in_game - else: - self._buddylist[_id] = ActiveStatus( - active=active, last_active=last_active, in_game=in_game - ) - - self.onChatTimestamp(buddylist=buddylist, msg=m) + self.onChatTimestamp(buddylist=statuses, msg=m) # Buddylist overlay elif mtype == "buddylist_overlay": statuses = dict() - for _id in m.get("overlay", {}): - payload = m["overlay"][_id] + for id_, data in m.get("overlay", {}).items(): + old_in_game = None + if id_ in self._buddylist: + old_in_game = self._buddylist[id_].in_game - last_active = payload.get("la") - active = payload.get("a") in [2, 3] - in_game = ( - self._buddylist[_id].in_game - if self._buddylist.get(_id) - else False + statuses[id_] = ActiveStatus._from_buddylist_overlay( + data, old_in_game ) - - status = ActiveStatus( - active=active, last_active=last_active, in_game=in_game - ) - - if self._buddylist.get(_id): - self._buddylist[_id].last_active = last_active - self._buddylist[_id].active = active - self._buddylist[_id].in_game = in_game - else: - self._buddylist[_id] = status - - statuses[_id] = status + self._buddylist[id_] = statuses[id_] self.onBuddylistOverlay(statuses=statuses, msg=m) diff --git a/fbchat/_user.py b/fbchat/_user.py index 26e8f4d..a42390f 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -74,3 +74,19 @@ class ActiveStatus(object): last_active = attr.ib(None) #: Whether the user is playing Messenger game now in_game = attr.ib(None) + + @classmethod + def _from_chatproxy_presence(cls, id_, data): + return cls( + active=data["p"] in [2, 3] if "p" in data else None, + last_active=data.get("lat"), + in_game=int(id_) in data.get("gamers", {}), + ) + + @classmethod + def _from_buddylist_overlay(cls, data, in_game=None): + return cls( + active=data["a"] in [2, 3] if "a" in data else None, + last_active=data.get("la"), + in_game=None, + ) From 403870e39ecabf5e9fc689fbe11496dd35dbb11f Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 18:13:05 +0100 Subject: [PATCH 02/29] Move emojisize pull parsing into the model --- fbchat/_client.py | 2 +- fbchat/_message.py | 16 ++++++++++++++++ fbchat/_util.py | 24 ------------------------ fbchat/utils.py | 2 -- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index fa4fb2c..607dd58 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -3096,7 +3096,7 @@ class Client(object): ) if metadata and metadata.get("tags"): - emoji_size = get_emojisize_from_tags(metadata.get("tags")) + emoji_size = EmojiSize._from_tags(metadata.get("tags")) message = Message( text=delta.get("body"), diff --git a/fbchat/_message.py b/fbchat/_message.py index 3f6f03d..59e52ba 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -13,6 +13,22 @@ class EmojiSize(Enum): MEDIUM = "369239343222814" SMALL = "369239263222822" + @classmethod + def _from_tags(cls, tags): + string_to_emojisize = { + "large": cls.LARGE, + "medium": cls.MEDIUM, + "small": cls.SMALL, + "l": cls.LARGE, + "m": cls.MEDIUM, + "s": cls.SMALL, + } + for tag in tags or (): + data = tag.split(":", maxsplit=1) + if len(data) > 1 and data[0] == "hot_emoji_size": + return string_to_emojisize.get(data[1]) + return None + class MessageReaction(Enum): """Used to specify a message reaction""" diff --git a/fbchat/_util.py b/fbchat/_util.py index 8705442..5794fe7 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -12,7 +12,6 @@ import warnings import logging import requests import aenum -from .models import * try: from urllib.parse import urlencode, parse_qs, urlparse @@ -47,15 +46,6 @@ USER_AGENTS = [ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", ] -LIKES = { - "large": EmojiSize.LARGE, - "medium": EmojiSize.MEDIUM, - "small": EmojiSize.SMALL, - "l": EmojiSize.LARGE, - "m": EmojiSize.MEDIUM, - "s": EmojiSize.SMALL, -} - GENDERS = { # For standard requests @@ -295,20 +285,6 @@ def get_jsmods_require(j, index): return None -def get_emojisize_from_tags(tags): - if tags is None: - return None - tmp = [tag for tag in tags if tag.startswith("hot_emoji_size:")] - if len(tmp) > 0: - try: - return LIKES[tmp[0].split(":")[1]] - except (KeyError, IndexError): - log.exception( - "Could not determine emoji size from {} - {}".format(tags, tmp) - ) - return None - - def require_list(list_): if isinstance(list_, list): return set(list_) diff --git a/fbchat/utils.py b/fbchat/utils.py index fedb1ea..4491d1c 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -7,7 +7,6 @@ from ._util import ( log, handler, USER_AGENTS, - LIKES, GENDERS, ReqUrl, facebookEncoding, @@ -25,7 +24,6 @@ from ._util import ( check_json, check_request, get_jsmods_require, - get_emojisize_from_tags, require_list, mimetype_to_key, get_files_from_urls, From d940b645174c3e9b4e6cb973a0032539083c4168 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 18:42:58 +0100 Subject: [PATCH 03/29] Move enum_extend_if_invalid -> Enum._extend_if_invalid --- fbchat/_core.py | 14 ++++++++++++++ fbchat/_graphql.py | 4 ++-- fbchat/_util.py | 14 -------------- fbchat/utils.py | 1 - 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/fbchat/_core.py b/fbchat/_core.py index e62df71..31f4019 100644 --- a/fbchat/_core.py +++ b/fbchat/_core.py @@ -1,8 +1,11 @@ # -*- coding: UTF-8 -*- from __future__ import unicode_literals +import logging import aenum +log = logging.getLogger("client") + class Enum(aenum.Enum): """Used internally by fbchat to support enumerations""" @@ -10,3 +13,14 @@ class Enum(aenum.Enum): def __repr__(self): # For documentation: return "{}.{}".format(type(self).__name__, self.name) + + @classmethod + def _extend_if_invalid(cls, value): + try: + return cls(value) + except ValueError: + log.warning( + "Failed parsing {.__name__}({!r}). Extending enum.".format(cls, value) + ) + aenum.extend_enum(cls, "UNKNOWN_{}".format(value).upper(), value) + return cls(value) diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 4bf8cab..7e83500 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -34,7 +34,7 @@ def graphql_color_to_enum(color): return ThreadColor.MESSENGER_BLUE color = color[2:] # Strip the alpha value color_value = "#{}".format(color.lower()) - return enum_extend_if_invalid(ThreadColor, color_value) + return ThreadColor._extend_if_invalid(color_value) def get_customization_info(thread): @@ -379,7 +379,7 @@ def graphql_to_message(message): if message.get("unread") is not None: rtn.is_read = not message["unread"] rtn.reactions = { - str(r["user"]["id"]): enum_extend_if_invalid(MessageReaction, r["reaction"]) + str(r["user"]["id"]): MessageReaction._extend_if_invalid(r["reaction"]) for r in message.get("message_reactions") } if message.get("blob_attachments") is not None: diff --git a/fbchat/_util.py b/fbchat/_util.py index 5794fe7..f18d6e1 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -11,7 +11,6 @@ from os.path import basename import warnings import logging import requests -import aenum try: from urllib.parse import urlencode, parse_qs, urlparse @@ -331,19 +330,6 @@ def get_files_from_paths(filenames): fp.close() -def enum_extend_if_invalid(enumeration, value): - try: - return enumeration(value) - except ValueError: - log.warning( - "Failed parsing {.__name__}({!r}). Extending enum.".format( - enumeration, value - ) - ) - aenum.extend_enum(enumeration, "UNKNOWN_{}".format(value).upper(), value) - return enumeration(value) - - def get_url_parameters(url, *args): params = parse_qs(urlparse(url).query) return [params[arg][0] for arg in args if params.get(arg)] diff --git a/fbchat/utils.py b/fbchat/utils.py index 4491d1c..66c5b9a 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -28,7 +28,6 @@ from ._util import ( mimetype_to_key, get_files_from_urls, get_files_from_paths, - enum_extend_if_invalid, get_url_parameters, get_url_parameter, ) From 279f637c7564ccdce299be9e9a9b58d7292019df Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 18:54:38 +0100 Subject: [PATCH 04/29] Move graphql_color_to_enum -> ThreadColor._from_graphql --- fbchat/_client.py | 2 +- fbchat/_graphql.py | 12 +----------- fbchat/_thread.py | 10 ++++++++++ fbchat/graphql.py | 1 - 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 607dd58..f5f30be 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -2577,7 +2577,7 @@ class Client(object): # Color change elif delta_type == "change_thread_theme": - new_color = graphql_color_to_enum(delta["untypedData"]["theme_color"]) + new_color = ThreadColor._from_graphql(delta["untypedData"]["theme_color"]) thread_id, thread_type = getThreadIdAndThreadType(metadata) self.onColorChange( mid=mid, diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 7e83500..160993c 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -27,16 +27,6 @@ class ConcatJSONDecoder(json.JSONDecoder): # End shameless copy -def graphql_color_to_enum(color): - if color is None: - return None - if not color: - return ThreadColor.MESSENGER_BLUE - color = color[2:] # Strip the alpha value - color_value = "#{}".format(color.lower()) - return ThreadColor._extend_if_invalid(color_value) - - def get_customization_info(thread): if thread is None or thread.get("customization_info") is None: return {} @@ -44,7 +34,7 @@ def get_customization_info(thread): rtn = { "emoji": info.get("emoji"), - "color": graphql_color_to_enum(info.get("outgoing_bubble_color")), + "color": ThreadColor._from_graphql(info.get("outgoing_bubble_color")), } if ( thread.get("thread_type") == "GROUP" diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 6b4641e..d4a8391 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -42,6 +42,16 @@ class ThreadColor(Enum): BRILLIANT_ROSE = "#ff5ca1" BILOBA_FLOWER = "#a695c7" + @classmethod + def _from_graphql(cls, color): + if color is None: + return None + if not color: + return cls.MESSENGER_BLUE + color = color[2:] # Strip the alpha value + value = "#{}".format(color.lower()) + return cls._extend_if_invalid(value) + @attr.s(cmp=False, init=False) class Thread(object): diff --git a/fbchat/graphql.py b/fbchat/graphql.py index aeef27c..00f46e4 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -8,7 +8,6 @@ from ._graphql import ( FLAGS, WHITESPACE, ConcatJSONDecoder, - graphql_color_to_enum, get_customization_info, graphql_to_sticker, graphql_to_attachment, From 3440039610a9fa7c42d5a81a60231c4baa23e984 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 19:07:00 +0100 Subject: [PATCH 05/29] Move graphql_to_sticker -> Sticker._from_graphql --- fbchat/_client.py | 4 +++- fbchat/_graphql.py | 23 +---------------------- fbchat/_sticker.py | 21 +++++++++++++++++++++ fbchat/graphql.py | 1 - 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index f5f30be..7c514c9 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -3077,7 +3077,9 @@ class Client(object): attachments.append(attachment) elif mercury.get("sticker_attachment"): - sticker = graphql_to_sticker(mercury["sticker_attachment"]) + sticker = Sticker._from_graphql( + mercury["sticker_attachment"] + ) elif mercury.get("extensible_attachment"): attachment = graphql_to_extensible_attachment( diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 160993c..9692d7a 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -60,27 +60,6 @@ def get_customization_info(thread): return rtn -def graphql_to_sticker(s): - if not s: - return None - sticker = Sticker(uid=s["id"]) - if s.get("pack"): - sticker.pack = s["pack"].get("id") - if s.get("sprite_image"): - sticker.is_animated = True - sticker.medium_sprite_image = s["sprite_image"].get("uri") - sticker.large_sprite_image = s["sprite_image_2x"].get("uri") - sticker.frames_per_row = s.get("frames_per_row") - sticker.frames_per_col = s.get("frames_per_column") - sticker.frame_rate = s.get("frame_rate") - sticker.url = s.get("url") - sticker.width = s.get("width") - sticker.height = s.get("height") - if s.get("label"): - sticker.label = s["label"] - return sticker - - def graphql_to_attachment(a): _type = a["__typename"] if _type in ["MessageImage", "MessageAnimatedImage"]: @@ -360,7 +339,7 @@ def graphql_to_message(message): for m in message.get("message").get("ranges", []) ], emoji_size=get_emojisize_from_tags(message.get("tags_list")), - sticker=graphql_to_sticker(message.get("sticker")), + sticker=Sticker._from_graphql(message.get("sticker")), ) rtn.uid = str(message.get("message_id")) rtn.author = str(message.get("message_sender").get("id")) diff --git a/fbchat/_sticker.py b/fbchat/_sticker.py index e90655d..db8b35f 100644 --- a/fbchat/_sticker.py +++ b/fbchat/_sticker.py @@ -37,3 +37,24 @@ class Sticker(Attachment): def __init__(self, uid=None): super(Sticker, self).__init__(uid=uid) + + @classmethod + def _from_graphql(cls, data): + if not data: + return None + self = cls(uid=data["id"]) + if data.get("pack"): + self.pack = data["pack"].get("id") + if data.get("sprite_image"): + self.is_animated = True + self.medium_sprite_image = data["sprite_image"].get("uri") + self.large_sprite_image = data["sprite_image_2x"].get("uri") + self.frames_per_row = data.get("frames_per_row") + self.frames_per_col = data.get("frames_per_column") + self.frame_rate = data.get("frame_rate") + self.url = data.get("url") + self.width = data.get("width") + self.height = data.get("height") + if data.get("label"): + self.label = data["label"] + return self diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 00f46e4..1f5dc85 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -9,7 +9,6 @@ from ._graphql import ( WHITESPACE, ConcatJSONDecoder, get_customization_info, - graphql_to_sticker, graphql_to_attachment, graphql_to_extensible_attachment, graphql_to_subattachment, From e51ce99c1ad96cd55494655938c4ff59eacebebd Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 19:47:36 +0100 Subject: [PATCH 06/29] Move graphql_to_poll_option -> PollOption._from_graphql --- fbchat/_client.py | 2 +- fbchat/_graphql.py | 24 +----------------------- fbchat/_poll.py | 24 ++++++++++++++++++++++++ fbchat/graphql.py | 1 - 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 7c514c9..cf1551a 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1248,7 +1248,7 @@ class Client(object): self.req_url.GET_POLL_OPTIONS, data, fix_request=True, as_json=True ) - return [graphql_to_poll_option(m) for m in j["payload"]] + return [PollOption._from_graphql(m) for m in j["payload"]] def fetchPlanInfo(self, plan_id): """ diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 9692d7a..366a7ee 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -220,35 +220,13 @@ def graphql_to_live_location(a): def graphql_to_poll(a): rtn = Poll( title=a.get("title") if a.get("title") else a.get("text"), - options=[graphql_to_poll_option(m) for m in a.get("options")], + options=[PollOption._from_graphql(m) for m in a.get("options")], ) rtn.uid = int(a["id"]) rtn.options_count = a.get("total_count") return rtn -def graphql_to_poll_option(a): - if a.get("viewer_has_voted") is None: - vote = None - elif isinstance(a["viewer_has_voted"], bool): - vote = a["viewer_has_voted"] - else: - vote = a["viewer_has_voted"] == "true" - rtn = PollOption(text=a.get("text"), vote=vote) - rtn.uid = int(a["id"]) - rtn.voters = ( - [m.get("node").get("id") for m in a.get("voters").get("edges")] - if isinstance(a.get("voters"), dict) - else a.get("voters") - ) - rtn.votes_count = ( - a.get("voters").get("count") - if isinstance(a.get("voters"), dict) - else a.get("total_count") - ) - return rtn - - def graphql_to_plan(a): if a.get("event_members"): rtn = Plan( diff --git a/fbchat/_poll.py b/fbchat/_poll.py index c2c02c0..d99e57e 100644 --- a/fbchat/_poll.py +++ b/fbchat/_poll.py @@ -32,3 +32,27 @@ class PollOption(object): voters = attr.ib(None, init=False) #: Votes count votes_count = attr.ib(None, init=False) + + @classmethod + def _from_graphql(cls, data): + if data.get("viewer_has_voted") is None: + vote = None + elif isinstance(data["viewer_has_voted"], bool): + vote = data["viewer_has_voted"] + else: + vote = data["viewer_has_voted"] == "true" + return cls( + uid=int(data["id"]), + text=data.get("text"), + vote=vote, + voters=( + [m.get("node").get("id") for m in data.get("voters").get("edges")] + if isinstance(data.get("voters"), dict) + else data.get("voters") + ), + votes_count=( + data.get("voters").get("count") + if isinstance(data.get("voters"), dict) + else data.get("total_count") + ), + ) diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 1f5dc85..b1fee72 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -14,7 +14,6 @@ from ._graphql import ( graphql_to_subattachment, graphql_to_live_location, graphql_to_poll, - graphql_to_poll_option, graphql_to_plan, graphql_to_quick_reply, graphql_to_message, From 0578ea2c3ccd4721f91d7c14d77fd8e0626f7988 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 19:52:29 +0100 Subject: [PATCH 07/29] Move graphql_to_poll -> Poll._from_graphql --- fbchat/_client.py | 2 +- fbchat/_graphql.py | 10 ---------- fbchat/_poll.py | 9 +++++++++ fbchat/graphql.py | 1 - 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index cf1551a..2c246b1 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -2836,7 +2836,7 @@ class Client(object): thread_id, thread_type = getThreadIdAndThreadType(metadata) event_type = delta["untypedData"]["event_type"] poll_json = json.loads(delta["untypedData"]["question_json"]) - poll = graphql_to_poll(poll_json) + poll = Poll._from_graphql(poll_json) if event_type == "question_creation": # User created group poll self.onPollCreated( diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 366a7ee..3e9e656 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -217,16 +217,6 @@ def graphql_to_live_location(a): ) -def graphql_to_poll(a): - rtn = Poll( - title=a.get("title") if a.get("title") else a.get("text"), - options=[PollOption._from_graphql(m) for m in a.get("options")], - ) - rtn.uid = int(a["id"]) - rtn.options_count = a.get("total_count") - return rtn - - def graphql_to_plan(a): if a.get("event_members"): rtn = Plan( diff --git a/fbchat/_poll.py b/fbchat/_poll.py index d99e57e..2b91122 100644 --- a/fbchat/_poll.py +++ b/fbchat/_poll.py @@ -17,6 +17,15 @@ class Poll(object): #: Options count options_count = attr.ib(None, init=False) + @classmethod + def _from_graphql(cls, data): + return cls( + uid=int(data["id"]), + title=data.get("title") if data.get("title") else data.get("text"), + options=[PollOption._from_graphql(m) for m in data.get("options")], + options_count=data.get("total_count"), + ) + @attr.s(cmp=False) class PollOption(object): diff --git a/fbchat/graphql.py b/fbchat/graphql.py index b1fee72..0d99a87 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -13,7 +13,6 @@ from ._graphql import ( graphql_to_extensible_attachment, graphql_to_subattachment, graphql_to_live_location, - graphql_to_poll, graphql_to_plan, graphql_to_quick_reply, graphql_to_message, From c28ca58537102a5caf549ac94bd3a1e49d0b0351 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 19:58:24 +0100 Subject: [PATCH 08/29] Add missing attributes to Poll and PollOption `__init__` --- fbchat/_poll.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fbchat/_poll.py b/fbchat/_poll.py index 2b91122..29fcca9 100644 --- a/fbchat/_poll.py +++ b/fbchat/_poll.py @@ -8,14 +8,14 @@ import attr class Poll(object): """Represents a poll""" - #: ID of the poll - uid = attr.ib(None, init=False) #: Title of the poll title = attr.ib() #: List of :class:`PollOption`, can be fetched with :func:`fbchat.Client.fetchPollOptions` options = attr.ib() #: Options count - options_count = attr.ib(None, init=False) + options_count = attr.ib(None) + #: ID of the poll + uid = attr.ib(None) @classmethod def _from_graphql(cls, data): @@ -31,16 +31,16 @@ class Poll(object): class PollOption(object): """Represents a poll option""" - #: ID of the poll option - uid = attr.ib(None, init=False) #: Text of the poll option text = attr.ib() #: Whether vote when creating or client voted vote = attr.ib(False) #: ID of the users who voted for this poll option - voters = attr.ib(None, init=False) + voters = attr.ib(None) #: Votes count - votes_count = attr.ib(None, init=False) + votes_count = attr.ib(None) + #: ID of the poll option + uid = attr.ib(None) @classmethod def _from_graphql(cls, data): From c374aca89029ea03edd92780afa485f20ef30aa8 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 19:59:25 +0100 Subject: [PATCH 09/29] Fix _util exception import --- fbchat/_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fbchat/_util.py b/fbchat/_util.py index f18d6e1..9bfe09c 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -11,6 +11,7 @@ from os.path import basename import warnings import logging import requests +from ._exception import FBchatException, FBchatFacebookError try: from urllib.parse import urlencode, parse_qs, urlparse From ee207e994f0fd88d08f448e0dcda6e353d938e56 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 20:17:29 +0100 Subject: [PATCH 10/29] Move graphql_to_live_location -> LiveLocationAttachment._from_pull --- fbchat/_client.py | 2 +- fbchat/_graphql.py | 15 --------------- fbchat/_location.py | 15 +++++++++++++++ fbchat/graphql.py | 1 - 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 2c246b1..df406e7 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -3010,7 +3010,7 @@ class Client(object): for l in i["messageLiveLocations"]: mid = l["messageId"] author_id = str(l["senderId"]) - location = graphql_to_live_location(l) + location = LiveLocationAttachment._from_pull(l) self.onLiveLocation( mid=mid, location=location, diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 3e9e656..ceffba0 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -202,21 +202,6 @@ def graphql_to_subattachment(a): ) -def graphql_to_live_location(a): - return LiveLocationAttachment( - uid=a["id"], - latitude=a["coordinate"]["latitude"] / (10 ** 8) - if not a.get("stopReason") - else None, - longitude=a["coordinate"]["longitude"] / (10 ** 8) - if not a.get("stopReason") - else None, - name=a.get("locationTitle"), - expiration_time=a["expirationTime"], - is_expired=bool(a.get("stopReason")), - ) - - def graphql_to_plan(a): if a.get("event_members"): rtn = Plan( diff --git a/fbchat/_location.py b/fbchat/_location.py index 5361bb5..af9b56d 100644 --- a/fbchat/_location.py +++ b/fbchat/_location.py @@ -46,3 +46,18 @@ class LiveLocationAttachment(LocationAttachment): super(LiveLocationAttachment, self).__init__(**kwargs) self.expiration_time = expiration_time self.is_expired = is_expired + + @classmethod + def _from_pull(cls, data): + return cls( + uid=data["id"], + latitude=data["coordinate"]["latitude"] / (10 ** 8) + if not data.get("stopReason") + else None, + longitude=data["coordinate"]["longitude"] / (10 ** 8) + if not data.get("stopReason") + else None, + name=data.get("locationTitle"), + expiration_time=data["expirationTime"], + is_expired=bool(data.get("stopReason")), + ) diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 0d99a87..173a215 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -12,7 +12,6 @@ from ._graphql import ( graphql_to_attachment, graphql_to_extensible_attachment, graphql_to_subattachment, - graphql_to_live_location, graphql_to_plan, graphql_to_quick_reply, graphql_to_message, From 2ce99a2c44df04dc0c5e35a7943cb2975996ad48 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 20:50:14 +0100 Subject: [PATCH 11/29] Split graphql_to_extensible_attachment into smaller methods --- fbchat/_attachment.py | 38 +++++++++++++++++ fbchat/_graphql.py | 98 +++++++------------------------------------ fbchat/_location.py | 49 ++++++++++++++++++++++ 3 files changed, 103 insertions(+), 82 deletions(-) diff --git a/fbchat/_attachment.py b/fbchat/_attachment.py index 4ee40b2..62f5ea0 100644 --- a/fbchat/_attachment.py +++ b/fbchat/_attachment.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import attr +from . import _util @attr.s(cmp=False) @@ -46,3 +47,40 @@ class ShareAttachment(Attachment): # Put here for backwards compatibility, so that the init argument order is preserved uid = attr.ib(None) + + @classmethod + def _from_graphql(cls, data): + from . import _graphql + + url = data.get("url") + rtn = cls( + uid=data.get("deduplication_key"), + author=data["target"]["actors"][0]["id"] + if data["target"].get("actors") + else None, + url=url, + original_url=_util.get_url_parameter(url, "u") + if "/l.php?u=" in url + else url, + title=data["title_with_entities"].get("text"), + description=data["description"].get("text") + if data.get("description") + else None, + source=data["source"].get("text"), + attachments=[ + _graphql.graphql_to_subattachment(attachment) + for attachment in data.get("subattachments") + ], + ) + media = data.get("media") + if media and media.get("image"): + image = media["image"] + rtn.image_url = image.get("uri") + rtn.original_image_url = ( + _util.get_url_parameter(rtn.image_url, "url") + if "/safe_image.php" in rtn.image_url + else rtn.image_url + ) + rtn.image_width = image.get("width") + rtn.image_height = image.get("height") + return rtn diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index ceffba0..df3c1a6 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -106,88 +106,22 @@ def graphql_to_attachment(a): def graphql_to_extensible_attachment(a): story = a.get("story_attachment") - if story: - target = story.get("target") - if target: - _type = target["__typename"] - if _type == "MessageLocation": - url = story.get("url") - address = get_url_parameter(get_url_parameter(url, "u"), "where1") - try: - latitude, longitude = [float(x) for x in address.split(", ")] - address = None - except ValueError: - latitude, longitude = None, None - rtn = LocationAttachment( - uid=int(story["deduplication_key"]), - latitude=latitude, - longitude=longitude, - address=address, - ) - media = story.get("media") - if media and media.get("image"): - image = media["image"] - rtn.image_url = image.get("uri") - rtn.image_width = image.get("width") - rtn.image_height = image.get("height") - rtn.url = url - return rtn - elif _type == "MessageLiveLocation": - rtn = LiveLocationAttachment( - uid=int(story["target"]["live_location_id"]), - latitude=story["target"]["coordinate"]["latitude"] - if story["target"].get("coordinate") - else None, - longitude=story["target"]["coordinate"]["longitude"] - if story["target"].get("coordinate") - else None, - name=story["title_with_entities"]["text"], - expiration_time=story["target"].get("expiration_time"), - is_expired=story["target"].get("is_expired"), - ) - media = story.get("media") - if media and media.get("image"): - image = media["image"] - rtn.image_url = image.get("uri") - rtn.image_width = image.get("width") - rtn.image_height = image.get("height") - rtn.url = story.get("url") - return rtn - elif _type in ["ExternalUrl", "Story"]: - url = story.get("url") - rtn = ShareAttachment( - uid=a.get("legacy_attachment_id"), - author=story["target"]["actors"][0]["id"] - if story["target"].get("actors") - else None, - url=url, - original_url=get_url_parameter(url, "u") - if "/l.php?u=" in url - else url, - title=story["title_with_entities"].get("text"), - description=story["description"].get("text") - if story.get("description") - else None, - source=story["source"].get("text"), - attachments=[ - graphql_to_subattachment(attachment) - for attachment in story.get("subattachments") - ], - ) - media = story.get("media") - if media and media.get("image"): - image = media["image"] - rtn.image_url = image.get("uri") - rtn.original_image_url = ( - get_url_parameter(rtn.image_url, "url") - if "/safe_image.php" in rtn.image_url - else rtn.image_url - ) - rtn.image_width = image.get("width") - rtn.image_height = image.get("height") - return rtn - else: - return UnsentMessage(uid=a.get("legacy_attachment_id")) + if not story: + return None + + target = story.get("target") + if not target: + return UnsentMessage(uid=a.get("legacy_attachment_id")) + + _type = target["__typename"] + if _type == "MessageLocation": + return LocationAttachment._from_graphql(story) + elif _type == "MessageLiveLocation": + return LiveLocationAttachment._from_graphql(story) + elif _type in ["ExternalUrl", "Story"]: + return ShareAttachment._from_graphql(story) + + return None def graphql_to_subattachment(a): diff --git a/fbchat/_location.py b/fbchat/_location.py index af9b56d..23c1351 100644 --- a/fbchat/_location.py +++ b/fbchat/_location.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import attr from ._attachment import Attachment +from . import _util @attr.s(cmp=False) @@ -30,6 +31,30 @@ class LocationAttachment(Attachment): # Put here for backwards compatibility, so that the init argument order is preserved uid = attr.ib(None) + @classmethod + def _from_graphql(cls, data): + url = data.get("url") + address = _util.get_url_parameter(_util.get_url_parameter(url, "u"), "where1") + try: + latitude, longitude = [float(x) for x in address.split(", ")] + address = None + except ValueError: + latitude, longitude = None, None + rtn = cls( + uid=int(data["deduplication_key"]), + latitude=latitude, + longitude=longitude, + address=address, + ) + media = data.get("media") + if media and media.get("image"): + image = media["image"] + rtn.image_url = image.get("uri") + rtn.image_width = image.get("width") + rtn.image_height = image.get("height") + rtn.url = url + return rtn + @attr.s(cmp=False, init=False) class LiveLocationAttachment(LocationAttachment): @@ -61,3 +86,27 @@ class LiveLocationAttachment(LocationAttachment): expiration_time=data["expirationTime"], is_expired=bool(data.get("stopReason")), ) + + @classmethod + def _from_graphql(cls, data): + target = data["target"] + rtn = cls( + uid=int(target["live_location_id"]), + latitude=target["coordinate"]["latitude"] + if target.get("coordinate") + else None, + longitude=target["coordinate"]["longitude"] + if target.get("coordinate") + else None, + name=data["title_with_entities"]["text"], + expiration_time=target.get("expiration_time"), + is_expired=target.get("is_expired"), + ) + media = data.get("media") + if media and media.get("image"): + image = media["image"] + rtn.image_url = image.get("uri") + rtn.image_width = image.get("width") + rtn.image_height = image.get("height") + rtn.url = data.get("url") + return rtn From 789d9d8ca15993ce938c0220e5cc93769c80a3ce Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 7 Mar 2019 21:22:56 +0100 Subject: [PATCH 12/29] Split graphql_to_attachment into smaller methods --- fbchat/_file.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ fbchat/_graphql.py | 38 ++++---------------------------------- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/fbchat/_file.py b/fbchat/_file.py index 85b2055..4b858a7 100644 --- a/fbchat/_file.py +++ b/fbchat/_file.py @@ -21,6 +21,15 @@ class FileAttachment(Attachment): # Put here for backwards compatibility, so that the init argument order is preserved uid = attr.ib(None) + @classmethod + def _from_graphql(cls, data): + return cls( + url=data.get("url"), + name=data.get("filename"), + is_malicious=data.get("is_malicious"), + uid=data.get("message_file_fbid"), + ) + @attr.s(cmp=False) class AudioAttachment(Attachment): @@ -38,6 +47,15 @@ class AudioAttachment(Attachment): # Put here for backwards compatibility, so that the init argument order is preserved uid = attr.ib(None) + @classmethod + def _from_graphql(cls, data): + return cls( + filename=data.get("filename"), + url=data.get("playable_url"), + duration=data.get("playable_duration_in_ms"), + audio_type=data.get("audio_type"), + ) + @attr.s(cmp=False, init=False) class ImageAttachment(Attachment): @@ -122,6 +140,21 @@ class ImageAttachment(Attachment): self.animated_preview_width = animated_preview.get("width") self.animated_preview_height = animated_preview.get("height") + @classmethod + def _from_graphql(cls, data): + return cls( + original_extension=data.get("original_extension") + or (data["filename"].split("-")[0] if data.get("filename") else None), + width=data.get("original_dimensions", {}).get("width"), + height=data.get("original_dimensions", {}).get("height"), + is_animated=data["__typename"] == "MessageAnimatedImage", + thumbnail_url=data.get("thumbnail", {}).get("uri"), + preview=data.get("preview") or data.get("preview_image"), + large_preview=data.get("large_preview"), + animated_preview=data.get("animated_image"), + uid=data.get("legacy_attachment_id"), + ) + @attr.s(cmp=False, init=False) class VideoAttachment(Attachment): @@ -195,3 +228,16 @@ class VideoAttachment(Attachment): self.large_image_url = large_image.get("uri") self.large_image_width = large_image.get("width") self.large_image_height = large_image.get("height") + + @classmethod + def _from_graphql(cls, data): + return cls( + width=data.get("original_dimensions", {}).get("width"), + height=data.get("original_dimensions", {}).get("height"), + duration=data.get("playable_duration_in_ms"), + preview_url=data.get("playable_url"), + small_image=data.get("chat_image"), + medium_image=data.get("inbox_image"), + large_image=data.get("large_image"), + uid=data.get("legacy_attachment_id"), + ) diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index df3c1a6..932c5c8 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -63,43 +63,13 @@ def get_customization_info(thread): def graphql_to_attachment(a): _type = a["__typename"] if _type in ["MessageImage", "MessageAnimatedImage"]: - return ImageAttachment( - original_extension=a.get("original_extension") - or (a["filename"].split("-")[0] if a.get("filename") else None), - width=a.get("original_dimensions", {}).get("width"), - height=a.get("original_dimensions", {}).get("height"), - is_animated=_type == "MessageAnimatedImage", - thumbnail_url=a.get("thumbnail", {}).get("uri"), - preview=a.get("preview") or a.get("preview_image"), - large_preview=a.get("large_preview"), - animated_preview=a.get("animated_image"), - uid=a.get("legacy_attachment_id"), - ) + return ImageAttachment._from_graphql(a) elif _type == "MessageVideo": - return VideoAttachment( - width=a.get("original_dimensions", {}).get("width"), - height=a.get("original_dimensions", {}).get("height"), - duration=a.get("playable_duration_in_ms"), - preview_url=a.get("playable_url"), - small_image=a.get("chat_image"), - medium_image=a.get("inbox_image"), - large_image=a.get("large_image"), - uid=a.get("legacy_attachment_id"), - ) + return VideoAttachment._from_graphql(a) elif _type == "MessageAudio": - return AudioAttachment( - filename=a.get("filename"), - url=a.get("playable_url"), - duration=a.get("playable_duration_in_ms"), - audio_type=a.get("audio_type"), - ) + return AudioAttachment._from_graphql(a) elif _type == "MessageFile": - return FileAttachment( - url=a.get("url"), - name=a.get("filename"), - is_malicious=a.get("is_malicious"), - uid=a.get("message_file_fbid"), - ) + return FileAttachment._from_graphql(a) else: return Attachment(uid=a.get("legacy_attachment_id")) From 968223690e7ab6f21f11fc1c55a84aeade024c90 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 16:21:22 +0100 Subject: [PATCH 13/29] Move plan parsing to the Plan model - Add `GuestStatus` enum - Add `Plan.guests` property - Made `Plan.going`, `Plan.declined` and `Plan.invited` property accessors to `Plan.guests` --- fbchat/_client.py | 18 ++++------ fbchat/_graphql.py | 86 ++++++--------------------------------------- fbchat/_plan.py | 87 ++++++++++++++++++++++++++++++++++++++++++---- fbchat/graphql.py | 1 - fbchat/models.py | 2 +- 5 files changed, 99 insertions(+), 95 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index df406e7..f6e4b6b 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1261,8 +1261,7 @@ class Client(object): """ data = {"event_reminder_id": plan_id} j = self._post(self.req_url.PLAN_INFO, data, fix_request=True, as_json=True) - plan = graphql_to_plan(j["payload"]) - return plan + return Plan._from_fetch(j["payload"]) def _getPrivateData(self): j = self.graphql_request(GraphQL(doc_id="1868889766468115")) @@ -2869,10 +2868,9 @@ class Client(object): # Plan created elif delta_type == "lightweight_event_create": thread_id, thread_type = getThreadIdAndThreadType(metadata) - plan = graphql_to_plan(delta["untypedData"]) self.onPlanCreated( mid=mid, - plan=plan, + plan=Plan._from_pull(delta["untypedData"]), author_id=author_id, thread_id=thread_id, thread_type=thread_type, @@ -2884,10 +2882,9 @@ class Client(object): # Plan ended elif delta_type == "lightweight_event_notify": thread_id, thread_type = getThreadIdAndThreadType(metadata) - plan = graphql_to_plan(delta["untypedData"]) self.onPlanEnded( mid=mid, - plan=plan, + plan=Plan._from_pull(delta["untypedData"]), thread_id=thread_id, thread_type=thread_type, ts=ts, @@ -2898,10 +2895,9 @@ class Client(object): # Plan edited elif delta_type == "lightweight_event_update": thread_id, thread_type = getThreadIdAndThreadType(metadata) - plan = graphql_to_plan(delta["untypedData"]) self.onPlanEdited( mid=mid, - plan=plan, + plan=Plan._from_pull(delta["untypedData"]), author_id=author_id, thread_id=thread_id, thread_type=thread_type, @@ -2913,10 +2909,9 @@ class Client(object): # Plan deleted elif delta_type == "lightweight_event_delete": thread_id, thread_type = getThreadIdAndThreadType(metadata) - plan = graphql_to_plan(delta["untypedData"]) self.onPlanDeleted( mid=mid, - plan=plan, + plan=Plan._from_pull(delta["untypedData"]), author_id=author_id, thread_id=thread_id, thread_type=thread_type, @@ -2928,11 +2923,10 @@ class Client(object): # Plan participation change elif delta_type == "lightweight_event_rsvp": thread_id, thread_type = getThreadIdAndThreadType(metadata) - plan = graphql_to_plan(delta["untypedData"]) take_part = delta["untypedData"]["guest_status"] == "GOING" self.onPlanParticipation( mid=mid, - plan=plan, + plan=Plan._from_pull(delta["untypedData"]), take_part=take_part, author_id=author_id, thread_id=thread_id, diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 932c5c8..a483c0e 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -106,57 +106,6 @@ def graphql_to_subattachment(a): ) -def graphql_to_plan(a): - if a.get("event_members"): - rtn = Plan( - time=a.get("event_time"), - title=a.get("title"), - location=a.get("location_name"), - ) - if a.get("location_id") != 0: - rtn.location_id = str(a.get("location_id")) - rtn.uid = a.get("oid") - rtn.author_id = a.get("creator_id") - guests = a.get("event_members") - rtn.going = [uid for uid in guests if guests[uid] == "GOING"] - rtn.declined = [uid for uid in guests if guests[uid] == "DECLINED"] - rtn.invited = [uid for uid in guests if guests[uid] == "INVITED"] - return rtn - elif a.get("id") is None: - rtn = Plan( - time=a.get("event_time"), - title=a.get("event_title"), - location=a.get("event_location_name"), - location_id=a.get("event_location_id"), - ) - rtn.uid = a.get("event_id") - rtn.author_id = a.get("event_creator_id") - guests = json.loads(a.get("guest_state_list")) - else: - rtn = Plan( - time=a.get("time"), - title=a.get("event_title"), - location=a.get("location_name"), - ) - rtn.uid = a.get("id") - rtn.author_id = a.get("lightweight_event_creator").get("id") - guests = a.get("event_reminder_members").get("edges") - rtn.going = [ - m.get("node").get("id") for m in guests if m.get("guest_list_state") == "GOING" - ] - rtn.declined = [ - m.get("node").get("id") - for m in guests - if m.get("guest_list_state") == "DECLINED" - ] - rtn.invited = [ - m.get("node").get("id") - for m in guests - if m.get("guest_list_state") == "INVITED" - ] - return rtn - - def graphql_to_quick_reply(q, is_response=False): data = dict() _type = q.get("content_type").lower() @@ -235,12 +184,9 @@ def graphql_to_user(user): user["profile_picture"] = {} c_info = get_customization_info(user) plan = None - if user.get("event_reminders"): - plan = ( - graphql_to_plan(user["event_reminders"]["nodes"][0]) - if user["event_reminders"].get("nodes") - else None - ) + if user.get("event_reminders") and user["event_reminders"].get("nodes"): + plan = Plan._from_graphql(user["event_reminders"]["nodes"][0]) + return User( user["id"], url=user.get("url"), @@ -286,12 +232,8 @@ def graphql_to_thread(thread): last_name = user.get("name").split(first_name, 1).pop().strip() plan = None - if thread.get("event_reminders"): - plan = ( - graphql_to_plan(thread["event_reminders"]["nodes"][0]) - if thread["event_reminders"].get("nodes") - else None - ) + if thread.get("event_reminders") and thread["event_reminders"].get("nodes"): + plan = Plan._from_graphql(thread["event_reminders"]["nodes"][0]) return User( user["id"], @@ -327,12 +269,9 @@ def graphql_to_group(group): if "last_message" in group: last_message_timestamp = group["last_message"]["nodes"][0]["timestamp_precise"] plan = None - if group.get("event_reminders"): - plan = ( - graphql_to_plan(group["event_reminders"]["nodes"][0]) - if group["event_reminders"].get("nodes") - else None - ) + if group.get("event_reminders") and group["event_reminders"].get("nodes"): + plan = Plan._from_graphql(group["event_reminders"]["nodes"][0]) + return Group( group["thread_key"]["thread_fbid"], participants=set( @@ -368,12 +307,9 @@ def graphql_to_page(page): if page.get("city") is None: page["city"] = {} plan = None - if page.get("event_reminders"): - plan = ( - graphql_to_plan(page["event_reminders"]["nodes"][0]) - if page["event_reminders"].get("nodes") - else None - ) + if page.get("event_reminders") and page["event_reminders"].get("nodes"): + plan = Plan._from_graphql(page["event_reminders"]["nodes"][0]) + return Page( page["id"], url=page.get("url"), diff --git a/fbchat/_plan.py b/fbchat/_plan.py index b6f7826..2e228e6 100644 --- a/fbchat/_plan.py +++ b/fbchat/_plan.py @@ -2,6 +2,14 @@ from __future__ import unicode_literals import attr +import json +from ._core import Enum + + +class GuestStatus(Enum): + INVITED = 1 + GOING = 2 + DECLINED = 3 @attr.s(cmp=False) @@ -20,9 +28,76 @@ class Plan(object): location_id = attr.ib(None, converter=lambda x: x or "") #: ID of the plan creator author_id = attr.ib(None, init=False) - #: List of the people IDs who will take part in the plan - going = attr.ib(factory=list, init=False) - #: List of the people IDs who won't take part in the plan - declined = attr.ib(factory=list, init=False) - #: List of the people IDs who are invited to the plan - invited = attr.ib(factory=list, init=False) + #: Dict of `User` IDs mapped to their `GuestStatus` + guests = attr.ib(None, init=False) + + @property + def going(self): + """List of the `User` IDs who will take part in the plan.""" + return [ + id_ + for id_, status in (self.guests or {}).items() + if status is GuestStatus.GOING + ] + + @property + def declined(self): + """List of the `User` IDs who won't take part in the plan.""" + return [ + id_ + for id_, status in (self.guests or {}).items() + if status is GuestStatus.DECLINED + ] + + @property + def invited(self): + """List of the `User` IDs who are invited to the plan.""" + return [ + id_ + for id_, status in (self.guests or {}).items() + if status is GuestStatus.INVITED + ] + + @classmethod + def _from_pull(cls, data): + rtn = cls( + time=data.get("event_time"), + title=data.get("event_title"), + location=data.get("event_location_name"), + location_id=data.get("event_location_id"), + ) + rtn.uid = data.get("event_id") + rtn.author_id = data.get("event_creator_id") + rtn.guests = { + x["node"]["id"]: GuestStatus[x["guest_list_state"]] + for x in json.loads(data["guest_state_list"]) + } + return rtn + + @classmethod + def _from_fetch(cls, data): + rtn = cls( + time=data.get("event_time"), + title=data.get("title"), + location=data.get("location_name"), + location_id=str(data["location_id"]) if data.get("location_id") else None, + ) + rtn.uid = data.get("oid") + rtn.author_id = data.get("creator_id") + rtn.guests = {id_: GuestStatus[s] for id_, s in data["event_members"].items()} + return rtn + + @classmethod + def _from_graphql(cls, data): + rtn = cls( + time=data.get("time"), + title=data.get("event_title"), + location=data.get("location_name"), + ) + rtn.uid = data.get("id") + rtn.author_id = data["lightweight_event_creator"].get("id") + rtn.guests = { + x["node"]["id"]: GuestStatus[x["guest_list_state"]] + for x in data["event_reminder_members"]["edges"] + } + return rtn diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 173a215..c094d6c 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -12,7 +12,6 @@ from ._graphql import ( graphql_to_attachment, graphql_to_extensible_attachment, graphql_to_subattachment, - graphql_to_plan, graphql_to_quick_reply, graphql_to_message, graphql_to_user, diff --git a/fbchat/models.py b/fbchat/models.py index 5c192c0..1bab61b 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -26,4 +26,4 @@ from ._quick_reply import ( QuickReplyEmail, ) from ._poll import Poll, PollOption -from ._plan import Plan +from ._plan import GuestStatus, Plan From ce469d5e5ac5de381d27daf5fabb40aaacb4bda6 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 17:02:03 +0100 Subject: [PATCH 14/29] Move get_customization_info -> Thread._parse_customization_info --- fbchat/_graphql.py | 41 ++++------------------------------------- fbchat/_thread.py | 33 +++++++++++++++++++++++++++++++++ fbchat/graphql.py | 1 - 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index a483c0e..cb2dd0a 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -27,39 +27,6 @@ class ConcatJSONDecoder(json.JSONDecoder): # End shameless copy -def get_customization_info(thread): - if thread is None or thread.get("customization_info") is None: - return {} - info = thread["customization_info"] - - rtn = { - "emoji": info.get("emoji"), - "color": ThreadColor._from_graphql(info.get("outgoing_bubble_color")), - } - if ( - thread.get("thread_type") == "GROUP" - or thread.get("is_group_thread") - or thread.get("thread_key", {}).get("thread_fbid") - ): - rtn["nicknames"] = {} - for k in info.get("participant_customizations", []): - rtn["nicknames"][k["participant_id"]] = k.get("nickname") - elif info.get("participant_customizations"): - uid = thread.get("thread_key", {}).get("other_user_id") or thread.get("id") - pc = info["participant_customizations"] - if len(pc) > 0: - if pc[0].get("participant_id") == uid: - rtn["nickname"] = pc[0].get("nickname") - else: - rtn["own_nickname"] = pc[0].get("nickname") - if len(pc) > 1: - if pc[1].get("participant_id") == uid: - rtn["nickname"] = pc[1].get("nickname") - else: - rtn["own_nickname"] = pc[1].get("nickname") - return rtn - - def graphql_to_attachment(a): _type = a["__typename"] if _type in ["MessageImage", "MessageAnimatedImage"]: @@ -144,7 +111,7 @@ def graphql_to_message(message): ) for m in message.get("message").get("ranges", []) ], - emoji_size=get_emojisize_from_tags(message.get("tags_list")), + emoji_size=EmojiSize._from_tags(message.get("tags_list")), sticker=Sticker._from_graphql(message.get("sticker")), ) rtn.uid = str(message.get("message_id")) @@ -182,7 +149,7 @@ def graphql_to_message(message): def graphql_to_user(user): if user.get("profile_picture") is None: user["profile_picture"] = {} - c_info = get_customization_info(user) + c_info = User._parse_customization_info(user) plan = None if user.get("event_reminders") and user["event_reminders"].get("nodes"): plan = Plan._from_graphql(user["event_reminders"]["nodes"][0]) @@ -212,7 +179,7 @@ def graphql_to_thread(thread): elif thread["thread_type"] == "ONE_TO_ONE": if thread.get("big_image_src") is None: thread["big_image_src"] = {} - c_info = get_customization_info(thread) + c_info = User._parse_customization_info(thread) participants = [ node["messaging_actor"] for node in thread["all_participants"]["nodes"] ] @@ -264,7 +231,7 @@ def graphql_to_thread(thread): def graphql_to_group(group): if group.get("image") is None: group["image"] = {} - c_info = get_customization_info(group) + c_info = Group._parse_customization_info(group) last_message_timestamp = None if "last_message" in group: last_message_timestamp = group["last_message"]["nodes"][0]["timestamp_precise"] diff --git a/fbchat/_thread.py b/fbchat/_thread.py index d4a8391..8ae8898 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -89,3 +89,36 @@ class Thread(object): self.last_message_timestamp = last_message_timestamp self.message_count = message_count self.plan = plan + + @staticmethod + def _parse_customization_info(data): + if data is None or data.get("customization_info") is None: + return {} + info = data["customization_info"] + + rtn = { + "emoji": info.get("emoji"), + "color": ThreadColor._from_graphql(info.get("outgoing_bubble_color")), + } + if ( + data.get("thread_type") == "GROUP" + or data.get("is_group_thread") + or data.get("thread_key", {}).get("thread_fbid") + ): + rtn["nicknames"] = {} + for k in info.get("participant_customizations", []): + rtn["nicknames"][k["participant_id"]] = k.get("nickname") + elif info.get("participant_customizations"): + uid = data.get("thread_key", {}).get("other_user_id") or data.get("id") + pc = info["participant_customizations"] + if len(pc) > 0: + if pc[0].get("participant_id") == uid: + rtn["nickname"] = pc[0].get("nickname") + else: + rtn["own_nickname"] = pc[0].get("nickname") + if len(pc) > 1: + if pc[1].get("participant_id") == uid: + rtn["nickname"] = pc[1].get("nickname") + else: + rtn["own_nickname"] = pc[1].get("nickname") + return rtn diff --git a/fbchat/graphql.py b/fbchat/graphql.py index c094d6c..7ccef05 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -8,7 +8,6 @@ from ._graphql import ( FLAGS, WHITESPACE, ConcatJSONDecoder, - get_customization_info, graphql_to_attachment, graphql_to_extensible_attachment, graphql_to_subattachment, From 3a5185fcc811b75ee4479e60b1a1bfacad12ebd4 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 17:21:06 +0100 Subject: [PATCH 15/29] Move graphql_to_user -> User._from_graphql --- fbchat/_client.py | 6 +++--- fbchat/_graphql.py | 27 --------------------------- fbchat/_user.py | 28 ++++++++++++++++++++++++++++ fbchat/graphql.py | 1 - 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index f6e4b6b..5fab73a 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -730,7 +730,7 @@ class Client(object): GraphQL(query=GraphQL.SEARCH_USER, params={"search": name, "limit": limit}) ) - return [graphql_to_user(node) for node in j[name]["users"]["nodes"]] + return [User._from_graphql(node) for node in j[name]["users"]["nodes"]] def searchForPages(self, name, limit=10): """ @@ -785,7 +785,7 @@ class Client(object): rtn = [] for node in j[name]["threads"]["nodes"]: if node["__typename"] == "User": - rtn.append(graphql_to_user(node)) + rtn.append(User._from_graphql(node)) elif node["__typename"] == "MessageThread": # MessageThread => Group thread rtn.append(graphql_to_group(node)) @@ -1056,7 +1056,7 @@ class Client(object): raise FBchatException("Could not fetch thread {}".format(_id)) entry.update(pages_and_users[_id]) if entry["type"] == ThreadType.USER: - rtn[_id] = graphql_to_user(entry) + rtn[_id] = User._from_graphql(entry) else: rtn[_id] = graphql_to_page(entry) else: diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index cb2dd0a..ab9b976 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -146,33 +146,6 @@ def graphql_to_message(message): return rtn -def graphql_to_user(user): - if user.get("profile_picture") is None: - user["profile_picture"] = {} - c_info = User._parse_customization_info(user) - plan = None - if user.get("event_reminders") and user["event_reminders"].get("nodes"): - plan = Plan._from_graphql(user["event_reminders"]["nodes"][0]) - - return User( - user["id"], - url=user.get("url"), - first_name=user.get("first_name"), - last_name=user.get("last_name"), - is_friend=user.get("is_viewer_friend"), - gender=GENDERS.get(user.get("gender")), - affinity=user.get("affinity"), - nickname=c_info.get("nickname"), - color=c_info.get("color"), - emoji=c_info.get("emoji"), - own_nickname=c_info.get("own_nickname"), - photo=user["profile_picture"].get("uri"), - name=user.get("name"), - message_count=user.get("messages_count"), - plan=plan, - ) - - def graphql_to_thread(thread): if thread["thread_type"] == "GROUP": return graphql_to_group(thread) diff --git a/fbchat/_user.py b/fbchat/_user.py index a42390f..3bb8015 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import attr from ._core import Enum +from . import _util, _plan from ._thread import ThreadType, Thread @@ -65,6 +66,33 @@ class User(Thread): self.color = color self.emoji = emoji + @classmethod + def _from_graphql(cls, data): + if data.get("profile_picture") is None: + data["profile_picture"] = {} + c_info = cls._parse_customization_info(data) + plan = None + if data.get("event_reminders") and data["event_reminders"].get("nodes"): + plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) + + return cls( + data["id"], + url=data.get("url"), + first_name=data.get("first_name"), + last_name=data.get("last_name"), + is_friend=data.get("is_viewer_friend"), + gender=_util.GENDERS.get(data.get("gender")), + affinity=data.get("affinity"), + nickname=c_info.get("nickname"), + color=c_info.get("color"), + emoji=c_info.get("emoji"), + own_nickname=c_info.get("own_nickname"), + photo=data["profile_picture"].get("uri"), + name=data.get("name"), + message_count=data.get("messages_count"), + plan=plan, + ) + @attr.s(cmp=False) class ActiveStatus(object): diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 7ccef05..0c27db1 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -13,7 +13,6 @@ from ._graphql import ( graphql_to_subattachment, graphql_to_quick_reply, graphql_to_message, - graphql_to_user, graphql_to_thread, graphql_to_group, graphql_to_page, From 60ebbd87d8d6dd367ab910b8386678b7473de864 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 17:26:47 +0100 Subject: [PATCH 16/29] Move graphql_to_thread user parsing to User._from_thread_fetch --- fbchat/_graphql.py | 44 +------------------------------------------- fbchat/_user.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index ab9b976..69067df 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -150,49 +150,7 @@ def graphql_to_thread(thread): if thread["thread_type"] == "GROUP": return graphql_to_group(thread) elif thread["thread_type"] == "ONE_TO_ONE": - if thread.get("big_image_src") is None: - thread["big_image_src"] = {} - c_info = User._parse_customization_info(thread) - participants = [ - node["messaging_actor"] for node in thread["all_participants"]["nodes"] - ] - user = next( - p for p in participants if p["id"] == thread["thread_key"]["other_user_id"] - ) - last_message_timestamp = None - if "last_message" in thread: - last_message_timestamp = thread["last_message"]["nodes"][0][ - "timestamp_precise" - ] - - first_name = user.get("short_name") - if first_name is None: - last_name = None - else: - last_name = user.get("name").split(first_name, 1).pop().strip() - - plan = None - if thread.get("event_reminders") and thread["event_reminders"].get("nodes"): - plan = Plan._from_graphql(thread["event_reminders"]["nodes"][0]) - - return User( - user["id"], - url=user.get("url"), - name=user.get("name"), - first_name=first_name, - last_name=last_name, - is_friend=user.get("is_viewer_friend"), - gender=GENDERS.get(user.get("gender")), - affinity=user.get("affinity"), - nickname=c_info.get("nickname"), - color=c_info.get("color"), - emoji=c_info.get("emoji"), - own_nickname=c_info.get("own_nickname"), - photo=user["big_image_src"].get("uri"), - message_count=thread.get("messages_count"), - last_message_timestamp=last_message_timestamp, - plan=plan, - ) + return User._from_thread_fetch(thread) else: raise FBchatException( "Unknown thread type: {}, with data: {}".format( diff --git a/fbchat/_user.py b/fbchat/_user.py index 3bb8015..45fb021 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -93,6 +93,52 @@ class User(Thread): plan=plan, ) + @classmethod + def _from_thread_fetch(cls, data): + if data.get("big_image_src") is None: + data["big_image_src"] = {} + c_info = cls._parse_customization_info(data) + participants = [ + node["messaging_actor"] for node in data["all_participants"]["nodes"] + ] + user = next( + p for p in participants if p["id"] == data["thread_key"]["other_user_id"] + ) + last_message_timestamp = None + if "last_message" in data: + last_message_timestamp = data["last_message"]["nodes"][0][ + "timestamp_precise" + ] + + first_name = user.get("short_name") + if first_name is None: + last_name = None + else: + last_name = user.get("name").split(first_name, 1).pop().strip() + + plan = None + if data.get("event_reminders") and data["event_reminders"].get("nodes"): + plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) + + return cls( + user["id"], + url=user.get("url"), + name=user.get("name"), + first_name=first_name, + last_name=last_name, + is_friend=user.get("is_viewer_friend"), + gender=_util.GENDERS.get(user.get("gender")), + affinity=user.get("affinity"), + nickname=c_info.get("nickname"), + color=c_info.get("color"), + emoji=c_info.get("emoji"), + own_nickname=c_info.get("own_nickname"), + photo=user["big_image_src"].get("uri"), + message_count=data.get("messages_count"), + last_message_timestamp=last_message_timestamp, + plan=plan, + ) + @attr.s(cmp=False) class ActiveStatus(object): From fd5553a9f505ea37a3b5732cc2ffbfe188cede95 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 17:33:46 +0100 Subject: [PATCH 17/29] Move graphql_to_group -> Group._from_graphql --- fbchat/_client.py | 6 +++--- fbchat/_graphql.py | 42 +----------------------------------------- fbchat/_group.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ fbchat/graphql.py | 1 - 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 5fab73a..746ce68 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -763,7 +763,7 @@ class Client(object): GraphQL(query=GraphQL.SEARCH_GROUP, params={"search": name, "limit": limit}) ) - return [graphql_to_group(node) for node in j["viewer"]["groups"]["nodes"]] + return [Group._from_graphql(node) for node in j["viewer"]["groups"]["nodes"]] def searchForThreads(self, name, limit=10): """ @@ -788,7 +788,7 @@ class Client(object): rtn.append(User._from_graphql(node)) elif node["__typename"] == "MessageThread": # MessageThread => Group thread - rtn.append(graphql_to_group(node)) + rtn.append(Group._from_graphql(node)) elif node["__typename"] == "Page": rtn.append(graphql_to_page(node)) elif node["__typename"] == "Group": @@ -1049,7 +1049,7 @@ class Client(object): entry = entry["message_thread"] if entry.get("thread_type") == "GROUP": _id = entry["thread_key"]["thread_fbid"] - rtn[_id] = graphql_to_group(entry) + rtn[_id] = Group._from_graphql(entry) elif entry.get("thread_type") == "ONE_TO_ONE": _id = entry["thread_key"]["other_user_id"] if pages_and_users.get(_id) is None: diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 69067df..0df4032 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -148,7 +148,7 @@ def graphql_to_message(message): def graphql_to_thread(thread): if thread["thread_type"] == "GROUP": - return graphql_to_group(thread) + return Group._from_graphql(thread) elif thread["thread_type"] == "ONE_TO_ONE": return User._from_thread_fetch(thread) else: @@ -159,46 +159,6 @@ def graphql_to_thread(thread): ) -def graphql_to_group(group): - if group.get("image") is None: - group["image"] = {} - c_info = Group._parse_customization_info(group) - last_message_timestamp = None - if "last_message" in group: - last_message_timestamp = group["last_message"]["nodes"][0]["timestamp_precise"] - plan = None - if group.get("event_reminders") and group["event_reminders"].get("nodes"): - plan = Plan._from_graphql(group["event_reminders"]["nodes"][0]) - - return Group( - group["thread_key"]["thread_fbid"], - participants=set( - [ - node["messaging_actor"]["id"] - for node in group["all_participants"]["nodes"] - ] - ), - nicknames=c_info.get("nicknames"), - color=c_info.get("color"), - emoji=c_info.get("emoji"), - admins=set([node.get("id") for node in group.get("thread_admins")]), - approval_mode=bool(group.get("approval_mode")) - if group.get("approval_mode") is not None - else None, - approval_requests=set( - node["requester"]["id"] for node in group["group_approval_queue"]["nodes"] - ) - if group.get("group_approval_queue") - else None, - join_link=group["joinable_mode"].get("link"), - photo=group["image"].get("uri"), - name=group.get("name"), - message_count=group.get("messages_count"), - last_message_timestamp=last_message_timestamp, - plan=plan, - ) - - def graphql_to_page(page): if page.get("profile_picture") is None: page["profile_picture"] = {} diff --git a/fbchat/_group.py b/fbchat/_group.py index 62cf020..5b01bec 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import attr +from . import _plan from ._thread import ThreadType, Thread @@ -60,6 +61,49 @@ class Group(Thread): self.approval_requests = approval_requests self.join_link = join_link + @classmethod + def _from_graphql(cls, data): + if data.get("image") is None: + data["image"] = {} + c_info = cls._parse_customization_info(data) + last_message_timestamp = None + if "last_message" in data: + last_message_timestamp = data["last_message"]["nodes"][0][ + "timestamp_precise" + ] + plan = None + if data.get("event_reminders") and data["event_reminders"].get("nodes"): + plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) + + return cls( + data["thread_key"]["thread_fbid"], + participants=set( + [ + node["messaging_actor"]["id"] + for node in data["all_participants"]["nodes"] + ] + ), + nicknames=c_info.get("nicknames"), + color=c_info.get("color"), + emoji=c_info.get("emoji"), + admins=set([node.get("id") for node in data.get("thread_admins")]), + approval_mode=bool(data.get("approval_mode")) + if data.get("approval_mode") is not None + else None, + approval_requests=set( + node["requester"]["id"] + for node in data["group_approval_queue"]["nodes"] + ) + if data.get("group_approval_queue") + else None, + join_link=data["joinable_mode"].get("link"), + photo=data["image"].get("uri"), + name=data.get("name"), + message_count=data.get("messages_count"), + last_message_timestamp=last_message_timestamp, + plan=plan, + ) + @attr.s(cmp=False, init=False) class Room(Group): diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 0c27db1..b7f0253 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -14,7 +14,6 @@ from ._graphql import ( graphql_to_quick_reply, graphql_to_message, graphql_to_thread, - graphql_to_group, graphql_to_page, graphql_queries_to_json, graphql_response_to_json, From cb2c68e25a38065e6a77762fc98f246dc417b81b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 17:36:41 +0100 Subject: [PATCH 18/29] Move graphql_to_page -> Page._from_graphql --- fbchat/_client.py | 6 +++--- fbchat/_graphql.py | 21 --------------------- fbchat/_page.py | 22 ++++++++++++++++++++++ fbchat/graphql.py | 1 - 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 746ce68..a8ca034 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -746,7 +746,7 @@ class Client(object): GraphQL(query=GraphQL.SEARCH_PAGE, params={"search": name, "limit": limit}) ) - return [graphql_to_page(node) for node in j[name]["pages"]["nodes"]] + return [Page._from_graphql(node) for node in j[name]["pages"]["nodes"]] def searchForGroups(self, name, limit=10): """ @@ -790,7 +790,7 @@ class Client(object): # MessageThread => Group thread rtn.append(Group._from_graphql(node)) elif node["__typename"] == "Page": - rtn.append(graphql_to_page(node)) + rtn.append(Page._from_graphql(node)) elif node["__typename"] == "Group": # We don't handle Facebook "Groups" pass @@ -1058,7 +1058,7 @@ class Client(object): if entry["type"] == ThreadType.USER: rtn[_id] = User._from_graphql(entry) else: - rtn[_id] = graphql_to_page(entry) + rtn[_id] = Page._from_graphql(entry) else: raise FBchatException( "{} had an unknown thread type: {}".format(thread_ids[i], entry) diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 0df4032..81accbb 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -159,27 +159,6 @@ def graphql_to_thread(thread): ) -def graphql_to_page(page): - if page.get("profile_picture") is None: - page["profile_picture"] = {} - if page.get("city") is None: - page["city"] = {} - plan = None - if page.get("event_reminders") and page["event_reminders"].get("nodes"): - plan = Plan._from_graphql(page["event_reminders"]["nodes"][0]) - - return Page( - page["id"], - url=page.get("url"), - city=page.get("city").get("name"), - category=page.get("category_type"), - photo=page["profile_picture"].get("uri"), - name=page.get("name"), - message_count=page.get("messages_count"), - plan=plan, - ) - - def graphql_queries_to_json(*queries): """ Queries should be a list of GraphQL objects diff --git a/fbchat/_page.py b/fbchat/_page.py index 9c696e0..b5846c0 100644 --- a/fbchat/_page.py +++ b/fbchat/_page.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import attr +from . import _plan from ._thread import ThreadType, Thread @@ -36,3 +37,24 @@ class Page(Thread): self.likes = likes self.sub_title = sub_title self.category = category + + @classmethod + def _from_graphql(cls, data): + if data.get("profile_picture") is None: + data["profile_picture"] = {} + if data.get("city") is None: + data["city"] = {} + plan = None + if data.get("event_reminders") and data["event_reminders"].get("nodes"): + plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) + + return cls( + data["id"], + url=data.get("url"), + city=data.get("city").get("name"), + category=data.get("category_type"), + photo=data["profile_picture"].get("uri"), + name=data.get("name"), + message_count=data.get("messages_count"), + plan=plan, + ) diff --git a/fbchat/graphql.py b/fbchat/graphql.py index b7f0253..e35f623 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -14,7 +14,6 @@ from ._graphql import ( graphql_to_quick_reply, graphql_to_message, graphql_to_thread, - graphql_to_page, graphql_queries_to_json, graphql_response_to_json, GraphQL, From 0b992386761b15a5cc5770fa64a6c3e94c2c412a Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 19:19:50 +0100 Subject: [PATCH 19/29] Move subattachment parsing to the respective models --- fbchat/_file.py | 10 ++++++++++ fbchat/_graphql.py | 14 +++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/fbchat/_file.py b/fbchat/_file.py index 4b858a7..c9049aa 100644 --- a/fbchat/_file.py +++ b/fbchat/_file.py @@ -241,3 +241,13 @@ class VideoAttachment(Attachment): large_image=data.get("large_image"), uid=data.get("legacy_attachment_id"), ) + + @classmethod + def _from_subattachment(cls, data): + media = data["media"] + return cls( + duration=media.get("playable_duration_in_ms"), + preview_url=media.get("playable_url"), + medium_image=media.get("image"), + uid=data["target"].get("video_id"), + ) diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 81accbb..a23865a 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -61,16 +61,12 @@ def graphql_to_extensible_attachment(a): return None -def graphql_to_subattachment(a): - _type = a["target"]["__typename"] +def graphql_to_subattachment(data): + _type = data["target"]["__typename"] if _type == "Video": - media = a["media"] - return VideoAttachment( - duration=media.get("playable_duration_in_ms"), - preview_url=media.get("playable_url"), - medium_image=media.get("image"), - uid=a["target"].get("video_id"), - ) + return VideoAttachment._from_subattachment(data) + + return None def graphql_to_quick_reply(q, is_response=False): From 53856a3622372956ff1510d2904069bdb4da0f73 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 19:26:01 +0100 Subject: [PATCH 20/29] Move attachment and subattachment dispatching to _file.py --- fbchat/_attachment.py | 4 ++-- fbchat/_client.py | 3 ++- fbchat/_file.py | 22 ++++++++++++++++++++++ fbchat/_graphql.py | 25 ++----------------------- fbchat/graphql.py | 2 -- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/fbchat/_attachment.py b/fbchat/_attachment.py index 62f5ea0..265301e 100644 --- a/fbchat/_attachment.py +++ b/fbchat/_attachment.py @@ -50,7 +50,7 @@ class ShareAttachment(Attachment): @classmethod def _from_graphql(cls, data): - from . import _graphql + from . import _file url = data.get("url") rtn = cls( @@ -68,7 +68,7 @@ class ShareAttachment(Attachment): else None, source=data["source"].get("text"), attachments=[ - _graphql.graphql_to_subattachment(attachment) + _file.graphql_to_subattachment(attachment) for attachment in data.get("subattachments") ], ) diff --git a/fbchat/_client.py b/fbchat/_client.py index a8ca034..168392e 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -8,6 +8,7 @@ from random import choice from bs4 import BeautifulSoup as bs from mimetypes import guess_type from collections import OrderedDict +from . import _file from ._util import * from .models import * from .graphql import * @@ -3057,7 +3058,7 @@ class Client(object): if mercury.get("blob_attachment"): image_metadata = a.get("imageMetadata", {}) attach_type = mercury["blob_attachment"]["__typename"] - attachment = graphql_to_attachment( + attachment = _file.graphql_to_attachment( mercury["blob_attachment"] ) diff --git a/fbchat/_file.py b/fbchat/_file.py index c9049aa..00c8a32 100644 --- a/fbchat/_file.py +++ b/fbchat/_file.py @@ -251,3 +251,25 @@ class VideoAttachment(Attachment): medium_image=media.get("image"), uid=data["target"].get("video_id"), ) + + +def graphql_to_attachment(data): + _type = data["__typename"] + if _type in ["MessageImage", "MessageAnimatedImage"]: + return ImageAttachment._from_graphql(data) + elif _type == "MessageVideo": + return VideoAttachment._from_graphql(data) + elif _type == "MessageAudio": + return AudioAttachment._from_graphql(data) + elif _type == "MessageFile": + return FileAttachment._from_graphql(data) + + return Attachment(uid=data.get("legacy_attachment_id")) + + +def graphql_to_subattachment(data): + _type = data["target"]["__typename"] + if _type == "Video": + return VideoAttachment._from_subattachment(data) + + return None diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index a23865a..1004f68 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import json import re +from . import _file from .models import * from ._util import * @@ -27,20 +28,6 @@ class ConcatJSONDecoder(json.JSONDecoder): # End shameless copy -def graphql_to_attachment(a): - _type = a["__typename"] - if _type in ["MessageImage", "MessageAnimatedImage"]: - return ImageAttachment._from_graphql(a) - elif _type == "MessageVideo": - return VideoAttachment._from_graphql(a) - elif _type == "MessageAudio": - return AudioAttachment._from_graphql(a) - elif _type == "MessageFile": - return FileAttachment._from_graphql(a) - else: - return Attachment(uid=a.get("legacy_attachment_id")) - - def graphql_to_extensible_attachment(a): story = a.get("story_attachment") if not story: @@ -61,14 +48,6 @@ def graphql_to_extensible_attachment(a): return None -def graphql_to_subattachment(data): - _type = data["target"]["__typename"] - if _type == "Video": - return VideoAttachment._from_subattachment(data) - - return None - - def graphql_to_quick_reply(q, is_response=False): data = dict() _type = q.get("content_type").lower() @@ -122,7 +101,7 @@ def graphql_to_message(message): } if message.get("blob_attachments") is not None: rtn.attachments = [ - graphql_to_attachment(attachment) + _file.graphql_to_attachment(attachment) for attachment in message["blob_attachments"] ] if message.get("platform_xmd_encoded"): diff --git a/fbchat/graphql.py b/fbchat/graphql.py index e35f623..06e416e 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -8,9 +8,7 @@ from ._graphql import ( FLAGS, WHITESPACE, ConcatJSONDecoder, - graphql_to_attachment, graphql_to_extensible_attachment, - graphql_to_subattachment, graphql_to_quick_reply, graphql_to_message, graphql_to_thread, From 6693ec9c3616a42521cb9774ad48130b98fdec8f Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 19:33:58 +0100 Subject: [PATCH 21/29] Move graphql_to_extensible_attachment into _message.py --- fbchat/_client.py | 4 ++-- fbchat/_graphql.py | 26 ++++---------------------- fbchat/_message.py | 21 +++++++++++++++++++++ fbchat/graphql.py | 1 - 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 168392e..9ff9c47 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -8,7 +8,7 @@ from random import choice from bs4 import BeautifulSoup as bs from mimetypes import guess_type from collections import OrderedDict -from . import _file +from . import _file, _message from ._util import * from .models import * from .graphql import * @@ -3077,7 +3077,7 @@ class Client(object): ) elif mercury.get("extensible_attachment"): - attachment = graphql_to_extensible_attachment( + attachment = _message.graphql_to_extensible_attachment( mercury["extensible_attachment"] ) if isinstance(attachment, UnsentMessage): diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 1004f68..370be44 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import json import re -from . import _file +from . import _file, _message from .models import * from ._util import * @@ -28,26 +28,6 @@ class ConcatJSONDecoder(json.JSONDecoder): # End shameless copy -def graphql_to_extensible_attachment(a): - story = a.get("story_attachment") - if not story: - return None - - target = story.get("target") - if not target: - return UnsentMessage(uid=a.get("legacy_attachment_id")) - - _type = target["__typename"] - if _type == "MessageLocation": - return LocationAttachment._from_graphql(story) - elif _type == "MessageLiveLocation": - return LiveLocationAttachment._from_graphql(story) - elif _type in ["ExternalUrl", "Story"]: - return ShareAttachment._from_graphql(story) - - return None - - def graphql_to_quick_reply(q, is_response=False): data = dict() _type = q.get("content_type").lower() @@ -113,7 +93,9 @@ def graphql_to_message(message): graphql_to_quick_reply(quick_replies, is_response=True) ] if message.get("extensible_attachment") is not None: - attachment = graphql_to_extensible_attachment(message["extensible_attachment"]) + attachment = _message.graphql_to_extensible_attachment( + message["extensible_attachment"] + ) if isinstance(attachment, UnsentMessage): rtn.unsent = True elif attachment: diff --git a/fbchat/_message.py b/fbchat/_message.py index 59e52ba..3ba4c10 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import attr from string import Formatter +from . import _attachment, _location from ._core import Enum @@ -137,3 +138,23 @@ class Message(object): message = cls(text=result, mentions=mentions) return message + + +def graphql_to_extensible_attachment(data): + story = data.get("story_attachment") + if not story: + return None + + target = story.get("target") + if not target: + return _attachment.UnsentMessage(uid=data.get("legacy_attachment_id")) + + _type = target["__typename"] + if _type == "MessageLocation": + return _location.LocationAttachment._from_graphql(story) + elif _type == "MessageLiveLocation": + return _location.LiveLocationAttachment._from_graphql(story) + elif _type in ["ExternalUrl", "Story"]: + return _attachment.ShareAttachment._from_graphql(story) + + return None diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 06e416e..412c42f 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -8,7 +8,6 @@ from ._graphql import ( FLAGS, WHITESPACE, ConcatJSONDecoder, - graphql_to_extensible_attachment, graphql_to_quick_reply, graphql_to_message, graphql_to_thread, From e579e0c76741bf087eff0ed2b75ad73d9871ec09 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 19:35:07 +0100 Subject: [PATCH 22/29] Move graphql_to_quick_reply into _quick_reply.py --- fbchat/_graphql.py | 31 +++++-------------------------- fbchat/_quick_reply.py | 23 +++++++++++++++++++++++ fbchat/graphql.py | 1 - 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 370be44..672558c 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import json import re -from . import _file, _message +from . import _file, _message, _quick_reply from .models import * from ._util import * @@ -28,29 +28,6 @@ class ConcatJSONDecoder(json.JSONDecoder): # End shameless copy -def graphql_to_quick_reply(q, is_response=False): - data = dict() - _type = q.get("content_type").lower() - if q.get("payload"): - data["payload"] = q["payload"] - if q.get("data"): - data["data"] = q["data"] - if q.get("image_url") and _type is not QuickReplyLocation._type: - data["image_url"] = q["image_url"] - data["is_response"] = is_response - if _type == QuickReplyText._type: - if q.get("title") is not None: - data["title"] = q["title"] - rtn = QuickReplyText(**data) - elif _type == QuickReplyLocation._type: - rtn = QuickReplyLocation(**data) - elif _type == QuickReplyPhoneNumber._type: - rtn = QuickReplyPhoneNumber(**data) - elif _type == QuickReplyEmail._type: - rtn = QuickReplyEmail(**data) - return rtn - - def graphql_to_message(message): if message.get("message_sender") is None: message["message_sender"] = {} @@ -87,10 +64,12 @@ def graphql_to_message(message): if message.get("platform_xmd_encoded"): quick_replies = json.loads(message["platform_xmd_encoded"]).get("quick_replies") if isinstance(quick_replies, list): - rtn.quick_replies = [graphql_to_quick_reply(q) for q in quick_replies] + rtn.quick_replies = [ + _quick_reply.graphql_to_quick_reply(q) for q in quick_replies + ] elif isinstance(quick_replies, dict): rtn.quick_replies = [ - graphql_to_quick_reply(quick_replies, is_response=True) + _quick_reply.graphql_to_quick_reply(quick_replies, is_response=True) ] if message.get("extensible_attachment") is not None: attachment = _message.graphql_to_extensible_attachment( diff --git a/fbchat/_quick_reply.py b/fbchat/_quick_reply.py index 60323bf..02c7f89 100644 --- a/fbchat/_quick_reply.py +++ b/fbchat/_quick_reply.py @@ -74,3 +74,26 @@ class QuickReplyEmail(QuickReply): def __init__(self, image_url=None, **kwargs): super(QuickReplyEmail, self).__init__(**kwargs) self.image_url = image_url + + +def graphql_to_quick_reply(q, is_response=False): + data = dict() + _type = q.get("content_type").lower() + if q.get("payload"): + data["payload"] = q["payload"] + if q.get("data"): + data["data"] = q["data"] + if q.get("image_url") and _type is not QuickReplyLocation._type: + data["image_url"] = q["image_url"] + data["is_response"] = is_response + if _type == QuickReplyText._type: + if q.get("title") is not None: + data["title"] = q["title"] + rtn = QuickReplyText(**data) + elif _type == QuickReplyLocation._type: + rtn = QuickReplyLocation(**data) + elif _type == QuickReplyPhoneNumber._type: + rtn = QuickReplyPhoneNumber(**data) + elif _type == QuickReplyEmail._type: + rtn = QuickReplyEmail(**data) + return rtn diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 412c42f..f0ea04f 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -8,7 +8,6 @@ from ._graphql import ( FLAGS, WHITESPACE, ConcatJSONDecoder, - graphql_to_quick_reply, graphql_to_message, graphql_to_thread, graphql_queries_to_json, From 1f961b2ca7d031d8da9c8127870e5d0119ca2e4b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 19:39:22 +0100 Subject: [PATCH 23/29] Move thread parser dispatching into _client.py I know, it's not pretty, but it doesn't belong in _graphql.py either --- fbchat/_client.py | 15 ++++++++++++--- fbchat/_graphql.py | 13 ------------- fbchat/graphql.py | 1 - 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 9ff9c47..12374b4 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1158,9 +1158,18 @@ class Client(object): ) ) - return [ - graphql_to_thread(node) for node in j["viewer"]["message_threads"]["nodes"] - ] + rtn = [] + for node in j["viewer"]["message_threads"]["nodes"]: + _type = node.get("thread_type") + if _type == "GROUP": + rtn.append(Group._from_graphql(node)) + elif _type == "ONE_TO_ONE": + rtn.append(User._from_thread_fetch(node)) + else: + raise FBchatException( + "Unknown thread type: {}, with data: {}".format(_type, node) + ) + return rtn def fetchUnread(self): """ diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 672558c..31d9931 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -82,19 +82,6 @@ def graphql_to_message(message): return rtn -def graphql_to_thread(thread): - if thread["thread_type"] == "GROUP": - return Group._from_graphql(thread) - elif thread["thread_type"] == "ONE_TO_ONE": - return User._from_thread_fetch(thread) - else: - raise FBchatException( - "Unknown thread type: {}, with data: {}".format( - thread.get("thread_type"), thread - ) - ) - - def graphql_queries_to_json(*queries): """ Queries should be a list of GraphQL objects diff --git a/fbchat/graphql.py b/fbchat/graphql.py index f0ea04f..4906656 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -9,7 +9,6 @@ from ._graphql import ( WHITESPACE, ConcatJSONDecoder, graphql_to_message, - graphql_to_thread, graphql_queries_to_json, graphql_response_to_json, GraphQL, From f20a04b2a0d35021cabc3baefccc3a97687f46d1 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 19:50:06 +0100 Subject: [PATCH 24/29] Move graphql_to_message -> Message._from_graphql --- fbchat/_client.py | 5 ++-- fbchat/_graphql.py | 54 ------------------------------------------- fbchat/_message.py | 57 +++++++++++++++++++++++++++++++++++++++++++++- fbchat/graphql.py | 1 - 4 files changed, 58 insertions(+), 59 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 12374b4..8d7cb8c 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1102,7 +1102,7 @@ class Client(object): messages = list( reversed( [ - graphql_to_message(message) + Message._from_graphql(message) for message in j["message_thread"]["messages"]["nodes"] ] ) @@ -1241,8 +1241,7 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) message_info = self._forcedFetch(thread_id, mid).get("message") - message = graphql_to_message(message_info) - return message + return Message._from_graphql(message_info) def fetchPollOptions(self, poll_id): """ diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 31d9931..37b5a3b 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -28,60 +28,6 @@ class ConcatJSONDecoder(json.JSONDecoder): # End shameless copy -def graphql_to_message(message): - if message.get("message_sender") is None: - message["message_sender"] = {} - if message.get("message") is None: - message["message"] = {} - rtn = Message( - text=message.get("message").get("text"), - mentions=[ - Mention( - m.get("entity", {}).get("id"), - offset=m.get("offset"), - length=m.get("length"), - ) - for m in message.get("message").get("ranges", []) - ], - emoji_size=EmojiSize._from_tags(message.get("tags_list")), - sticker=Sticker._from_graphql(message.get("sticker")), - ) - rtn.uid = str(message.get("message_id")) - rtn.author = str(message.get("message_sender").get("id")) - rtn.timestamp = message.get("timestamp_precise") - rtn.unsent = False - if message.get("unread") is not None: - rtn.is_read = not message["unread"] - rtn.reactions = { - str(r["user"]["id"]): MessageReaction._extend_if_invalid(r["reaction"]) - for r in message.get("message_reactions") - } - if message.get("blob_attachments") is not None: - rtn.attachments = [ - _file.graphql_to_attachment(attachment) - for attachment in message["blob_attachments"] - ] - if message.get("platform_xmd_encoded"): - quick_replies = json.loads(message["platform_xmd_encoded"]).get("quick_replies") - if isinstance(quick_replies, list): - rtn.quick_replies = [ - _quick_reply.graphql_to_quick_reply(q) for q in quick_replies - ] - elif isinstance(quick_replies, dict): - rtn.quick_replies = [ - _quick_reply.graphql_to_quick_reply(quick_replies, is_response=True) - ] - if message.get("extensible_attachment") is not None: - attachment = _message.graphql_to_extensible_attachment( - message["extensible_attachment"] - ) - if isinstance(attachment, UnsentMessage): - rtn.unsent = True - elif attachment: - rtn.attachments.append(attachment) - return rtn - - def graphql_queries_to_json(*queries): """ Queries should be a list of GraphQL objects diff --git a/fbchat/_message.py b/fbchat/_message.py index 3ba4c10..21eb5d3 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -2,8 +2,9 @@ from __future__ import unicode_literals import attr +import json from string import Formatter -from . import _attachment, _location +from . import _attachment, _location, _file, _quick_reply, _sticker from ._core import Enum @@ -139,6 +140,60 @@ class Message(object): message = cls(text=result, mentions=mentions) return message + @classmethod + def _from_graphql(cls, data): + if data.get("message_sender") is None: + data["message_sender"] = {} + if data.get("message") is None: + data["message"] = {} + rtn = cls( + text=data["message"].get("text"), + mentions=[ + Mention( + m.get("entity", {}).get("id"), + offset=m.get("offset"), + length=m.get("length"), + ) + for m in data["message"].get("ranges") or () + ], + emoji_size=EmojiSize._from_tags(data.get("tags_list")), + sticker=_sticker.Sticker._from_graphql(data.get("sticker")), + ) + rtn.uid = str(data["message_id"]) + rtn.author = str(data["message_sender"]["id"]) + rtn.timestamp = data.get("timestamp_precise") + rtn.unsent = False + if data.get("unread") is not None: + rtn.is_read = not data["unread"] + rtn.reactions = { + str(r["user"]["id"]): MessageReaction._extend_if_invalid(r["reaction"]) + for r in data["message_reactions"] + } + if data.get("blob_attachments") is not None: + rtn.attachments = [ + _file.graphql_to_attachment(attachment) + for attachment in data["blob_attachments"] + ] + if data.get("platform_xmd_encoded"): + quick_replies = json.loads(data["platform_xmd_encoded"]).get( + "quick_replies" + ) + if isinstance(quick_replies, list): + rtn.quick_replies = [ + _quick_reply.graphql_to_quick_reply(q) for q in quick_replies + ] + elif isinstance(quick_replies, dict): + rtn.quick_replies = [ + _quick_reply.graphql_to_quick_reply(quick_replies, is_response=True) + ] + if data.get("extensible_attachment") is not None: + attachment = graphql_to_extensible_attachment(data["extensible_attachment"]) + if isinstance(attachment, _attachment.UnsentMessage): + rtn.unsent = True + elif attachment: + rtn.attachments.append(attachment) + return rtn + def graphql_to_extensible_attachment(data): story = data.get("story_attachment") diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 4906656..81ce8eb 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -8,7 +8,6 @@ from ._graphql import ( FLAGS, WHITESPACE, ConcatJSONDecoder, - graphql_to_message, graphql_queries_to_json, graphql_response_to_json, GraphQL, From 28c867a115d6aa9cd0673768c33fd3f788f6f15b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 19:54:36 +0100 Subject: [PATCH 25/29] Simplify _graphql.py imports --- fbchat/_graphql.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 37b5a3b..72ba3c1 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -1,11 +1,10 @@ # -*- coding: UTF-8 -*- - from __future__ import unicode_literals + import json import re -from . import _file, _message, _quick_reply -from .models import * -from ._util import * +from . import _util +from ._exception import FBchatException, FBchatUserError # Shameless copy from https://stackoverflow.com/a/8730674 FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL @@ -39,7 +38,7 @@ def graphql_queries_to_json(*queries): def graphql_response_to_json(content): - content = strip_to_json(content) # Usually only needed in some error cases + content = _util.strip_to_json(content) # Usually only needed in some error cases try: j = json.loads(content, cls=ConcatJSONDecoder) except Exception: @@ -50,15 +49,15 @@ def graphql_response_to_json(content): if "error_results" in x: del rtn[-1] continue - check_json(x) + _util.check_json(x) [(key, value)] = x.items() - check_json(value) + _util.check_json(value) if "response" in value: rtn[int(key[1:])] = value["response"] else: rtn[int(key[1:])] = value["data"] - log.debug(rtn) + _util.log.debug(rtn) return rtn From e166b472c58def9250eca41c905a234bb7b97761 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 20:10:19 +0100 Subject: [PATCH 26/29] Move message pull parsing into Message._from_pull --- fbchat/_client.py | 78 ++-------------------------------------------- fbchat/_message.py | 67 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 76 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 8d7cb8c..60a6754 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -8,7 +8,6 @@ from random import choice from bs4 import BeautifulSoup as bs from mimetypes import guess_type from collections import OrderedDict -from . import _file, _message from ._util import * from .models import * from .graphql import * @@ -3042,85 +3041,14 @@ class Client(object): # New message elif delta.get("class") == "NewMessage": - mentions = [] - if delta.get("data") and delta["data"].get("prng"): - try: - mentions = [ - Mention( - str(mention.get("i")), - offset=mention.get("o"), - length=mention.get("l"), - ) - for mention in parse_json(delta["data"]["prng"]) - ] - except Exception: - log.exception("An exception occured while reading attachments") - - sticker = None - attachments = [] - unsent = False - if delta.get("attachments"): - try: - for a in delta["attachments"]: - mercury = a["mercury"] - if mercury.get("blob_attachment"): - image_metadata = a.get("imageMetadata", {}) - attach_type = mercury["blob_attachment"]["__typename"] - attachment = _file.graphql_to_attachment( - mercury["blob_attachment"] - ) - - if attach_type in [ - "MessageFile", - "MessageVideo", - "MessageAudio", - ]: - # TODO: Add more data here for audio files - attachment.size = int(a["fileSize"]) - attachments.append(attachment) - - elif mercury.get("sticker_attachment"): - sticker = Sticker._from_graphql( - mercury["sticker_attachment"] - ) - - elif mercury.get("extensible_attachment"): - attachment = _message.graphql_to_extensible_attachment( - mercury["extensible_attachment"] - ) - if isinstance(attachment, UnsentMessage): - unsent = True - elif attachment: - attachments.append(attachment) - - except Exception: - log.exception( - "An exception occured while reading attachments: {}".format( - delta["attachments"] - ) - ) - - if metadata and metadata.get("tags"): - emoji_size = EmojiSize._from_tags(metadata.get("tags")) - - message = Message( - text=delta.get("body"), - mentions=mentions, - emoji_size=emoji_size, - sticker=sticker, - attachments=attachments, - ) - message.uid = mid - message.author = author_id - message.timestamp = ts - # message.reactions = {} - message.unsent = unsent thread_id, thread_type = getThreadIdAndThreadType(metadata) self.onMessage( mid=mid, author_id=author_id, message=delta.get("body", ""), - message_object=message, + message_object=Message._from_pull( + delta, tags=metadata.get("tags"), author=author_id, timestamp=ts + ), thread_id=thread_id, thread_type=thread_type, ts=ts, diff --git a/fbchat/_message.py b/fbchat/_message.py index 21eb5d3..b5a9423 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import attr import json from string import Formatter -from . import _attachment, _location, _file, _quick_reply, _sticker +from . import _util, _attachment, _location, _file, _quick_reply, _sticker from ._core import Enum @@ -194,6 +194,71 @@ class Message(object): rtn.attachments.append(attachment) return rtn + @classmethod + def _from_pull(cls, data, mid=None, tags=None, author=None, timestamp=None): + rtn = cls(text=data.get("body")) + rtn.uid = mid + rtn.author = author + rtn.timestamp = timestamp + + if data.get("data") and data["data"].get("prng"): + try: + rtn.mentions = [ + Mention( + str(mention.get("i")), + offset=mention.get("o"), + length=mention.get("l"), + ) + for mention in _util.parse_json(data["data"]["prng"]) + ] + except Exception: + _util.log.exception("An exception occured while reading attachments") + + if data.get("attachments"): + try: + for a in data["attachments"]: + mercury = a["mercury"] + if mercury.get("blob_attachment"): + image_metadata = a.get("imageMetadata", {}) + attach_type = mercury["blob_attachment"]["__typename"] + attachment = _file.graphql_to_attachment( + mercury["blob_attachment"] + ) + + if attach_type in [ + "MessageFile", + "MessageVideo", + "MessageAudio", + ]: + # TODO: Add more data here for audio files + attachment.size = int(a["fileSize"]) + rtn.attachments.append(attachment) + + elif mercury.get("sticker_attachment"): + rtn.sticker = _sticker.Sticker._from_graphql( + mercury["sticker_attachment"] + ) + + elif mercury.get("extensible_attachment"): + attachment = graphql_to_extensible_attachment( + mercury["extensible_attachment"] + ) + if isinstance(attachment, _attachment.UnsentMessage): + rtn.unsent = True + elif attachment: + rtn.attachments.append(attachment) + + except Exception: + _util.log.exception( + "An exception occured while reading attachments: {}".format( + data["attachments"] + ) + ) + + rtn.emoji_size = EmojiSize._from_tags(tags) + + return rtn + def graphql_to_extensible_attachment(data): story = data.get("story_attachment") From 71f19dd3c75c604af10068044ecb5f792f652d1c Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 20:22:56 +0100 Subject: [PATCH 27/29] Move fetchAllUsers parsing into User._from_all_fetch --- fbchat/_client.py | 22 +++++----------------- fbchat/_user.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 60a6754..cbd0783 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -694,24 +694,12 @@ class Client(object): raise FBchatException("Missing payload while fetching users: {}".format(j)) users = [] - - for key in j["payload"]: - k = j["payload"][key] - if k["type"] in ["user", "friend"]: - if k["id"] in ["0", 0]: + for data in j["payload"].values(): + if data["type"] in ["user", "friend"]: + if data["id"] in ["0", 0]: # Skip invalid users - pass - users.append( - User( - k["id"], - first_name=k.get("firstName"), - url=k.get("uri"), - photo=k.get("thumbSrc"), - name=k.get("name"), - is_friend=k.get("is_friend"), - gender=GENDERS.get(k.get("gender")), - ) - ) + continue + users.append(User._from_all_fetch(data)) return users diff --git a/fbchat/_user.py b/fbchat/_user.py index 45fb021..17ba014 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -139,6 +139,18 @@ class User(Thread): plan=plan, ) + @classmethod + def _from_all_fetch(cls, data): + return cls( + data["id"], + first_name=data.get("firstName"), + url=data.get("uri"), + photo=data.get("thumbSrc"), + name=data.get("name"), + is_friend=data.get("is_friend"), + gender=_util.GENDERS.get(data.get("gender")), + ) + @attr.s(cmp=False) class ActiveStatus(object): From 8e6ee4636efb67e766cfcdfbb905d766d461e1fd Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 20:25:02 +0100 Subject: [PATCH 28/29] Move gender dict into _user.py --- fbchat/_user.py | 38 ++++++++++++++++++++++++++++++++++---- fbchat/_util.py | 30 ------------------------------ fbchat/utils.py | 1 - 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/fbchat/_user.py b/fbchat/_user.py index 17ba014..b58a946 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -3,10 +3,40 @@ from __future__ import unicode_literals import attr from ._core import Enum -from . import _util, _plan +from . import _plan from ._thread import ThreadType, Thread +GENDERS = { + # For standard requests + 0: "unknown", + 1: "female_singular", + 2: "male_singular", + 3: "female_singular_guess", + 4: "male_singular_guess", + 5: "mixed", + 6: "neuter_singular", + 7: "unknown_singular", + 8: "female_plural", + 9: "male_plural", + 10: "neuter_plural", + 11: "unknown_plural", + # For graphql requests + "UNKNOWN": "unknown", + "FEMALE": "female_singular", + "MALE": "male_singular", + # '': 'female_singular_guess', + # '': 'male_singular_guess', + # '': 'mixed', + "NEUTER": "neuter_singular", + # '': 'unknown_singular', + # '': 'female_plural', + # '': 'male_plural', + # '': 'neuter_plural', + # '': 'unknown_plural', +} + + class TypingStatus(Enum): """Used to specify whether the user is typing or has stopped typing""" @@ -81,7 +111,7 @@ class User(Thread): first_name=data.get("first_name"), last_name=data.get("last_name"), is_friend=data.get("is_viewer_friend"), - gender=_util.GENDERS.get(data.get("gender")), + gender=GENDERS.get(data.get("gender")), affinity=data.get("affinity"), nickname=c_info.get("nickname"), color=c_info.get("color"), @@ -127,7 +157,7 @@ class User(Thread): first_name=first_name, last_name=last_name, is_friend=user.get("is_viewer_friend"), - gender=_util.GENDERS.get(user.get("gender")), + gender=GENDERS.get(user.get("gender")), affinity=user.get("affinity"), nickname=c_info.get("nickname"), color=c_info.get("color"), @@ -148,7 +178,7 @@ class User(Thread): photo=data.get("thumbSrc"), name=data.get("name"), is_friend=data.get("is_friend"), - gender=_util.GENDERS.get(data.get("gender")), + gender=GENDERS.get(data.get("gender")), ) diff --git a/fbchat/_util.py b/fbchat/_util.py index 9bfe09c..6639b3e 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -47,36 +47,6 @@ USER_AGENTS = [ ] -GENDERS = { - # For standard requests - 0: "unknown", - 1: "female_singular", - 2: "male_singular", - 3: "female_singular_guess", - 4: "male_singular_guess", - 5: "mixed", - 6: "neuter_singular", - 7: "unknown_singular", - 8: "female_plural", - 9: "male_plural", - 10: "neuter_plural", - 11: "unknown_plural", - # For graphql requests - "UNKNOWN": "unknown", - "FEMALE": "female_singular", - "MALE": "male_singular", - # '': 'female_singular_guess', - # '': 'male_singular_guess', - # '': 'mixed', - "NEUTER": "neuter_singular", - # '': 'unknown_singular', - # '': 'female_plural', - # '': 'male_plural', - # '': 'neuter_plural', - # '': 'unknown_plural', -} - - class ReqUrl(object): """A class containing all urls used by `fbchat`""" diff --git a/fbchat/utils.py b/fbchat/utils.py index 66c5b9a..dc3385e 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -7,7 +7,6 @@ from ._util import ( log, handler, USER_AGENTS, - GENDERS, ReqUrl, facebookEncoding, now, From 6636d49cc02de15f1d186518df230fbf9b9b0110 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 20:30:22 +0100 Subject: [PATCH 29/29] Remove graphql.py --- fbchat/__init__.py | 3 ++- fbchat/_client.py | 4 ++-- fbchat/client.py | 1 - fbchat/graphql.py | 14 -------------- 4 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 fbchat/graphql.py diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 3bdd86f..e5acc44 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -8,8 +8,9 @@ from __future__ import unicode_literals # These imports are far too general, but they're needed for backwards compatbility. from .utils import * -from .graphql import * from .models import * + +from ._graphql import graphql_queries_to_json, graphql_response_to_json, GraphQL from ._client import Client __title__ = "fbchat" diff --git a/fbchat/_client.py b/fbchat/_client.py index cbd0783..e73e413 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1,6 +1,6 @@ # -*- coding: UTF-8 -*- - from __future__ import unicode_literals + import requests import urllib from uuid import uuid1 @@ -10,7 +10,7 @@ from mimetypes import guess_type from collections import OrderedDict from ._util import * from .models import * -from .graphql import * +from ._graphql import graphql_queries_to_json, graphql_response_to_json, GraphQL import time import json diff --git a/fbchat/client.py b/fbchat/client.py index 846c9db..9244327 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -4,5 +4,4 @@ from __future__ import unicode_literals from .utils import * from .models import * -from .graphql import * from ._client import Client diff --git a/fbchat/graphql.py b/fbchat/graphql.py deleted file mode 100644 index 81ce8eb..0000000 --- a/fbchat/graphql.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: UTF-8 -*- -"""This file is here to maintain backwards compatability.""" -from __future__ import unicode_literals - -from .models import * -from .utils import * -from ._graphql import ( - FLAGS, - WHITESPACE, - ConcatJSONDecoder, - graphql_queries_to_json, - graphql_response_to_json, - GraphQL, -)