added proper exception in user retrieval

This commit is contained in:
Nicolas Constant 2022-02-07 18:48:10 -05:00
parent 446b222881
commit 662f97e53c
No known key found for this signature in database
GPG key ID: 1E9F677FB01A5688
7 changed files with 212 additions and 72 deletions

View file

@ -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);
}
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class RateLimitExceededException : Exception
{
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class UserHasBeenSuspendedException : Exception
{
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class UserNotFoundException : Exception
{
}
}

View file

@ -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

View file

@ -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<IActionResult> 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")]

View file

@ -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<WellKnownController> _logger;
#region Ctor
public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository)
public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository, ILogger<WellKnownController> 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);