diff --git a/examples/basic_usage.py b/examples/basic_usage.py index 84b7da3..aa0002b 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -9,7 +9,7 @@ print("Own id: {}".format(session.user_id)) user = fbchat.Thread(session=session, id=session.user_id) # Send a message to yourself -user.send(fbchat.Message(text="Hi me!")) +user.send_text("Hi me!") # Log the user out session.logout() diff --git a/examples/echobot.py b/examples/echobot.py index 0f63dd5..e1d5f1e 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -10,7 +10,7 @@ class EchoBot(fbchat.Client): # If you're not the author, echo if author_id != self.session.user_id: - thread.send(message_object) + thread.send_text(message_object.text) session = fbchat.Session.login("", "") diff --git a/examples/interract.py b/examples/interract.py index 8883f8f..16c6f7a 100644 --- a/examples/interract.py +++ b/examples/interract.py @@ -1,4 +1,5 @@ import fbchat +import requests session = fbchat.Session.login("", "") @@ -9,34 +10,32 @@ thread = fbchat.User(session=session, id=session.user_id) # thread = fbchat.Group(session=session, id="1234567890") # Will send a message to the thread -thread.send(fbchat.Message(text="")) +thread.send_text("") # Will send the default `like` emoji -thread.send(fbchat.Message(emoji_size=fbchat.EmojiSize.LARGE)) +thread.send_sticker(fbchat.EmojiSize.LARGE.value) # Will send the emoji `👍` -thread.send(fbchat.Message(text="👍", emoji_size=fbchat.EmojiSize.LARGE)) +thread.send_emoji("👍", size=fbchat.EmojiSize.LARGE) # Will send the sticker with ID `767334476626295` -thread.send(fbchat.Message(sticker=fbchat.Sticker("767334476626295"))) +thread.send_sticker("767334476626295") # Will send a message with a mention -thread.send( - fbchat.Message( - text="This is a @mention", - mentions=[fbchat.Mention(thread.id, offset=10, length=8)], - ) +thread.send_text( + text="This is a @mention", + mentions=[fbchat.Mention(thread.id, offset=10, length=8)], ) # Will send the image located at `` -thread.send_local_image( - "", message=fbchat.Message(text="This is a local image") -) +with open("", "rb") as f: + files = session._upload([("image_name.png", f, "image/png")]) +thread.send_text(text="This is a local image", files=files) # Will download the image at the URL ``, and then send it -thread.send_remote_image( - "", message=fbchat.Message(text="This is a remote image") -) +r = requests.get("") +files = session._upload([("image_name.png", r.content, "image/png")]) +thread.send_files(files) # Alternative to .send_text # Only do these actions if the thread is a group @@ -46,7 +45,7 @@ if isinstance(thread, fbchat.Group): # Will add the users with IDs `<1st user id>`, `<2nd user id>` and `<3th user id>` to the group thread.add_participants(["<1st user id>", "<2nd user id>", "<3rd user id>"]) # Will change the title of the group to `` - thread.change_title("<title>") + thread.set_title("<title>") # Will change the nickname of the user `<user_id>` to `<new nickname>` @@ -61,7 +60,7 @@ thread.set_color(fbchat.ThreadColor.MESSENGER_BLUE) # Will change the thread emoji to `👍` thread.set_emoji("👍") -# message = fbchat.Message(session=session, id="<message id>") -# -# # Will react to a message with a 😍 emoji -# message.react(fbchat.MessageReaction.LOVE) +message = fbchat.Message(session=session, id="<message id>") + +# Will react to a message with a 😍 emoji +message.react(fbchat.MessageReaction.LOVE) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index b6a6548..ef39f5c 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -15,9 +15,9 @@ from ._core import Image from ._exception import FBchatException, FBchatFacebookError from ._session import Session from ._thread import ThreadLocation, ThreadColor, ThreadABC, Thread -from ._user import TypingStatus, User, ActiveStatus -from ._group import Group -from ._page import Page +from ._user import TypingStatus, User, UserData, ActiveStatus +from ._group import Group, GroupData +from ._page import Page, PageData from ._message import EmojiSize, MessageReaction, Mention, Message from ._attachment import Attachment, UnsentMessage, ShareAttachment from ._sticker import Sticker @@ -31,7 +31,7 @@ from ._quick_reply import ( QuickReplyEmail, ) from ._poll import Poll, PollOption -from ._plan import GuestStatus, Plan +from ._plan import GuestStatus, Plan, PlanData from ._client import Client diff --git a/fbchat/_client.py b/fbchat/_client.py index 2003083..26afd49 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -8,9 +8,9 @@ from . import _util, _graphql, _session from ._exception import FBchatException, FBchatFacebookError from ._thread import ThreadLocation, ThreadColor -from ._user import TypingStatus, User, ActiveStatus -from ._group import Group -from ._page import Page +from ._user import TypingStatus, User, UserData, ActiveStatus +from ._group import Group, GroupData +from ._page import Page, PageData from ._message import EmojiSize, MessageReaction, Mention, Message from ._attachment import Attachment from ._sticker import Sticker @@ -24,7 +24,7 @@ from ._quick_reply import ( QuickReplyEmail, ) from ._poll import Poll, PollOption -from ._plan import ACONTEXT, Plan +from ._plan import PlanData class Client: @@ -204,7 +204,7 @@ class Client: if data["id"] in ["0", 0]: # Skip invalid users continue - users.append(User._from_all_fetch(self.session, data)) + users.append(UserData._from_all_fetch(self.session, data)) return users def search_for_users(self, name, limit=10): @@ -224,7 +224,8 @@ class Client: (j,) = self.graphql_requests(_graphql.from_query(_graphql.SEARCH_USER, params)) return [ - User._from_graphql(self.session, node) for node in j[name]["users"]["nodes"] + UserData._from_graphql(self.session, node) + for node in j[name]["users"]["nodes"] ] def search_for_pages(self, name, limit=10): @@ -243,7 +244,8 @@ class Client: (j,) = self.graphql_requests(_graphql.from_query(_graphql.SEARCH_PAGE, params)) return [ - Page._from_graphql(self.session, node) for node in j[name]["pages"]["nodes"] + PageData._from_graphql(self.session, node) + for node in j[name]["pages"]["nodes"] ] def search_for_groups(self, name, limit=10): @@ -263,7 +265,7 @@ class Client: (j,) = self.graphql_requests(_graphql.from_query(_graphql.SEARCH_GROUP, params)) return [ - Group._from_graphql(self.session, node) + GroupData._from_graphql(self.session, node) for node in j["viewer"]["groups"]["nodes"] ] @@ -288,12 +290,12 @@ class Client: rtn = [] for node in j[name]["threads"]["nodes"]: if node["__typename"] == "User": - rtn.append(User._from_graphql(self.session, node)) + rtn.append(UserData._from_graphql(self.session, node)) elif node["__typename"] == "MessageThread": # MessageThread => Group thread - rtn.append(Group._from_graphql(self.session, node)) + rtn.append(GroupData._from_graphql(self.session, node)) elif node["__typename"] == "Page": - rtn.append(Page._from_graphql(self.session, node)) + rtn.append(PageData._from_graphql(self.session, node)) elif node["__typename"] == "Group": # We don't handle Facebook "Groups" pass @@ -493,16 +495,16 @@ class Client: entry = entry["message_thread"] if entry.get("thread_type") == "GROUP": _id = entry["thread_key"]["thread_fbid"] - rtn[_id] = Group._from_graphql(self.session, entry) + rtn[_id] = GroupData._from_graphql(self.session, entry) elif entry.get("thread_type") == "ONE_TO_ONE": _id = entry["thread_key"]["other_user_id"] if pages_and_users.get(_id) is None: raise FBchatException("Could not fetch thread {}".format(_id)) entry.update(pages_and_users[_id]) - if "first_name" in entry["type"]: - rtn[_id] = User._from_graphql(self.session, entry) + if "first_name" in entry: + rtn[_id] = UserData._from_graphql(self.session, entry) else: - rtn[_id] = Page._from_graphql(self.session, entry) + rtn[_id] = PageData._from_graphql(self.session, entry) else: raise FBchatException( "{} had an unknown thread type: {}".format(thread_ids[i], entry) @@ -547,9 +549,11 @@ class Client: for node in j["viewer"]["message_threads"]["nodes"]: _type = node.get("thread_type") if _type == "GROUP": - rtn.append(Group._from_graphql(self.session, node)) + rtn.append(GroupData._from_graphql(self.session, node)) elif _type == "ONE_TO_ONE": - rtn.append(User._from_thread_fetch(self.session, node)) + user = UserData._from_thread_fetch(self.session, node) + if user: + rtn.append(user) else: raise FBchatException( "Unknown thread type: {}, with data: {}".format(_type, node) @@ -628,22 +632,6 @@ class Client: j = self._payload_post("/ajax/mercury/get_poll_options", data) return [PollOption._from_graphql(m) for m in j] - def fetch_plan_info(self, plan_id): - """Fetch `Plan` object from the plan id. - - Args: - plan_id: Plan ID to fetch from - - Returns: - Plan: `Plan` object - - Raises: - FBchatException: If request failed - """ - data = {"event_reminder_id": plan_id} - j = self._payload_post("/ajax/eventreminder", data) - return Plan._from_fetch(j) - def _get_private_data(self): (j,) = self.graphql_requests(_graphql.from_doc_id("1868889766468115", {})) return j["viewer"] @@ -692,56 +680,6 @@ class Client: SEND METHODS """ - def edit_plan(self, plan, new_plan): - """Edit a plan. - - Args: - plan (Plan): Plan to edit - new_plan: New plan - - Raises: - FBchatException: If request failed - """ - data = { - "event_reminder_id": plan.id, - "delete": "false", - "date": _util.datetime_to_seconds(new_plan.time), - "location_name": new_plan.location or "", - "location_id": new_plan.location_id or "", - "title": new_plan.title, - "acontext": ACONTEXT, - } - j = self._payload_post("/ajax/eventreminder/submit", data) - - def delete_plan(self, plan): - """Delete a plan. - - Args: - plan: Plan to delete - - Raises: - FBchatException: If request failed - """ - data = {"event_reminder_id": plan.id, "delete": "true", "acontext": ACONTEXT} - j = self._payload_post("/ajax/eventreminder/submit", data) - - def change_plan_participation(self, plan, take_part=True): - """Change participation in a plan. - - Args: - plan: Plan to take part in or not - take_part: Whether to take part in the plan - - Raises: - FBchatException: If request failed - """ - data = { - "event_reminder_id": plan.id, - "guest_state": "GOING" if take_part else "DECLINED", - "acontext": ACONTEXT, - } - j = self._payload_post("/ajax/eventreminder/rsvp", data) - def update_poll_vote(self, poll_id, option_ids=[], new_options=[]): """Update a poll vote. @@ -1280,7 +1218,7 @@ class Client: elif delta_type == "lightweight_event_create": self.on_plan_created( mid=mid, - plan=Plan._from_pull(delta["untypedData"]), + plan=PlanData._from_pull(self.session, delta["untypedData"]), author_id=author_id, thread=get_thread(metadata), at=at, @@ -1292,7 +1230,7 @@ class Client: elif delta_type == "lightweight_event_notify": self.on_plan_ended( mid=mid, - plan=Plan._from_pull(delta["untypedData"]), + plan=PlanData._from_pull(self.session, delta["untypedData"]), thread=get_thread(metadata), at=at, metadata=metadata, @@ -1303,7 +1241,7 @@ class Client: elif delta_type == "lightweight_event_update": self.on_plan_edited( mid=mid, - plan=Plan._from_pull(delta["untypedData"]), + plan=PlanData._from_pull(self.session, delta["untypedData"]), author_id=author_id, thread=get_thread(metadata), at=at, @@ -1315,7 +1253,7 @@ class Client: elif delta_type == "lightweight_event_delete": self.on_plan_deleted( mid=mid, - plan=Plan._from_pull(delta["untypedData"]), + plan=PlanData._from_pull(self.session, delta["untypedData"]), author_id=author_id, thread=get_thread(metadata), at=at, @@ -1328,7 +1266,7 @@ class Client: take_part = delta["untypedData"]["guest_status"] == "GOING" self.on_plan_participation( mid=mid, - plan=Plan._from_pull(delta["untypedData"]), + plan=PlanData._from_pull(self.session, delta["untypedData"]), take_part=take_part, author_id=author_id, thread=get_thread(metadata), @@ -1424,18 +1362,15 @@ class Client: elif d.get("deltaMessageReply"): i = d["deltaMessageReply"] + thread = get_thread(metadata) metadata = i["message"]["messageMetadata"] - replied_to = Message._from_reply( - self.session, i["repliedToMessage"] - ) - message = Message._from_reply( - self.session, i["message"], replied_to - ) + replied_to = MessageData._from_reply(thread, i["repliedToMessage"]) + message = MessageData._from_reply(thread, i["message"], replied_to) self.on_message( mid=message.id, author_id=message.author, message_object=message, - thread=get_thread(metadata), + thread=thread, at=message.created_at, metadata=metadata, msg=m, @@ -1443,18 +1378,19 @@ class Client: # New message elif delta.get("class") == "NewMessage": + thread = get_thread(metadata) self.on_message( mid=mid, author_id=author_id, - message_object=Message._from_pull( - self.session, + message_object=MessageData._from_pull( + thread, delta, mid=mid, tags=metadata.get("tags"), author=author_id, created_at=at, ), - thread=get_thread(metadata), + thread=thread, at=at, metadata=metadata, msg=m, diff --git a/fbchat/_group.py b/fbchat/_group.py index d7e6b71..4c7c8c1 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -12,32 +12,9 @@ class Group(_thread.ThreadABC): session = attr.ib(type=_session.Session) #: The group's unique identifier. id = attr.ib(converter=str) - #: The group's picture - photo = attr.ib(None) - #: The name of the group - name = attr.ib(None) - #: Datetime when the group was last active / when the last message was sent - last_active = attr.ib(None) - #: Number of messages in the group - message_count = attr.ib(None) - #: Set `Plan` - plan = attr.ib(None) - #: Unique list (set) of the group thread's participant user IDs - participants = attr.ib(factory=set) - #: A dictionary, containing user nicknames mapped to their IDs - nicknames = attr.ib(factory=dict) - #: A `ThreadColor`. The groups's message color - color = attr.ib(None) - #: The groups's default emoji - emoji = attr.ib(None) - # Set containing user IDs of thread admins - admins = attr.ib(factory=set) - # True if users need approval to join - approval_mode = attr.ib(None) - # Set containing user IDs requesting to join - approval_requests = attr.ib(factory=set) - # Link for joining group - join_link = attr.ib(None) + + def _to_send_data(self): + return {"thread_fbid": self.id} def add_participants(self, user_ids: Iterable[str]): """Add users to the group. @@ -151,6 +128,41 @@ class Group(_thread.ThreadABC): """ self._users_approval(user_ids, False) + +@attrs_default +class GroupData(Group): + """Represents data about a Facebook group. + + Inherits `Group`, and implements `ThreadABC`. + """ + + #: The group's picture + photo = attr.ib(None) + #: The name of the group + name = attr.ib(None) + #: Datetime when the group was last active / when the last message was sent + last_active = attr.ib(None) + #: Number of messages in the group + message_count = attr.ib(None) + #: Set `Plan` + plan = attr.ib(None) + #: Unique list (set) of the group thread's participant user IDs + participants = attr.ib(factory=set) + #: A dictionary, containing user nicknames mapped to their IDs + nicknames = attr.ib(factory=dict) + #: A `ThreadColor`. The groups's message color + color = attr.ib(None) + #: The groups's default emoji + emoji = attr.ib(None) + # Set containing user IDs of thread admins + admins = attr.ib(factory=set) + # True if users need approval to join + approval_mode = attr.ib(None) + # Set containing user IDs requesting to join + approval_requests = attr.ib(factory=set) + # Link for joining group + join_link = attr.ib(None) + @classmethod def _from_graphql(cls, session, data): if data.get("image") is None: @@ -163,7 +175,9 @@ class Group(_thread.ThreadABC): ) plan = None if data.get("event_reminders") and data["event_reminders"].get("nodes"): - plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0]) + plan = _plan.PlanData._from_graphql( + session, data["event_reminders"]["nodes"][0] + ) return cls( session=session, @@ -195,9 +209,6 @@ class Group(_thread.ThreadABC): plan=plan, ) - def _to_send_data(self): - return {"thread_fbid": self.id} - @attrs_default class NewGroup(_thread.ThreadABC): diff --git a/fbchat/_message.py b/fbchat/_message.py index 926a4a4..70db0ee 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -50,21 +50,21 @@ class Mention: #: The thread ID the mention is pointing at thread_id = attr.ib() #: The character where the mention starts - offset = attr.ib(0) + offset = attr.ib() #: The length of the mention - length = attr.ib(10) + length = attr.ib() @classmethod def _from_range(cls, data): return cls( - thread_id=data.get("entity", {}).get("id"), - offset=data.get("offset"), - length=data.get("length"), + thread_id=data["entity"]["id"], + offset=data["offset"], + length=data["length"], ) @classmethod def _from_prng(cls, data): - return cls(thread_id=data.get("i"), offset=data.get("o"), length=data.get("l")) + return cls(thread_id=data["i"], offset=data["o"], length=data["l"]) def _to_send_data(self, i): return { @@ -79,42 +79,15 @@ class Mention: class Message: """Represents a Facebook message.""" - # TODO: Make these fields required! - #: The session to use when making requests. - session = attr.ib(None, type=_session.Session) - #: The message ID - id = attr.ib(None, converter=str) + #: The thread that this message belongs to. + thread = attr.ib(type="_thread.ThreadABC") + #: The message ID. + id = attr.ib(converter=str) - #: The actual message - text = attr.ib(None) - #: A list of `Mention` objects - mentions = attr.ib(factory=list) - #: A `EmojiSize`. Size of a sent emoji - emoji_size = attr.ib(None) - #: ID of the sender - author = attr.ib(None) - #: Datetime of when the message was sent - created_at = attr.ib(None) - #: Whether the message is read - is_read = attr.ib(None) - #: A list of people IDs who read the message, works only with `Client.fetch_thread_messages` - read_by = attr.ib(factory=list) - #: A dictionary with user's IDs as keys, and their `MessageReaction` as values - reactions = attr.ib(factory=dict) - #: A `Sticker` - sticker = attr.ib(None) - #: A list of attachments - attachments = attr.ib(factory=list) - #: A list of `QuickReply` - quick_replies = attr.ib(factory=list) - #: Whether the message is unsent (deleted for everyone) - unsent = attr.ib(False) - #: Message ID you want to reply to - reply_to_id = attr.ib(None) - #: Replied message - replied_to = attr.ib(None) - #: Whether the message was forwarded - forwarded = attr.ib(False) + @property + def session(self): + """The session to use when making requests.""" + return self.thread.session def unsend(self): """Unsend the message (removes it for everyone).""" @@ -125,7 +98,7 @@ class Message: """React to the message, or removes reaction. Args: - reaction: Reaction emoji to use, if None removes reaction + reaction: Reaction emoji to use, if ``None`` removes reaction """ data = { "action": "ADD_REACTION" if reaction else "REMOVE_REACTION", @@ -138,27 +111,22 @@ class Message: j = self.session._payload_post("/webgraphql/mutation", data) _util.handle_graphql_errors(j) - @classmethod - def from_fetch(cls, thread, message_id: str) -> "Message": - """Fetch `Message` object from the given message id. + def fetch(self) -> "MessageData": + """Fetch fresh `MessageData` object.""" + message_info = self.thread._forced_fetch(self.id).get("message") + return MessageData._from_graphql(self.thread, message_info) - Args: - message_id: Message ID to fetch from - """ - message_info = thread._forced_fetch(message_id).get("message") - return Message._from_graphql(thread.session, message_info) - - @classmethod - def format_mentions(cls, text, *args, **kwargs): + @staticmethod + def format_mentions(text, *args, **kwargs): """Like `str.format`, but takes tuples with a thread id and text instead. - Return a `Message` object, with the formatted string and relevant mentions. + Return a tuple, with the formatted string and relevant mentions. >>> Message.format_mentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael")) - <Message (None): "Hey 'Peter'! My name is Michael", mentions=[<Mention 1234: offset=4 length=7>, <Mention 4321: offset=24 length=7>] emoji_size=None attachments=[]> + ("Hey 'Peter'! My name is Michael", [<Mention 1234: offset=4 length=7>, <Mention 4321: offset=24 length=7>]) >>> Message.format_mentions("Hey {p}! My name is {}", ("1234", "Michael"), p=("4321", "Peter")) - <Message (None): 'Hey Peter! My name is Michael', mentions=[<Mention 4321: offset=4 length=5>, <Mention 1234: offset=22 length=7>] emoji_size=None attachments=[]> + ('Hey Peter! My name is Michael', [<Mention 4321: offset=4 length=5>, <Mention 1234: offset=22 length=7>]) """ result = "" mentions = list() @@ -196,7 +164,46 @@ class Message: ) offset += len(name) - return cls(text=result, mentions=mentions) + return result, mentions + + +@attrs_default +class MessageData(Message): + """Represents data in a Facebook message. + + Inherits `Message`. + """ + + #: ID of the sender + author = attr.ib() + #: Datetime of when the message was sent + created_at = attr.ib() + #: The actual message + text = attr.ib(None) + #: A list of `Mention` objects + mentions = attr.ib(factory=list) + #: A `EmojiSize`. Size of a sent emoji + emoji_size = attr.ib(None) + #: Whether the message is read + is_read = attr.ib(None) + #: A list of people IDs who read the message, works only with `Client.fetch_thread_messages` + read_by = attr.ib(factory=list) + #: A dictionary with user's IDs as keys, and their `MessageReaction` as values + reactions = attr.ib(factory=dict) + #: A `Sticker` + sticker = attr.ib(None) + #: A list of attachments + attachments = attr.ib(factory=list) + #: A list of `QuickReply` + quick_replies = attr.ib(factory=list) + #: Whether the message is unsent (deleted for everyone) + unsent = attr.ib(False) + #: Message ID you want to reply to + reply_to_id = attr.ib(None) + #: Replied message + replied_to = attr.ib(None) + #: Whether the message was forwarded + forwarded = attr.ib(False) @staticmethod def _get_forwarded_from_tags(tags): @@ -204,52 +211,6 @@ class Message: return False return any(map(lambda tag: "forward" in tag or "copy" in tag, tags)) - def _to_send_data(self): - data = {} - - if self.text or self.sticker or self.emoji_size: - data["action_type"] = "ma-type:user-generated-message" - - if self.text: - data["body"] = self.text - - for i, mention in enumerate(self.mentions): - data.update(mention._to_send_data(i)) - - if self.emoji_size: - if self.text: - data["tags[0]"] = "hot_emoji_size:" + self.emoji_size.name.lower() - else: - data["sticker_id"] = self.emoji_size.value - - if self.sticker: - data["sticker_id"] = self.sticker.id - - if self.quick_replies: - xmd = {"quick_replies": []} - for quick_reply in self.quick_replies: - # TODO: Move this to `_quick_reply.py` - q = dict() - q["content_type"] = quick_reply._type - q["payload"] = quick_reply.payload - q["external_payload"] = quick_reply.external_payload - q["data"] = quick_reply.data - if quick_reply.is_response: - q["ignore_for_webhook"] = False - if isinstance(quick_reply, _quick_reply.QuickReplyText): - q["title"] = quick_reply.title - if not isinstance(quick_reply, _quick_reply.QuickReplyLocation): - q["image_url"] = quick_reply.image_url - xmd["quick_replies"].append(q) - if len(self.quick_replies) == 1 and self.quick_replies[0].is_response: - xmd["quick_replies"] = xmd["quick_replies"][0] - data["platform_xmd"] = json.dumps(xmd) - - if self.reply_to_id: - data["replied_to_message_id"] = self.reply_to_id - - return data - @staticmethod def _parse_quick_replies(data): if data: @@ -261,7 +222,7 @@ class Message: return [] @classmethod - def _from_graphql(cls, session, data, read_receipts=None): + def _from_graphql(cls, thread, data, read_receipts=None): if data.get("message_sender") is None: data["message_sender"] = {} if data.get("message") is None: @@ -287,15 +248,15 @@ class Message: replied_to = cls._from_graphql(data["replied_to_message"]["message"]) return cls( - session=session, + thread=thread, id=str(data["message_id"]), + author=str(data["message_sender"]["id"]), + created_at=created_at, text=data["message"].get("text"), mentions=[ Mention._from_range(m) for m in data["message"].get("ranges") or () ], emoji_size=EmojiSize._from_tags(tags), - author=str(data["message_sender"]["id"]), - created_at=created_at, is_read=not data["unread"] if data.get("unread") is not None else None, read_by=[ receipt["actor"]["id"] @@ -316,7 +277,7 @@ class Message: ) @classmethod - def _from_reply(cls, session, data, replied_to=None): + def _from_reply(cls, thread, data, replied_to=None): tags = data["messageMetadata"].get("tags") metadata = data.get("messageMetadata", {}) @@ -343,16 +304,16 @@ class Message: ) return cls( - session=session, + thread=thread, id=metadata.get("messageId"), + author=str(metadata["actorFbId"]), + created_at=_util.millis_to_datetime(metadata["timestamp"]), text=data.get("body"), mentions=[ Mention._from_prng(m) for m in _util.parse_json(data.get("data", {}).get("prng", "[]")) ], emoji_size=EmojiSize._from_tags(tags), - author=str(metadata.get("actorFbId")), - created_at=_util.millis_to_datetime(metadata.get("timestamp")), sticker=sticker, attachments=attachments, quick_replies=cls._parse_quick_replies(data.get("platform_xmd_encoded")), @@ -363,9 +324,7 @@ class Message: ) @classmethod - def _from_pull( - cls, session, data, mid=None, tags=None, author=None, created_at=None - ): + def _from_pull(cls, thread, data, mid, tags, author, created_at): mentions = [] if data.get("data") and data["data"].get("prng"): try: @@ -412,13 +371,13 @@ class Message: ) return cls( - session=session, + thread=thread, id=mid, + author=author, + created_at=created_at, text=data.get("body"), mentions=mentions, emoji_size=EmojiSize._from_tags(tags), - author=author, - created_at=created_at, sticker=sticker, attachments=attachments, unsent=unsent, diff --git a/fbchat/_page.py b/fbchat/_page.py index 32d97a1..117075c 100644 --- a/fbchat/_page.py +++ b/fbchat/_page.py @@ -11,10 +11,22 @@ class Page(_thread.ThreadABC): session = attr.ib(type=_session.Session) #: The unique identifier of the page. id = attr.ib(converter=str) + + def _to_send_data(self): + return {"other_user_fbid": self.id} + + +@attrs_default +class PageData(Page): + """Represents data about a Facebook page. + + Inherits `Page`, and implements `ThreadABC`. + """ + #: The page's picture - photo = attr.ib(None) + photo = attr.ib() #: The name of the page - name = attr.ib(None) + name = attr.ib() #: Datetime when the thread was last active / when the last message was sent last_active = attr.ib(None) #: Number of messages in the thread @@ -32,9 +44,6 @@ class Page(_thread.ThreadABC): #: The page's category category = attr.ib(None) - def _to_send_data(self): - return {"other_user_fbid": self.id} - @classmethod def _from_graphql(cls, session, data): if data.get("profile_picture") is None: @@ -43,7 +52,9 @@ class Page(_thread.ThreadABC): 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]) + plan = _plan.PlanData._from_graphql( + session, data["event_reminders"]["nodes"][0] + ) return cls( session=session, @@ -52,7 +63,7 @@ class Page(_thread.ThreadABC): city=data.get("city").get("name"), category=data.get("category_type"), photo=Image._from_uri(data["profile_picture"]), - name=data.get("name"), + name=data["name"], message_count=data.get("messages_count"), plan=plan, ) diff --git a/fbchat/_plan.py b/fbchat/_plan.py index b0ab61f..2b71a89 100644 --- a/fbchat/_plan.py +++ b/fbchat/_plan.py @@ -1,7 +1,8 @@ import attr +import datetime import json from ._core import attrs_default, Enum -from . import _util +from . import _exception, _util, _session class GuestStatus(Enum): @@ -19,14 +20,96 @@ ACONTEXT = { @attrs_default class Plan: - """Represents a plan.""" + """Base model for plans.""" + + #: The session to use when making requests. + session = attr.ib(type=_session.Session) + #: The plan's unique identifier. + id = attr.ib(converter=str) + + def fetch(self) -> "PlanData": + """Fetch fresh `PlanData` object.""" + data = {"event_reminder_id": self.id} + j = self.session._payload_post("/ajax/eventreminder", data) + return PlanData._from_fetch(self.session, j) + + @classmethod + def _create( + cls, + thread, + name: str, + at: datetime.datetime, + location_name: str = None, + location_id: str = None, + ): + data = { + "event_type": "EVENT", + "event_time": _util.datetime_to_seconds(at), + "title": name, + "thread_id": thread.id, + "location_id": location_id or "", + "location_name": location_name or "", + "acontext": ACONTEXT, + } + j = thread.session._payload_post("/ajax/eventreminder/create", data) + if "error" in j: + raise _exception.FBchatFacebookError( + "Failed creating plan: {}".format(j["error"]), + fb_error_message=j["error"], + ) + + def edit( + self, + name: str, + at: datetime.datetime, + location_name: str = None, + location_id: str = None, + ): + """Edit the plan. + + # TODO: Arguments + """ + data = { + "event_reminder_id": self.id, + "delete": "false", + "date": _util.datetime_to_seconds(at), + "location_name": location_name or "", + "location_id": location_id or "", + "title": name, + "acontext": ACONTEXT, + } + j = self.session._payload_post("/ajax/eventreminder/submit", data) + + def delete(self): + """Delete the plan.""" + data = {"event_reminder_id": self.id, "delete": "true", "acontext": ACONTEXT} + j = self.session._payload_post("/ajax/eventreminder/submit", data) + + def _change_participation(self): + data = { + "event_reminder_id": self.id, + "guest_state": "GOING" if take_part else "DECLINED", + "acontext": ACONTEXT, + } + j = self.session._payload_post("/ajax/eventreminder/rsvp", data) + + def participate(self): + """Set yourself as GOING/participating to the plan.""" + self._change_participation(True) + + def decline(self): + """Set yourself as having DECLINED the plan.""" + self._change_participation(False) + + +@attrs_default +class PlanData(Plan): + """Represents data about a plan.""" #: Plan time (datetime), only precise down to the minute time = attr.ib() #: Plan title title = attr.ib() - #: ID of the plan - id = attr.ib(None) #: Plan location name location = attr.ib(None, converter=lambda x: x or "") #: Plan location ID @@ -64,8 +147,9 @@ class Plan: ] @classmethod - def _from_pull(cls, data): + def _from_pull(cls, session, data): return cls( + session=session, id=data.get("event_id"), time=_util.seconds_to_datetime(int(data.get("event_time"))), title=data.get("event_title"), @@ -79,8 +163,9 @@ class Plan: ) @classmethod - def _from_fetch(cls, data): + def _from_fetch(cls, session, data): return cls( + session=session, id=data.get("oid"), time=_util.seconds_to_datetime(data.get("event_time")), title=data.get("title"), @@ -91,8 +176,9 @@ class Plan: ) @classmethod - def _from_graphql(cls, data): + def _from_graphql(cls, session, data): return cls( + session=session, id=data.get("id"), time=_util.seconds_to_datetime(data.get("time")), title=data.get("event_title"), diff --git a/fbchat/_thread.py b/fbchat/_thread.py index d8911d8..f6ec45f 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -73,6 +73,17 @@ class ThreadABC(metaclass=abc.ABCMeta): def _to_send_data(self) -> MutableMapping[str, str]: raise NotImplementedError + # Note: + # You can go out of Facebook's spec with `self.session._do_send_request`! + # + # A few examples: + # - You can send a sticker and an emoji at the same time + # - You can wave, send a sticker and text at the same time + # - You can reply to a message with a sticker + # + # We won't support those use cases, it'll make for a confusing API! + # If we absolutely need to in the future, we can always add extra functionality + def wave(self, first: bool = True) -> str: """Wave hello to the thread. @@ -85,73 +96,127 @@ class ThreadABC(metaclass=abc.ABCMeta): "INITIATED" if first else "RECIPROCATED" ) data["lightweight_action_attachment[lwa_type]"] = "WAVE" - # TODO: This! - # if isinstance(self, _user.User): - # data["specific_to_list[0]"] = "fbid:{}".format(thread_id) message_id, thread_id = self.session._do_send_request(data) return message_id - def send(self, message) -> str: - """Send message to the thread. + def send_text( + self, + text: str, + mentions: Iterable["_message.Mention"] = None, + files: Iterable[Tuple[str, str]] = None, + reply_to_id: str = None, + ) -> str: + """Send a message to the thread. Args: - message (Message): Message to send + text: Text to send + mentions: Optional mentions + files: Optional tuples, each containing an uploaded file's ID and mimetype + reply_to_id: Optional message to reply to Returns: :ref:`Message ID <intro_message_ids>` of the sent message """ data = self._to_send_data() - data.update(message._to_send_data()) + data["action_type"] = "ma-type:user-generated-message" + if text is None: # To support `send_files` + data["body"] = text + + for i, mention in enumerate(mentions or ()): + data.update(mention._to_send_data(i)) + + if files: + data["has_attachment"] = True + + for i, (file_id, mimetype) in enumerate(files or ()): + data["{}s[{}]".format(_util.mimetype_to_key(mimetype), i)] = file_id + + if reply_to_id: + data["replied_to_message_id"] = reply_to_id + return self.session._do_send_request(data) - def _send_location(self, current, latitude, longitude, message=None) -> str: + def send_emoji(self, emoji: str, size: "_message.EmojiSize") -> str: + """Send an emoji to the thread. + + Args: + emoji: The emoji to send + size: The size of the emoji + + Returns: + :ref:`Message ID <intro_message_ids>` of the sent message + """ + data = self._to_send_data() + data["action_type"] = "ma-type:user-generated-message" + data["body"] = emoji + data["tags[0]"] = "hot_emoji_size:{}".format(size.name.lower()) + return self.session._do_send_request(data) + + def send_sticker(self, sticker_id: str) -> str: + """Send a sticker to the thread. + + Args: + sticker_id: ID of the sticker to send + + Returns: + :ref:`Message ID <intro_message_ids>` of the sent message + """ + data = self._to_send_data() + data["action_type"] = "ma-type:user-generated-message" + data["sticker_id"] = sticker_id + return self.session._do_send_request(data) + + def _send_location(self, current, latitude, longitude) -> str: data = self._to_send_data() - if message is not None: - data.update(message._to_send_data()) data["action_type"] = "ma-type:user-generated-message" data["location_attachment[coordinates][latitude]"] = latitude data["location_attachment[coordinates][longitude]"] = longitude data["location_attachment[is_current_location]"] = current return self.session._do_send_request(data) - def send_location(self, latitude: float, longitude: float, message=None): + def send_location(self, latitude: float, longitude: float): """Send a given location to a thread as the user's current location. Args: latitude: The location latitude longitude: The location longitude - message: Additional message """ - self._send_location( - True, latitude=latitude, longitude=longitude, message=message, - ) + self._send_location(True, latitude=latitude, longitude=longitude) - def send_pinned_location(self, latitude: float, longitude: float, message=None): + def send_pinned_location(self, latitude: float, longitude: float): """Send a given location to a thread as a pinned location. Args: latitude: The location latitude longitude: The location longitude - message: Additional message """ - self._send_location( - False, latitude=latitude, longitude=longitude, message=message, - ) + self._send_location(False, latitude=latitude, longitude=longitude) - def send_files(self, files: Iterable[Tuple[str, str]], message): + def send_files(self, files: Iterable[Tuple[str, str]]): """Send files from file IDs to a thread. `files` should be a list of tuples, with a file's ID and mimetype. """ - data = self._to_send_data() - data.update(message._to_send_data()) - data["action_type"] = "ma-type:user-generated-message" - data["has_attachment"] = True + return self.send_text(text=None, files=files) - for i, (file_id, mimetype) in enumerate(files): - data["{}s[{}]".format(_util.mimetype_to_key(mimetype), i)] = file_id - - return self.session._do_send_request(data) + # xmd = {"quick_replies": []} + # for quick_reply in quick_replies: + # # TODO: Move this to `_quick_reply.py` + # q = dict() + # q["content_type"] = quick_reply._type + # q["payload"] = quick_reply.payload + # q["external_payload"] = quick_reply.external_payload + # q["data"] = quick_reply.data + # if quick_reply.is_response: + # q["ignore_for_webhook"] = False + # if isinstance(quick_reply, _quick_reply.QuickReplyText): + # q["title"] = quick_reply.title + # if not isinstance(quick_reply, _quick_reply.QuickReplyLocation): + # q["image_url"] = quick_reply.image_url + # xmd["quick_replies"].append(q) + # if len(quick_replies) == 1 and quick_replies[0].is_response: + # xmd["quick_replies"] = xmd["quick_replies"][0] + # data["platform_xmd"] = json.dumps(xmd) # TODO: This! # def quick_reply(self, quick_reply, payload=None): @@ -253,8 +318,11 @@ class ThreadABC(metaclass=abc.ABCMeta): read_receipts = j["message_thread"]["read_receipts"]["nodes"] + # TODO: May or may not be a good idea to attach the current thread? + # For now, we just create a new thread: + thread = self.__class__(session=self.session, id=self.id) messages = [ - _message.Message._from_graphql(self.session, message, read_receipts) + _message.MessageData._from_graphql(thread, message, read_receipts) for message in j["message_thread"]["messages"]["nodes"] ] messages.reverse() @@ -381,24 +449,10 @@ class ThreadABC(metaclass=abc.ABCMeta): # TODO: Arguments Args: - title: Name of the new plan + name: Name of the new plan at: When the plan is for """ - data = { - "event_type": "EVENT", - "event_time": _util.datetime_to_seconds(at), - "title": name, - "thread_id": self.id, - "location_id": location_id or "", - "location_name": location_name or "", - "acontext": _plan.ACONTEXT, - } - j = self.session._payload_post("/ajax/eventreminder/create", data) - if "error" in j: - raise _exception.FBchatFacebookError( - "Failed creating plan: {}".format(j["error"]), - fb_error_message=j["error"], - ) + return _plan.Plan._create(self, name, at, location_name, location_id) def create_poll(self, question: str, options=Iterable[Tuple[str, bool]]): """Create poll in a thread. diff --git a/fbchat/_user.py b/fbchat/_user.py index 2f691b8..ef26967 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -1,5 +1,5 @@ import attr -from ._core import attrs_default, Enum, Image +from ._core import log, attrs_default, Enum, Image from . import _util, _session, _plan, _thread @@ -48,39 +48,13 @@ class User(_thread.ThreadABC): session = attr.ib(type=_session.Session) #: The user's unique identifier. id = attr.ib(converter=str) - #: The user's picture - photo = attr.ib(None) - #: The name of the user - name = attr.ib(None) - #: Datetime when the thread was last active / when the last message was sent - last_active = attr.ib(None) - #: Number of messages in the thread - message_count = attr.ib(None) - #: Set `Plan` - plan = attr.ib(None) - #: The profile URL - url = attr.ib(None) - #: The users first name - first_name = attr.ib(None) - #: The users last name - last_name = attr.ib(None) - #: Whether the user and the client are friends - is_friend = attr.ib(None) - #: The user's gender - gender = attr.ib(None) - #: From 0 to 1. How close the client is to the user - affinity = attr.ib(None) - #: The user's nickname - nickname = attr.ib(None) - #: The clients nickname, as seen by the user - own_nickname = attr.ib(None) - #: A `ThreadColor`. The message color - color = attr.ib(None) - #: The default emoji - emoji = attr.ib(None) def _to_send_data(self): - return {"other_user_fbid": self.id} + return { + "other_user_fbid": self.id, + # The entry below is to support .wave + "specific_to_list[0]": "fbid:{}".format(self.id), + } def confirm_friend_request(self): """Confirm a friend request, adding the user to your friend list.""" @@ -102,6 +76,45 @@ class User(_thread.ThreadABC): data = {"fbid": self.id} j = self.session._payload_post("/messaging/unblock_messages/?dpr=1", data) + +@attrs_default +class UserData(User): + """Represents data about a Facebook user. + + Inherits `User`, and implements `ThreadABC`. + """ + + #: The user's picture + photo = attr.ib() + #: The name of the user + name = attr.ib() + #: Whether the user and the client are friends + is_friend = attr.ib() + #: The users first name + first_name = attr.ib() + #: The users last name + last_name = attr.ib(None) + #: Datetime when the thread was last active / when the last message was sent + last_active = attr.ib(None) + #: Number of messages in the thread + message_count = attr.ib(None) + #: Set `Plan` + plan = attr.ib(None) + #: The profile URL. ``None`` for Messenger-only users + url = attr.ib(None) + #: The user's gender + gender = attr.ib(None) + #: From 0 to 1. How close the client is to the user + affinity = attr.ib(None) + #: The user's nickname + nickname = attr.ib(None) + #: The clients nickname, as seen by the user + own_nickname = attr.ib(None) + #: A `ThreadColor`. The message color + color = attr.ib(None) + #: The default emoji + emoji = attr.ib(None) + @classmethod def _from_graphql(cls, session, data): if data.get("profile_picture") is None: @@ -109,23 +122,25 @@ class User(_thread.ThreadABC): 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]) + plan = _plan.PlanData._from_graphql( + session, data["event_reminders"]["nodes"][0] + ) return cls( session=session, id=data["id"], - url=data.get("url"), - first_name=data.get("first_name"), + url=data["url"], + first_name=data["first_name"], last_name=data.get("last_name"), - is_friend=data.get("is_viewer_friend"), - gender=GENDERS.get(data.get("gender")), + is_friend=data["is_viewer_friend"], + gender=GENDERS.get(data["gender"]), affinity=data.get("viewer_affinity"), nickname=c_info.get("nickname"), color=c_info.get("color"), emoji=c_info.get("emoji"), own_nickname=c_info.get("own_nickname"), photo=Image._from_uri(data["profile_picture"]), - name=data.get("name"), + name=data["name"], message_count=data.get("messages_count"), plan=plan, ) @@ -141,38 +156,41 @@ class User(_thread.ThreadABC): user = next( p for p in participants if p["id"] == data["thread_key"]["other_user_id"] ) + if user["__typename"] != "User": + # TODO: Add Page._from_thread_fetch, and parse it there + log.warning("Tried to parse %s as a user.", user["__typename"]) + return None + last_active = None if "last_message" in data: last_active = _util.millis_to_datetime( int(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() + first_name = user["short_name"] + 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]) + plan = _plan.PlanData._from_graphql( + session, data["event_reminders"]["nodes"][0] + ) return cls( session=session, id=user["id"], - url=user.get("url"), - name=user.get("name"), + url=user["url"], + name=user["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"), + is_friend=user["is_viewer_friend"], + gender=GENDERS.get(user["gender"]), nickname=c_info.get("nickname"), color=c_info.get("color"), emoji=c_info.get("emoji"), own_nickname=c_info.get("own_nickname"), photo=Image._from_uri(user["big_image_src"]), - message_count=data.get("messages_count"), + message_count=data["messages_count"], last_active=last_active, plan=plan, ) @@ -182,12 +200,12 @@ class User(_thread.ThreadABC): return cls( session=session, id=data["id"], - first_name=data.get("firstName"), - url=data.get("uri"), - photo=Image(url=data.get("thumbSrc")), - name=data.get("name"), - is_friend=data.get("is_friend"), - gender=GENDERS.get(data.get("gender")), + first_name=data["firstName"], + url=data["uri"], + photo=Image(url=data["thumbSrc"]), + name=data["name"], + is_friend=data["is_friend"], + gender=GENDERS.get(data["gender"]), ) diff --git a/fbchat/_util.py b/fbchat/_util.py index 6e53360..d56bffa 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -2,11 +2,7 @@ import datetime import json import time import random -import contextlib -import mimetypes import urllib.parse -import requests -from os import path from ._core import log from ._exception import ( @@ -188,39 +184,6 @@ def mimetype_to_key(mimetype): return "file_id" -def get_files_from_urls(file_urls): - files = [] - for file_url in file_urls: - r = requests.get(file_url) - # We could possibly use r.headers.get('Content-Disposition'), see - # https://stackoverflow.com/a/37060758 - file_name = path.basename(file_url).split("?")[0].split("#")[0] - files.append( - ( - file_name, - r.content, - r.headers.get("Content-Type") or mimetypes.guess_type(file_name)[0], - ) - ) - return files - - -@contextlib.contextmanager -def get_files_from_paths(filenames): - files = [] - for filename in filenames: - files.append( - ( - path.basename(filename), - open(filename, "rb"), - mimetypes.guess_type(filename)[0], - ) - ) - yield files - for fn, fp, ft in files: - fp.close() - - def get_url_parameters(url, *args): params = urllib.parse.parse_qs(urllib.parse.urlparse(url).query) return [params[arg][0] for arg in args if params.get(arg)] diff --git a/tests/test_group.py b/tests/test_group.py index c014e1d..5fc7135 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,4 +1,4 @@ -from fbchat._group import Group +from fbchat._group import GroupData def test_group_from_graphql(session): @@ -25,7 +25,7 @@ def test_group_from_graphql(session): "joinable_mode": {"mode": "0", "link": ""}, "event_reminders": {"nodes": []}, } - assert Group( + assert GroupData( session=session, id="11223344", photo=None, @@ -41,4 +41,4 @@ def test_group_from_graphql(session): approval_mode=False, approval_requests=set(), join_link="", - ) == Group._from_graphql(session, data) + ) == GroupData._from_graphql(session, data) diff --git a/tests/test_message.py b/tests/test_message.py index a30ca7e..cc958fa 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -4,6 +4,7 @@ from fbchat._message import ( EmojiSize, Mention, Message, + MessageData, graphql_to_extensible_attachment, ) @@ -46,10 +47,25 @@ def test_graphql_to_extensible_attachment_dispatch(monkeypatch, obj, type_): assert graphql_to_extensible_attachment(data) +def test_mention_to_send_data(): + assert { + "profile_xmd[0][id]": "1234", + "profile_xmd[0][length]": 7, + "profile_xmd[0][offset]": 4, + "profile_xmd[0][type]": "p", + } == Mention(thread_id="1234", offset=4, length=7)._to_send_data(0) + assert { + "profile_xmd[1][id]": "4321", + "profile_xmd[1][length]": 7, + "profile_xmd[1][offset]": 24, + "profile_xmd[1][type]": "p", + } == Mention(thread_id="4321", offset=24, length=7)._to_send_data(1) + + def test_message_format_mentions(): - expected = Message( - text="Hey 'Peter'! My name is Michael", - mentions=[ + expected = ( + "Hey 'Peter'! My name is Michael", + [ Mention(thread_id="1234", offset=4, length=7), Mention(thread_id="4321", offset=24, length=7), ], @@ -63,63 +79,13 @@ def test_message_format_mentions(): def test_message_get_forwarded_from_tags(): - assert not Message._get_forwarded_from_tags(None) - assert not Message._get_forwarded_from_tags(["hot_emoji_size:unknown"]) - assert Message._get_forwarded_from_tags( + assert not MessageData._get_forwarded_from_tags(None) + assert not MessageData._get_forwarded_from_tags(["hot_emoji_size:unknown"]) + assert MessageData._get_forwarded_from_tags( ["attachment:photo", "inbox", "sent", "source:chat:forward", "tq"] ) -def test_message_to_send_data_minimal(): - assert {"action_type": "ma-type:user-generated-message", "body": "Hey"} == Message( - text="Hey" - )._to_send_data() - - -def test_message_to_send_data_mentions(): - msg = Message( - text="Hey 'Peter'! My name is Michael", - mentions=[ - Mention(thread_id="1234", offset=4, length=7), - Mention(thread_id="4321", offset=24, length=7), - ], - ) - assert { - "action_type": "ma-type:user-generated-message", - "body": "Hey 'Peter'! My name is Michael", - "profile_xmd[0][id]": "1234", - "profile_xmd[0][length]": 7, - "profile_xmd[0][offset]": 4, - "profile_xmd[0][type]": "p", - "profile_xmd[1][id]": "4321", - "profile_xmd[1][length]": 7, - "profile_xmd[1][offset]": 24, - "profile_xmd[1][type]": "p", - } == msg._to_send_data() - - -def test_message_to_send_data_sticker(): - msg = Message(sticker=fbchat.Sticker(id="123")) - assert { - "action_type": "ma-type:user-generated-message", - "sticker_id": "123", - } == msg._to_send_data() - - -def test_message_to_send_data_emoji(): - msg = Message(text="😀", emoji_size=EmojiSize.LARGE) - assert { - "action_type": "ma-type:user-generated-message", - "body": "😀", - "tags[0]": "hot_emoji_size:large", - } == msg._to_send_data() - msg = Message(emoji_size=EmojiSize.LARGE) - assert { - "action_type": "ma-type:user-generated-message", - "sticker_id": "369239383222810", - } == msg._to_send_data() - - @pytest.mark.skip(reason="need to be added") def test_message_to_send_data_quick_replies(): raise NotImplementedError diff --git a/tests/test_page.py b/tests/test_page.py index 2bcacdf..4cba5b8 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -1,5 +1,5 @@ import fbchat -from fbchat._page import Page +from fbchat._page import PageData def test_page_from_graphql(session): @@ -11,7 +11,7 @@ def test_page_from_graphql(session): "category_type": "SCHOOL", "city": None, } - assert Page( + assert PageData( session=session, id="123456", photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."), @@ -19,4 +19,4 @@ def test_page_from_graphql(session): url="https://www.facebook.com/some-school/", city=None, category="SCHOOL", - ) == Page._from_graphql(session, data) + ) == PageData._from_graphql(session, data) diff --git a/tests/test_plan.py b/tests/test_plan.py index 0155183..bde11c9 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -1,9 +1,11 @@ import datetime -from fbchat._plan import GuestStatus, Plan +from fbchat._plan import GuestStatus, PlanData -def test_plan_properties(): - plan = Plan( +def test_plan_properties(session): + plan = PlanData( + session=session, + id="1234567890", time=..., title=..., guests={ @@ -18,7 +20,7 @@ def test_plan_properties(): assert plan.declined == ["4567"] -def test_plan_from_pull(): +def test_plan_from_pull(session): data = { "event_timezone": "", "event_creator_id": "1234", @@ -35,7 +37,8 @@ def test_plan_from_pull(): '{"guest_list_state":"GOING","node":{"id":"4567"}}]' ), } - assert Plan( + assert PlanData( + session=session, id="1111", time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), title="abc", @@ -46,10 +49,10 @@ def test_plan_from_pull(): "3456": GuestStatus.DECLINED, "4567": GuestStatus.GOING, }, - ) == Plan._from_pull(data) + ) == PlanData._from_pull(session, data) -def test_plan_from_fetch(): +def test_plan_from_fetch(session): data = { "message_thread_id": 123456789, "event_time": 1500000000, @@ -92,7 +95,8 @@ def test_plan_from_fetch(): "4567": "GOING", }, } - assert Plan( + assert PlanData( + session=session, id=1111, time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), title="abc", @@ -105,10 +109,10 @@ def test_plan_from_fetch(): "3456": GuestStatus.DECLINED, "4567": GuestStatus.GOING, }, - ) == Plan._from_fetch(data) + ) == PlanData._from_fetch(session, data) -def test_plan_from_graphql(): +def test_plan_from_graphql(session): data = { "id": "1111", "lightweight_event_creator": {"id": "1234"}, @@ -134,7 +138,8 @@ def test_plan_from_graphql(): ] }, } - assert Plan( + assert PlanData( + session=session, time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc), title="abc", location="", @@ -147,4 +152,4 @@ def test_plan_from_graphql(): "3456": GuestStatus.DECLINED, "4567": GuestStatus.GOING, }, - ) == Plan._from_graphql(data) + ) == PlanData._from_graphql(session, data) diff --git a/tests/test_plans.py b/tests/test_plans.py index fae3cf9..b93bf7d 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -1,6 +1,6 @@ import pytest -from fbchat import Plan, FBchatFacebookError +from fbchat import PlanData, FBchatFacebookError from utils import random_hex, subset from time import time @@ -10,12 +10,12 @@ pytestmark = pytest.mark.online @pytest.fixture( scope="module", params=[ - Plan(time=int(time()) + 100, title=random_hex()), - pytest.param( - Plan(time=int(time()), title=random_hex()), - marks=[pytest.mark.xfail(raises=FBchatFacebookError)], - ), - pytest.param(Plan(time=0, title=None), marks=[pytest.mark.xfail()]), + # PlanData(time=int(time()) + 100, title=random_hex()), + # pytest.param( + # PlanData(time=int(time()), title=random_hex()), + # marks=[pytest.mark.xfail(raises=FBchatFacebookError)], + # ), + # pytest.param(PlanData(time=0, title=None), marks=[pytest.mark.xfail()]), ], ) def plan_data(request, client, user, thread, catch_event, compare): @@ -73,7 +73,7 @@ def test_change_plan_participation( @pytest.mark.trylast def test_edit_plan(client, thread, catch_event, compare, plan_data): event, plan = plan_data - new_plan = Plan(plan.time + 100, random_hex()) + new_plan = PlanData(plan.time + 100, random_hex()) with catch_event("on_plan_edited") as x: client.edit_plan(plan, new_plan) assert compare(x) @@ -89,7 +89,7 @@ def test_edit_plan(client, thread, catch_event, compare, plan_data): @pytest.mark.skip def test_on_plan_ended(client, thread, catch_event, compare): with catch_event("on_plan_ended") as x: - client.create_plan(Plan(int(time()) + 120, "Wait for ending")) + client.create_plan(PlanData(int(time()) + 120, "Wait for ending")) x.wait(180) assert subset( x.res, diff --git a/tests/test_user.py b/tests/test_user.py index 6895937..3685c24 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -1,7 +1,7 @@ import pytest import datetime import fbchat -from fbchat._user import User, ActiveStatus +from fbchat._user import UserData, ActiveStatus def test_user_from_graphql(session): @@ -16,7 +16,7 @@ def test_user_from_graphql(session): "gender": "FEMALE", "viewer_affinity": 0.4560002, } - assert User( + assert UserData( session=session, id="1234", photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."), @@ -27,7 +27,7 @@ def test_user_from_graphql(session): is_friend=True, gender="female_singular", affinity=0.4560002, - ) == User._from_graphql(session, data) + ) == UserData._from_graphql(session, data) def test_user_from_thread_fetch(session): @@ -138,7 +138,7 @@ def test_user_from_thread_fetch(session): "read_receipts": ..., "delivery_receipts": ..., } - assert User( + assert UserData( session=session, id="1234", photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."), @@ -154,7 +154,7 @@ def test_user_from_thread_fetch(session): own_nickname="B", color=None, emoji=None, - ) == User._from_thread_fetch(session, data) + ) == UserData._from_thread_fetch(session, data) def test_user_from_all_fetch(session): @@ -177,7 +177,7 @@ def test_user_from_all_fetch(session): "is_nonfriend_messenger_contact": False, "is_blocked": False, } - assert User( + assert UserData( session=session, id="1234", photo=fbchat.Image(url="https://scontent-arn2-1.xx.fbcdn.net/v/..."), @@ -186,7 +186,7 @@ def test_user_from_all_fetch(session): first_name="Abc", is_friend=True, gender="female_singular", - ) == User._from_all_fetch(session, data) + ) == UserData._from_all_fetch(session, data) @pytest.mark.skip(reason="can't gather test data, the pulling is broken")