/// <summary>
        /// Create a syndication feed from the child pages of this page.
        /// </summary>
        /// <param name="session">a session</param>
        /// <param name="page">blocks of type ChildPage only</param>
        /// <param name="maxItems">max number of child pages to return</param>
        /// <param name="maxBlocks">limit the parsing of each page to the first maxBlocks blocks</param>
        /// <param name="stopBeforeFirstSubHeader">when true, stop parsing a page when a line containing a sub_header is found</param>
        /// <param name="cancel"></param>
        /// <returns>A SyndicationFeed containing one SyndicationItem per child page</returns>
        /// <remarks>
        /// Call this method with an existing page, which is the container for child pages.
        /// All child pages will go into this feed's items.
        ///
        /// The created feed has the title and id of the page.
        ///
        /// Are included only child pages which are not restricted with the "integration".
        /// </remarks>
        public static async Task <SyndicationFeed> GetSyndicationFeed(this NotionSession session, Page page, int maxItems = 50, int maxBlocks = 20, bool stopBeforeFirstSubHeader = true, CancellationToken cancel = default)
        {
            var childPages = await session.GetBlockChildren(page.Id, cancel : cancel)
                             .Where(b => b.Type == BlockTypes.ChildPage && b.ChildPage != null) //Both checks are redundant. We could keep only one.
                             .Take(maxItems)
                             .ToListAsync(cancel).ConfigureAwait(false);

            if (childPages.Count == 0)
            {
                return new SyndicationFeed {
                           LastUpdatedTime = DateTimeOffset.Now
                }
            }
            ;

            var feed = await session.GetSyndicationFeed(childPages, maxBlocks, stopBeforeFirstSubHeader, cancel).ConfigureAwait(false);

            //"workspace" data not available in API
            //feed.Id = space.Id.ToString("N");
            //feed.Title = new TextSyndicationContent(space.Name);
            //feed.Description = new TextSyndicationContent(space.Domain);
            feed.Id    = page.Id;
            feed.Title = new TextSyndicationContent(page.Title?.Title?.FirstOrDefault()?.PlainText);
            return(feed);
        }
        public static async IAsyncEnumerable <User> GetUsers(this NotionSession session, int pageSize = Constants.DefaultPageSize, [EnumeratorCancellation] CancellationToken cancel = default)
        {
            var request = session.HttpSession.CreateRequest(Constants.ApiBaseUrl + "users")
                          .SetQueryParam("page_size", pageSize);

            while (true)
            {
                var result = await request.GetJsonAsync <PaginationResult <User> >(cancel).ConfigureAwait(false);

                if (result?.Results == null)
                {
                    yield break;
                }
                foreach (var item in result.Results)
                {
                    yield return(item);
                }
                if (!result.HasMore || result.NextCursor == null)
                {
                    yield break;
                }

                request.SetQueryParam("start_cursor", result.NextCursor);
            }
        }
        /// <summary>
        /// Searches all pages and child pages that are shared with the integration. The results may include databases.
        /// The query parameter matches against the page titles. If the query parameter is not provided, the response will contain all pages (and child pages) in the results.
        /// The filter parameter can be used to query specifically for only pages or only databases.
        /// </summary>
        /// <remarks>
        /// The query parameter matches against the page titles.
        /// pageSize max is 100. Default is 10.
        /// Search indexing is not immediate.
        /// Pages are fetched automatically when needed.
        /// </remarks>
        /// <example>
        /// await foreach(var item in SearchAsync().WithCancellation(cancel).ConfigureAwait(false))
        ///    DoSomething(item);
        /// </example>
        public static async IAsyncEnumerable <Page> Search(this NotionSession session,
                                                           string?query = null, SortOptions?sortOptions = null, FilterOptions?filterOptions = null, int pageSize = Constants.DefaultPageSize,
                                                           [EnumeratorCancellation] CancellationToken cancel = default)
        {
            var searchRequest = new SearchRequest {
                Query = query, Sort = sortOptions, Filter = filterOptions, PageSize = pageSize
            };
            var request = session.HttpSession.CreateRequest(Constants.ApiBaseUrl + "search");

            while (true)
            {
                var result = await request.PostJsonAsync(searchRequest, cancel).ReceiveJson <PaginationResult <Page> >().ConfigureAwait(false);

                if (result?.Results == null || cancel.IsCancellationRequested)
                {
                    yield break;
                }
                foreach (var item in result.Results)
                {
                    yield return(item);
                }
                if (!result.HasMore || result.NextCursor == null)
                {
                    yield break;
                }

                searchRequest.StartCursor = result.NextCursor;
            }
        }
        public static async Task <PaginationResult <Page>?> SearchPaged(this NotionSession session,
                                                                        string?query             = null, SortOptions?sortOptions = null, FilterOptions?filterOptions = null, PagingOptions?pagingOptions = null,
                                                                        CancellationToken cancel = default)
        {
            var searchRequest = new SearchRequest
            {
                Query = query, Sort = sortOptions, Filter = filterOptions, StartCursor = pagingOptions?.StartCursor, PageSize = pagingOptions?.PageSize ?? Constants.DefaultPageSize
            };

            var request = session.HttpSession.CreateRequest(Constants.ApiBaseUrl + "search");

            return(await request.PostJsonAsync(searchRequest, cancel).ReceiveJson <PaginationResult <Page> >().ConfigureAwait(false));
        }
        /// <summary>
        /// Create a syndication feed from a list of page
        /// </summary>
        /// <param name="session">a session</param>
        /// <param name="childPages">blocks of type ChildPage only</param>
        /// <param name="maxBlocks">limits the parsing of each page to the first maxBlocks blocks</param>
        /// <param name="stopBeforeFirstSubHeader">when true, stop parsing a page when a line containing a sub_header is found</param>
        /// <param name="cancel"></param>
        /// <returns>A SyndicationFeed containing one SyndicationItem per page</returns>
        /// <remarks>
        /// The created feed has no title/description
        /// </remarks>
        public static async Task <SyndicationFeed> GetSyndicationFeed(this NotionSession session, IEnumerable <Block> childPages, int maxBlocks = 20, bool stopBeforeFirstSubHeader = true, CancellationToken cancel = default)
        {
            var feedItems    = new List <SyndicationItem>();
            var htmlRenderer = new HtmlRenderer();

            foreach (var page in childPages)
            {
                if (page.Type != BlockTypes.ChildPage)
                {
                    throw new ArgumentException("All childPages must be of type BlockTypes.ChildPage", nameof(childPages));
                }

                //get blocks and extract an html content
                var blocks = await session.GetBlockChildren(page.Id, cancel : cancel)
                             .Take(maxBlocks)
                             .ToListAsync(cancel).ConfigureAwait(false);

                var content = htmlRenderer.GetHtml(blocks, stopBeforeFirstSubHeader);
                var title   = page.ChildPage !.Title;
                var pageUri = NotionUtils.GetPageUri(page.Id, title);

                var item = new SyndicationItem(title, content, pageUri)
                {
                    Id              = page.Id,
                    BaseUri         = pageUri,
                    Summary         = new TextSyndicationContent(content),
                    PublishDate     = page.CreatedTime,
                    LastUpdatedTime = page.LastEditedTime,
                };

                // Property not yet available in API
                // if (!String.IsNullOrWhiteSpace(page.Format?.PageIcon))
                // {
                //     if(Uri.TryCreate(page.Format.PageIcon, UriKind.Absolute, out _))
                //         item.AttributeExtensions.Add(new XmlQualifiedName("iconUrl"), pageBlock.Format.PageIcon);
                //     else
                //         item.AttributeExtensions.Add(new XmlQualifiedName("iconString"), pageBlock.Format.PageIcon);
                // }

                feedItems.Add(item);
            }

            var feed = new SyndicationFeed(feedItems)
            {
                LastUpdatedTime = feedItems.DefaultIfEmpty().Max(item => item?.LastUpdatedTime ?? DateTimeOffset.MinValue),
            };

            return(feed);
        }
        public static async Task <string> GetHtml(this NotionSession session, Page page, CancellationToken cancel = default)
        {
            var blocks = await session.GetBlockChildren(page.Id, cancel : cancel)
                         .Where(b => b.Type != BlockTypes.ChildPage)
                         .ToListAsync(cancel).ConfigureAwait(false);

            if (blocks.Count == 0)
            {
                return(string.Empty);
            }

            var htmlRenderer = new HtmlRenderer();

            return(htmlRenderer.GetHtml(blocks));
        }
 public static IAsyncEnumerable <Page> GetTopLevelPages(this NotionSession session, CancellationToken cancel = default)
 => session.Search(filterOptions: FilterOptions.ObjectPage, cancel: cancel)
 .Where(pageProp => pageProp.Parent.Type == Page.PageParent.TypeWorkspace);
        /// <summary>
        /// Get the properties of a page
        /// </summary>
        /// <remarks>
        /// (from API doc)
        /// Each page property is computed with a limit of 25 page references.
        /// Therefore relation property values feature a maximum of 25 relations,
        /// rollup property values are calculated based on a maximum of 25 relations,
        /// and rich text property values feature a maximum of 25 page mentions.
        /// </remarks>
        public static async Task <Page?> GetPage(this NotionSession session, string pageId, CancellationToken cancel = default)
        {
            var request = session.HttpSession.CreateRequest(Constants.ApiBaseUrl + "pages/" + pageId);

            return(await request.GetJson <Page>(cancel).ConfigureAwait(false));
        }
        /// <summary>
        /// Get a user
        /// </summary>
        public static async Task <User?> GetUser(this NotionSession session, string userId, CancellationToken cancel = default)
        {
            var request = session.HttpSession.CreateRequest(Constants.ApiBaseUrl + "users/" + userId);

            return(await request.GetJson <User>(cancel).ConfigureAwait(false));
        }