text: import attr
  import json
  from string import Formatter
  from ._core import Enum
  from . import _util, _attachment, _location, _file, _quick_reply, _sticker
  
  
  class EmojiSize(Enum):
      """Used to specify the size of a sent emoji."""
  
      LARGE = "369239383222810"
      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."""
  
      HEART = "❤"
      LOVE = "😍"
      SMILE = "😆"
      WOW = "😮"
      SAD = "😢"
      ANGRY = "😠"
      YES = "👍"
      NO = "👎"
  
  
  @attr.s
  class Mention:
      """Represents a ``@mention``."""
  
      #: The thread ID the mention is pointing at
      thread_id = attr.ib()
      #: The character where the mention starts
      offset = attr.ib(0)
      #: The length of the mention
      length = attr.ib(10)
  
  
  @attr.s
  class Message:
      """Represents a Facebook message."""
  
      #: The actual message
      text = attr.ib(None)
      #: A list of :class:`Mention` objects
      mentions = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
      #: A :class:`EmojiSize`. Size of a sent emoji
      emoji_size = attr.ib(None)
      #: The message ID
      uid = attr.ib(None, init=False)
      #: ID of the sender
      author = attr.ib(None, init=False)
      #: Datetime of when the message was sent
      created_at = attr.ib(None, init=False)
      #: Whether the message is read
      is_read = attr.ib(None, init=False)
      #: A list of people IDs who read the message, works only with :func:`fbchat.Client.fetch_thread_messages`
      read_by = attr.ib(factory=list, init=False)
      #: A dictionary with user's IDs as keys, and their :class:`MessageReaction` as values
      reactions = attr.ib(factory=dict, init=False)
      #: A :class:`Sticker`
      sticker = attr.ib(None)
      #: A list of attachments
      attachments = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
      #: A list of :class:`QuickReply`
      quick_replies = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
      #: Whether the message is unsent (deleted for everyone)
      unsent = attr.ib(False, init=False)
      #: Message ID you want to reply to
      reply_to_id = attr.ib(None)
      #: Replied message
      replied_to = attr.ib(None, init=False)
      #: Whether the message was forwarded
      forwarded = attr.ib(False, init=False)
  
      @classmethod
      def format_mentions(cls, 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.
  
          >>> 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=[]>
  
          >>> 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=[]>
          """
          result = ""
          mentions = list()
          offset = 0
          f = Formatter()
          field_names = [field_name[1] for field_name in f.parse(text)]
          automatic = "" in field_names
          i = 0
  
          for (literal_text, field_name, format_spec, conversion) in f.parse(text):
              offset += len(literal_text)
              result += literal_text
  
              if field_name is None:
                  continue
  
              if field_name == "":
                  field_name = str(i)
                  i += 1
              elif automatic and field_name.isdigit():
                  raise ValueError(
                      "cannot switch from automatic field numbering to manual field specification"
                  )
  
              thread_id, name = f.get_field(field_name, args, kwargs)[0]
  
              if format_spec:
                  name = f.format_field(name, format_spec)
              if conversion:
                  name = f.convert_field(name, conversion)
  
              result += name
              mentions.append(
                  Mention(thread_id=thread_id, offset=offset, length=len(name))
              )
              offset += len(name)
  
          message = cls(text=result, mentions=mentions)
          return message
  
      @staticmethod
      def _get_forwarded_from_tags(tags):
          if tags is None:
              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["profile_xmd[{}][id]".format(i)] = mention.thread_id
              data["profile_xmd[{}][offset]".format(i)] = mention.offset
              data["profile_xmd[{}][length]".format(i)] = mention.length
              data["profile_xmd[{}][type]".format(i)] = "p"
  
          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.uid
  
          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
  
      @classmethod
      def _from_graphql(cls, data):
          if data.get("message_sender") is None:
              data["message_sender"] = {}
          if data.get("message") is None:
              data["message"] = {}
          tags = data.get("tags_list")
          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(tags),
              sticker=_sticker.Sticker._from_graphql(data.get("sticker")),
          )
          rtn.forwarded = cls._get_forwarded_from_tags(tags)
          rtn.uid = str(data["message_id"])
          rtn.author = str(data["message_sender"]["id"])
          rtn.created_at = _util.millis_to_datetime(int(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)
          if data.get("replied_to_message") is not None:
              rtn.replied_to = cls._from_graphql(data["replied_to_message"]["message"])
              rtn.reply_to_id = rtn.replied_to.uid
          return rtn
  
      @classmethod
      def _from_reply(cls, data):
          tags = data["messageMetadata"].get("tags")
          rtn = cls(
              text=data.get("body"),
              mentions=[
                  Mention(m.get("i"), offset=m.get("o"), length=m.get("l"))
                  for m in json.loads(data.get("data", {}).get("prng", "[]"))
              ],
              emoji_size=EmojiSize._from_tags(tags),
          )
          metadata = data.get("messageMetadata", {})
          rtn.forwarded = cls._get_forwarded_from_tags(tags)
          rtn.uid = metadata.get("messageId")
          rtn.author = str(metadata.get("actorFbId"))
          rtn.created_at = _util.millis_to_datetime(metadata.get("timestamp"))
          rtn.unsent = False
          if data.get("data", {}).get("platform_xmd"):
              quick_replies = json.loads(data["data"]["platform_xmd"]).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("attachments") is not None:
              for attachment in data["attachments"]:
                  attachment = json.loads(attachment["mercuryJSON"])
                  if attachment.get("blob_attachment"):
                      rtn.attachments.append(
                          _file.graphql_to_attachment(attachment["blob_attachment"])
                      )
                  if attachment.get("extensible_attachment"):
                      extensible_attachment = graphql_to_extensible_attachment(
                          attachment["extensible_attachment"]
                      )
                      if isinstance(extensible_attachment, _attachment.UnsentMessage):
                          rtn.unsent = True
                      else:
                          rtn.attachments.append(extensible_attachment)
                  if attachment.get("sticker_attachment"):
                      rtn.sticker = _sticker.Sticker._from_graphql(
                          attachment["sticker_attachment"]
                      )
          return rtn
  
      @classmethod
      def _from_pull(cls, data, mid=None, tags=None, author=None, created_at=None, log=None):
          rtn = cls(text=data.get("body"))
          rtn.uid = mid
          rtn.author = author
          rtn.created_at = created_at
  
          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:
                  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:
                  log.exception(
                      "An exception occured while reading attachments: {}".format(
                          data["attachments"]
                      )
                  )
  
          rtn.emoji_size = EmojiSize._from_tags(tags)
          rtn.forwarded = cls._get_forwarded_from_tags(tags)
          return rtn
  
  
  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
  