public static FB_Message _from_pull(JToken data, string thread_id, string mid = null, List <string> tags = null, string author = null, string timestamp = null)
        {
            var rtn = new FB_Message(
                text: data.get("body")?.Value <string>());

            rtn.uid       = mid;
            rtn.thread_id = thread_id; // Added
            rtn.author    = author;
            rtn.timestamp = timestamp;

            rtn.mentions = JToken.Parse(data.get("data")?.get("prng")?.Value <string>() ?? "{}")?.Select((m) =>
                                                                                                         new FB_Mention(
                                                                                                             thread_id: m.get("i")?.Value <string>(),
                                                                                                             offset: data.get("o")?.Value <int>() ?? 0,
                                                                                                             length: data.get("l")?.Value <int>() ?? 0)
                                                                                                         ).ToList();

            if (data.get("attachments") != null)
            {
                foreach (var a in data.get("attachments"))
                {
                    var mercury = a.get("mercury");
                    if (mercury.get("blob_attachment") != null)
                    {
                        var image_metadata = a.get("imageMetadata");
                        var attach_type    = mercury.get("blob_attachment")?.get("__typename")?.Value <string>();
                        var attachment     = FB_Attachment.graphql_to_attachment(
                            mercury.get("blob_attachment")
                            );

                        if (new string[] { "MessageFile", "MessageVideo", "MessageAudio" }.Contains(attach_type))
                        {
                            // TODO: Add more data here for audio files
                            if (attachment is FB_FileAttachment)
                            {
                                ((FB_FileAttachment)attachment).size = a?.get("fileSize")?.Value <int>() ?? 0;
                            }
                            if (attachment is FB_VideoAttachment)
                            {
                                ((FB_VideoAttachment)attachment).size = a?.get("fileSize")?.Value <int>() ?? 0;
                            }
                        }
                        rtn.attachments.Add(attachment);
                    }
                    else if (mercury.get("sticker_attachment") != null)
                    {
                        rtn.sticker = FB_Sticker._from_graphql(
                            mercury.get("sticker_attachment")
                            );
                    }
                    else if (mercury.get("extensible_attachment") != null)
                    {
                        var attachment = FB_Attachment.graphql_to_extensible_attachment(
                            mercury.get("extensible_attachment")
                            );
                        if (attachment is FB_UnsentMessage)
                        {
                            rtn.unsent = true;
                        }
                        else if (attachment != null)
                        {
                            rtn.attachments.Add(attachment);
                        }
                    }
                }
            }

            rtn.emoji_size = EmojiSizeMethods._from_tags(tags);
            rtn.forwarded  = FB_Message._get_forwarded_from_tags(tags);
            return(rtn);
        }
        public static FB_Message _from_graphql(JToken data, string thread_id)
        {
            if (data["message_sender"] == null)
            {
                data["message_sender"] = new JObject(new JProperty("id", 0));
            }
            if (data["message"] == null)
            {
                data["message"] = new JObject(new JProperty("text", ""));
            }

            var tags = data.get("tags_list")?.ToObject <List <string> >();

            var rtn = new FB_Message(
                text: data.get("message")?.get("text")?.Value <string>(),
                mentions: data.get("message")?.get("ranges")?.Select((m) =>
                                                                     new FB_Mention(
                                                                         thread_id: m.get("entity")?.get("id")?.Value <string>(),
                                                                         offset: data.get("offset")?.Value <int>() ?? 0,
                                                                         length: data.get("length")?.Value <int>() ?? 0)
                                                                     ).ToList(),
                emoji_size: EmojiSizeMethods._from_tags(tags),
                sticker: FB_Sticker._from_graphql(data.get("sticker")));

            rtn.forwarded = FB_Message._get_forwarded_from_tags(tags);
            rtn.uid       = data.get("message_id")?.Value <string>();
            rtn.thread_id = thread_id; // Added
            rtn.author    = data.get("message_sender")?.get("id")?.Value <string>();
            rtn.timestamp = data.get("timestamp_precise")?.Value <string>();
            rtn.unsent    = false;

            if (data.get("unread") != null)
            {
                rtn.is_read = !data.get("unread").Value <bool>();
            }
            rtn.reactions = new Dictionary <string, MessageReaction>();
            foreach (var r in data.get("message_reactions"))
            {
                rtn.reactions.Add(r.get("user")?.get("id")?.Value <string>(), FB_Message_Constants.REACTIONS[r.get("reaction").Value <string>()]);
            }
            if (data.get("blob_attachments") != null)
            {
                rtn.attachments = new List <FB_Attachment>();
                foreach (var attachment in data.get("blob_attachments"))
                {
                    rtn.attachments.Add(FB_Attachment.graphql_to_attachment(attachment));
                }
            }
            if (data.get("platform_xmd_encoded") != null)
            {
                var quick_replies = JToken.Parse(data.get("platform_xmd_encoded")?.Value <string>()).get("quick_replies");
                if (quick_replies != null)
                {
                    if (quick_replies.Type == JTokenType.Array)
                    {
                        rtn.quick_replies = quick_replies.Select((q) => FB_QuickReply.graphql_to_quick_reply(q)).ToList();
                    }
                    else
                    {
                        rtn.quick_replies = new List <FB_QuickReply>()
                        {
                            FB_QuickReply.graphql_to_quick_reply(quick_replies)
                        }
                    };
                }
            }
            if (data.get("extensible_attachment") != null)
            {
                var attachment = FB_Attachment.graphql_to_extensible_attachment(data.get("extensible_attachment"));
                if (attachment is FB_UnsentMessage)
                {
                    rtn.unsent = true;
                }
                else if (attachment != null)
                {
                    rtn.attachments.Add(attachment);
                }
            }
            if (data.get("replied_to_message") != null)
            {
                rtn.replied_to  = FB_Message._from_graphql(data.get("replied_to_message")?.get("message"), thread_id);
                rtn.reply_to_id = rtn.replied_to.uid;
            }

            return(rtn);
        }