diff --git a/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs index 3a36be3..3d29d67 100644 --- a/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs +++ b/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs @@ -9,6 +9,7 @@ using BirdsiteLive.Moderation.Actions; using BirdsiteLive.Pipeline.Contracts; using BirdsiteLive.Pipeline.Models; using BirdsiteLive.Twitter; +using BirdsiteLive.Twitter.Models; namespace BirdsiteLive.Pipeline.Processors { @@ -35,26 +36,58 @@ namespace BirdsiteLive.Pipeline.Processors foreach (var user in syncTwitterUsers) { - var userView = _twitterUserService.GetUser(user.Acct); - if (userView == null) - { - await AnalyseFailingUserAsync(user); - } - else if (!userView.Protected) - { - user.FetchingErrorCount = 0; - var userWtData = new UserWithDataToSync - { - User = user - }; - usersWtData.Add(userWtData); - } - } + TwitterUser userView = null; + try + { + userView = _twitterUserService.GetUser(user.Acct); + } + catch (UserNotFoundException) + { + await ProcessNotFoundUserAsync(user); + } + catch (UserHasBeenSuspendedException) + { + await ProcessNotFoundUserAsync(user); + } + catch (RateLimitExceededException) + { + await ProcessRateLimitExceededAsync(user); + } + catch (Exception) + { + // ignored + } + + if (userView == null || userView.Protected) + { + await ProcessFailingUserAsync(user); + continue; + } + + user.FetchingErrorCount = 0; + var userWtData = new UserWithDataToSync + { + User = user + }; + usersWtData.Add(userWtData); + } return usersWtData.ToArray(); } - private async Task AnalyseFailingUserAsync(SyncTwitterUser user) + private async Task ProcessRateLimitExceededAsync(SyncTwitterUser user) + { + var dbUser = await _twitterUserDal.GetTwitterUserAsync(user.Acct); + dbUser.LastSync = DateTime.UtcNow; + await _twitterUserDal.UpdateTwitterUserAsync(dbUser); + } + + private async Task ProcessNotFoundUserAsync(SyncTwitterUser user) + { + await _removeTwitterAccountAction.ProcessAsync(user); + } + + private async Task ProcessFailingUserAsync(SyncTwitterUser user) { var dbUser = await _twitterUserDal.GetTwitterUserAsync(user.Acct); dbUser.FetchingErrorCount++; @@ -68,9 +101,6 @@ namespace BirdsiteLive.Pipeline.Processors { await _twitterUserDal.UpdateTwitterUserAsync(dbUser); } - - // Purge - _twitterUserService.PurgeUser(user.Acct); } } } \ No newline at end of file diff --git a/src/BirdsiteLive.Twitter/Exceptions/RateLimitExceededException.cs b/src/BirdsiteLive.Twitter/Exceptions/RateLimitExceededException.cs new file mode 100644 index 0000000..93a093a --- /dev/null +++ b/src/BirdsiteLive.Twitter/Exceptions/RateLimitExceededException.cs @@ -0,0 +1,9 @@ +using System; + +namespace BirdsiteLive.Twitter +{ + public class RateLimitExceededException : Exception + { + + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Twitter/Exceptions/UserHasBeenSuspendedException.cs b/src/BirdsiteLive.Twitter/Exceptions/UserHasBeenSuspendedException.cs new file mode 100644 index 0000000..03bd835 --- /dev/null +++ b/src/BirdsiteLive.Twitter/Exceptions/UserHasBeenSuspendedException.cs @@ -0,0 +1,9 @@ +using System; + +namespace BirdsiteLive.Twitter +{ + public class UserHasBeenSuspendedException : Exception + { + + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Twitter/Exceptions/UserNotFoundException.cs b/src/BirdsiteLive.Twitter/Exceptions/UserNotFoundException.cs new file mode 100644 index 0000000..1dffc72 --- /dev/null +++ b/src/BirdsiteLive.Twitter/Exceptions/UserNotFoundException.cs @@ -0,0 +1,9 @@ +using System; + +namespace BirdsiteLive.Twitter +{ + public class UserNotFoundException : Exception + { + + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Twitter/TwitterUserService.cs b/src/BirdsiteLive.Twitter/TwitterUserService.cs index adc8d6b..7fb6439 100644 --- a/src/BirdsiteLive.Twitter/TwitterUserService.cs +++ b/src/BirdsiteLive.Twitter/TwitterUserService.cs @@ -6,6 +6,7 @@ using BirdsiteLive.Twitter.Models; using BirdsiteLive.Twitter.Tools; using Microsoft.Extensions.Logging; using Tweetinvi; +using Tweetinvi.Exceptions; using Tweetinvi.Models; namespace BirdsiteLive.Twitter @@ -45,17 +46,34 @@ namespace BirdsiteLive.Twitter try { user = User.GetUserFromScreenName(username); - _statisticsHandler.CalledUserApi(); - if (user == null) + } + catch (TwitterException e) + { + if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User has been suspended".ToLowerInvariant()))) { - _logger.LogWarning("User {username} not found", username); - return null; + throw new UserHasBeenSuspendedException(); + } + else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User not found".ToLowerInvariant()))) + { + throw new UserNotFoundException(); + } + else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("Rate limit exceeded".ToLowerInvariant()))) + { + throw new RateLimitExceededException(); + } + else + { + throw; } } catch (Exception e) { _logger.LogError(e, "Error retrieving user {Username}", username); - return null; + throw; + } + finally + { + _statisticsHandler.CalledUserApi(); } // Expand URLs diff --git a/src/BirdsiteLive/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs index 965f988..f59ae21 100644 --- a/src/BirdsiteLive/Controllers/UsersController.cs +++ b/src/BirdsiteLive/Controllers/UsersController.cs @@ -66,13 +66,42 @@ namespace BirdsiteLive.Controllers id = id.Trim(new[] { ' ', '@' }).ToLowerInvariant(); + TwitterUser user = null; + var isSaturated = false; + var notFound = false; + // Ensure valid username // https://help.twitter.com/en/managing-your-account/twitter-username-rules - TwitterUser user = null; if (!string.IsNullOrWhiteSpace(id) && UserRegexes.TwitterAccount.IsMatch(id) && id.Length <= 15) - user = _twitterUserService.GetUser(id); + { + try + { + user = _twitterUserService.GetUser(id); + } + catch (UserNotFoundException) + { + notFound = true; + } + catch (UserHasBeenSuspendedException) + { + notFound = true; + } + catch (RateLimitExceededException) + { + isSaturated = true; + } + catch (Exception e) + { + _logger.LogError(e, "Exception getting {Id}", id); + throw; + } + } + else + { + notFound = true; + } - var isSaturated = _twitterUserService.IsUserApiRateLimited(); + //var isSaturated = _twitterUserService.IsUserApiRateLimited(); var acceptHeaders = Request.Headers["Accept"]; if (acceptHeaders.Any()) @@ -80,17 +109,17 @@ namespace BirdsiteLive.Controllers var r = acceptHeaders.First(); if (r.Contains("application/activity+json")) { - if (user == null && isSaturated) return new ObjectResult("Too Many Requests") { StatusCode = 429 }; - if (user == null) return NotFound(); + if (isSaturated) return new ObjectResult("Too Many Requests") { StatusCode = 429 }; + if (notFound) return NotFound(); var apUser = _userService.GetUser(user); var jsonApUser = JsonConvert.SerializeObject(apUser); return Content(jsonApUser, "application/activity+json; charset=utf-8"); } } - if (user == null && isSaturated) return View("ApiSaturated"); - if (user == null) return View("UserNotFound"); - + if (isSaturated) return View("ApiSaturated"); + if (notFound) return View("UserNotFound"); + var displayableUser = new DisplayTwitterUser { Name = user.Name, @@ -138,46 +167,61 @@ namespace BirdsiteLive.Controllers [HttpPost] public async Task Inbox() { - var r = Request; - using (var reader = new StreamReader(Request.Body)) + try { - var body = await reader.ReadToEndAsync(); - - _logger.LogTrace("User Inbox: {Body}", body); - //System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body); - - var activity = ApDeserializer.ProcessActivity(body); - var signature = r.Headers["Signature"].First(); - - switch (activity?.type) + var r = Request; + using (var reader = new StreamReader(Request.Body)) { - case "Follow": - { - var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path, - r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityFollow, body); - 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(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityUndoFollow, body); - if (succeeded) return Accepted(); - else return Unauthorized(); - } - return Accepted(); - case "Delete": - { - var succeeded = await _userService.DeleteRequestedAsync(signature, r.Method, r.Path, - r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityDelete, body); - if (succeeded) return Accepted(); - else return Unauthorized(); - } - default: - return Accepted(); + var body = await reader.ReadToEndAsync(); + + _logger.LogTrace("User Inbox: {Body}", body); + //System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body); + + var activity = ApDeserializer.ProcessActivity(body); + var signature = r.Headers["Signature"].First(); + + switch (activity?.type) + { + case "Follow": + { + var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path, + r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityFollow, body); + 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(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityUndoFollow, body); + if (succeeded) return Accepted(); + else return Unauthorized(); + } + return Accepted(); + case "Delete": + { + var succeeded = await _userService.DeleteRequestedAsync(signature, r.Method, r.Path, + r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityDelete, body); + if (succeeded) return Accepted(); + else return Unauthorized(); + } + default: + return Accepted(); + } } } + catch (UserNotFoundException) + { + return NotFound(); + } + catch (UserHasBeenSuspendedException) + { + return NotFound(); + } + catch (RateLimitExceededException) + { + return new ObjectResult("Too Many Requests") { StatusCode = 429 }; + } } [Route("/users/{id}/followers")] diff --git a/src/BirdsiteLive/Controllers/WellKnownController.cs b/src/BirdsiteLive/Controllers/WellKnownController.cs index 501f783..272d789 100644 --- a/src/BirdsiteLive/Controllers/WellKnownController.cs +++ b/src/BirdsiteLive/Controllers/WellKnownController.cs @@ -12,6 +12,7 @@ using BirdsiteLive.Models; using BirdsiteLive.Models.WellKnownModels; using BirdsiteLive.Twitter; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BirdsiteLive.Controllers @@ -23,13 +24,15 @@ namespace BirdsiteLive.Controllers private readonly ITwitterUserService _twitterUserService; private readonly ITwitterUserDal _twitterUserDal; private readonly InstanceSettings _settings; - + private readonly ILogger _logger; + #region Ctor - public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository) + public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository, ILogger logger) { _twitterUserService = twitterUserService; _twitterUserDal = twitterUserDal; _moderationRepository = moderationRepository; + _logger = logger; _settings = settings; } #endregion @@ -174,9 +177,27 @@ namespace BirdsiteLive.Controllers if (!string.IsNullOrWhiteSpace(domain) && domain != _settings.Domain) return NotFound(); - var user = _twitterUserService.GetUser(name); - if (user == null) + try + { + _twitterUserService.GetUser(name); + } + catch (UserNotFoundException) + { return NotFound(); + } + catch (UserHasBeenSuspendedException) + { + return NotFound(); + } + catch (RateLimitExceededException) + { + return new ObjectResult("Too Many Requests") { StatusCode = 429 }; + } + catch (Exception e) + { + _logger.LogError(e, "Exception getting {Name}", name); + throw; + } var actorUrl = UrlFactory.GetActorUrl(_settings.Domain, name);