diff --git a/src/BirdsiteLive.ActivityPub/Converters/ContextArrayConverter.cs b/src/BirdsiteLive.ActivityPub/Converters/ContextArrayConverter.cs new file mode 100644 index 0000000..21e139f --- /dev/null +++ b/src/BirdsiteLive.ActivityPub/Converters/ContextArrayConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace BirdsiteLive.ActivityPub.Converters +{ + public class ContextArrayConverter : JsonConverter + { + public override bool CanWrite { get { return false; } } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var result = new List(); + + var list = serializer.Deserialize>(reader); + foreach (var l in list) + { + if (l is string s) + result.Add(s); + else + { + var str = JsonConvert.SerializeObject(l); + result.Add(str); + } + } + + return result.ToArray(); + } + + public override bool CanConvert(Type objectType) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.ActivityPub/Models/Actor.cs b/src/BirdsiteLive.ActivityPub/Models/Actor.cs index cbebba1..59ea71f 100644 --- a/src/BirdsiteLive.ActivityPub/Models/Actor.cs +++ b/src/BirdsiteLive.ActivityPub/Models/Actor.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using BirdsiteLive.ActivityPub.Converters; using Newtonsoft.Json; -using JsonConverter = Newtonsoft.Json.JsonConverter; namespace BirdsiteLive.ActivityPub { @@ -22,37 +20,4 @@ namespace BirdsiteLive.ActivityPub public Image icon { get; set; } public Image image { get; set; } } - - public class ContextArrayConverter : JsonConverter - { - public override bool CanWrite { get { return false; } } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var result = new List(); - - var list = serializer.Deserialize>(reader); - foreach (var l in list) - { - if (l is string s) - result.Add(s); - else - { - var str = JsonConvert.SerializeObject(l); - result.Add(str); - } - } - - return result.ToArray(); - } - - public override bool CanConvert(Type objectType) - { - throw new NotImplementedException(); - } - } } diff --git a/src/BirdsiteLive.ActivityPub/Models/Note.cs b/src/BirdsiteLive.ActivityPub/Models/Note.cs index 0dc9c16..cc3d561 100644 --- a/src/BirdsiteLive.ActivityPub/Models/Note.cs +++ b/src/BirdsiteLive.ActivityPub/Models/Note.cs @@ -1,10 +1,16 @@ using System; using System.Collections.Generic; +using BirdsiteLive.ActivityPub.Converters; +using Newtonsoft.Json; namespace BirdsiteLive.ActivityPub { public class Note { + [JsonProperty("@context")] + [JsonConverter(typeof(ContextArrayConverter))] + public string[] context { get; set; } = new[] { "https://www.w3.org/ns/activitystreams" }; + public string id { get; set; } public string type { get; } = "Note"; public string summary { get; set; } diff --git a/src/BirdsiteLive.Domain/UserService.cs b/src/BirdsiteLive.Domain/UserService.cs index 5c569f5..d084e97 100644 --- a/src/BirdsiteLive.Domain/UserService.cs +++ b/src/BirdsiteLive.Domain/UserService.cs @@ -10,6 +10,7 @@ using BirdsiteLive.Common.Settings; using BirdsiteLive.Cryptography; using BirdsiteLive.Twitter.Models; using Tweetinvi.Core.Exceptions; +using Tweetinvi.Models; namespace BirdsiteLive.Domain { @@ -17,6 +18,7 @@ namespace BirdsiteLive.Domain { Actor GetUser(TwitterUser twitterUser); Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity); + Note GetStatus(TwitterUser user, ITweet tweet); } public class UserService : IUserService @@ -65,6 +67,39 @@ namespace BirdsiteLive.Domain return user; } + public Note GetStatus(TwitterUser user, ITweet tweet) + { + var actor = GetUser(user); + + var actorUrl = $"{_host}/users/{user.Acct}"; + var noteId = $"{_host}/users/{user.Acct}/statuses/{tweet.Id}"; + var noteUrl = $"{_host}/@{user.Acct}/{tweet.Id}"; + + var to = $"{actor}/followers"; + var apPublic = "https://www.w3.org/ns/activitystreams#Public"; + + var note = new Note + { + id = $"{noteId}/activity", + + published = tweet.CreatedAt.ToString("s") + "Z", + url = noteUrl, + attributedTo = actorUrl, + + //to = new [] {to}, + //cc = new [] { apPublic }, + + to = new[] { apPublic }, + cc = new[] { to }, + + sensitive = false, + content = $"

{tweet.Text}

", + attachment = new string[0], + tag = new string[0] + }; + return note; + } + public async Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity) { // Validate @@ -91,7 +126,7 @@ namespace BirdsiteLive.Domain var result = await _activityPubService.PostDataAsync(acceptFollow, targetHost, activity.apObject); return result == HttpStatusCode.Accepted; } - + private async Task ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary requestHeaders) { var signatures = rawSig.Split(','); diff --git a/src/BirdsiteLive.Twitter/TwitterService.cs b/src/BirdsiteLive.Twitter/TwitterService.cs index 3317496..3bec34a 100644 --- a/src/BirdsiteLive.Twitter/TwitterService.cs +++ b/src/BirdsiteLive.Twitter/TwitterService.cs @@ -3,12 +3,14 @@ using System.Threading.Tasks; using BirdsiteLive.Common.Settings; using BirdsiteLive.Twitter.Models; using Tweetinvi; +using Tweetinvi.Models; namespace BirdsiteLive.Twitter { public interface ITwitterService { TwitterUser GetUser(string username); + ITweet GetTweet(long statusId); } public class TwitterService : ITwitterService @@ -40,5 +42,12 @@ namespace BirdsiteLive.Twitter ProfileBannerURL = user.ProfileBannerURL }; } + + public ITweet GetTweet(long statusId) + { + Auth.SetApplicationOnlyCredentials(_settings.ConsumerKey, _settings.ConsumerSecret, true); + var tweet = Tweet.GetTweet(statusId); + return tweet; + } } } diff --git a/src/BirdsiteLive/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs index 05a6aac..b431aa0 100644 --- a/src/BirdsiteLive/Controllers/UsersController.cs +++ b/src/BirdsiteLive/Controllers/UsersController.cs @@ -33,7 +33,7 @@ namespace BirdsiteLive.Controllers { var user = _twitterService.GetUser(id); if (user == null) return NotFound(); - + var r = Request.Headers["Accept"].First(); if (r.Contains("application/activity+json")) { @@ -45,6 +45,31 @@ namespace BirdsiteLive.Controllers return View(user); } + [Route("/@{id}/{statusId}")] + [Route("/users/{id}/statuses/{statusId}")] + public IActionResult Tweet(string id, string statusId) + { + var r = Request.Headers["Accept"].First(); + if (r.Contains("application/activity+json")) + { + if (!long.TryParse(statusId, out var parsedStatusId)) + return NotFound(); + + var tweet = _twitterService.GetTweet(parsedStatusId); + if(tweet == null) + return NotFound(); + + var user = _twitterService.GetUser(id); + if (user == null) return NotFound(); + + var status = _userService.GetStatus(user, tweet); + var jsonApUser = JsonConvert.SerializeObject(status); + return Content(jsonApUser, "application/json"); + } + + return View("Tweet", statusId); + } + [Route("/users/{id}/inbox")] [HttpPost] public async Task Inbox() @@ -58,7 +83,7 @@ namespace BirdsiteLive.Controllers switch (activity.type) { - case "Follow": + case "Follow": var succeeded = await _userService.FollowRequestedAsync(r.Headers["Signature"].First(), r.Method, r.Path, r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow); if (succeeded) return Accepted(); else return Unauthorized(); diff --git a/src/BirdsiteLive/Properties/launchSettings.json b/src/BirdsiteLive/Properties/launchSettings.json index c94543d..4dcf5f1 100644 --- a/src/BirdsiteLive/Properties/launchSettings.json +++ b/src/BirdsiteLive/Properties/launchSettings.json @@ -21,7 +21,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + "applicationUrl": "http://localhost:5000" }, "Docker": { "commandName": "Docker", diff --git a/src/BirdsiteLive/Views/Users/Tweet.cshtml b/src/BirdsiteLive/Views/Users/Tweet.cshtml new file mode 100644 index 0000000..a1aa7fe --- /dev/null +++ b/src/BirdsiteLive/Views/Users/Tweet.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Tweet"; +} + +
+ + +
\ No newline at end of file