commit
ed3faab924
34 changed files with 1336 additions and 187 deletions
|
@ -49,6 +49,8 @@ If both whitelisting and blacklisting are set, only the whitelisting will be act
|
||||||
* `Instance:UnlistedTwitterAccounts` (default: null) to enable unlisted publication for selected twitter accounts, separated by `;` (please limit this to brands and other public profiles).
|
* `Instance:UnlistedTwitterAccounts` (default: null) to enable unlisted publication for selected twitter accounts, separated by `;` (please limit this to brands and other public profiles).
|
||||||
* `Instance:SensitiveTwitterAccounts` (default: null) mark all media from given accounts as sensitive by default, separated by `;`.
|
* `Instance:SensitiveTwitterAccounts` (default: null) mark all media from given accounts as sensitive by default, separated by `;`.
|
||||||
* `Instance:FailingTwitterUserCleanUpThreshold` (default: 700) set the max allowed errors (due to a banned/deleted/private account) from a Twitter Account retrieval before auto-removal. (by default an account is called every 15 mins)
|
* `Instance:FailingTwitterUserCleanUpThreshold` (default: 700) set the max allowed errors (due to a banned/deleted/private account) from a Twitter Account retrieval before auto-removal. (by default an account is called every 15 mins)
|
||||||
|
* `Instance:FailingFollowerCleanUpThreshold` (default: 30000) set the max allowed errors from a Follower (Fediverse) Account before auto-removal. (often due to account suppression, instance issues, etc)
|
||||||
|
* `Instance:UserCacheCapacity` (default: 10000) set the caching limit of the Twitter User retrieval. Must be higher than the number of synchronized accounts on the instance.
|
||||||
|
|
||||||
# Docker Compose full example
|
# Docker Compose full example
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using BirdsiteLive.ActivityPub.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BirdsiteLive.ActivityPub
|
namespace BirdsiteLive.ActivityPub
|
||||||
|
@ -19,6 +20,8 @@ namespace BirdsiteLive.ActivityPub
|
||||||
if(a.apObject.type == "Follow")
|
if(a.apObject.type == "Follow")
|
||||||
return JsonConvert.DeserializeObject<ActivityUndoFollow>(json);
|
return JsonConvert.DeserializeObject<ActivityUndoFollow>(json);
|
||||||
break;
|
break;
|
||||||
|
case "Delete":
|
||||||
|
return JsonConvert.DeserializeObject<ActivityDelete>(json);
|
||||||
case "Accept":
|
case "Accept":
|
||||||
var accept = JsonConvert.DeserializeObject<ActivityAccept>(json);
|
var accept = JsonConvert.DeserializeObject<ActivityAccept>(json);
|
||||||
//var acceptType = JsonConvert.DeserializeObject<Activity>(accept.apObject);
|
//var acceptType = JsonConvert.DeserializeObject<Activity>(accept.apObject);
|
||||||
|
|
10
src/BirdsiteLive.ActivityPub/Models/ActivityDelete.cs
Normal file
10
src/BirdsiteLive.ActivityPub/Models/ActivityDelete.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.ActivityPub.Models
|
||||||
|
{
|
||||||
|
public class ActivityDelete : Activity
|
||||||
|
{
|
||||||
|
[JsonProperty("object")]
|
||||||
|
public object apObject { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,5 +13,8 @@
|
||||||
public string SensitiveTwitterAccounts { get; set; }
|
public string SensitiveTwitterAccounts { get; set; }
|
||||||
|
|
||||||
public int FailingTwitterUserCleanUpThreshold { get; set; }
|
public int FailingTwitterUserCleanUpThreshold { get; set; }
|
||||||
|
public int FailingFollowerCleanUpThreshold { get; set; } = -1;
|
||||||
|
|
||||||
|
public int UserCacheCapacity { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ using BirdsiteLive.ActivityPub.Models;
|
||||||
using BirdsiteLive.Common.Settings;
|
using BirdsiteLive.Common.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Org.BouncyCastle.Bcpg;
|
|
||||||
|
|
||||||
namespace BirdsiteLive.Domain
|
namespace BirdsiteLive.Domain
|
||||||
{
|
{
|
||||||
|
@ -45,6 +44,12 @@ namespace BirdsiteLive.Domain
|
||||||
var httpClient = _httpClientFactory.CreateClient();
|
var httpClient = _httpClientFactory.CreateClient();
|
||||||
httpClient.DefaultRequestHeaders.Add("Accept", "application/activity+json");
|
httpClient.DefaultRequestHeaders.Add("Accept", "application/activity+json");
|
||||||
var result = await httpClient.GetAsync(objectId);
|
var result = await httpClient.GetAsync(objectId);
|
||||||
|
|
||||||
|
if (result.StatusCode == HttpStatusCode.Gone)
|
||||||
|
throw new FollowerIsGoneException();
|
||||||
|
|
||||||
|
result.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var content = await result.Content.ReadAsStringAsync();
|
var content = await result.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
var actor = JsonConvert.DeserializeObject<Actor>(content);
|
var actor = JsonConvert.DeserializeObject<Actor>(content);
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BirdsiteLive.DAL.Contracts;
|
||||||
|
using BirdsiteLive.DAL.Models;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.Domain.BusinessUseCases
|
||||||
|
{
|
||||||
|
public interface IProcessDeleteUser
|
||||||
|
{
|
||||||
|
Task ExecuteAsync(Follower follower);
|
||||||
|
Task ExecuteAsync(string followerUsername, string followerDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProcessDeleteUser : IProcessDeleteUser
|
||||||
|
{
|
||||||
|
private readonly IFollowersDal _followersDal;
|
||||||
|
private readonly ITwitterUserDal _twitterUserDal;
|
||||||
|
|
||||||
|
#region Ctor
|
||||||
|
public ProcessDeleteUser(IFollowersDal followersDal, ITwitterUserDal twitterUserDal)
|
||||||
|
{
|
||||||
|
_followersDal = followersDal;
|
||||||
|
_twitterUserDal = twitterUserDal;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public async Task ExecuteAsync(string followerUsername, string followerDomain)
|
||||||
|
{
|
||||||
|
// Get Follower and Twitter Users
|
||||||
|
var follower = await _followersDal.GetFollowerAsync(followerUsername, followerDomain);
|
||||||
|
if (follower == null) return;
|
||||||
|
|
||||||
|
await ExecuteAsync(follower);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteAsync(Follower follower)
|
||||||
|
{
|
||||||
|
// Remove twitter users if no more followers
|
||||||
|
var followings = follower.Followings;
|
||||||
|
foreach (var following in followings)
|
||||||
|
{
|
||||||
|
var followers = await _followersDal.GetFollowersAsync(following);
|
||||||
|
if (followers.Length == 1 && followers.First().Id == follower.Id)
|
||||||
|
await _twitterUserDal.DeleteTwitterUserAsync(following);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove follower from DB
|
||||||
|
await _followersDal.DeleteFollowerAsync(follower.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.Domain
|
||||||
|
{
|
||||||
|
public class FollowerIsGoneException : Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.Domain.Tools
|
||||||
|
{
|
||||||
|
public class SigValidationResultExtractor
|
||||||
|
{
|
||||||
|
public static string GetUserName(SignatureValidationResult result)
|
||||||
|
{
|
||||||
|
return result.User.preferredUsername.ToLowerInvariant().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetHost(SignatureValidationResult result)
|
||||||
|
{
|
||||||
|
return result.User.url.Replace("https://", string.Empty).Split('/').First();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetSharedInbox(SignatureValidationResult result)
|
||||||
|
{
|
||||||
|
return result.User?.endpoints?.sharedInbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BirdsiteLive.ActivityPub;
|
using BirdsiteLive.ActivityPub;
|
||||||
using BirdsiteLive.ActivityPub.Converters;
|
using BirdsiteLive.ActivityPub.Converters;
|
||||||
|
using BirdsiteLive.ActivityPub.Models;
|
||||||
using BirdsiteLive.Common.Regexes;
|
using BirdsiteLive.Common.Regexes;
|
||||||
using BirdsiteLive.Common.Settings;
|
using BirdsiteLive.Common.Settings;
|
||||||
using BirdsiteLive.Cryptography;
|
using BirdsiteLive.Cryptography;
|
||||||
|
@ -28,10 +29,12 @@ namespace BirdsiteLive.Domain
|
||||||
Task<bool> UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityUndoFollow activity, string body);
|
Task<bool> UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityUndoFollow activity, string body);
|
||||||
|
|
||||||
Task<bool> SendRejectFollowAsync(ActivityFollow activity, string followerHost);
|
Task<bool> SendRejectFollowAsync(ActivityFollow activity, string followerHost);
|
||||||
|
Task<bool> DeleteRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityDelete activity, string body);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserService : IUserService
|
public class UserService : IUserService
|
||||||
{
|
{
|
||||||
|
private readonly IProcessDeleteUser _processDeleteUser;
|
||||||
private readonly IProcessFollowUser _processFollowUser;
|
private readonly IProcessFollowUser _processFollowUser;
|
||||||
private readonly IProcessUndoFollowUser _processUndoFollowUser;
|
private readonly IProcessUndoFollowUser _processUndoFollowUser;
|
||||||
|
|
||||||
|
@ -46,7 +49,7 @@ namespace BirdsiteLive.Domain
|
||||||
private readonly IModerationRepository _moderationRepository;
|
private readonly IModerationRepository _moderationRepository;
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser, IStatusExtractor statusExtractor, IExtractionStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, IModerationRepository moderationRepository)
|
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser, IStatusExtractor statusExtractor, IExtractionStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, IModerationRepository moderationRepository, IProcessDeleteUser processDeleteUser)
|
||||||
{
|
{
|
||||||
_instanceSettings = instanceSettings;
|
_instanceSettings = instanceSettings;
|
||||||
_cryptoService = cryptoService;
|
_cryptoService = cryptoService;
|
||||||
|
@ -57,6 +60,7 @@ namespace BirdsiteLive.Domain
|
||||||
_statisticsHandler = statisticsHandler;
|
_statisticsHandler = statisticsHandler;
|
||||||
_twitterUserService = twitterUserService;
|
_twitterUserService = twitterUserService;
|
||||||
_moderationRepository = moderationRepository;
|
_moderationRepository = moderationRepository;
|
||||||
|
_processDeleteUser = processDeleteUser;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -126,10 +130,10 @@ namespace BirdsiteLive.Domain
|
||||||
if (!sigValidation.SignatureIsValidated) return false;
|
if (!sigValidation.SignatureIsValidated) return false;
|
||||||
|
|
||||||
// Prepare data
|
// Prepare data
|
||||||
var followerUserName = sigValidation.User.preferredUsername.ToLowerInvariant().Trim();
|
var followerUserName = SigValidationResultExtractor.GetUserName(sigValidation);
|
||||||
var followerHost = sigValidation.User.url.Replace("https://", string.Empty).Split('/').First();
|
var followerHost = SigValidationResultExtractor.GetHost(sigValidation);
|
||||||
var followerInbox = sigValidation.User.inbox;
|
var followerInbox = sigValidation.User.inbox;
|
||||||
var followerSharedInbox = sigValidation.User?.endpoints?.sharedInbox;
|
var followerSharedInbox = SigValidationResultExtractor.GetSharedInbox(sigValidation);
|
||||||
var twitterUser = activity.apObject.Split('/').Last().Replace("@", string.Empty).ToLowerInvariant().Trim();
|
var twitterUser = activity.apObject.Split('/').Last().Replace("@", string.Empty).ToLowerInvariant().Trim();
|
||||||
|
|
||||||
// Make sure to only keep routes
|
// Make sure to only keep routes
|
||||||
|
@ -213,7 +217,7 @@ namespace BirdsiteLive.Domain
|
||||||
return result == HttpStatusCode.Accepted ||
|
return result == HttpStatusCode.Accepted ||
|
||||||
result == HttpStatusCode.OK; //TODO: revamp this for better error handling
|
result == HttpStatusCode.OK; //TODO: revamp this for better error handling
|
||||||
}
|
}
|
||||||
|
|
||||||
private string OnlyKeepRoute(string inbox, string host)
|
private string OnlyKeepRoute(string inbox, string host)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(inbox))
|
if (string.IsNullOrWhiteSpace(inbox))
|
||||||
|
@ -258,6 +262,22 @@ namespace BirdsiteLive.Domain
|
||||||
return result == HttpStatusCode.Accepted || result == HttpStatusCode.OK; //TODO: revamp this for better error handling
|
return result == HttpStatusCode.Accepted || result == HttpStatusCode.OK; //TODO: revamp this for better error handling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders,
|
||||||
|
ActivityDelete activity, string body)
|
||||||
|
{
|
||||||
|
// Validate
|
||||||
|
var sigValidation = await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders, body);
|
||||||
|
if (!sigValidation.SignatureIsValidated) return false;
|
||||||
|
|
||||||
|
// Remove user and followings
|
||||||
|
var followerUserName = SigValidationResultExtractor.GetUserName(sigValidation);
|
||||||
|
var followerHost = SigValidationResultExtractor.GetHost(sigValidation);
|
||||||
|
|
||||||
|
await _processDeleteUser.ExecuteAsync(followerUserName, followerHost);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<SignatureValidationResult> ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary<string, string> requestHeaders, string body)
|
private async Task<SignatureValidationResult> ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary<string, string> requestHeaders, string body)
|
||||||
{
|
{
|
||||||
//Check Date Validity
|
//Check Date Validity
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BirdsiteLive.ActivityPub;
|
|
||||||
using BirdsiteLive.ActivityPub.Converters;
|
|
||||||
using BirdsiteLive.Common.Settings;
|
|
||||||
using BirdsiteLive.DAL.Contracts;
|
|
||||||
using BirdsiteLive.DAL.Models;
|
using BirdsiteLive.DAL.Models;
|
||||||
using BirdsiteLive.Domain;
|
using BirdsiteLive.Domain.BusinessUseCases;
|
||||||
|
|
||||||
namespace BirdsiteLive.Moderation.Actions
|
namespace BirdsiteLive.Moderation.Actions
|
||||||
{
|
{
|
||||||
|
@ -17,16 +11,14 @@ namespace BirdsiteLive.Moderation.Actions
|
||||||
|
|
||||||
public class RemoveFollowerAction : IRemoveFollowerAction
|
public class RemoveFollowerAction : IRemoveFollowerAction
|
||||||
{
|
{
|
||||||
private readonly IFollowersDal _followersDal;
|
|
||||||
private readonly ITwitterUserDal _twitterUserDal;
|
|
||||||
private readonly IRejectAllFollowingsAction _rejectAllFollowingsAction;
|
private readonly IRejectAllFollowingsAction _rejectAllFollowingsAction;
|
||||||
|
private readonly IProcessDeleteUser _processDeleteUser;
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public RemoveFollowerAction(IFollowersDal followersDal, ITwitterUserDal twitterUserDal, IRejectAllFollowingsAction rejectAllFollowingsAction)
|
public RemoveFollowerAction(IRejectAllFollowingsAction rejectAllFollowingsAction, IProcessDeleteUser processDeleteUser)
|
||||||
{
|
{
|
||||||
_followersDal = followersDal;
|
|
||||||
_twitterUserDal = twitterUserDal;
|
|
||||||
_rejectAllFollowingsAction = rejectAllFollowingsAction;
|
_rejectAllFollowingsAction = rejectAllFollowingsAction;
|
||||||
|
_processDeleteUser = processDeleteUser;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -36,16 +28,7 @@ namespace BirdsiteLive.Moderation.Actions
|
||||||
await _rejectAllFollowingsAction.ProcessAsync(follower);
|
await _rejectAllFollowingsAction.ProcessAsync(follower);
|
||||||
|
|
||||||
// Remove twitter users if no more followers
|
// Remove twitter users if no more followers
|
||||||
var followings = follower.Followings;
|
await _processDeleteUser.ExecuteAsync(follower);
|
||||||
foreach (var following in followings)
|
|
||||||
{
|
|
||||||
var followers = await _followersDal.GetFollowersAsync(following);
|
|
||||||
if (followers.Length == 1 && followers.First().Id == follower.Id)
|
|
||||||
await _twitterUserDal.DeleteTwitterUserAsync(following);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove follower from DB
|
|
||||||
await _followersDal.DeleteFollowerAsync(follower.Id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,6 +9,7 @@ using BirdsiteLive.Moderation.Actions;
|
||||||
using BirdsiteLive.Pipeline.Contracts;
|
using BirdsiteLive.Pipeline.Contracts;
|
||||||
using BirdsiteLive.Pipeline.Models;
|
using BirdsiteLive.Pipeline.Models;
|
||||||
using BirdsiteLive.Twitter;
|
using BirdsiteLive.Twitter;
|
||||||
|
using BirdsiteLive.Twitter.Models;
|
||||||
|
|
||||||
namespace BirdsiteLive.Pipeline.Processors
|
namespace BirdsiteLive.Pipeline.Processors
|
||||||
{
|
{
|
||||||
|
@ -35,26 +36,61 @@ namespace BirdsiteLive.Pipeline.Processors
|
||||||
|
|
||||||
foreach (var user in syncTwitterUsers)
|
foreach (var user in syncTwitterUsers)
|
||||||
{
|
{
|
||||||
var userView = _twitterUserService.GetUser(user.Acct);
|
TwitterUser userView = null;
|
||||||
if (userView == null)
|
|
||||||
{
|
|
||||||
await AnalyseFailingUserAsync(user);
|
|
||||||
}
|
|
||||||
else if (!userView.Protected)
|
|
||||||
{
|
|
||||||
user.FetchingErrorCount = 0;
|
|
||||||
var userWtData = new UserWithDataToSync
|
|
||||||
{
|
|
||||||
User = user
|
|
||||||
};
|
|
||||||
usersWtData.Add(userWtData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userView = _twitterUserService.GetUser(user.Acct);
|
||||||
|
}
|
||||||
|
catch (UserNotFoundException)
|
||||||
|
{
|
||||||
|
await ProcessNotFoundUserAsync(user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (UserHasBeenSuspendedException)
|
||||||
|
{
|
||||||
|
await ProcessNotFoundUserAsync(user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (RateLimitExceededException)
|
||||||
|
{
|
||||||
|
await ProcessRateLimitExceededAsync(user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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();
|
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);
|
var dbUser = await _twitterUserDal.GetTwitterUserAsync(user.Acct);
|
||||||
dbUser.FetchingErrorCount++;
|
dbUser.FetchingErrorCount++;
|
||||||
|
@ -68,9 +104,6 @@ namespace BirdsiteLive.Pipeline.Processors
|
||||||
{
|
{
|
||||||
await _twitterUserDal.UpdateTwitterUserAsync(dbUser);
|
await _twitterUserDal.UpdateTwitterUserAsync(dbUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purge
|
|
||||||
_twitterUserService.PurgeUser(user.Acct);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,9 +5,11 @@ using System.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using BirdsiteLive.Common.Settings;
|
||||||
using BirdsiteLive.DAL.Contracts;
|
using BirdsiteLive.DAL.Contracts;
|
||||||
using BirdsiteLive.DAL.Models;
|
using BirdsiteLive.DAL.Models;
|
||||||
using BirdsiteLive.Domain;
|
using BirdsiteLive.Domain;
|
||||||
|
using BirdsiteLive.Moderation.Actions;
|
||||||
using BirdsiteLive.Pipeline.Contracts;
|
using BirdsiteLive.Pipeline.Contracts;
|
||||||
using BirdsiteLive.Pipeline.Models;
|
using BirdsiteLive.Pipeline.Models;
|
||||||
using BirdsiteLive.Pipeline.Processors.SubTasks;
|
using BirdsiteLive.Pipeline.Processors.SubTasks;
|
||||||
|
@ -23,14 +25,18 @@ namespace BirdsiteLive.Pipeline.Processors
|
||||||
private readonly ISendTweetsToInboxTask _sendTweetsToInboxTask;
|
private readonly ISendTweetsToInboxTask _sendTweetsToInboxTask;
|
||||||
private readonly ISendTweetsToSharedInboxTask _sendTweetsToSharedInbox;
|
private readonly ISendTweetsToSharedInboxTask _sendTweetsToSharedInbox;
|
||||||
private readonly IFollowersDal _followersDal;
|
private readonly IFollowersDal _followersDal;
|
||||||
|
private readonly InstanceSettings _instanceSettings;
|
||||||
private readonly ILogger<SendTweetsToFollowersProcessor> _logger;
|
private readonly ILogger<SendTweetsToFollowersProcessor> _logger;
|
||||||
|
private readonly IRemoveFollowerAction _removeFollowerAction;
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox, IFollowersDal followersDal, ILogger<SendTweetsToFollowersProcessor> logger)
|
public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox, IFollowersDal followersDal, ILogger<SendTweetsToFollowersProcessor> logger, InstanceSettings instanceSettings, IRemoveFollowerAction removeFollowerAction)
|
||||||
{
|
{
|
||||||
_sendTweetsToInboxTask = sendTweetsToInboxTask;
|
_sendTweetsToInboxTask = sendTweetsToInboxTask;
|
||||||
_sendTweetsToSharedInbox = sendTweetsToSharedInbox;
|
_sendTweetsToSharedInbox = sendTweetsToSharedInbox;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_instanceSettings = instanceSettings;
|
||||||
|
_removeFollowerAction = removeFollowerAction;
|
||||||
_followersDal = followersDal;
|
_followersDal = followersDal;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -107,7 +113,17 @@ namespace BirdsiteLive.Pipeline.Processors
|
||||||
private async Task ProcessFailingUserAsync(Follower follower)
|
private async Task ProcessFailingUserAsync(Follower follower)
|
||||||
{
|
{
|
||||||
follower.PostingErrorCount++;
|
follower.PostingErrorCount++;
|
||||||
await _followersDal.UpdateFollowerAsync(follower);
|
|
||||||
|
if (follower.PostingErrorCount > _instanceSettings.FailingFollowerCleanUpThreshold
|
||||||
|
&& _instanceSettings.FailingFollowerCleanUpThreshold > 0
|
||||||
|
|| follower.PostingErrorCount > 2147483600)
|
||||||
|
{
|
||||||
|
await _removeFollowerAction.ProcessAsync(follower);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _followersDal.UpdateFollowerAsync(follower);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using BirdsiteLive.Common.Settings;
|
||||||
using BirdsiteLive.Twitter.Models;
|
using BirdsiteLive.Twitter.Models;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
@ -13,11 +14,8 @@ namespace BirdsiteLive.Twitter
|
||||||
{
|
{
|
||||||
private readonly ITwitterUserService _twitterService;
|
private readonly ITwitterUserService _twitterService;
|
||||||
|
|
||||||
private MemoryCache _userCache = new MemoryCache(new MemoryCacheOptions()
|
private readonly MemoryCache _userCache;
|
||||||
{
|
private readonly MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
|
||||||
SizeLimit = 5000
|
|
||||||
});
|
|
||||||
private MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
|
|
||||||
.SetSize(1)//Size amount
|
.SetSize(1)//Size amount
|
||||||
//Priority on removing when reaching size limit (memory pressure)
|
//Priority on removing when reaching size limit (memory pressure)
|
||||||
.SetPriority(CacheItemPriority.High)
|
.SetPriority(CacheItemPriority.High)
|
||||||
|
@ -27,9 +25,14 @@ namespace BirdsiteLive.Twitter
|
||||||
.SetAbsoluteExpiration(TimeSpan.FromDays(7));
|
.SetAbsoluteExpiration(TimeSpan.FromDays(7));
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public CachedTwitterUserService(ITwitterUserService twitterService)
|
public CachedTwitterUserService(ITwitterUserService twitterService, InstanceSettings settings)
|
||||||
{
|
{
|
||||||
_twitterService = twitterService;
|
_twitterService = twitterService;
|
||||||
|
|
||||||
|
_userCache = new MemoryCache(new MemoryCacheOptions()
|
||||||
|
{
|
||||||
|
SizeLimit = settings.UserCacheCapacity
|
||||||
|
});
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -44,6 +47,11 @@ namespace BirdsiteLive.Twitter
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsUserApiRateLimited()
|
||||||
|
{
|
||||||
|
return _twitterService.IsUserApiRateLimited();
|
||||||
|
}
|
||||||
|
|
||||||
public void PurgeUser(string username)
|
public void PurgeUser(string username)
|
||||||
{
|
{
|
||||||
_userCache.Remove(username);
|
_userCache.Remove(username);
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.Twitter
|
||||||
|
{
|
||||||
|
public class RateLimitExceededException : Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.Twitter
|
||||||
|
{
|
||||||
|
public class UserHasBeenSuspendedException : Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.Twitter
|
||||||
|
{
|
||||||
|
public class UserNotFoundException : Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ namespace BirdsiteLive.Statistics.Domain
|
||||||
void CalledTweetApi();
|
void CalledTweetApi();
|
||||||
void CalledTimelineApi();
|
void CalledTimelineApi();
|
||||||
ApiStatistics GetStatistics();
|
ApiStatistics GetStatistics();
|
||||||
|
|
||||||
|
int GetCurrentUserCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Rate limits: https://developer.twitter.com/en/docs/twitter-api/v1/rate-limits
|
//Rate limits: https://developer.twitter.com/en/docs/twitter-api/v1/rate-limits
|
||||||
|
@ -60,7 +62,12 @@ namespace BirdsiteLive.Statistics.Domain
|
||||||
foreach (var old in oldSnapshots) _snapshots.TryRemove(old, out var data);
|
foreach (var old in oldSnapshots) _snapshots.TryRemove(old, out var data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CalledUserApi() //GET users/show - 900/15mins
|
public int GetCurrentUserCalls()
|
||||||
|
{
|
||||||
|
return _userCalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CalledUserApi() //GET users/show - 300/15mins
|
||||||
{
|
{
|
||||||
Interlocked.Increment(ref _userCalls);
|
Interlocked.Increment(ref _userCalls);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ using BirdsiteLive.Twitter.Models;
|
||||||
using BirdsiteLive.Twitter.Tools;
|
using BirdsiteLive.Twitter.Tools;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Tweetinvi;
|
using Tweetinvi;
|
||||||
|
using Tweetinvi.Exceptions;
|
||||||
using Tweetinvi.Models;
|
using Tweetinvi.Models;
|
||||||
|
|
||||||
namespace BirdsiteLive.Twitter
|
namespace BirdsiteLive.Twitter
|
||||||
|
@ -13,6 +14,7 @@ namespace BirdsiteLive.Twitter
|
||||||
public interface ITwitterUserService
|
public interface ITwitterUserService
|
||||||
{
|
{
|
||||||
TwitterUser GetUser(string username);
|
TwitterUser GetUser(string username);
|
||||||
|
bool IsUserApiRateLimited();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TwitterUserService : ITwitterUserService
|
public class TwitterUserService : ITwitterUserService
|
||||||
|
@ -32,27 +34,46 @@ namespace BirdsiteLive.Twitter
|
||||||
|
|
||||||
public TwitterUser GetUser(string username)
|
public TwitterUser GetUser(string username)
|
||||||
{
|
{
|
||||||
|
//Check if API is saturated
|
||||||
|
if (IsUserApiRateLimited()) throw new RateLimitExceededException();
|
||||||
|
|
||||||
|
//Proceed to account retrieval
|
||||||
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
||||||
ExceptionHandler.SwallowWebExceptions = false;
|
ExceptionHandler.SwallowWebExceptions = false;
|
||||||
|
RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
|
||||||
|
|
||||||
IUser user;
|
IUser user;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
user = User.GetUserFromScreenName(username);
|
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);
|
throw new UserHasBeenSuspendedException();
|
||||||
return null;
|
}
|
||||||
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, "Error retrieving user {Username}", username);
|
_logger.LogError(e, "Error retrieving user {Username}", username);
|
||||||
|
throw;
|
||||||
// TODO keep track of error, see where to remove user if too much errors
|
}
|
||||||
|
finally
|
||||||
return null;
|
{
|
||||||
|
_statisticsHandler.CalledUserApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand URLs
|
// Expand URLs
|
||||||
|
@ -73,5 +94,32 @@ namespace BirdsiteLive.Twitter
|
||||||
Protected = user.Protected
|
Protected = user.Protected
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsUserApiRateLimited()
|
||||||
|
{
|
||||||
|
// Retrieve limit from tooling
|
||||||
|
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
||||||
|
ExceptionHandler.SwallowWebExceptions = false;
|
||||||
|
RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var queryRateLimits = RateLimit.GetQueryRateLimit("https://api.twitter.com/1.1/users/show.json?screen_name=mastodon");
|
||||||
|
|
||||||
|
if (queryRateLimits != null)
|
||||||
|
{
|
||||||
|
return queryRateLimits.Remaining <= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Error retrieving rate limits");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
var currentCalls = _statisticsHandler.GetCurrentUserCalls();
|
||||||
|
var maxCalls = _statisticsHandler.GetStatistics().UserCallsMax;
|
||||||
|
return currentCalls >= maxCalls;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId>
|
<UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<Version>0.19.1</Version>
|
<Version>0.20.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -14,6 +14,7 @@ using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BirdsiteLive.Controllers
|
namespace BirdsiteLive.Controllers
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
public class DebugingController : Controller
|
public class DebugingController : Controller
|
||||||
{
|
{
|
||||||
private readonly InstanceSettings _instanceSettings;
|
private readonly InstanceSettings _instanceSettings;
|
||||||
|
@ -67,7 +68,7 @@ namespace BirdsiteLive.Controllers
|
||||||
var noteGuid = Guid.NewGuid();
|
var noteGuid = Guid.NewGuid();
|
||||||
var noteId = $"https://{_instanceSettings.Domain}/users/{username}/statuses/{noteGuid}";
|
var noteId = $"https://{_instanceSettings.Domain}/users/{username}/statuses/{noteGuid}";
|
||||||
var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{noteGuid}";
|
var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{noteGuid}";
|
||||||
|
|
||||||
var to = $"{actor}/followers";
|
var to = $"{actor}/followers";
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
@ -80,12 +81,12 @@ namespace BirdsiteLive.Controllers
|
||||||
type = "Create",
|
type = "Create",
|
||||||
actor = actor,
|
actor = actor,
|
||||||
published = nowString,
|
published = nowString,
|
||||||
to = new []{ to },
|
to = new[] { to },
|
||||||
//cc = new [] { "https://www.w3.org/ns/activitystreams#Public" },
|
//cc = new [] { "https://www.w3.org/ns/activitystreams#Public" },
|
||||||
apObject = new Note()
|
apObject = new Note()
|
||||||
{
|
{
|
||||||
id = noteId,
|
id = noteId,
|
||||||
summary = null,
|
summary = null,
|
||||||
inReplyTo = null,
|
inReplyTo = null,
|
||||||
published = nowString,
|
published = nowString,
|
||||||
url = noteUrl,
|
url = noteUrl,
|
||||||
|
@ -93,7 +94,7 @@ namespace BirdsiteLive.Controllers
|
||||||
|
|
||||||
// Unlisted
|
// Unlisted
|
||||||
to = new[] { to },
|
to = new[] { to },
|
||||||
cc = new [] { "https://www.w3.org/ns/activitystreams#Public" },
|
cc = new[] { "https://www.w3.org/ns/activitystreams#Public" },
|
||||||
|
|
||||||
//// Public
|
//// Public
|
||||||
//to = new[] { "https://www.w3.org/ns/activitystreams#Public" },
|
//to = new[] { "https://www.w3.org/ns/activitystreams#Public" },
|
||||||
|
@ -125,6 +126,7 @@ namespace BirdsiteLive.Controllers
|
||||||
return View("Index");
|
return View("Index");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
public static class HtmlHelperExtensions
|
public static class HtmlHelperExtensions
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,6 +3,10 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BirdsiteLive.ActivityPub;
|
||||||
|
using BirdsiteLive.ActivityPub.Models;
|
||||||
|
using BirdsiteLive.Domain;
|
||||||
|
using BirdsiteLive.Tools;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -13,11 +17,13 @@ namespace BirdsiteLive.Controllers
|
||||||
public class InboxController : ControllerBase
|
public class InboxController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<InboxController> _logger;
|
private readonly ILogger<InboxController> _logger;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public InboxController(ILogger<InboxController> logger)
|
public InboxController(ILogger<InboxController> logger, IUserService userService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_userService = userService;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -25,15 +31,32 @@ namespace BirdsiteLive.Controllers
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Inbox()
|
public async Task<IActionResult> Inbox()
|
||||||
{
|
{
|
||||||
var r = Request;
|
try
|
||||||
using (var reader = new StreamReader(Request.Body))
|
|
||||||
{
|
{
|
||||||
var body = await reader.ReadToEndAsync();
|
var r = Request;
|
||||||
|
using (var reader = new StreamReader(Request.Body))
|
||||||
|
{
|
||||||
|
var body = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
_logger.LogTrace("Inbox: {Body}", body);
|
_logger.LogTrace("Inbox: {Body}", body);
|
||||||
//System.IO.File.WriteAllText($@"C:\apdebug\inbox\{Guid.NewGuid()}.json", body);
|
//System.IO.File.WriteAllText($@"C:\apdebug\inbox\{Guid.NewGuid()}.json", body);
|
||||||
|
|
||||||
|
var activity = ApDeserializer.ProcessActivity(body);
|
||||||
|
var signature = r.Headers["Signature"].First();
|
||||||
|
|
||||||
|
switch (activity?.type)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch (FollowerIsGoneException) { } //TODO: check if user in DB
|
||||||
|
|
||||||
return Accepted();
|
return Accepted();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ using BirdsiteLive.Common.Regexes;
|
||||||
using BirdsiteLive.Common.Settings;
|
using BirdsiteLive.Common.Settings;
|
||||||
using BirdsiteLive.Domain;
|
using BirdsiteLive.Domain;
|
||||||
using BirdsiteLive.Models;
|
using BirdsiteLive.Models;
|
||||||
|
using BirdsiteLive.Tools;
|
||||||
using BirdsiteLive.Twitter;
|
using BirdsiteLive.Twitter;
|
||||||
using BirdsiteLive.Twitter.Models;
|
using BirdsiteLive.Twitter.Models;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
@ -65,11 +66,42 @@ namespace BirdsiteLive.Controllers
|
||||||
|
|
||||||
id = id.Trim(new[] { ' ', '@' }).ToLowerInvariant();
|
id = id.Trim(new[] { ' ', '@' }).ToLowerInvariant();
|
||||||
|
|
||||||
|
TwitterUser user = null;
|
||||||
|
var isSaturated = false;
|
||||||
|
var notFound = false;
|
||||||
|
|
||||||
// Ensure valid username
|
// Ensure valid username
|
||||||
// https://help.twitter.com/en/managing-your-account/twitter-username-rules
|
// 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)
|
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 acceptHeaders = Request.Headers["Accept"];
|
var acceptHeaders = Request.Headers["Accept"];
|
||||||
if (acceptHeaders.Any())
|
if (acceptHeaders.Any())
|
||||||
|
@ -77,14 +109,16 @@ namespace BirdsiteLive.Controllers
|
||||||
var r = acceptHeaders.First();
|
var r = acceptHeaders.First();
|
||||||
if (r.Contains("application/activity+json"))
|
if (r.Contains("application/activity+json"))
|
||||||
{
|
{
|
||||||
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 apUser = _userService.GetUser(user);
|
||||||
var jsonApUser = JsonConvert.SerializeObject(apUser);
|
var jsonApUser = JsonConvert.SerializeObject(apUser);
|
||||||
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user == null) return View("UserNotFound");
|
if (isSaturated) return View("ApiSaturated");
|
||||||
|
if (notFound) return View("UserNotFound");
|
||||||
|
|
||||||
var displayableUser = new DisplayTwitterUser
|
var displayableUser = new DisplayTwitterUser
|
||||||
{
|
{
|
||||||
|
@ -133,40 +167,69 @@ namespace BirdsiteLive.Controllers
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Inbox()
|
public async Task<IActionResult> Inbox()
|
||||||
{
|
{
|
||||||
var r = Request;
|
try
|
||||||
using (var reader = new StreamReader(Request.Body))
|
|
||||||
{
|
{
|
||||||
var body = await reader.ReadToEndAsync();
|
var r = Request;
|
||||||
|
using (var reader = new StreamReader(Request.Body))
|
||||||
_logger.LogTrace("User Inbox: {Body}", body);
|
|
||||||
//System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body);
|
|
||||||
|
|
||||||
var activity = ApDeserializer.ProcessActivity(body);
|
|
||||||
// Do something
|
|
||||||
var signature = r.Headers["Signature"].First();
|
|
||||||
|
|
||||||
switch (activity?.type)
|
|
||||||
{
|
{
|
||||||
case "Follow":
|
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,
|
var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path,
|
||||||
r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow, body);
|
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers),
|
||||||
|
activity as ActivityFollow, body);
|
||||||
if (succeeded) return Accepted();
|
if (succeeded) return Accepted();
|
||||||
else return Unauthorized();
|
else return Unauthorized();
|
||||||
}
|
}
|
||||||
case "Undo":
|
case "Undo":
|
||||||
if (activity is ActivityUndoFollow)
|
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.UndoFollowRequestedAsync(signature, r.Method, r.Path,
|
var succeeded = await _userService.DeleteRequestedAsync(signature, r.Method, r.Path,
|
||||||
r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityUndoFollow, body);
|
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers),
|
||||||
|
activity as ActivityDelete, body);
|
||||||
if (succeeded) return Accepted();
|
if (succeeded) return Accepted();
|
||||||
else return Unauthorized();
|
else return Unauthorized();
|
||||||
}
|
}
|
||||||
return Accepted();
|
default:
|
||||||
default:
|
return Accepted();
|
||||||
return Accepted();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (FollowerIsGoneException) //TODO: check if user in DB
|
||||||
|
{
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
catch (UserNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
catch (UserHasBeenSuspendedException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
catch (RateLimitExceededException)
|
||||||
|
{
|
||||||
|
return new ObjectResult("Too Many Requests") { StatusCode = 429 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("/users/{id}/followers")]
|
[Route("/users/{id}/followers")]
|
||||||
|
@ -183,10 +246,5 @@ namespace BirdsiteLive.Controllers
|
||||||
var jsonApUser = JsonConvert.SerializeObject(followers);
|
var jsonApUser = JsonConvert.SerializeObject(followers);
|
||||||
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
return Content(jsonApUser, "application/activity+json; charset=utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<string, string> RequestHeaders(IHeaderDictionary header)
|
|
||||||
{
|
|
||||||
return header.ToDictionary<KeyValuePair<string, StringValues>, string, string>(h => h.Key.ToLowerInvariant(), h => h.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -12,6 +12,7 @@ using BirdsiteLive.Models;
|
||||||
using BirdsiteLive.Models.WellKnownModels;
|
using BirdsiteLive.Models.WellKnownModels;
|
||||||
using BirdsiteLive.Twitter;
|
using BirdsiteLive.Twitter;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace BirdsiteLive.Controllers
|
namespace BirdsiteLive.Controllers
|
||||||
|
@ -23,13 +24,15 @@ namespace BirdsiteLive.Controllers
|
||||||
private readonly ITwitterUserService _twitterUserService;
|
private readonly ITwitterUserService _twitterUserService;
|
||||||
private readonly ITwitterUserDal _twitterUserDal;
|
private readonly ITwitterUserDal _twitterUserDal;
|
||||||
private readonly InstanceSettings _settings;
|
private readonly InstanceSettings _settings;
|
||||||
|
private readonly ILogger<WellKnownController> _logger;
|
||||||
|
|
||||||
#region Ctor
|
#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;
|
_twitterUserService = twitterUserService;
|
||||||
_twitterUserDal = twitterUserDal;
|
_twitterUserDal = twitterUserDal;
|
||||||
_moderationRepository = moderationRepository;
|
_moderationRepository = moderationRepository;
|
||||||
|
_logger = logger;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -141,30 +144,54 @@ namespace BirdsiteLive.Controllers
|
||||||
[Route("/.well-known/webfinger")]
|
[Route("/.well-known/webfinger")]
|
||||||
public IActionResult Webfinger(string resource = null)
|
public IActionResult Webfinger(string resource = null)
|
||||||
{
|
{
|
||||||
var acct = resource.Split("acct:")[1].Trim();
|
if (string.IsNullOrWhiteSpace(resource))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
string name = null;
|
string name = null;
|
||||||
string domain = null;
|
string domain = null;
|
||||||
|
|
||||||
var splitAcct = acct.Split('@', StringSplitOptions.RemoveEmptyEntries);
|
if (resource.StartsWith("acct:"))
|
||||||
|
{
|
||||||
|
var acct = resource.Split("acct:")[1].Trim();
|
||||||
|
var splitAcct = acct.Split('@', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
var atCount = acct.Count(x => x == '@');
|
var atCount = acct.Count(x => x == '@');
|
||||||
if (atCount == 1 && acct.StartsWith('@'))
|
if (atCount == 1 && acct.StartsWith('@'))
|
||||||
{
|
{
|
||||||
name = splitAcct[1];
|
name = splitAcct[1];
|
||||||
|
}
|
||||||
|
else if (atCount == 1 || atCount == 2)
|
||||||
|
{
|
||||||
|
name = splitAcct[0];
|
||||||
|
domain = splitAcct[1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (atCount == 1 || atCount == 2)
|
else if (resource.StartsWith("https://"))
|
||||||
{
|
{
|
||||||
name = splitAcct[0];
|
try
|
||||||
domain = splitAcct[1];
|
{
|
||||||
|
name = resource.Split('/').Last().Trim();
|
||||||
|
domain = resource.Split("https://", StringSplitOptions.RemoveEmptyEntries)[0].Split('/')[0].Trim();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Error parsing {Resource}", resource);
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return BadRequest();
|
_logger.LogError("Error parsing {Resource}", resource);
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure lowercase
|
// Ensure lowercase
|
||||||
name = name.ToLowerInvariant();
|
name = name.ToLowerInvariant();
|
||||||
|
domain = domain?.ToLowerInvariant();
|
||||||
|
|
||||||
// Ensure valid username
|
// Ensure valid username
|
||||||
// https://help.twitter.com/en/managing-your-account/twitter-username-rules
|
// https://help.twitter.com/en/managing-your-account/twitter-username-rules
|
||||||
|
@ -174,9 +201,27 @@ namespace BirdsiteLive.Controllers
|
||||||
if (!string.IsNullOrWhiteSpace(domain) && domain != _settings.Domain)
|
if (!string.IsNullOrWhiteSpace(domain) && domain != _settings.Domain)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
var user = _twitterUserService.GetUser(name);
|
try
|
||||||
if (user == null)
|
{
|
||||||
|
_twitterUserService.GetUser(name);
|
||||||
|
}
|
||||||
|
catch (UserNotFoundException)
|
||||||
|
{
|
||||||
return NotFound();
|
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);
|
var actorUrl = UrlFactory.GetActorUrl(_settings.Domain, name);
|
||||||
|
|
||||||
|
|
15
src/BirdsiteLive/Tools/HeaderHandler.cs
Normal file
15
src/BirdsiteLive/Tools/HeaderHandler.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.Tools
|
||||||
|
{
|
||||||
|
public class HeaderHandler
|
||||||
|
{
|
||||||
|
public static Dictionary<string, string> RequestHeaders(IHeaderDictionary header)
|
||||||
|
{
|
||||||
|
return header.ToDictionary<KeyValuePair<string, StringValues>, string, string>(h => h.Key.ToLowerInvariant(), h => h.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src/BirdsiteLive/Views/Users/ApiSaturated.cshtml
Normal file
13
src/BirdsiteLive/Views/Users/ApiSaturated.cshtml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
@using BirdsiteLive.Controllers;
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Api Saturated";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="display-4">429 Too Many Requests</h1>
|
||||||
|
<p>
|
||||||
|
<br />
|
||||||
|
The API is saturated.<br/>
|
||||||
|
Please consider using another instance.
|
||||||
|
</p>
|
||||||
|
</div>
|
|
@ -23,7 +23,9 @@
|
||||||
"MaxUsersCapacity": 1000,
|
"MaxUsersCapacity": 1000,
|
||||||
"UnlistedTwitterAccounts": null,
|
"UnlistedTwitterAccounts": null,
|
||||||
"SensitiveTwitterAccounts": null,
|
"SensitiveTwitterAccounts": null,
|
||||||
"FailingTwitterUserCleanUpThreshold": 700
|
"FailingTwitterUserCleanUpThreshold": 700,
|
||||||
|
"FailingFollowerCleanUpThreshold": 30000,
|
||||||
|
"UserCacheCapacity": 10000
|
||||||
},
|
},
|
||||||
"Db": {
|
"Db": {
|
||||||
"Type": "postgres",
|
"Type": "postgres",
|
||||||
|
|
|
@ -23,7 +23,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
||||||
public class DbInitializerPostgresDal : PostgresBase, IDbInitializerDal
|
public class DbInitializerPostgresDal : PostgresBase, IDbInitializerDal
|
||||||
{
|
{
|
||||||
private readonly PostgresTools _tools;
|
private readonly PostgresTools _tools;
|
||||||
private readonly Version _currentVersion = new Version(2, 3);
|
private readonly Version _currentVersion = new Version(2, 4);
|
||||||
private const string DbVersionType = "db-version";
|
private const string DbVersionType = "db-version";
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
|
@ -134,7 +134,8 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
||||||
new Tuple<Version, Version>(new Version(1,0), new Version(2,0)),
|
new Tuple<Version, Version>(new Version(1,0), new Version(2,0)),
|
||||||
new Tuple<Version, Version>(new Version(2,0), new Version(2,1)),
|
new Tuple<Version, Version>(new Version(2,0), new Version(2,1)),
|
||||||
new Tuple<Version, Version>(new Version(2,1), new Version(2,2)),
|
new Tuple<Version, Version>(new Version(2,1), new Version(2,2)),
|
||||||
new Tuple<Version, Version>(new Version(2,2), new Version(2,3))
|
new Tuple<Version, Version>(new Version(2,2), new Version(2,3)),
|
||||||
|
new Tuple<Version, Version>(new Version(2,3), new Version(2,4))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +164,14 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
|
||||||
var addPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ADD postingErrorCount SMALLINT";
|
var addPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ADD postingErrorCount SMALLINT";
|
||||||
await _tools.ExecuteRequestAsync(addPostingError);
|
await _tools.ExecuteRequestAsync(addPostingError);
|
||||||
}
|
}
|
||||||
|
else if (from == new Version(2, 3) && to == new Version(2, 4))
|
||||||
|
{
|
||||||
|
var alterLastSync = $@"ALTER TABLE {_settings.TwitterUserTableName} ALTER COLUMN fetchingErrorCount TYPE INTEGER";
|
||||||
|
await _tools.ExecuteRequestAsync(alterLastSync);
|
||||||
|
|
||||||
|
var alterPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ALTER COLUMN postingErrorCount TYPE INTEGER";
|
||||||
|
await _tools.ExecuteRequestAsync(alterPostingError);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using BirdsiteLive.ActivityPub.Models;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BirdsiteLive.ActivityPub.Tests
|
namespace BirdsiteLive.ActivityPub.Tests
|
||||||
|
@ -48,6 +49,20 @@ namespace BirdsiteLive.ActivityPub.Tests
|
||||||
Assert.AreEqual("https://mamot.fr/users/testtest", data.apObject.apObject);
|
Assert.AreEqual("https://mamot.fr/users/testtest", data.apObject.apObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void DeleteDeserializationTest()
|
||||||
|
{
|
||||||
|
var json =
|
||||||
|
"{\"@context\": \"https://www.w3.org/ns/activitystreams\", \"id\": \"https://mastodon.technology/users/deleteduser#delete\", \"type\": \"Delete\", \"actor\": \"https://mastodon.technology/users/deleteduser\", \"to\": [\"https://www.w3.org/ns/activitystreams#Public\"],\"object\": \"https://mastodon.technology/users/deleteduser\",\"signature\": {\"type\": \"RsaSignature2017\",\"creator\": \"https://mastodon.technology/users/deleteduser#main-key\",\"created\": \"2020-11-19T22:43:01Z\",\"signatureValue\": \"peksQao4v5N+sMZgHXZ6xZnGaZrd0s+LqZimu63cnp7O5NBJM6gY9AAu/vKUgrh4C50r66f9OQdHg5yChQhc4ViE+yLR/3/e59YQimelmXJPpcC99Nt0YLU/iTRLsBehY3cDdC6+ogJKgpkToQvB6tG2KrPdrkreYh4Il4eXLKMfiQhgdKluOvenLnl2erPWfE02hIu/jpuljyxSuvJunMdU4yQVSZHTtk/I8q3jjzIzhgyb7ICWU5Hkx0H/47Q24ztsvOgiTWNgO+v6l9vA7qIhztENiRPhzGP5RCCzUKRAe6bcSu1Wfa3NKWqB9BeJ7s+2y2bD7ubPbiEE1MQV7Q==\"}}";
|
||||||
|
|
||||||
|
var data = ApDeserializer.ProcessActivity(json) as ActivityDelete;
|
||||||
|
|
||||||
|
Assert.AreEqual("https://mastodon.technology/users/deleteduser#delete", data.id);
|
||||||
|
Assert.AreEqual("Delete", data.type);
|
||||||
|
Assert.AreEqual("https://mastodon.technology/users/deleteduser", data.actor);
|
||||||
|
Assert.AreEqual("https://mastodon.technology/users/deleteduser", data.apObject);
|
||||||
|
}
|
||||||
|
|
||||||
//[TestMethod]
|
//[TestMethod]
|
||||||
//public void NoteDeserializationTest()
|
//public void NoteDeserializationTest()
|
||||||
//{
|
//{
|
||||||
|
|
|
@ -340,6 +340,48 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
||||||
Assert.AreEqual(10, result.PostingErrorCount);
|
Assert.AreEqual(10, result.PostingErrorCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task CreateUpdateAndGetFollower_Integer()
|
||||||
|
{
|
||||||
|
var acct = "myhandle";
|
||||||
|
var host = "domain.ext";
|
||||||
|
var following = new[] { 12, 19, 23 };
|
||||||
|
var followingSync = new Dictionary<int, long>()
|
||||||
|
{
|
||||||
|
{12, 165L},
|
||||||
|
{19, 166L},
|
||||||
|
{23, 167L}
|
||||||
|
};
|
||||||
|
var inboxRoute = "/myhandle/inbox";
|
||||||
|
var sharedInboxRoute = "/inbox";
|
||||||
|
var actorId = $"https://{host}/{acct}";
|
||||||
|
|
||||||
|
var dal = new FollowersPostgresDal(_settings);
|
||||||
|
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
|
||||||
|
var result = await dal.GetFollowerAsync(acct, host);
|
||||||
|
|
||||||
|
var updatedFollowing = new List<int> { 12, 19, 23, 24 };
|
||||||
|
var updatedFollowingSync = new Dictionary<int, long>(){
|
||||||
|
{12, 170L},
|
||||||
|
{19, 171L},
|
||||||
|
{23, 172L},
|
||||||
|
{24, 173L}
|
||||||
|
};
|
||||||
|
result.Followings = updatedFollowing.ToList();
|
||||||
|
result.FollowingsSyncStatus = updatedFollowingSync;
|
||||||
|
result.PostingErrorCount = 32768;
|
||||||
|
|
||||||
|
await dal.UpdateFollowerAsync(result);
|
||||||
|
result = await dal.GetFollowerAsync(acct, host);
|
||||||
|
|
||||||
|
Assert.AreEqual(updatedFollowing.Count, result.Followings.Count);
|
||||||
|
Assert.AreEqual(updatedFollowing[0], result.Followings[0]);
|
||||||
|
Assert.AreEqual(updatedFollowingSync.Count, result.FollowingsSyncStatus.Count);
|
||||||
|
Assert.AreEqual(updatedFollowingSync.First().Key, result.FollowingsSyncStatus.First().Key);
|
||||||
|
Assert.AreEqual(updatedFollowingSync.First().Value, result.FollowingsSyncStatus.First().Value);
|
||||||
|
Assert.AreEqual(32768, result.PostingErrorCount);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task CreateUpdateAndGetFollower_Remove()
|
public async Task CreateUpdateAndGetFollower_Remove()
|
||||||
{
|
{
|
||||||
|
|
|
@ -130,6 +130,38 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
|
||||||
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
|
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task CreateUpdate3AndGetUser()
|
||||||
|
{
|
||||||
|
var acct = "myid";
|
||||||
|
var lastTweetId = 1548L;
|
||||||
|
|
||||||
|
var dal = new TwitterUserPostgresDal(_settings);
|
||||||
|
|
||||||
|
await dal.CreateTwitterUserAsync(acct, lastTweetId);
|
||||||
|
var result = await dal.GetTwitterUserAsync(acct);
|
||||||
|
|
||||||
|
|
||||||
|
var updatedLastTweetId = 1600L;
|
||||||
|
var updatedLastSyncId = 1550L;
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var errors = 32768;
|
||||||
|
|
||||||
|
result.LastTweetPostedId = updatedLastTweetId;
|
||||||
|
result.LastTweetSynchronizedForAllFollowersId = updatedLastSyncId;
|
||||||
|
result.FetchingErrorCount = errors;
|
||||||
|
result.LastSync = now;
|
||||||
|
await dal.UpdateTwitterUserAsync(result);
|
||||||
|
|
||||||
|
result = await dal.GetTwitterUserAsync(acct);
|
||||||
|
|
||||||
|
Assert.AreEqual(acct, result.Acct);
|
||||||
|
Assert.AreEqual(updatedLastTweetId, result.LastTweetPostedId);
|
||||||
|
Assert.AreEqual(updatedLastSyncId, result.LastTweetSynchronizedForAllFollowersId);
|
||||||
|
Assert.AreEqual(errors, result.FetchingErrorCount);
|
||||||
|
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
[ExpectedException(typeof(ArgumentException))]
|
||||||
public async Task Update_NoId()
|
public async Task Update_NoId()
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BirdsiteLive.DAL.Contracts;
|
||||||
|
using BirdsiteLive.DAL.Models;
|
||||||
|
using BirdsiteLive.Domain.BusinessUseCases;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace BirdsiteLive.Domain.Tests.BusinessUseCases
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class ProcessDeleteUserTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ExecuteAsync_NoMoreFollowings()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var follower = new Follower
|
||||||
|
{
|
||||||
|
Id = 12,
|
||||||
|
Followings = new List<int> { 1 }
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||||
|
followersDalMock
|
||||||
|
.Setup(x => x.GetFollowersAsync(
|
||||||
|
It.Is<int>(y => y == 1)))
|
||||||
|
.ReturnsAsync(new[] { follower });
|
||||||
|
|
||||||
|
followersDalMock
|
||||||
|
.Setup(x => x.DeleteFollowerAsync(
|
||||||
|
It.Is<int>(y => y == 12)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
|
twitterUserDalMock
|
||||||
|
.Setup(x => x.DeleteTwitterUserAsync(
|
||||||
|
It.Is<int>(y => y == 1)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var action = new ProcessDeleteUser(followersDalMock.Object, twitterUserDalMock.Object);
|
||||||
|
await action.ExecuteAsync(follower);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
followersDalMock.VerifyAll();
|
||||||
|
twitterUserDalMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ExecuteAsync_HaveFollowings()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var follower = new Follower
|
||||||
|
{
|
||||||
|
Id = 12,
|
||||||
|
Followings = new List<int> { 1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
var followers = new List<Follower>
|
||||||
|
{
|
||||||
|
follower,
|
||||||
|
new Follower
|
||||||
|
{
|
||||||
|
Id = 11
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||||
|
followersDalMock
|
||||||
|
.Setup(x => x.GetFollowersAsync(
|
||||||
|
It.Is<int>(y => y == 1)))
|
||||||
|
.ReturnsAsync(followers.ToArray());
|
||||||
|
|
||||||
|
followersDalMock
|
||||||
|
.Setup(x => x.DeleteFollowerAsync(
|
||||||
|
It.Is<int>(y => y == 12)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var action = new ProcessDeleteUser(followersDalMock.Object, twitterUserDalMock.Object);
|
||||||
|
await action.ExecuteAsync(follower);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
followersDalMock.VerifyAll();
|
||||||
|
twitterUserDalMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BirdsiteLive.DAL.Contracts;
|
using BirdsiteLive.DAL.Contracts;
|
||||||
using BirdsiteLive.DAL.Models;
|
using BirdsiteLive.DAL.Models;
|
||||||
|
using BirdsiteLive.Domain.BusinessUseCases;
|
||||||
using BirdsiteLive.Moderation.Actions;
|
using BirdsiteLive.Moderation.Actions;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
@ -29,31 +30,19 @@ namespace BirdsiteLive.Moderation.Tests.Actions
|
||||||
It.Is<Follower>(y => y.Id == follower.Id)))
|
It.Is<Follower>(y => y.Id == follower.Id)))
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
var processDeleteUserMock = new Mock<IProcessDeleteUser>(MockBehavior.Strict);
|
||||||
followersDalMock
|
processDeleteUserMock
|
||||||
.Setup(x => x.GetFollowersAsync(
|
.Setup(x => x.ExecuteAsync(
|
||||||
It.Is<int>(y => y == 1)))
|
It.Is<Follower>(y => y.Id == follower.Id)))
|
||||||
.ReturnsAsync(new[] {follower});
|
|
||||||
|
|
||||||
followersDalMock
|
|
||||||
.Setup(x => x.DeleteFollowerAsync(
|
|
||||||
It.Is<int>(y => y == 12)))
|
|
||||||
.Returns(Task.CompletedTask);
|
|
||||||
|
|
||||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
|
||||||
twitterUserDalMock
|
|
||||||
.Setup(x => x.DeleteTwitterUserAsync(
|
|
||||||
It.Is<int>(y => y == 1)))
|
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var action = new RemoveFollowerAction(followersDalMock.Object, twitterUserDalMock.Object, rejectAllFollowingsActionMock.Object);
|
var action = new RemoveFollowerAction(rejectAllFollowingsActionMock.Object, processDeleteUserMock.Object);
|
||||||
await action.ProcessAsync(follower);
|
await action.ProcessAsync(follower);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
followersDalMock.VerifyAll();
|
|
||||||
twitterUserDalMock.VerifyAll();
|
|
||||||
rejectAllFollowingsActionMock.VerifyAll();
|
rejectAllFollowingsActionMock.VerifyAll();
|
||||||
|
processDeleteUserMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,15 +55,6 @@ namespace BirdsiteLive.Moderation.Tests.Actions
|
||||||
Id = 12,
|
Id = 12,
|
||||||
Followings = new List<int> { 1 }
|
Followings = new List<int> { 1 }
|
||||||
};
|
};
|
||||||
|
|
||||||
var followers = new List<Follower>
|
|
||||||
{
|
|
||||||
follower,
|
|
||||||
new Follower
|
|
||||||
{
|
|
||||||
Id = 11
|
|
||||||
}
|
|
||||||
};
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Mocks
|
#region Mocks
|
||||||
|
@ -84,27 +64,19 @@ namespace BirdsiteLive.Moderation.Tests.Actions
|
||||||
It.Is<Follower>(y => y.Id == follower.Id)))
|
It.Is<Follower>(y => y.Id == follower.Id)))
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
var processDeleteUserMock = new Mock<IProcessDeleteUser>(MockBehavior.Strict);
|
||||||
followersDalMock
|
processDeleteUserMock
|
||||||
.Setup(x => x.GetFollowersAsync(
|
.Setup(x => x.ExecuteAsync(
|
||||||
It.Is<int>(y => y == 1)))
|
It.Is<Follower>(y => y.Id == follower.Id)))
|
||||||
.ReturnsAsync(followers.ToArray());
|
|
||||||
|
|
||||||
followersDalMock
|
|
||||||
.Setup(x => x.DeleteFollowerAsync(
|
|
||||||
It.Is<int>(y => y == 12)))
|
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var action = new RemoveFollowerAction(followersDalMock.Object, twitterUserDalMock.Object, rejectAllFollowingsActionMock.Object);
|
var action = new RemoveFollowerAction(rejectAllFollowingsActionMock.Object, processDeleteUserMock.Object);
|
||||||
await action.ProcessAsync(follower);
|
await action.ProcessAsync(follower);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
followersDalMock.VerifyAll();
|
|
||||||
twitterUserDalMock.VerifyAll();
|
|
||||||
rejectAllFollowingsActionMock.VerifyAll();
|
rejectAllFollowingsActionMock.VerifyAll();
|
||||||
|
processDeleteUserMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -159,11 +160,136 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
|
|
||||||
twitterUserServiceMock
|
twitterUserServiceMock
|
||||||
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
||||||
.Returns((TwitterUser) null);
|
.Throws(new UserNotFoundException());
|
||||||
|
|
||||||
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
|
||||||
|
removeTwitterAccountActionMock
|
||||||
|
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Acct == acct2)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
|
||||||
|
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
Assert.AreEqual(1, result.Length);
|
||||||
|
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
|
||||||
|
|
||||||
|
twitterUserServiceMock.VerifyAll();
|
||||||
|
twitterUserDalMock.VerifyAll();
|
||||||
|
removeTwitterAccountActionMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ProcessAsync_Suspended_Test()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var userId1 = 1;
|
||||||
|
var acct1 = "user1";
|
||||||
|
|
||||||
|
var userId2 = 2;
|
||||||
|
var acct2 = "user2";
|
||||||
|
|
||||||
|
var users = new List<SyncTwitterUser>
|
||||||
|
{
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId1,
|
||||||
|
Acct = acct1
|
||||||
|
},
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
Acct = acct2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var settings = new InstanceSettings
|
||||||
|
{
|
||||||
|
FailingTwitterUserCleanUpThreshold = 300
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
|
||||||
|
twitterUserServiceMock
|
||||||
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
|
||||||
|
.Returns(new TwitterUser
|
||||||
|
{
|
||||||
|
Protected = false
|
||||||
|
});
|
||||||
|
|
||||||
twitterUserServiceMock
|
twitterUserServiceMock
|
||||||
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct2)));
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
||||||
|
.Throws(new UserHasBeenSuspendedException());
|
||||||
|
|
||||||
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
|
||||||
|
removeTwitterAccountActionMock
|
||||||
|
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Acct == acct2)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
|
||||||
|
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
Assert.AreEqual(1, result.Length);
|
||||||
|
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
|
||||||
|
|
||||||
|
twitterUserServiceMock.VerifyAll();
|
||||||
|
twitterUserDalMock.VerifyAll();
|
||||||
|
removeTwitterAccountActionMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ProcessAsync_Exception_Test()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var userId1 = 1;
|
||||||
|
var acct1 = "user1";
|
||||||
|
|
||||||
|
var userId2 = 2;
|
||||||
|
var acct2 = "user2";
|
||||||
|
|
||||||
|
var users = new List<SyncTwitterUser>
|
||||||
|
{
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId1,
|
||||||
|
Acct = acct1
|
||||||
|
},
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
Acct = acct2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var settings = new InstanceSettings
|
||||||
|
{
|
||||||
|
FailingTwitterUserCleanUpThreshold = 300
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
|
||||||
|
twitterUserServiceMock
|
||||||
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
|
||||||
|
.Returns(new TwitterUser
|
||||||
|
{
|
||||||
|
Protected = false
|
||||||
|
});
|
||||||
|
|
||||||
|
twitterUserServiceMock
|
||||||
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
||||||
|
.Throws(new Exception());
|
||||||
|
|
||||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
twitterUserDalMock
|
twitterUserDalMock
|
||||||
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
|
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
|
||||||
|
@ -194,7 +320,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task ProcessAsync_Unfound_OverThreshold_Test()
|
public async Task ProcessAsync_Error_Test()
|
||||||
{
|
{
|
||||||
#region Stubs
|
#region Stubs
|
||||||
var userId1 = 1;
|
var userId1 = 1;
|
||||||
|
@ -235,10 +361,79 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
twitterUserServiceMock
|
twitterUserServiceMock
|
||||||
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
||||||
.Returns((TwitterUser)null);
|
.Returns((TwitterUser)null);
|
||||||
|
|
||||||
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
|
twitterUserDalMock
|
||||||
|
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
|
||||||
|
.ReturnsAsync(new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
FetchingErrorCount = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
twitterUserDalMock
|
||||||
|
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 1)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
|
||||||
|
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
Assert.AreEqual(1, result.Length);
|
||||||
|
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
|
||||||
|
|
||||||
|
twitterUserServiceMock.VerifyAll();
|
||||||
|
twitterUserDalMock.VerifyAll();
|
||||||
|
removeTwitterAccountActionMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ProcessAsync_Error_OverThreshold_Test()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var userId1 = 1;
|
||||||
|
var acct1 = "user1";
|
||||||
|
|
||||||
|
var userId2 = 2;
|
||||||
|
var acct2 = "user2";
|
||||||
|
|
||||||
|
var users = new List<SyncTwitterUser>
|
||||||
|
{
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId1,
|
||||||
|
Acct = acct1
|
||||||
|
},
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
Acct = acct2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var settings = new InstanceSettings
|
||||||
|
{
|
||||||
|
FailingTwitterUserCleanUpThreshold = 300
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
|
||||||
|
twitterUserServiceMock
|
||||||
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
|
||||||
|
.Returns(new TwitterUser
|
||||||
|
{
|
||||||
|
Protected = false
|
||||||
|
});
|
||||||
|
|
||||||
twitterUserServiceMock
|
twitterUserServiceMock
|
||||||
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct2)));
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
||||||
|
.Returns((TwitterUser)null);
|
||||||
|
|
||||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
twitterUserDalMock
|
twitterUserDalMock
|
||||||
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
|
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
|
||||||
|
@ -312,8 +507,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
{
|
{
|
||||||
Protected = true
|
Protected = true
|
||||||
});
|
});
|
||||||
|
|
||||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
|
twitterUserDalMock
|
||||||
|
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
|
||||||
|
.ReturnsAsync(new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
FetchingErrorCount = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
twitterUserDalMock
|
||||||
|
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 1)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
|
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -331,7 +538,81 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task ProcessAsync_Unfound_NotInit_Test()
|
public async Task ProcessAsync_Protected_OverThreshold_Test()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var userId1 = 1;
|
||||||
|
var acct1 = "user1";
|
||||||
|
|
||||||
|
var userId2 = 2;
|
||||||
|
var acct2 = "user2";
|
||||||
|
|
||||||
|
var users = new List<SyncTwitterUser>
|
||||||
|
{
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId1,
|
||||||
|
Acct = acct1
|
||||||
|
},
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
Acct = acct2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var settings = new InstanceSettings
|
||||||
|
{
|
||||||
|
FailingTwitterUserCleanUpThreshold = 300
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
|
||||||
|
twitterUserServiceMock
|
||||||
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
|
||||||
|
.Returns(new TwitterUser
|
||||||
|
{
|
||||||
|
Protected = false
|
||||||
|
});
|
||||||
|
|
||||||
|
twitterUserServiceMock
|
||||||
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
||||||
|
.Returns(new TwitterUser
|
||||||
|
{
|
||||||
|
Protected = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
|
twitterUserDalMock
|
||||||
|
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
|
||||||
|
.ReturnsAsync(new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
FetchingErrorCount = 500
|
||||||
|
});
|
||||||
|
|
||||||
|
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
|
||||||
|
removeTwitterAccountActionMock
|
||||||
|
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
|
||||||
|
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
Assert.AreEqual(1, result.Length);
|
||||||
|
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
|
||||||
|
|
||||||
|
twitterUserServiceMock.VerifyAll();
|
||||||
|
twitterUserDalMock.VerifyAll();
|
||||||
|
removeTwitterAccountActionMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ProcessAsync_Error_NotInit_Test()
|
||||||
{
|
{
|
||||||
#region Stubs
|
#region Stubs
|
||||||
var userId1 = 1;
|
var userId1 = 1;
|
||||||
|
@ -361,9 +642,6 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
|
||||||
.Returns((TwitterUser)null);
|
.Returns((TwitterUser)null);
|
||||||
|
|
||||||
twitterUserServiceMock
|
|
||||||
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct1)));
|
|
||||||
|
|
||||||
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
twitterUserDalMock
|
twitterUserDalMock
|
||||||
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct1)))
|
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct1)))
|
||||||
|
@ -388,5 +666,77 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
removeTwitterAccountActionMock.VerifyAll();
|
removeTwitterAccountActionMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ProcessAsync_RateLimited_Test()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var userId1 = 1;
|
||||||
|
var acct1 = "user1";
|
||||||
|
|
||||||
|
var userId2 = 2;
|
||||||
|
var acct2 = "user2";
|
||||||
|
|
||||||
|
var users = new List<SyncTwitterUser>
|
||||||
|
{
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId1,
|
||||||
|
Acct = acct1
|
||||||
|
},
|
||||||
|
new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
Acct = acct2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var settings = new InstanceSettings
|
||||||
|
{
|
||||||
|
FailingTwitterUserCleanUpThreshold = 300
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
|
||||||
|
twitterUserServiceMock
|
||||||
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
|
||||||
|
.Returns(new TwitterUser
|
||||||
|
{
|
||||||
|
Protected = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
twitterUserServiceMock
|
||||||
|
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
|
||||||
|
.Throws(new RateLimitExceededException());
|
||||||
|
|
||||||
|
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
|
||||||
|
twitterUserDalMock
|
||||||
|
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
|
||||||
|
.ReturnsAsync(new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
FetchingErrorCount = 20
|
||||||
|
});
|
||||||
|
|
||||||
|
twitterUserDalMock
|
||||||
|
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 20)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
|
||||||
|
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
Assert.AreEqual(1, result.Length);
|
||||||
|
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
|
||||||
|
|
||||||
|
twitterUserServiceMock.VerifyAll();
|
||||||
|
twitterUserDalMock.VerifyAll();
|
||||||
|
removeTwitterAccountActionMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,8 +2,10 @@
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using BirdsiteLive.Common.Settings;
|
||||||
using BirdsiteLive.DAL.Contracts;
|
using BirdsiteLive.DAL.Contracts;
|
||||||
using BirdsiteLive.DAL.Models;
|
using BirdsiteLive.DAL.Models;
|
||||||
|
using BirdsiteLive.Moderation.Actions;
|
||||||
using BirdsiteLive.Pipeline.Models;
|
using BirdsiteLive.Pipeline.Models;
|
||||||
using BirdsiteLive.Pipeline.Processors;
|
using BirdsiteLive.Pipeline.Processors;
|
||||||
using BirdsiteLive.Pipeline.Processors.SubTasks;
|
using BirdsiteLive.Pipeline.Processors.SubTasks;
|
||||||
|
@ -72,17 +74,22 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,15 +154,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,15 +241,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,15 +329,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -400,15 +422,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,15 +498,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,15 +575,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -623,15 +660,196 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_Error_SettingsThreshold_Test()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var tweetId = 1;
|
||||||
|
var host1 = "domain1.ext";
|
||||||
|
var host2 = "domain2.ext";
|
||||||
|
var inbox = "/user/inbox";
|
||||||
|
var userId1 = 2;
|
||||||
|
var userId2 = 3;
|
||||||
|
var userAcct = "user";
|
||||||
|
|
||||||
|
var userWithTweets = new UserWithDataToSync()
|
||||||
|
{
|
||||||
|
Tweets = new[]
|
||||||
|
{
|
||||||
|
new ExtractedTweet
|
||||||
|
{
|
||||||
|
Id = tweetId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
User = new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Acct = userAcct
|
||||||
|
},
|
||||||
|
Followers = new[]
|
||||||
|
{
|
||||||
|
new Follower
|
||||||
|
{
|
||||||
|
Id = userId1,
|
||||||
|
Host = host1,
|
||||||
|
InboxRoute = inbox
|
||||||
|
},
|
||||||
|
new Follower
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
Host = host2,
|
||||||
|
InboxRoute = inbox,
|
||||||
|
PostingErrorCount = 42
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
|
||||||
|
sendTweetsToInboxTaskMock
|
||||||
|
.Setup(x => x.ExecuteAsync(
|
||||||
|
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||||
|
It.Is<Follower>(y => y.Id == userId1),
|
||||||
|
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
sendTweetsToInboxTaskMock
|
||||||
|
.Setup(x => x.ExecuteAsync(
|
||||||
|
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||||
|
It.Is<Follower>(y => y.Id == userId2),
|
||||||
|
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
|
||||||
|
.Throws(new Exception());
|
||||||
|
|
||||||
|
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings
|
||||||
|
{
|
||||||
|
FailingFollowerCleanUpThreshold = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
|
removeFollowerMock
|
||||||
|
.Setup(x => x.ProcessAsync(It.Is<Follower>(y => y.Id == userId2)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_Error_MaxThreshold_Test()
|
||||||
|
{
|
||||||
|
#region Stubs
|
||||||
|
var tweetId = 1;
|
||||||
|
var host1 = "domain1.ext";
|
||||||
|
var host2 = "domain2.ext";
|
||||||
|
var inbox = "/user/inbox";
|
||||||
|
var userId1 = 2;
|
||||||
|
var userId2 = 3;
|
||||||
|
var userAcct = "user";
|
||||||
|
|
||||||
|
var userWithTweets = new UserWithDataToSync()
|
||||||
|
{
|
||||||
|
Tweets = new[]
|
||||||
|
{
|
||||||
|
new ExtractedTweet
|
||||||
|
{
|
||||||
|
Id = tweetId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
User = new SyncTwitterUser
|
||||||
|
{
|
||||||
|
Acct = userAcct
|
||||||
|
},
|
||||||
|
Followers = new[]
|
||||||
|
{
|
||||||
|
new Follower
|
||||||
|
{
|
||||||
|
Id = userId1,
|
||||||
|
Host = host1,
|
||||||
|
InboxRoute = inbox
|
||||||
|
},
|
||||||
|
new Follower
|
||||||
|
{
|
||||||
|
Id = userId2,
|
||||||
|
Host = host2,
|
||||||
|
InboxRoute = inbox,
|
||||||
|
PostingErrorCount = 2147483600
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mocks
|
||||||
|
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
|
||||||
|
sendTweetsToInboxTaskMock
|
||||||
|
.Setup(x => x.ExecuteAsync(
|
||||||
|
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||||
|
It.Is<Follower>(y => y.Id == userId1),
|
||||||
|
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
sendTweetsToInboxTaskMock
|
||||||
|
.Setup(x => x.ExecuteAsync(
|
||||||
|
It.Is<ExtractedTweet[]>(y => y.Length == 1),
|
||||||
|
It.Is<Follower>(y => y.Id == userId2),
|
||||||
|
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
|
||||||
|
.Throws(new Exception());
|
||||||
|
|
||||||
|
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings
|
||||||
|
{
|
||||||
|
FailingFollowerCleanUpThreshold = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
|
removeFollowerMock
|
||||||
|
.Setup(x => x.ProcessAsync(It.Is<Follower>(y => y.Id == userId2)))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
|
#region Validations
|
||||||
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -704,15 +922,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -790,15 +1013,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
|
||||||
|
|
||||||
|
var settings = new InstanceSettings();
|
||||||
|
|
||||||
|
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
|
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
|
||||||
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
|
||||||
|
|
||||||
#region Validations
|
#region Validations
|
||||||
sendTweetsToInboxTaskMock.VerifyAll();
|
sendTweetsToInboxTaskMock.VerifyAll();
|
||||||
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
sendTweetsToSharedInboxTaskMock.VerifyAll();
|
||||||
followersDalMock.VerifyAll();
|
followersDalMock.VerifyAll();
|
||||||
|
removeFollowerMock.VerifyAll();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue