diff --git a/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptUndoFollow.cs b/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptUndoFollow.cs new file mode 100644 index 0000000..9453f25 --- /dev/null +++ b/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptUndoFollow.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace BirdsiteLive.ActivityPub +{ + public class ActivityAcceptUndoFollow : Activity + { + [JsonProperty("object")] + public ActivityUndoFollow apObject { get; set; } + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Domain/BusinessUseCases/ProcessUnfollowUser.cs b/src/BirdsiteLive.Domain/BusinessUseCases/ProcessUnfollowUser.cs index fe4035f..4d5483a 100644 --- a/src/BirdsiteLive.Domain/BusinessUseCases/ProcessUnfollowUser.cs +++ b/src/BirdsiteLive.Domain/BusinessUseCases/ProcessUnfollowUser.cs @@ -1,7 +1,45 @@ -namespace BirdsiteLive.Domain.BusinessUseCases +using System.Threading.Tasks; +using BirdsiteLive.DAL.Contracts; + +namespace BirdsiteLive.Domain.BusinessUseCases { - public class ProcessUnfollowUser + public interface IProcessUndoFollowUser { - + Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername); + } + + public class ProcessUndoFollowUser : IProcessUndoFollowUser + { + private readonly IFollowersDal _followerDal; + private readonly ITwitterUserDal _twitterUserDal; + + #region Ctor + public ProcessUndoFollowUser(IFollowersDal followerDal, ITwitterUserDal twitterUserDal) + { + _followerDal = followerDal; + _twitterUserDal = twitterUserDal; + } + #endregion + + public async Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername) + { + // Get Follower and Twitter Users + var follower = await _followerDal.GetFollowerAsync(followerUsername, followerDomain); + if (follower == null) return; + + var twitterUser = await _twitterUserDal.GetTwitterUserAsync(twitterUsername); + if (twitterUser == null) return; + + // Update Follower + var twitterUserId = twitterUser.Id; + if (follower.Followings.Contains(twitterUserId)) + follower.Followings.Remove(twitterUserId); + + if (follower.FollowingsSyncStatus.ContainsKey(twitterUserId)) + follower.FollowingsSyncStatus.Remove(twitterUserId); + + // Save Follower + await _followerDal.UpdateFollowerAsync(follower); + } } } \ No newline at end of file diff --git a/src/BirdsiteLive.Domain/UserService.cs b/src/BirdsiteLive.Domain/UserService.cs index c6ead30..fe9b627 100644 --- a/src/BirdsiteLive.Domain/UserService.cs +++ b/src/BirdsiteLive.Domain/UserService.cs @@ -18,24 +18,27 @@ namespace BirdsiteLive.Domain public interface IUserService { Actor GetUser(TwitterUser twitterUser); - Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity); Note GetStatus(TwitterUser user, ITweet tweet); + Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity); + Task UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityUndoFollow activity); } public class UserService : IUserService { private readonly IProcessFollowUser _processFollowUser; + private readonly IProcessUndoFollowUser _processUndoFollowUser; private readonly ICryptoService _cryptoService; private readonly IActivityPubService _activityPubService; private readonly string _host; #region Ctor - public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser) + public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser) { _cryptoService = cryptoService; _activityPubService = activityPubService; _processFollowUser = processFollowUser; + _processUndoFollowUser = processUndoFollowUser; _host = $"https://{instanceSettings.Domain.Replace("https://",string.Empty).Replace("http://", string.Empty).TrimEnd('/')}"; } #endregion @@ -104,6 +107,8 @@ namespace BirdsiteLive.Domain return note; } + + public async Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity) { // Validate @@ -118,7 +123,6 @@ namespace BirdsiteLive.Domain await _processFollowUser.ExecuteAsync(followerUserName, followerHost, followerInbox, twitterUser); // Send Accept Activity - //var followerHost = activity.actor.Replace("https://", string.Empty).Split('/').First(); var acceptFollow = new ActivityAcceptFollow() { context = "https://www.w3.org/ns/activitystreams", @@ -136,7 +140,40 @@ namespace BirdsiteLive.Domain var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject); return result == HttpStatusCode.Accepted; } - + + public async Task UndoFollowRequestedAsync(string signature, string method, string path, string queryString, + Dictionary requestHeaders, ActivityUndoFollow activity) + { + // Validate + var sigValidation = await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders); + if (!sigValidation.SignatureIsValidated) return false; + + // Save Follow in DB + var followerUserName = sigValidation.User.name.ToLowerInvariant(); + var followerHost = sigValidation.User.url.Replace("https://", string.Empty).Split('/').First(); + //var followerInbox = sigValidation.User.inbox; + var twitterUser = activity.apObject.apObject.Split('/').Last().Replace("@", string.Empty); + await _processUndoFollowUser.ExecuteAsync(followerUserName, followerHost, twitterUser); + + // Send Accept Activity + var acceptFollow = new ActivityAcceptUndoFollow() + { + context = "https://www.w3.org/ns/activitystreams", + id = $"{activity.apObject.apObject}#accepts/undofollows/{Guid.NewGuid()}", + type = "Accept", + actor = activity.apObject.apObject, + apObject = new ActivityUndoFollow() + { + id = activity.id, + type = activity.type, + actor = activity.actor, + apObject = activity.apObject + } + }; + var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject.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/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs index 2430cae..5c756d4 100644 --- a/src/BirdsiteLive/Controllers/UsersController.cs +++ b/src/BirdsiteLive/Controllers/UsersController.cs @@ -54,9 +54,9 @@ namespace BirdsiteLive.Controllers { if (!long.TryParse(statusId, out var parsedStatusId)) return NotFound(); - + var tweet = _twitterService.GetTweet(parsedStatusId); - if(tweet == null) + if (tweet == null) return NotFound(); var user = _twitterService.GetUser(id); @@ -80,15 +80,25 @@ namespace BirdsiteLive.Controllers var body = await reader.ReadToEndAsync(); var activity = ApDeserializer.ProcessActivity(body); // Do something + var signature = r.Headers["Signature"].First(); switch (activity?.type) { 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(); - break; + { + var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path, + r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow); + if (succeeded) return Accepted(); + else return Unauthorized(); + } case "Undo": + if (activity is ActivityUndoFollow) + { + var succeeded = await _userService.UndoFollowRequestedAsync(signature, r.Method, r.Path, + r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityUndoFollow); + if (succeeded) return Accepted(); + else return Unauthorized(); + } return Accepted(); default: return Accepted();