diff --git a/Src/Notion.Client/Api/ApiEndpoints.cs b/Src/Notion.Client/Api/ApiEndpoints.cs index f9a9549b..ec6f5b1a 100644 --- a/Src/Notion.Client/Api/ApiEndpoints.cs +++ b/Src/Notion.Client/Api/ApiEndpoints.cs @@ -61,5 +61,12 @@ public static class SearchApiUrls { public static string Search() => "/v1/search"; } + + public static class CommentsApiUrls + { + public static string Retrieve() => "/v1/comments"; + + public static string Create() => "/v1/comments"; + } } } diff --git a/Src/Notion.Client/Api/Comments/CommentsClient.cs b/Src/Notion.Client/Api/Comments/CommentsClient.cs new file mode 100644 index 00000000..51dadeff --- /dev/null +++ b/Src/Notion.Client/Api/Comments/CommentsClient.cs @@ -0,0 +1,12 @@ +namespace Notion.Client +{ + public partial class CommentsClient : ICommentsClient + { + private readonly IRestClient _client; + + public CommentsClient(IRestClient restClient) + { + _client = restClient; + } + } +} diff --git a/Src/Notion.Client/Api/Comments/Create/CommentsClient.cs b/Src/Notion.Client/Api/Comments/Create/CommentsClient.cs new file mode 100644 index 00000000..12f1207b --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Create/CommentsClient.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace Notion.Client +{ + public partial class CommentsClient + { + public async Task Create(CreateCommentParameters parameters) + { + var body = (ICreateCommentsBodyParameters)parameters; + + return await _client.PostAsync( + ApiEndpoints.CommentsApiUrls.Create(), + body + ); + } + } +} diff --git a/Src/Notion.Client/Api/Comments/Create/Request/CreateCommentParameters.cs b/Src/Notion.Client/Api/Comments/Create/Request/CreateCommentParameters.cs new file mode 100644 index 00000000..aadaf1f1 --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Create/Request/CreateCommentParameters.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Notion.Client +{ + public interface ICreateCommentsBodyParameters + { + [JsonProperty("rich_text")] + public IEnumerable RichText { get; set; } + } + + public interface ICreateDiscussionCommentBodyParameters : ICreateCommentsBodyParameters + { + [JsonProperty("discussion_id")] + public string DiscussionId { get; set; } + } + + public interface ICreatePageCommentBodyParameters : ICreateCommentsBodyParameters + { + [JsonProperty("parent")] + public ParentPageInput Parent { get; set; } + } + + public class CreateCommentParameters : ICreateDiscussionCommentBodyParameters, ICreatePageCommentBodyParameters + { + public string DiscussionId { get; set; } + public IEnumerable RichText { get; set; } + public ParentPageInput Parent { get; set; } + + public static CreateCommentParameters CreatePageComment(ParentPageInput parent, IEnumerable richText) + { + return new CreateCommentParameters + { + Parent = parent, + RichText = richText + }; + } + + public static CreateCommentParameters CreateDiscussionComment(string discussionId, IEnumerable richText) + { + return new CreateCommentParameters + { + DiscussionId = discussionId, + RichText = richText + }; + } + } +} diff --git a/Src/Notion.Client/Api/Comments/Create/Response/CreateCommentResponse.cs b/Src/Notion.Client/Api/Comments/Create/Response/CreateCommentResponse.cs new file mode 100644 index 00000000..39bebd6b --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Create/Response/CreateCommentResponse.cs @@ -0,0 +1,6 @@ +namespace Notion.Client +{ + public class CreateCommentResponse : Comment + { + } +} diff --git a/Src/Notion.Client/Api/Comments/ICommentsClient.cs b/Src/Notion.Client/Api/Comments/ICommentsClient.cs new file mode 100644 index 00000000..d936203c --- /dev/null +++ b/Src/Notion.Client/Api/Comments/ICommentsClient.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Notion.Client +{ + public interface ICommentsClient + { + /// + /// Retrieves a list of un-resolved Comment objects from a page or block. + /// + /// Retrieve comments parameters + /// + Task Retrieve(RetrieveCommentsParameters retrieveCommentsParameters); + + Task Create(CreateCommentParameters createCommentParameters); + } +} diff --git a/Src/Notion.Client/Api/Comments/Retrieve/CommentsClient.cs b/Src/Notion.Client/Api/Comments/Retrieve/CommentsClient.cs new file mode 100644 index 00000000..d228200c --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Retrieve/CommentsClient.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Notion.Client +{ + public partial class CommentsClient + { + public async Task Retrieve(RetrieveCommentsParameters parameters) + { + var qp = (IRetrieveCommentsQueryParameters)parameters; + + var queryParams = new Dictionary() + { + { "block_id", qp.BlockId }, + { "start_cursor", qp.StartCursor }, + { "page_size", qp.PageSize.ToString() }, + }; + + return await _client.GetAsync( + ApiEndpoints.CommentsApiUrls.Retrieve(), + queryParams + ); + } + } +} diff --git a/Src/Notion.Client/Api/Comments/Retrieve/Request/IRetrieveCommentsQueryParameters.cs b/Src/Notion.Client/Api/Comments/Retrieve/Request/IRetrieveCommentsQueryParameters.cs new file mode 100644 index 00000000..123a5844 --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Retrieve/Request/IRetrieveCommentsQueryParameters.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Notion.Client +{ + public interface IRetrieveCommentsQueryParameters : IPaginationParameters + { + [JsonProperty("block_id")] + string BlockId { get; set; } + } +} diff --git a/Src/Notion.Client/Api/Comments/Retrieve/Request/RetrieveCommentsParameters.cs b/Src/Notion.Client/Api/Comments/Retrieve/Request/RetrieveCommentsParameters.cs new file mode 100644 index 00000000..e78c4cad --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Retrieve/Request/RetrieveCommentsParameters.cs @@ -0,0 +1,9 @@ +namespace Notion.Client +{ + public class RetrieveCommentsParameters : IRetrieveCommentsQueryParameters + { + public string BlockId { get; set; } + public string StartCursor { get; set; } + public int? PageSize { get; set; } + } +} diff --git a/Src/Notion.Client/Api/Comments/Retrieve/Response/Comment.cs b/Src/Notion.Client/Api/Comments/Retrieve/Response/Comment.cs new file mode 100644 index 00000000..531fb43c --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Retrieve/Response/Comment.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class Comment : IObject + { + public string Id { get; set; } + + public ObjectType Object => ObjectType.Comment; + + [JsonProperty("parent")] + public ICommentParent Parent { get; set; } + + [JsonProperty("discussion_id")] + public string DiscussionId { get; set; } + + [JsonProperty("rich_text")] + public IEnumerable RichText { get; set; } + + [JsonProperty("created_by")] + public PartialUser CreatedBy { get; set; } + + [JsonProperty("created_time")] + public DateTime CreatedTime { get; set; } + + [JsonProperty("last_edited_time")] + public DateTime LastEditedTime { get; set; } + } +} diff --git a/Src/Notion.Client/Api/Comments/Retrieve/Response/Comments.cs b/Src/Notion.Client/Api/Comments/Retrieve/Response/Comments.cs new file mode 100644 index 00000000..c89182a5 --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Retrieve/Response/Comments.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class RetrieveCommentsResponse : PaginatedList + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("comment")] + public Dictionary Comment { get; set; } + } +} diff --git a/Src/Notion.Client/Api/Comments/Retrieve/Response/ICommentParent.cs b/Src/Notion.Client/Api/Comments/Retrieve/Response/ICommentParent.cs new file mode 100644 index 00000000..e323c7e1 --- /dev/null +++ b/Src/Notion.Client/Api/Comments/Retrieve/Response/ICommentParent.cs @@ -0,0 +1,12 @@ +using JsonSubTypes; +using Newtonsoft.Json; + +namespace Notion.Client +{ + [JsonConverter(typeof(JsonSubtypes), "type")] + [JsonSubtypes.KnownSubType(typeof(PageParent), ParentType.PageId)] + [JsonSubtypes.KnownSubType(typeof(BlockParent), ParentType.BlockId)] + public interface ICommentParent + { + } +} diff --git a/Src/Notion.Client/Models/ObjectType.cs b/Src/Notion.Client/Models/ObjectType.cs index d493254c..21995840 100644 --- a/Src/Notion.Client/Models/ObjectType.cs +++ b/Src/Notion.Client/Models/ObjectType.cs @@ -15,5 +15,8 @@ public enum ObjectType [EnumMember(Value = "user")] User, + + [EnumMember(Value = "comment")] + Comment, } } diff --git a/Src/Notion.Client/Models/Parents/BlockParent.cs b/Src/Notion.Client/Models/Parents/BlockParent.cs index 5cff9458..1ac1fd51 100644 --- a/Src/Notion.Client/Models/Parents/BlockParent.cs +++ b/Src/Notion.Client/Models/Parents/BlockParent.cs @@ -2,7 +2,7 @@ namespace Notion.Client { - public class BlockParent : IPageParent, IDatabaseParent, IBlockParent + public class BlockParent : IPageParent, IDatabaseParent, IBlockParent, ICommentParent { /// /// Always has a value "block_id" diff --git a/Src/Notion.Client/Models/Parents/PageParent.cs b/Src/Notion.Client/Models/Parents/PageParent.cs index a1f32a01..13b1c95a 100644 --- a/Src/Notion.Client/Models/Parents/PageParent.cs +++ b/Src/Notion.Client/Models/Parents/PageParent.cs @@ -2,7 +2,7 @@ namespace Notion.Client { - public class PageParent : IPageParent, IDatabaseParent, IBlockParent + public class PageParent : IPageParent, IDatabaseParent, IBlockParent, ICommentParent { /// /// Always "page_id". diff --git a/Src/Notion.Client/NotionClient.cs b/Src/Notion.Client/NotionClient.cs index 5bbc6dad..f1650b29 100644 --- a/Src/Notion.Client/NotionClient.cs +++ b/Src/Notion.Client/NotionClient.cs @@ -7,6 +7,7 @@ public interface INotionClient IPagesClient Pages { get; } ISearchClient Search { get; } IBlocksClient Blocks { get; } + ICommentsClient Comments { get; } IRestClient RestClient { get; } } @@ -18,6 +19,7 @@ public NotionClient( DatabasesClient databases, PagesClient pages, SearchClient search, + CommentsClient comments, BlocksClient blocks) { RestClient = restClient; @@ -25,6 +27,7 @@ public NotionClient( Databases = databases; Pages = pages; Search = search; + Comments = comments; Blocks = blocks; } @@ -33,6 +36,7 @@ public NotionClient( public IPagesClient Pages { get; } public ISearchClient Search { get; } public IBlocksClient Blocks { get; } + public ICommentsClient Comments { get; } public IRestClient RestClient { get; } } } diff --git a/Src/Notion.Client/NotionClientFactory.cs b/Src/Notion.Client/NotionClientFactory.cs index 4010eb8c..0a02eb3c 100644 --- a/Src/Notion.Client/NotionClientFactory.cs +++ b/Src/Notion.Client/NotionClientFactory.cs @@ -13,6 +13,7 @@ public static NotionClient Create(ClientOptions options) , pages: new PagesClient(restClient) , search: new SearchClient(restClient) , blocks: new BlocksClient(restClient) + , comments: new CommentsClient(restClient) ); } } diff --git a/Test/Notion.IntegrationTests/CommentsClientTests.cs b/Test/Notion.IntegrationTests/CommentsClientTests.cs new file mode 100644 index 00000000..2ac7219d --- /dev/null +++ b/Test/Notion.IntegrationTests/CommentsClientTests.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Notion.Client; +using Xunit; + +namespace Notion.IntegrationTests +{ + public class CommentsClientTests : IDisposable + { + private readonly INotionClient _client; + private readonly string _pageParentId; + private readonly Page _page; + + public CommentsClientTests() + { + var options = new ClientOptions + { + AuthToken = Environment.GetEnvironmentVariable("NOTION_AUTH_TOKEN") + }; + + _client = NotionClientFactory.Create(options); + _pageParentId = Environment.GetEnvironmentVariable("NOTION_PAGE_PARENT_ID") ?? throw new ArgumentNullException("Page parent id is required."); + + _page = _client.Pages.CreateAsync( + PagesCreateParametersBuilder.Create( + new ParentPageInput() + { + PageId = _pageParentId + } + ).Build() + ).Result; + } + + [Fact] + public async Task ShouldCreatePageComment() + { + // Arrange + var parameters = CreateCommentParameters.CreatePageComment( + new ParentPageInput + { + PageId = _page.Id + }, + new List { + new RichTextTextInput + { + Text = new Text + { + Content = "This is a comment" + } + } + } + ); + + // Act + var response = await _client.Comments.Create(parameters); + + // Arrange + + Assert.NotNull(response.Parent); + Assert.NotNull(response.Id); + Assert.NotNull(response.DiscussionId); + + Assert.NotNull(response.RichText); + Assert.Single(response.RichText); + var richText = Assert.IsType(response.RichText.First()); + Assert.Equal("This is a comment", richText.Text.Content); + + var pageParent = Assert.IsType(response.Parent); + Assert.Equal(_page.Id, pageParent.PageId); + } + + [Fact] + public async Task ShouldCreateADiscussionComment() + { + // Arrange + var comment = await _client.Comments.Create( + CreateCommentParameters.CreatePageComment( + new ParentPageInput + { + PageId = _page.Id + }, + new List { + new RichTextTextInput + { + Text = new Text + { + Content = "This is a comment" + } + } + } + ) + ); + + // Act + var response = await _client.Comments.Create( + CreateCommentParameters.CreateDiscussionComment( + comment.DiscussionId, + new List { + new RichTextTextInput + { + Text = new Text + { + Content = "This is a sub comment" + } + } + } + ) + ); + + // Arrange + Assert.Null(response.Parent); + Assert.NotNull(response.Id); + Assert.Equal(comment.DiscussionId, response.DiscussionId); + + Assert.NotNull(response.RichText); + Assert.Single(response.RichText); + var richText = Assert.IsType(response.RichText.First()); + Assert.Equal("This is a sub comment", richText.Text.Content); + + var pageParent = Assert.IsType(response.Parent); + Assert.Equal(_page.Id, pageParent.PageId); + } + + public void Dispose() + { + _client.Pages.UpdateAsync(_page.Id, new PagesUpdateParameters + { + Archived = true, + }).Wait(); + } + } +}