Refactor message sending

This commit is contained in:
Mads Marquart
2020-01-09 16:15:52 +01:00
parent 6d6f779d26
commit a1b3fd3ffa
8 changed files with 131 additions and 181 deletions

View File

@@ -9,7 +9,7 @@ print("Own id: {}".format(session.user_id))
user = fbchat.Thread(session=session, id=session.user_id) user = fbchat.Thread(session=session, id=session.user_id)
# Send a message to yourself # Send a message to yourself
user.send(fbchat.Message(text="Hi me!")) user.send_text("Hi me!")
# Log the user out # Log the user out
session.logout() session.logout()

View File

@@ -10,7 +10,7 @@ class EchoBot(fbchat.Client):
# If you're not the author, echo # If you're not the author, echo
if author_id != self.session.user_id: if author_id != self.session.user_id:
thread.send(message_object) thread.send_text(message_object.text)
session = fbchat.Session.login("<email>", "<password>") session = fbchat.Session.login("<email>", "<password>")

View File

@@ -1,4 +1,5 @@
import fbchat import fbchat
import requests
session = fbchat.Session.login("<email>", "<password>") session = fbchat.Session.login("<email>", "<password>")
@@ -9,34 +10,32 @@ thread = fbchat.User(session=session, id=session.user_id)
# thread = fbchat.Group(session=session, id="1234567890") # thread = fbchat.Group(session=session, id="1234567890")
# Will send a message to the thread # Will send a message to the thread
thread.send(fbchat.Message(text="<message>")) thread.send_text("<message>")
# Will send the default `like` emoji # 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 `👍` # 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` # 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 # Will send a message with a mention
thread.send( thread.send_text(
fbchat.Message( text="This is a @mention",
text="This is a @mention", mentions=[fbchat.Mention(thread.id, offset=10, length=8)],
mentions=[fbchat.Mention(thread.id, offset=10, length=8)],
)
) )
# Will send the image located at `<image path>` # Will send the image located at `<image path>`
thread.send_local_image( with open("<image path>", "rb") as f:
"<image path>", message=fbchat.Message(text="This is a local image") 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 `<image url>`, and then send it # Will download the image at the URL `<image url>`, and then send it
thread.send_remote_image( r = requests.get("<image url>")
"<image url>", message=fbchat.Message(text="This is a remote image") 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 # 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 # 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>"]) thread.add_participants(["<1st user id>", "<2nd user id>", "<3rd user id>"])
# Will change the title of the group to `<title>` # Will change the title of the group to `<title>`
thread.change_title("<title>") thread.set_title("<title>")
# Will change the nickname of the user `<user_id>` to `<new nickname>` # Will change the nickname of the user `<user_id>` to `<new nickname>`

View File

@@ -204,52 +204,6 @@ class Message:
return False return False
return any(map(lambda tag: "forward" in tag or "copy" in tag, tags)) 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 @staticmethod
def _parse_quick_replies(data): def _parse_quick_replies(data):
if data: if data:

View File

@@ -73,6 +73,17 @@ class ThreadABC(metaclass=abc.ABCMeta):
def _to_send_data(self) -> MutableMapping[str, str]: def _to_send_data(self) -> MutableMapping[str, str]:
raise NotImplementedError 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: def wave(self, first: bool = True) -> str:
"""Wave hello to the thread. """Wave hello to the thread.
@@ -85,73 +96,127 @@ class ThreadABC(metaclass=abc.ABCMeta):
"INITIATED" if first else "RECIPROCATED" "INITIATED" if first else "RECIPROCATED"
) )
data["lightweight_action_attachment[lwa_type]"] = "WAVE" 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) message_id, thread_id = self.session._do_send_request(data)
return message_id return message_id
def send(self, message) -> str: def send_text(
"""Send message to the thread. 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: 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: Returns:
:ref:`Message ID <intro_message_ids>` of the sent message :ref:`Message ID <intro_message_ids>` of the sent message
""" """
data = self._to_send_data() 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) 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() 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["action_type"] = "ma-type:user-generated-message"
data["location_attachment[coordinates][latitude]"] = latitude data["location_attachment[coordinates][latitude]"] = latitude
data["location_attachment[coordinates][longitude]"] = longitude data["location_attachment[coordinates][longitude]"] = longitude
data["location_attachment[is_current_location]"] = current data["location_attachment[is_current_location]"] = current
return self.session._do_send_request(data) 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. """Send a given location to a thread as the user's current location.
Args: Args:
latitude: The location latitude latitude: The location latitude
longitude: The location longitude longitude: The location longitude
message: Additional message
""" """
self._send_location( self._send_location(True, latitude=latitude, longitude=longitude)
True, latitude=latitude, longitude=longitude, message=message,
)
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. """Send a given location to a thread as a pinned location.
Args: Args:
latitude: The location latitude latitude: The location latitude
longitude: The location longitude longitude: The location longitude
message: Additional message
""" """
self._send_location( self._send_location(False, latitude=latitude, longitude=longitude)
False, latitude=latitude, longitude=longitude, message=message,
)
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. """Send files from file IDs to a thread.
`files` should be a list of tuples, with a file's ID and mimetype. `files` should be a list of tuples, with a file's ID and mimetype.
""" """
data = self._to_send_data() return self.send_text(text=None, files=files)
data.update(message._to_send_data())
data["action_type"] = "ma-type:user-generated-message"
data["has_attachment"] = True
for i, (file_id, mimetype) in enumerate(files): # xmd = {"quick_replies": []}
data["{}s[{}]".format(_util.mimetype_to_key(mimetype), i)] = file_id # for quick_reply in quick_replies:
# # TODO: Move this to `_quick_reply.py`
return self.session._do_send_request(data) # 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! # TODO: This!
# def quick_reply(self, quick_reply, payload=None): # def quick_reply(self, quick_reply, payload=None):

View File

@@ -50,7 +50,11 @@ class User(_thread.ThreadABC):
id = attr.ib(converter=str) id = attr.ib(converter=str)
def _to_send_data(self): 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): def confirm_friend_request(self):
"""Confirm a friend request, adding the user to your friend list.""" """Confirm a friend request, adding the user to your friend list."""

View File

@@ -2,11 +2,7 @@ import datetime
import json import json
import time import time
import random import random
import contextlib
import mimetypes
import urllib.parse import urllib.parse
import requests
from os import path
from ._core import log from ._core import log
from ._exception import ( from ._exception import (
@@ -188,39 +184,6 @@ def mimetype_to_key(mimetype):
return "file_id" 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): def get_url_parameters(url, *args):
params = urllib.parse.parse_qs(urllib.parse.urlparse(url).query) params = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
return [params[arg][0] for arg in args if params.get(arg)] return [params[arg][0] for arg in args if params.get(arg)]

View File

@@ -46,6 +46,21 @@ def test_graphql_to_extensible_attachment_dispatch(monkeypatch, obj, type_):
assert graphql_to_extensible_attachment(data) 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(): def test_message_format_mentions():
expected = Message( expected = Message(
text="Hey 'Peter'! My name is Michael", 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") @pytest.mark.skip(reason="need to be added")
def test_message_to_send_data_quick_replies(): def test_message_to_send_data_quick_replies():
raise NotImplementedError raise NotImplementedError