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..42d55b9 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>` diff --git a/fbchat/_message.py b/fbchat/_message.py index 926a4a4..bedbf2a 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -204,52 +204,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: diff --git a/fbchat/_thread.py b/fbchat/_thread.py index dd11ab6..957bd00 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): diff --git a/fbchat/_user.py b/fbchat/_user.py index 17a50fd..ef26967 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -50,7 +50,11 @@ class User(_thread.ThreadABC): id = attr.ib(converter=str) 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.""" 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_message.py b/tests/test_message.py index a30ca7e..19672ce 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -46,6 +46,21 @@ 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", @@ -70,56 +85,6 @@ def test_message_get_forwarded_from_tags(): ) -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