diff --git a/src/BirdsiteLive.ActivityPub/ApDeserializer.cs b/src/BirdsiteLive.ActivityPub/ApDeserializer.cs index ba60c29..17fa3a0 100644 --- a/src/BirdsiteLive.ActivityPub/ApDeserializer.cs +++ b/src/BirdsiteLive.ActivityPub/ApDeserializer.cs @@ -56,10 +56,5 @@ namespace BirdsiteLive.ActivityPub return null; } - private class Ac : Activity - { - [JsonPropertyName("object")] - public Activity apObject { get; set; } - } } } \ No newline at end of file diff --git a/src/BirdsiteLive.ActivityPub/Models/Activity.cs b/src/BirdsiteLive.ActivityPub/Models/Activity.cs index e6826fa..0e21b48 100644 --- a/src/BirdsiteLive.ActivityPub/Models/Activity.cs +++ b/src/BirdsiteLive.ActivityPub/Models/Activity.cs @@ -5,7 +5,7 @@ namespace BirdsiteLive.ActivityPub public class Activity { [JsonPropertyName("@context")] - public object context { get; set; } + public string context { get; set; } public string id { get; set; } public string type { get; set; } public string actor { get; set; } diff --git a/src/BirdsiteLive.ActivityPub/Models/NestedActivity.cs b/src/BirdsiteLive.ActivityPub/Models/NestedActivity.cs index f23022c..9117311 100644 --- a/src/BirdsiteLive.ActivityPub/Models/NestedActivity.cs +++ b/src/BirdsiteLive.ActivityPub/Models/NestedActivity.cs @@ -5,6 +5,7 @@ namespace BirdsiteLive.ActivityPub public class NestedActivity { [JsonPropertyName("@context")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public object context { get; set; } public string id { get; set; } public string type { get; set; } diff --git a/src/BirdsiteLive.Domain/ActivityPubService.cs b/src/BirdsiteLive.Domain/ActivityPubService.cs index 914eae6..3a3137c 100644 --- a/src/BirdsiteLive.Domain/ActivityPubService.cs +++ b/src/BirdsiteLive.Domain/ActivityPubService.cs @@ -21,6 +21,8 @@ namespace BirdsiteLive.Domain Task PostDataAsync(T data, string targetHost, string actorUrl, string inbox = null); Task PostNewActivity(ActivityCreateNote note, string username, string noteId, string targetHost, string targetInbox); + + ActivityAcceptFollow BuildAcceptFollow(ActivityFollow activity); } public class ActivityPubService : IActivityPubService @@ -73,13 +75,33 @@ namespace BirdsiteLive.Domain } } - public async Task PostDataAsync(T data, string targetHost, string actorUrl, string inbox = null) + public ActivityAcceptFollow BuildAcceptFollow(ActivityFollow activity) + { + var acceptFollow = new ActivityAcceptFollow() + { + context = "https://www.w3.org/ns/activitystreams", + id = $"{activity.apObject}#accepts/follows/{Guid.NewGuid()}", + type = "Accept", + actor = activity.apObject, + apObject = new NestedActivity() + { + id = activity.id, + type = activity.type, + actor = activity.actor, + apObject = activity.apObject + } + }; + return acceptFollow; + } + public HttpRequestMessage BuildRequest(T data, string targetHost, string actorUrl, + string inbox = null) { var usedInbox = $"/inbox"; if (!string.IsNullOrWhiteSpace(inbox)) usedInbox = inbox; - var json = JsonSerializer.Serialize(data, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); + var json = JsonSerializer.Serialize(data, + new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); var date = DateTime.UtcNow.ToUniversalTime(); var httpDate = date.ToString("r"); @@ -88,26 +110,33 @@ namespace BirdsiteLive.Domain var signature = _cryptoService.SignAndGetSignatureHeader(date, actorUrl, targetHost, digest, usedInbox); - var client = _httpClientFactory.CreateClient(); - client.Timeout = TimeSpan.FromSeconds(2); var httpRequestMessage = new HttpRequestMessage { Method = HttpMethod.Post, RequestUri = new Uri($"https://{targetHost}{usedInbox}"), Headers = { - {"Host", targetHost}, - {"Date", httpDate}, - {"Signature", signature}, - {"Digest", $"SHA-256={digest}"} + { "Host", targetHost }, + { "Date", httpDate }, + { "Signature", signature }, + { "Digest", $"SHA-256={digest}" } }, Content = new StringContent(json, Encoding.UTF8, "application/ld+json") }; + return httpRequestMessage; + } + + public async Task PostDataAsync(T data, string targetHost, string actorUrl, string inbox = null) + { + var httpRequestMessage = BuildRequest(data, targetHost, actorUrl, inbox); + + var client = _httpClientFactory.CreateClient(); + client.Timeout = TimeSpan.FromSeconds(2); + var response = await client.SendAsync(httpRequestMessage); response.EnsureSuccessStatusCode(); _logger.LogInformation("Sent tweet to " + targetHost); - _logger.LogInformation("Tweet content is " + json); return response.StatusCode; } diff --git a/src/BirdsiteLive.Domain/UserService.cs b/src/BirdsiteLive.Domain/UserService.cs index ab248e4..2b38d90 100644 --- a/src/BirdsiteLive.Domain/UserService.cs +++ b/src/BirdsiteLive.Domain/UserService.cs @@ -182,20 +182,7 @@ namespace BirdsiteLive.Domain private async Task SendAcceptFollowAsync(ActivityFollow activity, string followerHost) { - var acceptFollow = new ActivityAcceptFollow() - { - context = "https://www.w3.org/ns/activitystreams", - id = $"{activity.apObject}#accepts/follows/{Guid.NewGuid()}", - type = "Accept", - actor = activity.apObject, - apObject = new NestedActivity() - { - id = activity.id, - type = activity.type, - actor = activity.actor, - apObject = activity.apObject - } - }; + var acceptFollow = _activityPubService.BuildAcceptFollow(activity); var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject); return result == HttpStatusCode.Accepted || result == HttpStatusCode.OK; //TODO: revamp this for better error handling diff --git a/src/Tests/BirdsiteLive.ActivityPub.Tests/ActivityTests.cs b/src/Tests/BirdsiteLive.ActivityPub.Tests/ActivityTests.cs index 1687ac4..6dba0de 100644 --- a/src/Tests/BirdsiteLive.ActivityPub.Tests/ActivityTests.cs +++ b/src/Tests/BirdsiteLive.ActivityPub.Tests/ActivityTests.cs @@ -3,31 +3,21 @@ using Newtonsoft.Json; namespace BirdsiteLive.ActivityPub.Tests { - //[TestClass] - //public class ActivityTests - //{ - // [TestMethod] - // public void FollowDeserializationTest() - // { - // var json = "{ \"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe\",\"type\":\"Follow\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":\"https://4a120ca2680e.ngrok.io/users/manu\"}"; + [TestClass] + public class ActivityTests + { + [TestMethod] + public void Serialize() + { + var obj = new Actor + { + type = "Person", + id = "id" + }; - // var data = JsonConvert.DeserializeObject(json); + var json = JsonConvert.SerializeObject(obj); - // Assert.AreEqual("https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe", data.id); - // Assert.AreEqual("Follow", data.type); - // Assert.AreEqual("https://4a120ca2680e.ngrok.io/users/manu", data.apObject); - // } - // [TestMethod] - // public void UndoDeserializationTest() - // { - // var json = - // "{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.technology/users/testtest#follows/225982/undo\",\"type\":\"Undo\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":{\"id\":\"https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe\",\"type\":\"Follow\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":\"https://4a120ca2680e.ngrok.io/users/manu\"}}"; - - // var data = JsonConvert.DeserializeObject(json); - // Assert.AreEqual("https://mastodon.technology/users/testtest#follows/225982/undo", data.id); - // Assert.AreEqual("Undo", data.type); - // Assert.AreEqual("https://4a120ca2680e.ngrok.io/users/manu", data.apObject); - // } - //} + } + } } diff --git a/src/Tests/BirdsiteLive.ActivityPub.Tests/ApDeserializerTests.cs b/src/Tests/BirdsiteLive.ActivityPub.Tests/ApDeserializerTests.cs index 3d64e90..f583d7f 100644 --- a/src/Tests/BirdsiteLive.ActivityPub.Tests/ApDeserializerTests.cs +++ b/src/Tests/BirdsiteLive.ActivityPub.Tests/ApDeserializerTests.cs @@ -1,6 +1,6 @@ using BirdsiteLive.ActivityPub.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; +using System.Text.Json; namespace BirdsiteLive.ActivityPub.Tests { @@ -28,9 +28,11 @@ namespace BirdsiteLive.ActivityPub.Tests var data = ApDeserializer.ProcessActivity(json) as ActivityUndoFollow; Assert.AreEqual("https://mastodon.technology/users/testtest#follows/225982/undo", data.id); Assert.AreEqual("Undo", data.type); + Assert.AreEqual("https://www.w3.org/ns/activitystreams", data.context); Assert.AreEqual("Follow", data.apObject.type); Assert.AreEqual("https://mastodon.technology/users/testtest", data.apObject.actor); Assert.AreEqual("https://4a120ca2680e.ngrok.io/users/manu", data.apObject.apObject); + Assert.AreEqual(null, data.apObject.context); } [TestMethod] @@ -62,6 +64,9 @@ namespace BirdsiteLive.ActivityPub.Tests Assert.AreEqual("https://mastodon.technology/users/deleteduser", data.actor); Assert.AreEqual("https://mastodon.technology/users/deleteduser", data.apObject); } + // {"object":{"object":"https://bird.makeup/users/spectatorindex","id":"https://masto.ai/b89eb86e-c902-48bc-956f-94f081617f18","type":"Follow","actor":"https://masto.ai/users/singha"},"@context":"https://www.w3.org/ns/activitystreams","id":"https://bird.makeup/users/spectatorindex#accepts/follows/27363118-e61e-4710-a41c-75dd5d54912f","type":"Accept","actor":"https://bird.makeup/users/spectatorindex"} + // {"object":{"object":"https://bird.makeup/users/moltke","id":"https://universeodon.com/81cddd78-d7d6-4665-aa21-7bcfbea82b6b","type":"Follow","actor":"https://universeodon.com/users/amhrasmussen"},"@context":"https://www.w3.org/ns/activitystreams","id":"https://bird.makeup/users/moltke#accepts/follows/d28146be-e884-4e91-8385-19fa004f35b3","type":"Accept","actor":"https://bird.makeup/users/moltke"} + //[TestMethod] //public void NoteDeserializationTest() diff --git a/src/Tests/BirdsiteLive.Domain.Tests/ActivityServiceTests.cs b/src/Tests/BirdsiteLive.Domain.Tests/ActivityServiceTests.cs new file mode 100644 index 0000000..20525f6 --- /dev/null +++ b/src/Tests/BirdsiteLive.Domain.Tests/ActivityServiceTests.cs @@ -0,0 +1,80 @@ +using System.Net.Http; +using System.Threading.Tasks; +using BirdsiteLive.ActivityPub; +using BirdsiteLive.Common.Settings; +using BirdsiteLive.Domain.Factories; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + + +namespace BirdsiteLive.Domain.Tests +{ + [TestClass] + public class ActivityServiceTests + { + private readonly InstanceSettings _settings; + + #region Ctor + public ActivityServiceTests() + { + _settings = new InstanceSettings + { + Domain = "domain.name" + }; + } + #endregion + + [TestMethod] + public async Task ActivityTest() + { + var logger1 = new Mock>(); + var httpFactory = new Mock(); + var keyFactory = new Mock(); + var cryptoService = new CryptoService(keyFactory.Object); + httpFactory.Setup(_ => _.CreateClient(string.Empty)).Returns(new HttpClient()); + var service = new ActivityPubService(cryptoService, _settings, httpFactory.Object, logger1.Object); + + var activity = new ActivityAcceptFollow() + { + id = "awef", + }; + var json = "{\"id\":\"awef\"}"; + #region Validations + + var req = service.BuildRequest(activity, "google.com", "tata", "awef"); + + Assert.AreEqual(await req.Content.ReadAsStringAsync(), json); + + #endregion + } + [TestMethod] + public async Task AcceptFollow() + { + + + var logger1 = new Mock>(); + var httpFactory = new Mock(); + var keyFactory = new Mock(); + var cryptoService = new CryptoService(keyFactory.Object); + httpFactory.Setup(_ => _.CreateClient(string.Empty)).Returns(new HttpClient()); + var service = new ActivityPubService(cryptoService, _settings, httpFactory.Object, logger1.Object); + + var json = "{ \"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe\",\"type\":\"Follow\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":\"https://4a120ca2680e.ngrok.io/users/manu\"}"; + var activity = ApDeserializer.ProcessActivity(json) as ActivityFollow; + + var jsonres = + "{\"object\":{\"id\":\"https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe\",\"type\":\"Follow\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":\"https://4a120ca2680e.ngrok.io/users/manu\"},\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://4a120ca2680e.ngrok.io/users/manu#accepts/follows/32e5fbda-9159-4ede-8249-9d008092d26f\",\"type\":\"Accept\",\"actor\":\"https://4a120ca2680e.ngrok.io/users/manu\"}"; + var activityRes = ApDeserializer.ProcessActivity(jsonres) as ActivityAcceptFollow; + #region Validations + + var req = service.BuildAcceptFollow(activity); + + Assert.AreEqual(req.actor, activityRes.actor); + Assert.AreEqual(req.context, activityRes.context); + + #endregion + } + + } +}