From feec2169e0ba91e899f6e6ae96a0ffa30868b957 Mon Sep 17 00:00:00 2001 From: Joe Mayo Date: Sun, 24 Oct 2021 22:10:10 -0700 Subject: [PATCH] Implemented v2 Compliance APIs. (#243) --- .../ConsoleDemo.CSharp/ComplianceDemos.cs | 21 +- .../ComplianceCommandsTests.cs | 44 ++-- .../ComplianceRequestProcessorTests.cs | 190 ++++++++++-------- .../LinqToTwitter/Compliance/ComplianceJob.cs | 26 ++- .../Compliance/ComplianceJobCreate.cs | 17 ++ .../Compliance/ComplianceJobType.cs | 9 + .../Compliance/ComplianceQuery.cs | 14 +- .../Compliance/ComplianceQuerySingle.cs | 41 ++++ .../Compliance/ComplianceRequestProcessor.cs | 50 ++--- .../Compliance/ComplianceStatus.cs | 4 +- .../TwitterContextComplianceCommands.cs | 27 ++- 11 files changed, 279 insertions(+), 164 deletions(-) create mode 100644 src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJobCreate.cs create mode 100644 src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJobType.cs create mode 100644 src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceQuerySingle.cs diff --git a/Samples/LinqToTwitter6/Console/ConsoleDemo.CSharp/ConsoleDemo.CSharp/ComplianceDemos.cs b/Samples/LinqToTwitter6/Console/ConsoleDemo.CSharp/ConsoleDemo.CSharp/ComplianceDemos.cs index d13af948..e7bc4c66 100644 --- a/Samples/LinqToTwitter6/Console/ConsoleDemo.CSharp/ConsoleDemo.CSharp/ComplianceDemos.cs +++ b/Samples/LinqToTwitter6/Console/ConsoleDemo.CSharp/ConsoleDemo.CSharp/ComplianceDemos.cs @@ -77,7 +77,8 @@ static async Task FindComplianceJobAsync(TwitterContext twitterCtx) if (response?.Jobs?.Any() ?? false) response.Jobs.ForEach(job => Console.WriteLine( - $"\nName: {job.JobName}" + + $"\nID: {job.ID}" + + $"\nName: {job.Name}" + $"\nStatus: {job.Status}")); else Console.WriteLine("No entries found."); @@ -89,16 +90,16 @@ static async Task GetMultipleComplianceJobsAsync(TwitterContext twitterCtx) await (from job in twitterCtx.Compliance where job.Type == ComplianceType.MultipleJobs && - job.StartTime == DateTime.Now.AddDays(-2) && - job.EndTime == DateTime.Now && - job.Status == ComplianceStatus.All + job.JobType == ComplianceJobType.Tweets //&& + //job.Status == ComplianceStatus.InProgress select job) .SingleOrDefaultAsync(); if (response?.Jobs?.Any() ?? false) response.Jobs.ForEach(job => Console.WriteLine( - $"\nName: {job.JobName}" + + $"\nID: {job.ID}" + + $"\nName: {job.Name}" + $"\nStatus: {job.Status}")); else Console.WriteLine("No entries found."); @@ -106,11 +107,17 @@ static async Task GetMultipleComplianceJobsAsync(TwitterContext twitterCtx) static async Task CreateComplianceJobAsync(TwitterContext twitterCtx) { - ComplianceJob? job = await twitterCtx.CreateComplianceJobAsync("test", true); + string jobName = $"test-{DateTime.Now.ToString("yyyyMMddhhmm")}"; + + ComplianceQuerySingle? response = + await twitterCtx.CreateComplianceJobAsync(ComplianceJobType.Tweets, jobName, true); + + ComplianceJob? job = response?.Job; if (job is not null) Console.WriteLine( - $"\nName: {job.JobName}" + + $"\nID: {job.ID}" + + $"\nName: {job.Name}" + $"\nStatus: {job.Status}"); else Console.WriteLine("Job not returned"); diff --git a/src/LinqToTwitter6/LinqToTwitter.Tests/ComplianceTests/ComplianceCommandsTests.cs b/src/LinqToTwitter6/LinqToTwitter.Tests/ComplianceTests/ComplianceCommandsTests.cs index 7b6ab602..7e6c162d 100644 --- a/src/LinqToTwitter6/LinqToTwitter.Tests/ComplianceTests/ComplianceCommandsTests.cs +++ b/src/LinqToTwitter6/LinqToTwitter.Tests/ComplianceTests/ComplianceCommandsTests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using LinqToTwitter.Compliance; using LinqToTwitter.OAuth; using LinqToTwitter.Provider; using LinqToTwitter.Tests.Common; @@ -33,10 +35,11 @@ static async Task InitializeTwitterContextAsync(string result) execMock.SetupGet(exec => exec.Authorizer).Returns(authMock.Object); execMock.Setup(exec => - exec.PostFormUrlEncodedToTwitterAsync( - HttpMethod.Post.ToString(), + exec.SendJsonToTwitterAsync( + It.IsAny(), It.IsAny(), It.IsAny>(), + It.IsAny(), It.IsAny())) .Returns(tcsResponse.Task); var ctx = new TwitterContext(execMock.Object); @@ -46,26 +49,41 @@ static async Task InitializeTwitterContextAsync(string result) [TestMethod] public async Task CreateComplianceJobAsync_WithReply_ReturnsTrue() { + const string JobType = ComplianceJobType.Tweets; const string JobName = "abc"; const bool Resumable = true; var ctx = await InitializeTwitterContextAsync(JobResponse); - ComplianceJob job = await ctx.CreateComplianceJobAsync(JobName, Resumable); + ComplianceQuerySingle query = await ctx.CreateComplianceJobAsync(JobType, JobName, Resumable); + Assert.IsNotNull(query); + ComplianceJob job = query.Job; Assert.IsNotNull(job); - Assert.AreEqual("YIAh2p", job.JobID); - Assert.AreEqual("https://storage.googleapis.com/twitter-compliance/test_tweet_ids", job.DownloadUrl); - Assert.AreEqual(DateTime.Parse("2020-06-16T21:17:43.819+00:00"), job.DownloadExpiresAt); - Assert.AreEqual("https://storage.googleapis.com/twitter-compliance/customer_test_object_123456_d8ske9.json", job.UploadUrl); - Assert.AreEqual(DateTime.Parse("2020-06-16T21:17:43.818+00:00"), job.UploadExpiresAt); + Assert.AreEqual("1452446437015314435", job.ID); + Assert.AreEqual(DateTime.Parse("2021-11-01T01:26:30.000Z").ToUniversalTime(), job.DownloadExpiresAt); + Assert.AreEqual(ComplianceStatus.Created, job.Status); + Assert.AreEqual("https://storage.googleapis.com/up", job.UploadUrl); + Assert.AreEqual("https://storage.googleapis.com/down", job.DownloadUrl); + Assert.AreEqual(DateTime.Parse("2021-10-25T01:41:30.000Z").ToUniversalTime(), job.UploadExpiresAt); + Assert.AreEqual("test-202110240626", job.Name); + Assert.AreEqual(DateTime.Parse("2021-10-25T01:26:30.000Z").ToUniversalTime(), job.CreatedAt); + Assert.AreEqual(ComplianceJobType.Tweets, job.JobType); + Assert.AreEqual(true, job.Resumable); } const string JobResponse = @"{ - ""upload_url"" : ""https://storage.googleapis.com/twitter-compliance/customer_test_object_123456_d8ske9.json"", - ""upload_expires_at"" : ""2020-06-16T21:17:43.818+00:00"", - ""download_url"" : ""https://storage.googleapis.com/twitter-compliance/test_tweet_ids"", - ""download_expires_at"" : ""2020-06-16T21:17:43.819+00:00"", - ""job_id"" : ""YIAh2p"" + ""data"": { + ""id"": ""1452446437015314435"", + ""download_expires_at"": ""2021-11-01T01:26:30.000Z"", + ""status"": ""created"", + ""upload_url"": ""https://storage.googleapis.com/up"", + ""download_url"": ""https://storage.googleapis.com/down"", + ""upload_expires_at"": ""2021-10-25T01:41:30.000Z"", + ""name"": ""test-202110240626"", + ""created_at"": ""2021-10-25T01:26:30.000Z"", + ""type"": ""tweets"", + ""resumable"": true + } }"; } diff --git a/src/LinqToTwitter6/LinqToTwitter.Tests/ComplianceTests/ComplianceRequestProcessorTests.cs b/src/LinqToTwitter6/LinqToTwitter.Tests/ComplianceTests/ComplianceRequestProcessorTests.cs index 17f7398a..be22cd50 100644 --- a/src/LinqToTwitter6/LinqToTwitter.Tests/ComplianceTests/ComplianceRequestProcessorTests.cs +++ b/src/LinqToTwitter6/LinqToTwitter.Tests/ComplianceTests/ComplianceRequestProcessorTests.cs @@ -26,14 +26,12 @@ public void GetParametersTest() var endTime = new DateTime(2020, 8, 30); var startTime = new DateTime(2020, 8, 1); - string status = string.Join(',', new[] { ComplianceStatus.InProgress, ComplianceStatus.Expired }); Expression> expression = - search => - search.Type == ComplianceType.MultipleJobs && - search.EndTime == endTime && - search.ID == "123" && - search.StartTime == startTime && - search.Status == status; + job => + job.Type == ComplianceType.MultipleJobs && + job.ID == "123" && + job.JobType == ComplianceJobType.Tweets && + job.Status == ComplianceStatus.Created; var lambdaExpression = expression as LambdaExpression; @@ -42,36 +40,31 @@ public void GetParametersTest() Assert.IsTrue( queryParams.Contains( new KeyValuePair(nameof(ComplianceQuery.Type), ((int)ComplianceType.MultipleJobs).ToString(CultureInfo.InvariantCulture)))); - Assert.IsTrue( - queryParams.Contains( - new KeyValuePair(nameof(ComplianceQuery.EndTime), "08/30/2020 00:00:00"))); Assert.IsTrue( queryParams.Contains( new KeyValuePair(nameof(ComplianceQuery.ID), "123"))); Assert.IsTrue( queryParams.Contains( - new KeyValuePair(nameof(ComplianceQuery.StartTime), "08/01/2020 00:00:00"))); + new KeyValuePair(nameof(ComplianceQuery.JobType), ComplianceJobType.Tweets))); Assert.IsTrue( queryParams.Contains( - new KeyValuePair(nameof(ComplianceQuery.Status), "in_progress,expired"))); + new KeyValuePair(nameof(ComplianceQuery.Status), ComplianceStatus.Created))); } [TestMethod] public void BuildUrl_ForMultipleJobs_IncludesParameters() { const string ExpectedUrl = - BaseUrl2 + "tweets/compliance/jobs?" + - "end_time=2021-01-01T12%3A59%3A59Z&" + - "start_time=2020-12-31T00%3A00%3A01Z&" + - "status=in_progress%2Cexpired"; + BaseUrl2 + "compliance/jobs?" + + "type=tweets&" + + "status=in_progress"; var reqProc = new ComplianceRequestProcessor { BaseUrl = BaseUrl2 }; var parameters = new Dictionary { { nameof(ComplianceQuery.Type), ComplianceType.MultipleJobs.ToString() }, - { nameof(ComplianceQuery.EndTime), new DateTime(2021, 1, 1, 12, 59, 59).ToString() }, - { nameof(ComplianceQuery.StartTime), new DateTime(2020, 12, 31, 0, 0, 1).ToString() }, - { nameof(ComplianceQuery.Status), string.Join(',', new[] { ComplianceStatus.InProgress, ComplianceStatus.Expired }) } + { nameof(ComplianceQuery.JobType), ComplianceJobType.Tweets }, + { nameof(ComplianceQuery.Status), ComplianceStatus.InProgress } }; Request req = reqProc.BuildUrl(parameters); @@ -79,10 +72,28 @@ public void BuildUrl_ForMultipleJobs_IncludesParameters() Assert.AreEqual(ExpectedUrl, req.FullUrl); } + [TestMethod] + public void BuildUrl_ForMultipleJobsWithMissingType_Throws() + { + var reqProc = new ComplianceRequestProcessor { BaseUrl = BaseUrl2 }; + var parameters = + new Dictionary + { + { nameof(ComplianceQuery.Type), ComplianceType.MultipleJobs.ToString() }, + //{ nameof(ComplianceQuery.JobType), ComplianceJobType.Tweets } + }; + + ArgumentException ex = + L2TAssert.Throws(() => + reqProc.BuildUrl(parameters)); + + Assert.AreEqual(nameof(ComplianceQuery.JobType), ex.ParamName); + } + [TestMethod] public void BuildUrl_ForSingleJob_IncludesID() { - const string ExpectedUrl = BaseUrl2 + "tweets/compliance/jobs/123"; + const string ExpectedUrl = BaseUrl2 + "compliance/jobs/123"; var reqProc = new ComplianceRequestProcessor { BaseUrl = BaseUrl2 }; var parameters = new Dictionary @@ -111,7 +122,7 @@ public void BuildUrl_ForSingleJobWithoutID_Throws() L2TAssert.Throws(() => reqProc.BuildUrl(parameters)); - Assert.AreEqual(nameof(TweetQuery.ID), ex.ParamName); + Assert.AreEqual(nameof(ComplianceQuery.ID), ex.ParamName); } [TestMethod] @@ -125,30 +136,6 @@ public void BuildUrl_WithNullParameters_Throws() }); } - [TestMethod] - public void BuildUrl_WithSpacesInFields_FixesSpaces() - { - const string ExpectedUrl = - BaseUrl2 + "tweets/compliance/jobs?" + - "end_time=2021-01-01T12%3A59%3A59Z&" + - "start_time=2020-12-31T00%3A00%3A01Z&" + - "status=in_progress%2Cexpired"; - const string StatusWithSpaces = "in_progress, expired"; - var reqProc = new ComplianceRequestProcessor { BaseUrl = BaseUrl2 }; - var parameters = - new Dictionary - { - { nameof(ComplianceQuery.Type), ComplianceType.MultipleJobs.ToString() }, - { nameof(ComplianceQuery.EndTime), new DateTime(2021, 1, 1, 12, 59, 59).ToString() }, - { nameof(ComplianceQuery.StartTime), new DateTime(2020, 12, 31, 0, 0, 1).ToString() }, - { nameof(ComplianceQuery.Status), StatusWithSpaces } - }; - - Request req = reqProc.BuildUrl(parameters); - - Assert.AreEqual(ExpectedUrl, req.FullUrl); - } - [TestMethod] public void ProcessResults_Populates_SingleJob() { @@ -168,11 +155,16 @@ public void ProcessResults_Populates_SingleJob() Assert.AreEqual(1, jobs.Count); ComplianceJob job = jobs.FirstOrDefault(); Assert.IsNotNull(job); - Assert.AreEqual("jU8rFK", job.JobID); - Assert.AreEqual("Troglomyces twitteri", job.JobName); - Assert.AreEqual("https://storage.googleapis.com/twitter-compliance/test_user_ids", job.DownloadUrl); - Assert.AreEqual(DateTime.Parse("2020-09-04T20:04:41.819+00:00"), job.DownloadExpiresAt); - Assert.AreEqual(ComplianceStatus.Complete, job.Status); + Assert.AreEqual("1452446437015314435", job.ID); + Assert.AreEqual(DateTime.Parse("2021-11-01T01:26:30.000Z").ToUniversalTime(), job.DownloadExpiresAt); + Assert.AreEqual(ComplianceStatus.Created, job.Status); + Assert.AreEqual("https://storage.googleapis.com/up", job.UploadUrl); + Assert.AreEqual("https://storage.googleapis.com/down", job.DownloadUrl); + Assert.AreEqual(DateTime.Parse("2021-10-25T01:41:30.000Z").ToUniversalTime(), job.UploadExpiresAt); + Assert.AreEqual("test-202110240626", job.Name); + Assert.AreEqual(DateTime.Parse("2021-10-25T01:26:30.000Z").ToUniversalTime(), job.CreatedAt); + Assert.AreEqual(ComplianceJobType.Tweets, job.JobType); + Assert.AreEqual(true, job.Resumable); } [TestMethod] @@ -191,14 +183,18 @@ public void ProcessResults_Populates_MultipleJobs() Assert.IsNotNull(complianceQuery); List jobs = complianceQuery.Jobs; Assert.IsNotNull(jobs); - Assert.AreEqual(2, jobs.Count); + Assert.AreEqual(3, jobs.Count); ComplianceJob job = jobs.FirstOrDefault(); - Assert.IsNotNull(job); - Assert.AreEqual("NIXh2p", job.JobID); - Assert.AreEqual("Feline species research", job.JobName); - Assert.AreEqual("https://storage.googleapis.com/twitter-compliance/test_user_ids", job.DownloadUrl); - Assert.AreEqual(DateTime.Parse("2020-06-16T11:17:32.819+00:00"), job.DownloadExpiresAt); - Assert.AreEqual(ComplianceStatus.Complete, job.Status); + Assert.AreEqual("1452493500373553153", job.ID); + Assert.AreEqual(DateTime.Parse("2021-11-01T04:33:31.000Z").ToUniversalTime(), job.DownloadExpiresAt); + Assert.AreEqual(ComplianceStatus.Created, job.Status); + Assert.AreEqual("https://storage.googleapis.com/up", job.UploadUrl); + Assert.AreEqual("https://storage.googleapis.com/down", job.DownloadUrl); + Assert.AreEqual(DateTime.Parse("2021-10-25T04:48:31.000Z").ToUniversalTime(), job.UploadExpiresAt); + Assert.AreEqual("test-202110240933", job.Name); + Assert.AreEqual(DateTime.Parse("2021-10-25T04:33:31.000Z").ToUniversalTime(), job.CreatedAt); + Assert.AreEqual(ComplianceJobType.Tweets, job.JobType); + Assert.AreEqual(true, job.Resumable); } [TestMethod] @@ -242,49 +238,75 @@ public void ProcessResults_ForMultipleJobs_PopulatesInputParameters() { BaseUrl = BaseUrl2, Type = ComplianceType.MultipleJobs, - EndTime = new DateTime(2020, 12, 31), - StartTime = new DateTime(2020, 1, 1), + JobType = ComplianceJobType.Tweets, Status = ComplianceStatus.Complete }; - var results = reqProc.ProcessResults(SingleJob); + var results = reqProc.ProcessResults(MultipleJobs); Assert.IsNotNull(results); Assert.AreEqual(1, results.Count); var complianceQuery = results.Single(); Assert.IsNotNull(complianceQuery); Assert.AreEqual(ComplianceType.MultipleJobs, complianceQuery.Type); - Assert.AreEqual(new DateTime(2020, 12, 31), complianceQuery.EndTime); - Assert.AreEqual(new DateTime(2020, 1, 1), complianceQuery.StartTime); + Assert.AreEqual(ComplianceJobType.Tweets, complianceQuery.JobType); Assert.AreEqual(ComplianceStatus.Complete, complianceQuery.Status); } const string SingleJob = @"{ - ""job_id"": ""jU8rFK"", - ""job_name"": ""Troglomyces twitteri"", - ""download_url"": ""https://storage.googleapis.com/twitter-compliance/test_user_ids"", - ""download_expires_at"": ""2020-09-04T20:04:41.819+00:00"", - ""status"": ""complete"" + ""data"": { + ""id"": ""1452446437015314435"", + ""download_expires_at"": ""2021-11-01T01:26:30.000Z"", + ""status"": ""created"", + ""upload_url"": ""https://storage.googleapis.com/up"", + ""download_url"": ""https://storage.googleapis.com/down"", + ""upload_expires_at"": ""2021-10-25T01:41:30.000Z"", + ""name"": ""test-202110240626"", + ""created_at"": ""2021-10-25T01:26:30.000Z"", + ""type"": ""tweets"", + ""resumable"": true + } }"; const string MultipleJobs = @"{ - ""jobs"": [ - { - ""job_id"": ""NIXh2p"", - ""job_name"": ""Feline species research"", - ""created_at"": ""2020-09-03T21:17:43.819+00:00"", - ""download_url"": ""https://storage.googleapis.com/twitter-compliance/test_user_ids"", - ""download_expires_at"": ""2020-06-16T11:17:32.819+00:00"", - ""status"": ""complete"" - }, - { - ""job_id"": ""jU8rFK"", - ""job_name"": ""Troglomyces twitteri"", - ""download_url"": ""https://storage.googleapis.com/twitter-compliance/test_user_ids"", - ""download_expires_at"": ""2020-09-04T20:04:41.819+00:00"", - ""status"": ""complete"" - } - ] + ""data"": [ + { + ""download_url"": ""https://storage.googleapis.com/down"", + ""id"": ""1452493500373553153"", + ""download_expires_at"": ""2021-11-01T04:33:31.000Z"", + ""name"": ""test-202110240933"", + ""status"": ""created"", + ""type"": ""tweets"", + ""upload_url"": ""https://storage.googleapis.com/up"", + ""resumable"": true, + ""upload_expires_at"": ""2021-10-25T04:48:31.000Z"", + ""created_at"": ""2021-10-25T04:33:31.000Z"" + }, + { + ""download_url"": ""https://storage.googleapis.com/twttr-tweet-compliance/1452446437015314435/delivery/1021848369956700166_1452446437015314435?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=complianceapi-public-svc-acct%40twttr-compliance-public-prod.iam.gserviceaccount.com%2F20211025%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20211025T012630Z&X-Goog-Expires=604800&X-Goog-SignedHeaders=host&X-Goog-Signature=15c34a51d8778d867be1d5c59b5e4a4f38e0850d84febd5c8836d44851c08348a497ffb0b6cc9d6fb4e3f5fe50dbd7782fc3b6cfd926b9c433846fc93a3a9cd204055519578a87432c6d29cde179c251d22daa40a04edc5b1c12bf2d517dbb667461b902ac4aae1153b490e8694ab9d24bf4808ed2f8970bde12589a6ea4cdc8e22998af960fd10f1fb9274a51cc2b51543e9bb299191a1b043d777b74814b07921d2c0c503b1fa3e7b997ded194d34ac088d9d3dd952394d27849f35ee2f2f095d10fb79e8b4ba29c60a38e8cb70d45dd19798d03dc36d3eb083178d69da73c72d952505102bb2366c2ebed824f73380b56fc7ef2ab2cc5013cec950dd6b3a9"", + ""id"": ""1452446437015314435"", + ""download_expires_at"": ""2021-11-01T01:26:30.000Z"", + ""name"": ""test-202110240626"", + ""status"": ""expired"", + ""type"": ""tweets"", + ""upload_url"": ""https://storage.googleapis.com/twttr-tweet-compliance/1452446437015314435/submission/1021848369956700166_1452446437015314435?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=complianceapi-public-svc-acct%40twttr-compliance-public-prod.iam.gserviceaccount.com%2F20211025%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20211025T012630Z&X-Goog-Expires=900&X-Goog-SignedHeaders=content-length%3Bcontent-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=0e2e456a6a530a69fa31c9e5c65bbeff8f6b087635bf3ce5680a0a309281efd7e58067df61487e41a270d52e63f1dae6a7d67894965a92e8a062614abd6fc9e682860b814b13f761211aceac5334d4de6c382b7f39d29b50f8d68c11aef9b25641867f8f2b9cef68ea6ae1af724ca690cc22a8d97cb7e893a8eadeec3a97188c17fda4d00477460dfb31dd89dc8272fd1488c6c5fc621538d6fcc6d257cfb655592d28e4ec3c5bfd9e3d1ee8f99541a1f85ba9719e44418432c7c356c6785f17b1b521744666ed7b904f9b7d36fcce74621fc554c4ab1cad859c40caa727b70a2282a519e93a02e9bce769b76d20e0bc21ee6edd99d73f6442c5146dc7f6205f"", + ""resumable"": true, + ""upload_expires_at"": ""2021-10-25T01:41:30.000Z"", + ""created_at"": ""2021-10-25T01:26:30.000Z"" + }, + { + ""download_url"": ""https://storage.googleapis.com/twttr-tweet-compliance/1452416010036727813/delivery/1021848369956700166_1452416010036727813?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=complianceapi-public-svc-acct%40twttr-compliance-public-prod.iam.gserviceaccount.com%2F20211024%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20211024T232536Z&X-Goog-Expires=604800&X-Goog-SignedHeaders=host&X-Goog-Signature=30e8eba838c366aa6315a61e83ff029fc26d472787e6f2aa826df06678bdb915408d195da0035f90b0ffd7484d6d4854479dd8c14b785f795f5127d88ca852cff72023ff29f503f021bd40801175eac3a6eb9d5a065f6b54f30c3b7548a0e67fdd68b014b4856f710b7e5c7514f589f60db3d0d74e324fd8783a4d894756c0799789da78c16bb80dca71025ac586d02da5f680982726fee362ac2f88beff2deb119d4705ffcb434e359cd95cbad3771c60cde29b1e912f0a34534010fc0b18e14d2d928219cae13371b3d5b7e29df9bae44acd8742fc9fc83b4f13773430f847cbc2ea71e8df5ad18cccafa5c1ab1bf846e229af0bd10d7c949e095018633b77"", + ""id"": ""1452416010036727813"", + ""download_expires_at"": ""2021-10-31T23:25:36.000Z"", + ""name"": ""test-202110240425"", + ""status"": ""expired"", + ""type"": ""tweets"", + ""upload_url"": ""https://storage.googleapis.com/twttr-tweet-compliance/1452416010036727813/submission/1021848369956700166_1452416010036727813?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=complianceapi-public-svc-acct%40twttr-compliance-public-prod.iam.gserviceaccount.com%2F20211024%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20211024T232536Z&X-Goog-Expires=900&X-Goog-SignedHeaders=content-length%3Bcontent-type%3Bhost%3Bx-goog-resumable&X-Goog-Signature=6a3a1a71e67579f97748ba3e81b1ca75570514a8e6c866a4778c0d938fe40d4379fdbb7ab1aeed8309c352389c2fe2a1e2e238c5990b8c128da7db294324908ce9ae6036206f642319769cbd8507b1def8107b43c0c77bc5d2cc68782c0ff2bfc5bc47b31074c372c4f016745c828d5feca9949135be9bcec18e361d7055eec9a06f83c373c728ddcadbe33b5059b2e36fe0fc3f3e0ff9b5cea10e00957eeb787451d4759d10ac5b40920cd151295065355d04e9f2cb710469a141b273dd954c11c2568ace1c5b9e025744724c4216c9959730cc04062e24fd69c574df794edaafcaa429466e8c4a2ec7c31980c138a01012299f170ab88de8f4826efa74be35"", + ""resumable"": true, + ""upload_expires_at"": ""2021-10-24T23:40:36.000Z"", + ""created_at"": ""2021-10-24T23:25:36.000Z"" + } + ] }"; const string ErrorTweet = @"{ diff --git a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJob.cs b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJob.cs index 3d80ae4a..aa34af9f 100644 --- a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJob.cs +++ b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJob.cs @@ -11,14 +11,20 @@ public record ComplianceJob /// /// Unique ID for job /// - [JsonPropertyName("job_id")] - public string? JobID { get; set; } + [JsonPropertyName("id")] + public string? ID { get; set; } + + /// + /// Date/time when job was created + /// + [JsonPropertyName("created_at")] + public DateTime CreatedAt { get; set; } /// /// Name of the job /// - [JsonPropertyName("job_name")] - public string? JobName { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } /// /// URL where to download job results @@ -49,5 +55,17 @@ public record ComplianceJob /// [JsonPropertyName("upload_expires_at")] public DateTime UploadExpiresAt { get; set; } + + /// + /// Type of job (tweets or users) + /// + [JsonPropertyName("type")] + public string? JobType { get; set; } + + /// + /// Job can be resumed + /// + [JsonPropertyName("resumable")] + public bool Resumable { get; set; } } } diff --git a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJobCreate.cs b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJobCreate.cs new file mode 100644 index 00000000..d859ad8a --- /dev/null +++ b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJobCreate.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace LinqToTwitter.Compliance +{ + public record ComplianceJobCreate + { + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("resumable")] + public bool Resumable { get; set; } + + } +} diff --git a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJobType.cs b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJobType.cs new file mode 100644 index 00000000..966f5090 --- /dev/null +++ b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceJobType.cs @@ -0,0 +1,9 @@ +namespace LinqToTwitter +{ + public class ComplianceJobType + { + public const string Tweets = "tweets"; + + public const string Users = "users"; + } +} diff --git a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceQuery.cs b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceQuery.cs index 1cab0c3e..077de6f3 100644 --- a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceQuery.cs +++ b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceQuery.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace LinqToTwitter @@ -15,20 +14,15 @@ public record ComplianceQuery /// public ComplianceType Type { get; init; } - /// - /// UTC date/time to search to - /// - public DateTime EndTime { get; init; } - /// /// ID for a single job query /// public string? ID { get; set; } /// - /// Date to search from + /// Type of compliance job to query (tweets or users) /// - public DateTime StartTime { get; init; } + public string? JobType { get; set; } /// /// Comma-separated list of job statuses @@ -42,7 +36,7 @@ public record ComplianceQuery /// /// Compliance job data returned from the search /// - [JsonPropertyName("jobs")] + [JsonPropertyName("data")] public List? Jobs { get; init; } } } diff --git a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceQuerySingle.cs b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceQuerySingle.cs new file mode 100644 index 00000000..56b96461 --- /dev/null +++ b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceQuerySingle.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace LinqToTwitter +{ + public record ComplianceQuerySingle + { + // + // Query input fields + // + + /// + /// type of compliance job query + /// + public ComplianceType Type { get; init; } + + /// + /// ID for a single job query + /// + public string? ID { get; set; } + + /// + /// Type of compliance job to query (tweets or users) + /// + public string? JobType { get; set; } + + /// + /// Comma-separated list of job statuses + /// + public string? Status { get; init; } + + // + // Output results + // + + /// + /// Compliance job data returned from the search + /// + [JsonPropertyName("data")] + public ComplianceJob? Job { get; init; } + } +} diff --git a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceRequestProcessor.cs b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceRequestProcessor.cs index c1edcb09..e97bf140 100644 --- a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceRequestProcessor.cs +++ b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceRequestProcessor.cs @@ -21,24 +21,19 @@ public class ComplianceRequestProcessor : IRequestProcessor, IRequestProce public string? BaseUrl { get; set; } /// - /// type of tweet + /// type of compliance job /// public ComplianceType Type { get; set; } - /// - /// UTC date/time to search to - /// - public DateTime EndTime { get; set; } - /// /// ID for a single job query /// public string? ID { get; set; } /// - /// Date to search from + /// Type of compliance job to query (tweets or users) /// - public DateTime StartTime { get; set; } + public string? JobType { get; set; } /// /// Comma-separated list of job statuses - @@ -57,9 +52,8 @@ public Dictionary GetParameters(LambdaExpression lambdaExpressio lambdaExpression.Body, new List { nameof(Type), - nameof(EndTime), nameof(ID), - nameof(StartTime), + nameof(JobType), nameof(Status) }) ; @@ -98,27 +92,23 @@ public Request BuildUrl(Dictionary parameters) /// base url + parameters Request BuildMultipleJobsUrlParameters(Dictionary parameters) { - var req = new Request(BaseUrl + "tweets/compliance/jobs"); + var req = new Request(BaseUrl + "compliance/jobs"); var urlParams = req.RequestParameters; - - if (parameters.ContainsKey(nameof(EndTime))) + if (parameters.ContainsKey(nameof(JobType))) { - EndTime = DateTime.Parse(parameters[nameof(EndTime)]); - urlParams.Add(new QueryParameter("end_time", EndTime.ToString(L2TKeys.ISO8601, CultureInfo.InvariantCulture))); + JobType = parameters[nameof(JobType)]; + urlParams.Add(new QueryParameter("type", JobType)); } - - - if (parameters.ContainsKey(nameof(StartTime))) + else { - StartTime = DateTime.Parse(parameters[nameof(StartTime)]); - urlParams.Add(new QueryParameter("start_time", StartTime.ToString(L2TKeys.ISO8601, CultureInfo.InvariantCulture))); + throw new ArgumentException($"{nameof(JobType)} is required", nameof(JobType)); } if (parameters.ContainsKey(nameof(Status))) { Status = parameters[nameof(Status)]; - urlParams.Add(new QueryParameter("status", Status.Replace(" ", ""))); + urlParams.Add(new QueryParameter("status", Status)); } return req; @@ -136,7 +126,7 @@ Request BuildSingleJobUrl(Dictionary parameters) else throw new ArgumentException($"{nameof(ID)} is required", nameof(ID)); - var req = new Request($"{BaseUrl}tweets/compliance/jobs/{ID}"); + var req = new Request($"{BaseUrl}compliance/jobs/{ID}"); return req; } @@ -176,14 +166,12 @@ ComplianceQuery JsonDeserialize(string responseJson) } else { - complianceQuery = new ComplianceQuery(); - - ComplianceJob? job = JsonSerializer.Deserialize(responseJson, options); - if (job is not null) + ComplianceQuerySingle? singleQuery = JsonSerializer.Deserialize(responseJson, options); + if (singleQuery?.Job is not null) { - complianceQuery = complianceQuery with + complianceQuery = new ComplianceQuery { - Jobs = new List { job } + Jobs = new List { singleQuery.Job } }; }; } @@ -192,18 +180,16 @@ ComplianceQuery JsonDeserialize(string responseJson) return new ComplianceQuery { Type = Type, - EndTime = EndTime, ID = ID, - StartTime = StartTime, + JobType = JobType, Status = Status }; else return complianceQuery with { Type = Type, - EndTime = EndTime, ID = ID, - StartTime = StartTime, + JobType = JobType, Status = Status }; } diff --git a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceStatus.cs b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceStatus.cs index c370b487..82047cd8 100644 --- a/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceStatus.cs +++ b/src/LinqToTwitter6/LinqToTwitter/Compliance/ComplianceStatus.cs @@ -2,14 +2,12 @@ { public class ComplianceStatus { - public const string All = "all"; + public const string Created = "created"; public const string InProgress = "in_progress"; public const string Failed = "failed"; public const string Complete = "complete"; - - public const string Expired = "expired"; } } diff --git a/src/LinqToTwitter6/LinqToTwitter/Compliance/TwitterContextComplianceCommands.cs b/src/LinqToTwitter6/LinqToTwitter/Compliance/TwitterContextComplianceCommands.cs index 6be9c4e9..fef25d18 100644 --- a/src/LinqToTwitter6/LinqToTwitter/Compliance/TwitterContextComplianceCommands.cs +++ b/src/LinqToTwitter6/LinqToTwitter/Compliance/TwitterContextComplianceCommands.cs @@ -1,4 +1,4 @@ -using System; +using LinqToTwitter.Compliance; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -12,29 +12,34 @@ public partial class TwitterContext /// /// Creates a new compliance job /// - /// - /// + /// Type of job - e.g. tweets or users + /// Name of job + /// Allows resuming uploads /// Optional cancellation token - /// New details - public async Task CreateComplianceJobAsync(string jobName, bool resumable, CancellationToken cancelToken = default) + /// New details + public async Task CreateComplianceJobAsync(string jobType, string jobName, bool resumable, CancellationToken cancelToken = default) { - string url = $"{BaseUrl2}tweets/compliance/jobs"; + string url = $"{BaseUrl2}compliance/jobs"; - var postData = new Dictionary + var postData = new Dictionary(); + + var postObj = new ComplianceJobCreate { - { "job_name", jobName }, - { "resumable", resumable ? "true" : "false" } + Type = jobType, + Name = jobName, + Resumable = resumable, }; RawResult = - await TwitterExecutor.PostFormUrlEncodedToTwitterAsync( + await TwitterExecutor.SendJsonToTwitterAsync( HttpMethod.Post.ToString(), url, postData, + postObj, cancelToken) .ConfigureAwait(false); - ComplianceJob? job = JsonSerializer.Deserialize(RawResult); + ComplianceQuerySingle? job = JsonSerializer.Deserialize(RawResult); return job; }