Merge pull request #94 from NicolasConstant/develop

0.15.0 PR
This commit is contained in:
Nicolas Constant 2021-02-25 05:20:27 +01:00 committed by GitHub
commit 27312dd3c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 3131 additions and 109 deletions

View File

@ -24,5 +24,5 @@ jobs:
run: dotnet build --configuration Release --no-restore
working-directory: ${{env.working-directory}}
- name: Test
run: dotnet test --no-restore --verbosity quiet
run: dotnet test --no-restore --verbosity minimal
working-directory: ${{env.working-directory}}

View File

@ -2,6 +2,40 @@
You can configure some of BirdsiteLIVE's settings via environment variables (those are optionnals):
## Blacklisting & Whitelisting
### Fediverse users and instances
Here are the supported patterns to describe Fediverse users and/or instances:
* `@user@instance.ext` to describe a Fediverse user
* `instance.ext` to describe an instance under a domain name
* `*.instance.ext` to describe instances from all subdomains of a domain name (this doesn't include the instance.ext, if you want both you need to add both)
You can whitelist or blacklist fediverses users by settings the followings variables with the above patterns separated by `;`:
* `Moderation:FollowersWhiteListing` Fediverse Whitelisting
* `Moderation:FollowersBlackListing` Fediverse Blacklisting
If the whitelisting is set, only given patterns can follow twitter accounts on the instance.
If blacklisted, the given patterns can't follow twitter accounts on the instance.
If both whitelisting and blacklisting are set, only the whitelisting will be active.
### Twitter users
Here is the supported pattern to describe Twitter users:
* `twitter_handle` to describe a Twitter user
You can whitelist or blacklist twitter users by settings the followings variables with the above pattern separated by `;`:
* `Moderation:TwitterAccountsWhiteListing` Twitter Whitelisting
* `Moderation:TwitterAccountsBlackListing` Twitter Blacklisting
If the whitelisting is set, only given patterns can be followed on the instance.
If blacklisted, the given patterns can't be followed on the instance.
If both whitelisting and blacklisting are set, only the whitelisting will be active.
## Logging
* `Logging:Type` (default: none) set the type of the logging and monitoring system, currently the only type supported is `insights` for *Azure Application Insights* (PR welcome to support other types)
@ -11,4 +45,45 @@ You can configure some of BirdsiteLIVE's settings via environment variables (tho
* `Instance:Name` (default: BirdsiteLIVE) the name of the instance
* `Instance:ResolveMentionsInProfiles` (default: true) to enable or disable mentions parsing in profile's description. Resolving it will consume more User's API calls since newly discovered account can also contain references to others accounts as well. On a big instance it is recommended to disable it.
* `Instance:PublishReplies` (default: false) to enable or disable replies publishing.
* `Instance:PublishReplies` (default: false) to enable or disable replies publishing.
# Docker Compose full example
In order to illustrate above variables, here is an example of an updated `docker-compose.yml` file:
```diff
version: "3"
networks:
[...]
services:
server:
image: nicolasconstant/birdsitelive:latest
[...]
environment:
- Instance:Domain=domain.name
- Instance:AdminEmail=name@domain.ext
- Db:Type=postgres
- Db:Host=db
- Db:Name=birdsitelive
- Db:User=birdsitelive
- Db:Password=birdsitelive
- Twitter:ConsumerKey=twitter.api.key
- Twitter:ConsumerSecret=twitter.api.key
+ - Moderation:FollowersWhiteListing=@me@my-instance.ca;friend-instance.com;*.friend-instance.com
+ - Moderation:TwitterAccountsBlackListing=douchebag;jerk_88;theRealIdiot
+ - Instance:Name=MyTwitterRelay
+ - Instance:ResolveMentionsInProfiles=false
+ - Instance:PublishReplies=true
networks:
[...]
db:
image: postgres:9.6
[...]
```
## Apply the modifications
After the modification of the `docker-compose.yml` file, you will need to run `docker-compose up -d` to apply the changes.

View File

@ -41,7 +41,6 @@ namespace BirdsiteLive.ActivityPub
}
};
return acceptFollow;
break;
}
break;
}

View File

@ -0,0 +1,10 @@
namespace BirdsiteLive.Common.Settings
{
public class ModerationSettings
{
public string FollowersWhiteListing { get; set; }
public string FollowersBlackListing { get; set; }
public string TwitterAccountsWhiteListing { get; set; }
public string TwitterAccountsBlackListing { get; set; }
}
}

View File

@ -5,7 +5,7 @@ namespace BirdsiteLive.Domain.BusinessUseCases
{
public interface IProcessFollowUser
{
Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername, string followerInbox, string sharedInbox);
Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername, string followerInbox, string sharedInbox, string followerActorId);
}
public class ProcessFollowUser : IProcessFollowUser
@ -21,13 +21,13 @@ namespace BirdsiteLive.Domain.BusinessUseCases
}
#endregion
public async Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername, string followerInbox, string sharedInbox)
public async Task ExecuteAsync(string followerUsername, string followerDomain, string twitterUsername, string followerInbox, string sharedInbox, string followerActorId)
{
// Get Follower and Twitter Users
var follower = await _followerDal.GetFollowerAsync(followerUsername, followerDomain);
if (follower == null)
{
await _followerDal.CreateFollowerAsync(followerUsername, followerDomain, followerInbox, sharedInbox);
await _followerDal.CreateFollowerAsync(followerUsername, followerDomain, followerInbox, sharedInbox, followerActorId);
follower = await _followerDal.GetFollowerAsync(followerUsername, followerDomain);
}

View File

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.Domain.Tools;
namespace BirdsiteLive.Domain.Repository
{
public interface IModerationRepository
{
ModerationTypeEnum GetModerationType(ModerationEntityTypeEnum type);
ModeratedTypeEnum CheckStatus(ModerationEntityTypeEnum type, string entity);
}
public class ModerationRepository : IModerationRepository
{
private readonly Regex[] _followersWhiteListing;
private readonly Regex[] _followersBlackListing;
private readonly Regex[] _twitterAccountsWhiteListing;
private readonly Regex[] _twitterAccountsBlackListing;
private readonly Dictionary<ModerationEntityTypeEnum, ModerationTypeEnum> _modMode =
new Dictionary<ModerationEntityTypeEnum, ModerationTypeEnum>();
#region Ctor
public ModerationRepository(ModerationSettings settings)
{
var parsedFollowersWhiteListing = ModerationParser.Parse(settings.FollowersWhiteListing);
var parsedFollowersBlackListing = ModerationParser.Parse(settings.FollowersBlackListing);
var parsedTwitterAccountsWhiteListing = ModerationParser.Parse(settings.TwitterAccountsWhiteListing);
var parsedTwitterAccountsBlackListing = ModerationParser.Parse(settings.TwitterAccountsBlackListing);
_followersWhiteListing = parsedFollowersWhiteListing
.Select(x => ModerationRegexParser.Parse(ModerationEntityTypeEnum.Follower, x))
.ToArray();
_followersBlackListing = parsedFollowersBlackListing
.Select(x => ModerationRegexParser.Parse(ModerationEntityTypeEnum.Follower, x))
.ToArray();
_twitterAccountsWhiteListing = parsedTwitterAccountsWhiteListing
.Select(x => ModerationRegexParser.Parse(ModerationEntityTypeEnum.TwitterAccount, x))
.ToArray();
_twitterAccountsBlackListing = parsedTwitterAccountsBlackListing
.Select(x => ModerationRegexParser.Parse(ModerationEntityTypeEnum.TwitterAccount, x))
.ToArray();
// Set Follower moderation politic
if (_followersWhiteListing.Any())
_modMode.Add(ModerationEntityTypeEnum.Follower, ModerationTypeEnum.WhiteListing);
else if (_followersBlackListing.Any())
_modMode.Add(ModerationEntityTypeEnum.Follower, ModerationTypeEnum.BlackListing);
else
_modMode.Add(ModerationEntityTypeEnum.Follower, ModerationTypeEnum.None);
// Set Twitter account moderation politic
if (_twitterAccountsWhiteListing.Any())
_modMode.Add(ModerationEntityTypeEnum.TwitterAccount, ModerationTypeEnum.WhiteListing);
else if (_twitterAccountsBlackListing.Any())
_modMode.Add(ModerationEntityTypeEnum.TwitterAccount, ModerationTypeEnum.BlackListing);
else
_modMode.Add(ModerationEntityTypeEnum.TwitterAccount, ModerationTypeEnum.None);
}
#endregion
public ModerationTypeEnum GetModerationType(ModerationEntityTypeEnum type)
{
return _modMode[type];
}
public ModeratedTypeEnum CheckStatus(ModerationEntityTypeEnum type, string entity)
{
if (_modMode[type] == ModerationTypeEnum.None) return ModeratedTypeEnum.None;
switch (type)
{
case ModerationEntityTypeEnum.Follower:
return ProcessFollower(entity);
case ModerationEntityTypeEnum.TwitterAccount:
return ProcessTwitterAccount(entity);
}
throw new NotImplementedException($"Type {type} is not supported");
}
private ModeratedTypeEnum ProcessFollower(string entity)
{
var politic = _modMode[ModerationEntityTypeEnum.Follower];
switch (politic)
{
case ModerationTypeEnum.None:
return ModeratedTypeEnum.None;
case ModerationTypeEnum.BlackListing:
if (_followersBlackListing.Any(x => x.IsMatch(entity)))
return ModeratedTypeEnum.BlackListed;
return ModeratedTypeEnum.None;
case ModerationTypeEnum.WhiteListing:
if (_followersWhiteListing.Any(x => x.IsMatch(entity)))
return ModeratedTypeEnum.WhiteListed;
return ModeratedTypeEnum.None;
default:
throw new ArgumentOutOfRangeException();
}
}
private ModeratedTypeEnum ProcessTwitterAccount(string entity)
{
var politic = _modMode[ModerationEntityTypeEnum.TwitterAccount];
switch (politic)
{
case ModerationTypeEnum.None:
return ModeratedTypeEnum.None;
case ModerationTypeEnum.BlackListing:
if (_twitterAccountsBlackListing.Any(x => x.IsMatch(entity)))
return ModeratedTypeEnum.BlackListed;
return ModeratedTypeEnum.None;
case ModerationTypeEnum.WhiteListing:
if (_twitterAccountsWhiteListing.Any(x => x.IsMatch(entity)))
return ModeratedTypeEnum.WhiteListed;
return ModeratedTypeEnum.None;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public enum ModerationEntityTypeEnum
{
Unknown = 0,
Follower = 1,
TwitterAccount = 2
}
public enum ModerationTypeEnum
{
None = 0,
BlackListing = 1,
WhiteListing = 2
}
public enum ModeratedTypeEnum
{
None = 0,
BlackListed = 1,
WhiteListed = 2
}
}

View File

@ -41,7 +41,6 @@ namespace BirdsiteLive.Domain
var noteUrl = UrlFactory.GetNoteUrl(_instanceSettings.Domain, username, tweet.Id.ToString());
var to = $"{actorUrl}/followers";
var apPublic = "https://www.w3.org/ns/activitystreams#Public";
var extractedTags = _statusExtractor.Extract(tweet.MessageContent);
_statisticsHandler.ExtractedStatus(extractedTags.tags.Count(x => x.type == "Mention"));
@ -70,11 +69,9 @@ namespace BirdsiteLive.Domain
attributedTo = actorUrl,
inReplyTo = inReplyTo,
//to = new [] {to},
//cc = new [] { apPublic },
to = new[] { to },
//cc = new[] { apPublic },
//cc = new[] { "https://www.w3.org/ns/activitystreams#Public" },
cc = new string[0],
sensitive = false,

View File

@ -0,0 +1,23 @@
using System;
using System.Linq;
namespace BirdsiteLive.Domain.Tools
{
public class ModerationParser
{
public static string[] Parse(string entry)
{
if (string.IsNullOrWhiteSpace(entry)) return new string[0];
var separationChar = '|';
if (entry.Contains(";")) separationChar = ';';
else if (entry.Contains(",")) separationChar = ',';
var splitEntries = entry
.Split(new[] {separationChar}, StringSplitOptions.RemoveEmptyEntries)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.ToLowerInvariant().Trim());
return splitEntries.ToArray();
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Text.RegularExpressions;
using BirdsiteLive.Domain.Repository;
using Org.BouncyCastle.Pkcs;
namespace BirdsiteLive.Domain.Tools
{
public class ModerationRegexParser
{
public static Regex Parse(ModerationEntityTypeEnum type, string data)
{
data = data.ToLowerInvariant().Trim();
if (type == ModerationEntityTypeEnum.Follower)
{
if (data.StartsWith("@"))
return new Regex($@"^{data}$");
if (data.StartsWith("*"))
data = data.Replace("*", "(.+)");
return new Regex($@"^@(.+)@{data}$");
}
return new Regex($@"^{data}$");
}
}
}

View File

@ -11,6 +11,7 @@ using BirdsiteLive.Common.Regexes;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.Cryptography;
using BirdsiteLive.Domain.BusinessUseCases;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Domain.Statistics;
using BirdsiteLive.Domain.Tools;
using BirdsiteLive.Twitter;
@ -25,6 +26,8 @@ namespace BirdsiteLive.Domain
Actor GetUser(TwitterUser twitterUser);
Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow 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);
}
public class UserService : IUserService
@ -40,8 +43,10 @@ namespace BirdsiteLive.Domain
private readonly ITwitterUserService _twitterUserService;
private readonly IModerationRepository _moderationRepository;
#region Ctor
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser, IStatusExtractor statusExtractor, IExtractionStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService)
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser, IStatusExtractor statusExtractor, IExtractionStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, IModerationRepository moderationRepository)
{
_instanceSettings = instanceSettings;
_cryptoService = cryptoService;
@ -51,6 +56,7 @@ namespace BirdsiteLive.Domain
_statusExtractor = statusExtractor;
_statisticsHandler = statisticsHandler;
_twitterUserService = twitterUserService;
_moderationRepository = moderationRepository;
}
#endregion
@ -119,62 +125,94 @@ namespace BirdsiteLive.Domain
var sigValidation = await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders, body);
if (!sigValidation.SignatureIsValidated) return false;
// Save Follow in DB
var followerUserName = sigValidation.User.preferredUsername.ToLowerInvariant();
// Prepare data
var followerUserName = sigValidation.User.preferredUsername.ToLowerInvariant().Trim();
var followerHost = sigValidation.User.url.Replace("https://", string.Empty).Split('/').First();
var followerInbox = sigValidation.User.inbox;
var followerSharedInbox = sigValidation.User?.endpoints?.sharedInbox;
var twitterUser = activity.apObject.Split('/').Last().Replace("@", string.Empty);
var twitterUser = activity.apObject.Split('/').Last().Replace("@", string.Empty).ToLowerInvariant().Trim();
// Make sure to only keep routes
followerInbox = OnlyKeepRoute(followerInbox, followerHost);
followerSharedInbox = OnlyKeepRoute(followerSharedInbox, followerHost);
// Validate Moderation status
var followerModPolicy = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.Follower);
if (followerModPolicy != ModerationTypeEnum.None)
{
var followerStatus = _moderationRepository.CheckStatus(ModerationEntityTypeEnum.Follower, $"@{followerUserName}@{followerHost}");
if(followerModPolicy == ModerationTypeEnum.WhiteListing && followerStatus != ModeratedTypeEnum.WhiteListed ||
followerModPolicy == ModerationTypeEnum.BlackListing && followerStatus == ModeratedTypeEnum.BlackListed)
return await SendRejectFollowAsync(activity, followerHost);
}
// Validate TwitterAccount status
var twitterAccountModPolicy = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.TwitterAccount);
if (twitterAccountModPolicy != ModerationTypeEnum.None)
{
var twitterUserStatus = _moderationRepository.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, twitterUser);
if (twitterAccountModPolicy == ModerationTypeEnum.WhiteListing && twitterUserStatus != ModeratedTypeEnum.WhiteListed ||
twitterAccountModPolicy == ModerationTypeEnum.BlackListing && twitterUserStatus == ModeratedTypeEnum.BlackListed)
return await SendRejectFollowAsync(activity, followerHost);
}
// Validate User Protected
var user = _twitterUserService.GetUser(twitterUser);
if (!user.Protected)
{
// Execute
await _processFollowUser.ExecuteAsync(followerUserName, followerHost, twitterUser, followerInbox, followerSharedInbox);
await _processFollowUser.ExecuteAsync(followerUserName, followerHost, twitterUser, followerInbox, followerSharedInbox, activity.actor);
// Send Accept Activity
var acceptFollow = new ActivityAcceptFollow()
{
context = "https://www.w3.org/ns/activitystreams",
id = $"{activity.apObject}#accepts/follows/{Guid.NewGuid()}",
type = "Accept",
actor = activity.apObject,
apObject = new ActivityFollow()
{
id = activity.id,
type = activity.type,
actor = activity.actor,
apObject = activity.apObject
}
};
var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject);
return result == HttpStatusCode.Accepted || result == HttpStatusCode.OK; //TODO: revamp this for better error handling
return await SendAcceptFollowAsync(activity, followerHost);
}
else
{
// Send Reject Activity
var acceptFollow = new ActivityRejectFollow()
{
context = "https://www.w3.org/ns/activitystreams",
id = $"{activity.apObject}#rejects/follows/{Guid.NewGuid()}",
type = "Reject",
actor = activity.apObject,
apObject = new ActivityFollow()
{
id = activity.id,
type = activity.type,
actor = activity.actor,
apObject = activity.apObject
}
};
var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject);
return result == HttpStatusCode.Accepted || result == HttpStatusCode.OK; //TODO: revamp this for better error handling
return await SendRejectFollowAsync(activity, followerHost);
}
}
private async Task<bool> SendAcceptFollowAsync(ActivityFollow activity, string followerHost)
{
var acceptFollow = new ActivityAcceptFollow()
{
context = "https://www.w3.org/ns/activitystreams",
id = $"{activity.apObject}#accepts/follows/{Guid.NewGuid()}",
type = "Accept",
actor = activity.apObject,
apObject = new ActivityFollow()
{
id = activity.id,
type = activity.type,
actor = activity.actor,
apObject = activity.apObject
}
};
var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject);
return result == HttpStatusCode.Accepted ||
result == HttpStatusCode.OK; //TODO: revamp this for better error handling
}
public async Task<bool> SendRejectFollowAsync(ActivityFollow activity, string followerHost)
{
var acceptFollow = new ActivityRejectFollow()
{
context = "https://www.w3.org/ns/activitystreams",
id = $"{activity.apObject}#rejects/follows/{Guid.NewGuid()}",
type = "Reject",
actor = activity.apObject,
apObject = new ActivityFollow()
{
id = activity.id,
type = activity.type,
actor = activity.actor,
apObject = activity.apObject
}
};
var result = await _activityPubService.PostDataAsync(acceptFollow, followerHost, activity.apObject);
return result == HttpStatusCode.Accepted ||
result == HttpStatusCode.OK; //TODO: revamp this for better error handling
}
private string OnlyKeepRoute(string inbox, string host)
{

View File

@ -0,0 +1,52 @@
using System;
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.Domain;
namespace BirdsiteLive.Moderation.Actions
{
public interface IRejectAllFollowingsAction
{
Task ProcessAsync(Follower follower);
}
public class RejectAllFollowingsAction : IRejectAllFollowingsAction
{
private readonly ITwitterUserDal _twitterUserDal;
private readonly IUserService _userService;
private readonly InstanceSettings _instanceSettings;
#region Ctor
public RejectAllFollowingsAction(ITwitterUserDal twitterUserDal, IUserService userService, InstanceSettings instanceSettings)
{
_twitterUserDal = twitterUserDal;
_userService = userService;
_instanceSettings = instanceSettings;
}
#endregion
public async Task ProcessAsync(Follower follower)
{
foreach (var following in follower.Followings)
{
try
{
var f = await _twitterUserDal.GetTwitterUserAsync(following);
var activityFollowing = new ActivityFollow
{
type = "Follow",
actor = follower.ActorId,
apObject = UrlFactory.GetActorUrl(_instanceSettings.Domain, f.Acct)
};
await _userService.SendRejectFollowAsync(activityFollowing, follower.Host);
}
catch (Exception) { }
}
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Converters;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain;
namespace BirdsiteLive.Moderation.Actions
{
public interface IRejectFollowingAction
{
Task ProcessAsync(Follower follower, SyncTwitterUser twitterUser);
}
public class RejectFollowingAction : IRejectFollowingAction
{
private readonly IUserService _userService;
private readonly InstanceSettings _instanceSettings;
#region Ctor
public RejectFollowingAction(IUserService userService, InstanceSettings instanceSettings)
{
_userService = userService;
_instanceSettings = instanceSettings;
}
#endregion
public async Task ProcessAsync(Follower follower, SyncTwitterUser twitterUser)
{
try
{
var activityFollowing = new ActivityFollow
{
type = "Follow",
actor = follower.ActorId,
apObject = UrlFactory.GetActorUrl(_instanceSettings.Domain, twitterUser.Acct)
};
await _userService.SendRejectFollowAsync(activityFollowing, follower.Host);
}
catch (Exception) { }
}
}
}

View File

@ -0,0 +1,51 @@
using System;
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.Domain;
namespace BirdsiteLive.Moderation.Actions
{
public interface IRemoveFollowerAction
{
Task ProcessAsync(Follower follower);
}
public class RemoveFollowerAction : IRemoveFollowerAction
{
private readonly IFollowersDal _followersDal;
private readonly ITwitterUserDal _twitterUserDal;
private readonly IRejectAllFollowingsAction _rejectAllFollowingsAction;
#region Ctor
public RemoveFollowerAction(IFollowersDal followersDal, ITwitterUserDal twitterUserDal, IRejectAllFollowingsAction rejectAllFollowingsAction)
{
_followersDal = followersDal;
_twitterUserDal = twitterUserDal;
_rejectAllFollowingsAction = rejectAllFollowingsAction;
}
#endregion
public async Task ProcessAsync(Follower follower)
{
// Perform undo following to user instance
await _rejectAllFollowingsAction.ProcessAsync(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);
}
}
}

View File

@ -0,0 +1,57 @@
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
namespace BirdsiteLive.Moderation.Actions
{
public interface IRemoveTwitterAccountAction
{
Task ProcessAsync(SyncTwitterUser twitterUser);
}
public class RemoveTwitterAccountAction : IRemoveTwitterAccountAction
{
private readonly IFollowersDal _followersDal;
private readonly ITwitterUserDal _twitterUserDal;
private readonly IRejectFollowingAction _rejectFollowingAction;
#region Ctor
public RemoveTwitterAccountAction(IFollowersDal followersDal, ITwitterUserDal twitterUserDal, IRejectFollowingAction rejectFollowingAction)
{
_followersDal = followersDal;
_twitterUserDal = twitterUserDal;
_rejectFollowingAction = rejectFollowingAction;
}
#endregion
public async Task ProcessAsync(SyncTwitterUser twitterUser)
{
// Check Followers
var twitterUserId = twitterUser.Id;
var followers = await _followersDal.GetFollowersAsync(twitterUserId);
// Remove all Followers
foreach (var follower in followers)
{
// Perform undo following to user instance
await _rejectFollowingAction.ProcessAsync(follower, twitterUser);
// Remove following from DB
if (follower.Followings.Contains(twitterUserId))
follower.Followings.Remove(twitterUserId);
if (follower.FollowingsSyncStatus.ContainsKey(twitterUserId))
follower.FollowingsSyncStatus.Remove(twitterUserId);
if (follower.Followings.Any())
await _followersDal.UpdateFollowerAsync(follower);
else
await _followersDal.DeleteFollowerAsync(follower.Id);
}
// Remove twitter user
await _twitterUserDal.DeleteTwitterUserAsync(twitterUserId);
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BirdsiteLive.Domain\BirdsiteLive.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Actions\" />
<Folder Include="Processors\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,61 @@
using System;
using System.Threading.Tasks;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Moderation.Processors;
using Microsoft.Extensions.Logging;
namespace BirdsiteLive.Moderation
{
public interface IModerationPipeline
{
Task ApplyModerationSettingsAsync();
}
public class ModerationPipeline : IModerationPipeline
{
private readonly IModerationRepository _moderationRepository;
private readonly IFollowerModerationProcessor _followerModerationProcessor;
private readonly ITwitterAccountModerationProcessor _twitterAccountModerationProcessor;
private readonly ILogger<ModerationPipeline> _logger;
#region Ctor
public ModerationPipeline(IModerationRepository moderationRepository, IFollowerModerationProcessor followerModerationProcessor, ITwitterAccountModerationProcessor twitterAccountModerationProcessor, ILogger<ModerationPipeline> logger)
{
_moderationRepository = moderationRepository;
_followerModerationProcessor = followerModerationProcessor;
_twitterAccountModerationProcessor = twitterAccountModerationProcessor;
_logger = logger;
}
#endregion
public async Task ApplyModerationSettingsAsync()
{
try
{
await CheckFollowerModerationPolicyAsync();
await CheckTwitterAccountModerationPolicyAsync();
}
catch (Exception e)
{
_logger.LogCritical(e, "ModerationPipeline execution failed.");
}
}
private async Task CheckFollowerModerationPolicyAsync()
{
var followerPolicy = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.Follower);
if (followerPolicy == ModerationTypeEnum.None) return;
await _followerModerationProcessor.ProcessAsync(followerPolicy);
}
private async Task CheckTwitterAccountModerationPolicyAsync()
{
var twitterAccountPolicy = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.TwitterAccount);
if (twitterAccountPolicy == ModerationTypeEnum.None) return;
await _twitterAccountModerationProcessor.ProcessAsync(twitterAccountPolicy);
}
}
}

View File

@ -0,0 +1,45 @@
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Moderation.Actions;
namespace BirdsiteLive.Moderation.Processors
{
public interface IFollowerModerationProcessor
{
Task ProcessAsync(ModerationTypeEnum type);
}
public class FollowerModerationProcessor : IFollowerModerationProcessor
{
private readonly IFollowersDal _followersDal;
private readonly IModerationRepository _moderationRepository;
private readonly IRemoveFollowerAction _removeFollowerAction;
#region Ctor
public FollowerModerationProcessor(IFollowersDal followersDal, IModerationRepository moderationRepository, IRemoveFollowerAction removeFollowerAction)
{
_followersDal = followersDal;
_moderationRepository = moderationRepository;
_removeFollowerAction = removeFollowerAction;
}
#endregion
public async Task ProcessAsync(ModerationTypeEnum type)
{
if (type == ModerationTypeEnum.None) return;
var followers = await _followersDal.GetAllFollowersAsync();
foreach (var follower in followers)
{
var followerHandle = $"@{follower.Acct.Trim()}@{follower.Host.Trim()}".ToLowerInvariant();
var status = _moderationRepository.CheckStatus(ModerationEntityTypeEnum.Follower, followerHandle);
if (type == ModerationTypeEnum.WhiteListing && status != ModeratedTypeEnum.WhiteListed ||
type == ModerationTypeEnum.BlackListing && status == ModeratedTypeEnum.BlackListed)
await _removeFollowerAction.ProcessAsync(follower);
}
}
}
}

View File

@ -0,0 +1,45 @@
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Moderation.Actions;
namespace BirdsiteLive.Moderation.Processors
{
public interface ITwitterAccountModerationProcessor
{
Task ProcessAsync(ModerationTypeEnum type);
}
public class TwitterAccountModerationProcessor : ITwitterAccountModerationProcessor
{
private readonly ITwitterUserDal _twitterUserDal;
private readonly IModerationRepository _moderationRepository;
private readonly IRemoveTwitterAccountAction _removeTwitterAccountAction;
#region Ctor
public TwitterAccountModerationProcessor(ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository, IRemoveTwitterAccountAction removeTwitterAccountAction)
{
_twitterUserDal = twitterUserDal;
_moderationRepository = moderationRepository;
_removeTwitterAccountAction = removeTwitterAccountAction;
}
#endregion
public async Task ProcessAsync(ModerationTypeEnum type)
{
if (type == ModerationTypeEnum.None) return;
var twitterUsers = await _twitterUserDal.GetAllTwitterUsersAsync();
foreach (var user in twitterUsers)
{
var userHandle = user.Acct.ToLowerInvariant().Trim();
var status = _moderationRepository.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, userHandle);
if (type == ModerationTypeEnum.WhiteListing && status != ModeratedTypeEnum.WhiteListed ||
type == ModerationTypeEnum.BlackListing && status == ModeratedTypeEnum.BlackListed)
await _removeTwitterAccountAction.ProcessAsync(user);
}
}
}
}

View File

@ -41,6 +41,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Pipeline.Tests
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.DAL.Tests", "Tests\BirdsiteLive.DAL.Tests\BirdsiteLive.DAL.Tests.csproj", "{5A1E3EB5-6CBB-470D-8A0D-10F8C18353D5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Moderation", "BirdsiteLive.Moderation\BirdsiteLive.Moderation.csproj", "{4BE541AC-8A93-4FA3-98AC-956CC2D5B748}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.Moderation.Tests", "Tests\BirdsiteLive.Moderation.Tests\BirdsiteLive.Moderation.Tests.csproj", "{0A311BF3-4FD9-4303-940A-A3778890561C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Common.Tests", "Tests\BirdsiteLive.Common.Tests\BirdsiteLive.Common.Tests.csproj", "{C69F7582-6050-44DC-BAAB-7C8F0BDA525C}"
EndProject
Global
@ -109,6 +113,14 @@ Global
{5A1E3EB5-6CBB-470D-8A0D-10F8C18353D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A1E3EB5-6CBB-470D-8A0D-10F8C18353D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A1E3EB5-6CBB-470D-8A0D-10F8C18353D5}.Release|Any CPU.Build.0 = Release|Any CPU
{4BE541AC-8A93-4FA3-98AC-956CC2D5B748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4BE541AC-8A93-4FA3-98AC-956CC2D5B748}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4BE541AC-8A93-4FA3-98AC-956CC2D5B748}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4BE541AC-8A93-4FA3-98AC-956CC2D5B748}.Release|Any CPU.Build.0 = Release|Any CPU
{0A311BF3-4FD9-4303-940A-A3778890561C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A311BF3-4FD9-4303-940A-A3778890561C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A311BF3-4FD9-4303-940A-A3778890561C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A311BF3-4FD9-4303-940A-A3778890561C}.Release|Any CPU.Build.0 = Release|Any CPU
{C69F7582-6050-44DC-BAAB-7C8F0BDA525C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C69F7582-6050-44DC-BAAB-7C8F0BDA525C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C69F7582-6050-44DC-BAAB-7C8F0BDA525C}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -132,6 +144,8 @@ Global
{F544D745-89A8-4DEA-B61C-A7E6C53C1D63} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
{BF51CA81-5A7A-46F8-B4FB-861C6BE59298} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
{5A1E3EB5-6CBB-470D-8A0D-10F8C18353D5} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
{4BE541AC-8A93-4FA3-98AC-956CC2D5B748} = {DA3C160C-4811-4E26-A5AD-42B81FAF2D7C}
{0A311BF3-4FD9-4303-940A-A3778890561C} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
{C69F7582-6050-44DC-BAAB-7C8F0BDA525C} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

View File

@ -4,7 +4,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Version>0.14.5</Version>
<Version>0.15.0</Version>
</PropertyGroup>
<ItemGroup>
@ -18,6 +18,7 @@
<ProjectReference Include="..\BirdsiteLive.Common\BirdsiteLive.Common.csproj" />
<ProjectReference Include="..\BirdsiteLive.Cryptography\BirdsiteLive.Cryptography.csproj" />
<ProjectReference Include="..\BirdsiteLive.Domain\BirdsiteLive.Domain.csproj" />
<ProjectReference Include="..\BirdsiteLive.Moderation\BirdsiteLive.Moderation.csproj" />
<ProjectReference Include="..\BirdsiteLive.Pipeline\BirdsiteLive.Pipeline.csproj" />
<ProjectReference Include="..\BirdsiteLive.Twitter\BirdsiteLive.Twitter.csproj" />
<ProjectReference Include="..\DataAccessLayers\BirdsiteLive.DAL.Postgres\BirdsiteLive.DAL.Postgres.csproj" />

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Services;
using BirdsiteLive.Statistics.Domain;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace BirdsiteLive.Component
{
public class NodeInfoViewComponent : ViewComponent
{
private readonly IModerationRepository _moderationRepository;
private readonly ICachedStatisticsService _cachedStatisticsService;
#region Ctor
public NodeInfoViewComponent(IModerationRepository moderationRepository, ICachedStatisticsService cachedStatisticsService)
{
_moderationRepository = moderationRepository;
_cachedStatisticsService = cachedStatisticsService;
}
#endregion
public async Task<IViewComponentResult> InvokeAsync()
{
var followerPolicy = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.Follower);
var twitterAccountPolicy = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.TwitterAccount);
var statistics = await _cachedStatisticsService.GetStatisticsAsync();
var viewModel = new NodeInfoViewModel
{
BlacklistingEnabled = followerPolicy == ModerationTypeEnum.BlackListing ||
twitterAccountPolicy == ModerationTypeEnum.BlackListing,
WhitelistingEnabled = followerPolicy == ModerationTypeEnum.WhiteListing ||
twitterAccountPolicy == ModerationTypeEnum.WhiteListing,
InstanceSaturation = statistics.Saturation
};
//viewModel = new NodeInfoViewModel
//{
// BlacklistingEnabled = false,
// WhitelistingEnabled = false,
// InstanceSaturation = 175
//};
return View(viewModel);
}
}
public class NodeInfoViewModel
{
public bool BlacklistingEnabled { get; set; }
public bool WhitelistingEnabled { get; set; }
public int InstanceSaturation { get; set; }
}
}

View File

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Services;
namespace BirdsiteLive.Controllers
{
public class AboutController : Controller
{
private readonly IModerationRepository _moderationRepository;
private readonly ICachedStatisticsService _cachedStatisticsService;
#region Ctor
public AboutController(IModerationRepository moderationRepository, ICachedStatisticsService cachedStatisticsService)
{
_moderationRepository = moderationRepository;
_cachedStatisticsService = cachedStatisticsService;
}
#endregion
public async Task<IActionResult> Index()
{
var stats = await _cachedStatisticsService.GetStatisticsAsync();
return View(stats);
}
public IActionResult Blacklisting()
{
var status = GetModerationStatus();
return View("Blacklisting", status);
}
public IActionResult Whitelisting()
{
var status = GetModerationStatus();
return View("Whitelisting", status);
}
private ModerationStatus GetModerationStatus()
{
var status = new ModerationStatus
{
Followers = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.Follower),
TwitterAccounts = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.TwitterAccount)
};
return status;
}
}
public class ModerationStatus
{
public ModerationTypeEnum Followers { get; set; }
public ModerationTypeEnum TwitterAccounts { get; set; }
}
}

View File

@ -19,13 +19,15 @@ namespace BirdsiteLive.Controllers
private readonly InstanceSettings _instanceSettings;
private readonly ICryptoService _cryptoService;
private readonly IActivityPubService _activityPubService;
private readonly IUserService _userService;
#region Ctor
public DebugingController(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService)
public DebugingController(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IUserService userService)
{
_instanceSettings = instanceSettings;
_cryptoService = cryptoService;
_activityPubService = activityPubService;
_userService = userService;
}
#endregion
@ -67,7 +69,6 @@ namespace BirdsiteLive.Controllers
var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{noteGuid}";
var to = $"{actor}/followers";
var apPublic = "https://www.w3.org/ns/activitystreams#Public";
var now = DateTime.UtcNow;
var nowString = now.ToString("s") + "Z";
@ -80,7 +81,7 @@ namespace BirdsiteLive.Controllers
actor = actor,
published = nowString,
to = new []{ to },
//cc = new [] { apPublic },
//cc = new [] { "https://www.w3.org/ns/activitystreams#Public" },
apObject = new Note()
{
id = noteId,
@ -90,7 +91,7 @@ namespace BirdsiteLive.Controllers
url = noteUrl,
attributedTo = actor,
to = new[] { to },
//cc = new [] { apPublic },
//cc = new [] { "https://www.w3.org/ns/activitystreams#Public" },
sensitive = false,
content = "<p>Woooot</p>",
attachment = new Attachment[0],
@ -102,6 +103,20 @@ namespace BirdsiteLive.Controllers
return View("Index");
}
[HttpPost]
public async Task<IActionResult> PostRejectFollow()
{
var activityFollow = new ActivityFollow
{
type = "Follow",
actor = "https://mastodon.technology/users/testtest",
apObject = $"https://{_instanceSettings.Domain}/users/afp"
};
await _userService.SendRejectFollowAsync(activityFollow, "mastodon.technology");
return View("Index");
}
}
public static class HtmlHelperExtensions

View File

@ -159,13 +159,11 @@ namespace BirdsiteLive.Controllers
return Accepted();
}
}
return Accepted();
}
[Route("/users/{id}/followers")]
[HttpGet]
public async Task<IActionResult> Followers(string id)
public IActionResult Followers(string id)
{
var r = Request.Headers["Accept"].First();
if (!r.Contains("application/activity+json")) return NotFound();

View File

@ -7,6 +7,7 @@ using BirdsiteLive.ActivityPub.Converters;
using BirdsiteLive.Common.Regexes;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Models;
using BirdsiteLive.Models.WellKnownModels;
using BirdsiteLive.Twitter;
@ -18,15 +19,17 @@ namespace BirdsiteLive.Controllers
[ApiController]
public class WellKnownController : ControllerBase
{
private readonly IModerationRepository _moderationRepository;
private readonly ITwitterUserService _twitterUserService;
private readonly ITwitterUserDal _twitterUserDal;
private readonly InstanceSettings _settings;
#region Ctor
public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal)
public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository)
{
_twitterUserService = twitterUserService;
_twitterUserDal = twitterUserDal;
_moderationRepository = moderationRepository;
_settings = settings;
}
#endregion
@ -58,6 +61,7 @@ namespace BirdsiteLive.Controllers
{
var version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(3);
var twitterUsersCount = await _twitterUserDal.GetTwitterUsersCountAsync();
var isOpenRegistration = _moderationRepository.GetModerationType(ModerationEntityTypeEnum.Follower) != ModerationTypeEnum.WhiteListing;
if (id == "2.0")
{
@ -81,7 +85,7 @@ namespace BirdsiteLive.Controllers
{
"activitypub"
},
openRegistrations = false,
openRegistrations = isOpenRegistration,
services = new Models.WellKnownModels.Services()
{
inbound = new object[0],
@ -117,7 +121,7 @@ namespace BirdsiteLive.Controllers
{
"activitypub"
},
openRegistrations = false,
openRegistrations = isOpenRegistration,
services = new Models.WellKnownModels.Services()
{
inbound = new object[0],

View File

@ -0,0 +1,53 @@
using System;
using System.Threading.Tasks;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts;
namespace BirdsiteLive.Services
{
public interface ICachedStatisticsService
{
Task<CachedStatistics> GetStatisticsAsync();
}
public class CachedStatisticsService : ICachedStatisticsService
{
private readonly ITwitterUserDal _twitterUserDal;
private static CachedStatistics _cachedStatistics;
private readonly InstanceSettings _instanceSettings;
#region Ctor
public CachedStatisticsService(ITwitterUserDal twitterUserDal, InstanceSettings instanceSettings)
{
_twitterUserDal = twitterUserDal;
_instanceSettings = instanceSettings;
}
#endregion
public async Task<CachedStatistics> GetStatisticsAsync()
{
if (_cachedStatistics == null ||
(DateTime.UtcNow - _cachedStatistics.RefreshedTime).TotalMinutes > 15)
{
var twitterUserMax = _instanceSettings.MaxUsersCapacity;
var twitterUserCount = await _twitterUserDal.GetTwitterUsersCountAsync();
var saturation = (int)((double)twitterUserCount / twitterUserMax * 100);
_cachedStatistics = new CachedStatistics
{
RefreshedTime = DateTime.UtcNow,
Saturation = saturation
};
}
return _cachedStatistics;
}
}
public class CachedStatistics
{
public DateTime RefreshedTime { get; set; }
public int Saturation { get; set; }
}
}

View File

@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using BirdsiteLive.DAL;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.Moderation;
using BirdsiteLive.Pipeline;
using Microsoft.Extensions.Hosting;
@ -12,13 +13,15 @@ namespace BirdsiteLive.Services
public class FederationService : BackgroundService
{
private readonly IDatabaseInitializer _databaseInitializer;
private readonly IModerationPipeline _moderationPipeline;
private readonly IStatusPublicationPipeline _statusPublicationPipeline;
private readonly IHostApplicationLifetime _applicationLifetime;
#region Ctor
public FederationService(IDatabaseInitializer databaseInitializer, IStatusPublicationPipeline statusPublicationPipeline, IHostApplicationLifetime applicationLifetime)
public FederationService(IDatabaseInitializer databaseInitializer, IModerationPipeline moderationPipeline, IStatusPublicationPipeline statusPublicationPipeline, IHostApplicationLifetime applicationLifetime)
{
_databaseInitializer = databaseInitializer;
_moderationPipeline = moderationPipeline;
_statusPublicationPipeline = statusPublicationPipeline;
_applicationLifetime = applicationLifetime;
}
@ -29,6 +32,7 @@ namespace BirdsiteLive.Services
try
{
await _databaseInitializer.InitAndMigrateDbAsync();
await _moderationPipeline.ApplyModerationSettingsAsync();
await _statusPublicationPipeline.ExecuteAsync(stoppingToken);
}
finally

View File

@ -66,7 +66,10 @@ namespace BirdsiteLive
var logsSettings = Configuration.GetSection("Logging").Get<LogsSettings>();
services.For<LogsSettings>().Use(x => logsSettings);
var moderationSettings = Configuration.GetSection("Moderation").Get<ModerationSettings>();
services.For<ModerationSettings>().Use(x => moderationSettings);
if (string.Equals(dbSettings.Type, DbTypes.Postgres, StringComparison.OrdinalIgnoreCase))
{
var connString = $"Host={dbSettings.Host};Username={dbSettings.User};Password={dbSettings.Password};Database={dbSettings.Name}";
@ -96,6 +99,7 @@ namespace BirdsiteLive
_.Assembly("BirdsiteLive.Domain");
_.Assembly("BirdsiteLive.DAL");
_.Assembly("BirdsiteLive.DAL.Postgres");
_.Assembly("BirdsiteLive.Moderation");
_.Assembly("BirdsiteLive.Pipeline");
_.TheCallingAssembly();

View File

@ -0,0 +1,27 @@
@using BirdsiteLive.Domain.Repository
@model BirdsiteLive.Controllers.ModerationStatus
@{
ViewData["Title"] = "Blacklisting";
}
<div class="col-12 col-sm-12 col-md-10 col-lg-8 mx-auto">
<h2>Blacklisting</h2>
@if (Model.Followers == ModerationTypeEnum.BlackListing)
{
<p><br />This node is blacklisting some instances and/or Fediverse users.<br /><br /></p>
}
@if (Model.TwitterAccounts == ModerationTypeEnum.BlackListing)
{
<p><br />This node is blacklisting some twitter users.<br /><br /></p>
}
@if (Model.Followers != ModerationTypeEnum.BlackListing && Model.TwitterAccounts != ModerationTypeEnum.BlackListing)
{
<p><br />This node is not using blacklisting.<br /><br /></p>
}
@*<h2>FAQ</h2>
<p>TODO</p>*@
</div>

View File

@ -0,0 +1,30 @@
@model BirdsiteLive.Services.CachedStatistics
@{
ViewData["Title"] = "About";
}
<div class="col-12 col-sm-12 col-md-10 col-lg-8 mx-auto">
<h2>Node Saturation</h2>
<p>
<br/>
This node usage is at @Model.Saturation%<br/>
<br/>
</p>
<h2>FAQ</h2>
<h4>Why is there a limit on the node?</h4>
<p>BirdsiteLIVE rely on the Twitter API to provide high quality content. This API has limitations and therefore limits node capacity.</p>
<h4>What happen when the node is saturated?</h4>
<p>
When the saturation rate goes above 100% the node will no longer update all accounts every 15 minutes and instead will reduce the pooling rate to stay under the API limits, the more saturated a node is the less efficient it will be.<br />
The software doesn't scale, and it's by design.
</p>
<h4>How can I reduce the node's saturation?</h4>
<p>If you're not on your own node, be reasonable and don't follow too much accounts. And if you can, host your own node. BirdsiteLIVE doesn't require a lot of resources to work and therefore is really cheap to self-host.</p>
</div>

View File

@ -0,0 +1,27 @@
@using BirdsiteLive.Domain.Repository
@model BirdsiteLive.Controllers.ModerationStatus
@{
ViewData["Title"] = "Whitelisting";
}
<div class="col-12 col-sm-12 col-md-10 col-lg-8 mx-auto">
<h2>Whitelisting</h2>
@if (Model.Followers == ModerationTypeEnum.WhiteListing)
{
<p><br />This node is whitelisting some instances and/or Fediverse users.<br /><br /></p>
}
@if (Model.TwitterAccounts == ModerationTypeEnum.WhiteListing)
{
<p><br />This node is whitelisting some twitter users.<br /><br /></p>
}
@if (Model.Followers != ModerationTypeEnum.WhiteListing && Model.TwitterAccounts != ModerationTypeEnum.WhiteListing)
{
<p><br />This node is not using whitelisting.<br /><br /></p>
}
@*<h2>FAQ</h2>
<p>TODO</p>*@
</div>

View File

@ -16,4 +16,11 @@
<!-- Input and Submit elements -->
<button type="submit" value="Submit">Post Note</button>
</form>
<form asp-controller="Debuging" asp-action="PostRejectFollow" method="post">
<!-- Input and Submit elements -->
<button type="submit" value="Submit">Reject Follow</button>
</form>

View File

@ -0,0 +1,22 @@
@model BirdsiteLive.Component.NodeInfoViewModel
<div>
@if (ViewData.Model.WhitelistingEnabled)
{
<a asp-controller="About" asp-action="Whitelisting" class="badge badge-light" title="What does this mean?">Whitelisting Enabled</a>
}
@if (ViewData.Model.BlacklistingEnabled)
{
<a asp-controller="About" asp-action="Blacklisting" class="badge badge-light" title="What does this mean?">Blacklisting Enabled</a>
}
<div class="node-progress-bar">
<div class="node-progress-bar__label"><a asp-controller="About" asp-action="Index">Instance saturation:</a></div>
<div class="progress node-progress-bar__bar">
<div class="progress-bar
@((ViewData.Model.InstanceSaturation > 50 && ViewData.Model.InstanceSaturation < 75) ? "bg-warning ":"")
@((ViewData.Model.InstanceSaturation > 75 && ViewData.Model.InstanceSaturation < 100) ? "bg-danger ":"")
@((ViewData.Model.InstanceSaturation > 100) ? "bg-saturation-danger ":"")" style="width: @ViewData.Model.InstanceSaturation%">@ViewData.Model.InstanceSaturation%</div>
</div>
</div>
</div>

View File

@ -9,6 +9,7 @@
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
<link rel="stylesheet" href="~/css/birdsite.css" />
<link rel="stylesheet" href="~/css/pattern.css" />
</head>
<body>
<header>
@ -39,6 +40,11 @@
</div>
<footer class="border-top footer text-muted">
<div class="wrapper-nodeinfo">
<div class="container container-nodeinfo">
@await Component.InvokeAsync("NodeInfo")
</div>
</div>
<div class="container">
<a href="https://github.com/NicolasConstant/BirdsiteLive">Github</a> @*<a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>*@

View File

@ -2,6 +2,11 @@
"Logging": {
"Type": "none",
"InstrumentationKey": "key",
"ApplicationInsights": {
"LogLevel": {
"Default": "Warning"
}
},
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
@ -10,7 +15,7 @@
},
"AllowedHosts": "*",
"Instance": {
"Name": "BirdsiteLIVE",
"Name": "BirdsiteLIVE",
"Domain": "domain.name",
"AdminEmail": "me@domain.name",
"ResolveMentionsInProfiles": true,
@ -27,5 +32,11 @@
"Twitter": {
"ConsumerKey": "twitter.api.key",
"ConsumerSecret": "twitter.api.key"
},
"Moderation": {
"FollowersWhiteListing": null,
"FollowersBlackListing": null,
"TwitterAccountsWhiteListing": null,
"TwitterAccountsBlackListing": null
}
}

View File

@ -0,0 +1,71 @@
.container-nodeinfo {
line-height: 30px;
text-align: center;
}
.wrapper-nodeinfo {
border-bottom: 1px solid #dee2e6;
}
.bg-saturation-danger {
background-color: #800000 !important;
}
@media (max-width: 767px) {
.node-progress-bar {
display: block;
width: 100%;
margin-top: 3px;
margin-bottom: 7px;
}
.node-progress-bar__label {
display: block;
font-size: 12px;
/*float: left;*/
padding: 0 5px 0 0;
/*height: 15px;*/
line-height: 15px;
}
.node-progress-bar__bar {
width: 80%;
margin: auto;
margin-top: 5px;
}
body {
margin-bottom: 135px;
}
}
@media (min-width: 768px) {
.node-progress-bar {
display: inline-block;
width: 400px;
}
.node-progress-bar__label {
display: inline-block;
font-size: 12px;
float: left;
padding: 0 5px 0 0;
/*height: 15px;*/
line-height: 15px;
}
.node-progress-bar__bar {
width: 200px;
position: relative;
top: -1px;
}
body {
margin-bottom: 95px;
}
}
.node-progress-bar__label a {
color: gray;
}

View File

@ -23,7 +23,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
public class DbInitializerPostgresDal : PostgresBase, IDbInitializerDal
{
private readonly PostgresTools _tools;
private readonly Version _currentVersion = new Version(2, 0);
private readonly Version _currentVersion = new Version(2, 1);
private const string DbVersionType = "db-version";
#region Ctor
@ -131,7 +131,8 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
{
return new[]
{
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))
};
}
@ -144,12 +145,19 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
var addIndex = $@"CREATE INDEX IF NOT EXISTS lastsync_twitteruser ON {_settings.TwitterUserTableName}(lastSync)";
await _tools.ExecuteRequestAsync(addIndex);
await UpdateDbVersionAsync(to);
return to;
}
else if (from == new Version(2, 0) && to == new Version(2, 1))
{
var addActorId = $@"ALTER TABLE {_settings.FollowersTableName} ADD actorId VARCHAR(2048)";
await _tools.ExecuteRequestAsync(addActorId);
}
else
{
throw new NotImplementedException();
}
throw new NotImplementedException();
await UpdateDbVersionAsync(to);
return to;
}
private async Task UpdateDbVersionAsync(Version newVersion)

View File

@ -20,7 +20,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
#endregion
public async Task CreateFollowerAsync(string acct, string host, string inboxRoute, string sharedInboxRoute, int[] followings = null, Dictionary<int, long> followingSyncStatus = null)
public async Task CreateFollowerAsync(string acct, string host, string inboxRoute, string sharedInboxRoute, string actorId, int[] followings = null, Dictionary<int, long> followingSyncStatus = null)
{
if(followings == null) followings = new int[0];
if(followingSyncStatus == null) followingSyncStatus = new Dictionary<int, long>();
@ -35,8 +35,8 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
dbConnection.Open();
await dbConnection.ExecuteAsync(
$"INSERT INTO {_settings.FollowersTableName} (acct,host,inboxRoute,sharedInboxRoute,followings,followingsSyncStatus) VALUES(@acct,@host,@inboxRoute,@sharedInboxRoute,@followings,CAST(@followingsSyncStatus as json))",
new { acct, host, inboxRoute, sharedInboxRoute, followings, followingsSyncStatus = serializedDic });
$"INSERT INTO {_settings.FollowersTableName} (acct,host,inboxRoute,sharedInboxRoute,followings,followingsSyncStatus,actorId) VALUES(@acct,@host,@inboxRoute,@sharedInboxRoute,@followings,CAST(@followingsSyncStatus as json),@actorId)",
new { acct, host, inboxRoute, sharedInboxRoute, followings, followingsSyncStatus = serializedDic, actorId });
}
}
@ -84,6 +84,19 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
public async Task<Follower[]> GetAllFollowersAsync()
{
var query = $"SELECT * FROM {_settings.FollowersTableName}";
using (var dbConnection = Connection)
{
dbConnection.Open();
var result = await dbConnection.QueryAsync<SerializedFollower>(query);
return result.Select(Convert).ToArray();
}
}
public async Task UpdateFollowerAsync(Follower follower)
{
if (follower == default) throw new ArgumentException("follower");
@ -116,8 +129,8 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
public async Task DeleteFollowerAsync(string acct, string host)
{
if (acct == default) throw new ArgumentException("acct");
if (host == default) throw new ArgumentException("host");
if (string.IsNullOrWhiteSpace(acct)) throw new ArgumentException("acct");
if (string.IsNullOrWhiteSpace(host)) throw new ArgumentException("host");
acct = acct.ToLowerInvariant();
host = host.ToLowerInvariant();
@ -142,6 +155,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
Acct = follower.Acct,
Host = follower.Host,
InboxRoute = follower.InboxRoute,
ActorId = follower.ActorId,
SharedInboxRoute = follower.SharedInboxRoute,
Followings = follower.Followings.ToList(),
FollowingsSyncStatus = JsonConvert.DeserializeObject<Dictionary<int,long>>(follower.FollowingsSyncStatus)
@ -159,5 +173,6 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
public string Host { get; set; }
public string InboxRoute { get; set; }
public string SharedInboxRoute { get; set; }
public string ActorId { get; set; }
}
}

View File

@ -5,9 +5,7 @@ using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.DAL.Postgres.DataAccessLayers.Base;
using BirdsiteLive.DAL.Postgres.Settings;
using BirdsiteLive.DAL.Postgres.Tools;
using Dapper;
using Npgsql;
namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
{
@ -44,7 +42,20 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
{
dbConnection.Open();
var result = (await dbConnection.QueryAsync<SyncTwitterUser>(query, new { acct = acct })).FirstOrDefault();
var result = (await dbConnection.QueryAsync<SyncTwitterUser>(query, new { acct })).FirstOrDefault();
return result;
}
}
public async Task<SyncTwitterUser> GetTwitterUserAsync(int id)
{
var query = $"SELECT * FROM {_settings.TwitterUserTableName} WHERE id = @id";
using (var dbConnection = Connection)
{
dbConnection.Open();
var result = (await dbConnection.QueryAsync<SyncTwitterUser>(query, new { id })).FirstOrDefault();
return result;
}
}
@ -75,6 +86,19 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
public async Task<SyncTwitterUser[]> GetAllTwitterUsersAsync()
{
var query = $"SELECT * FROM {_settings.TwitterUserTableName}";
using (var dbConnection = Connection)
{
dbConnection.Open();
var result = await dbConnection.QueryAsync<SyncTwitterUser>(query);
return result.ToArray();
}
}
public async Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, DateTime lastSync)
{
if(id == default) throw new ArgumentException("id");
@ -94,7 +118,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
public async Task DeleteTwitterUserAsync(string acct)
{
if (acct == default) throw new ArgumentException("acct");
if (string.IsNullOrWhiteSpace(acct)) throw new ArgumentException("acct");
acct = acct.ToLowerInvariant();
@ -107,5 +131,19 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
await dbConnection.QueryAsync(query, new { acct });
}
}
public async Task DeleteTwitterUserAsync(int id)
{
if (id == default) throw new ArgumentException("id");
var query = $"DELETE FROM {_settings.TwitterUserTableName} WHERE id = @id";
using (var dbConnection = Connection)
{
dbConnection.Open();
await dbConnection.QueryAsync(query, new { id });
}
}
}
}

View File

@ -7,9 +7,10 @@ namespace BirdsiteLive.DAL.Contracts
public interface IFollowersDal
{
Task<Follower> GetFollowerAsync(string acct, string host);
Task CreateFollowerAsync(string acct, string host, string inboxRoute, string sharedInboxRoute, int[] followings = null,
Task CreateFollowerAsync(string acct, string host, string inboxRoute, string sharedInboxRoute, string actorId, int[] followings = null,
Dictionary<int, long> followingSyncStatus = null);
Task<Follower[]> GetFollowersAsync(int followedUserId);
Task<Follower[]> GetAllFollowersAsync();
Task UpdateFollowerAsync(Follower follower);
Task DeleteFollowerAsync(int id);
Task DeleteFollowerAsync(string acct, string host);

View File

@ -8,9 +8,12 @@ namespace BirdsiteLive.DAL.Contracts
{
Task CreateTwitterUserAsync(string acct, long lastTweetPostedId);
Task<SyncTwitterUser> GetTwitterUserAsync(string acct);
Task<SyncTwitterUser> GetTwitterUserAsync(int id);
Task<SyncTwitterUser[]> GetAllTwitterUsersAsync(int maxNumber);
Task<SyncTwitterUser[]> GetAllTwitterUsersAsync();
Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, DateTime lastSync);
Task DeleteTwitterUserAsync(string acct);
Task DeleteTwitterUserAsync(int id);
Task<int> GetTwitterUsersCountAsync();
}
}

View File

@ -9,6 +9,7 @@ namespace BirdsiteLive.DAL.Models
public List<int> Followings { get; set; }
public Dictionary<int, long> FollowingsSyncStatus { get; set; }
public string ActorId { get; set; }
public string Acct { get; set; }
public string Host { get; set; }
public string InboxRoute { get; set; }

View File

@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NuGet.Frameworks;
namespace BirdsiteLive.Cryptography.Tests
{
@ -7,13 +8,12 @@ namespace BirdsiteLive.Cryptography.Tests
public class MagicKeyTests
{
[TestMethod]
public async Task Test()
public void Test()
{
var g = MagicKey.Generate();
var magicKey = new MagicKey(g.PrivateKey);
Assert.IsNotNull(magicKey);
}
}
}

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.DAL.Postgres.DataAccessLayers;
using BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers.Base;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -41,9 +41,10 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
};
var inboxRoute = "/myhandle/inbox";
var sharedInboxRoute = "/inbox";
var actorId = $"https://{host}/{acct}";
var dal = new FollowersPostgresDal(_settings);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetFollowerAsync(acct, host);
@ -52,6 +53,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(host, result.Host);
Assert.AreEqual(inboxRoute, result.InboxRoute);
Assert.AreEqual(sharedInboxRoute, result.SharedInboxRoute);
Assert.AreEqual(actorId, result.ActorId);
Assert.AreEqual(following.Length, result.Followings.Count);
Assert.AreEqual(following[0], result.Followings[0]);
Assert.AreEqual(followingSync.Count, result.FollowingsSyncStatus.Count);
@ -59,6 +61,38 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(followingSync.First().Value, result.FollowingsSyncStatus.First().Value);
}
[TestMethod]
public async Task CreateAndGetFollower_NoFollowings()
{
var acct = "myhandle";
var host = "domain.ext";
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, null, null);
var result = await dal.GetFollowerAsync(acct, host);
Assert.IsNotNull(result);
Assert.AreEqual(acct, result.Acct);
Assert.AreEqual(host, result.Host);
Assert.AreEqual(actorId, result.ActorId);
Assert.AreEqual(inboxRoute, result.InboxRoute);
Assert.AreEqual(sharedInboxRoute, result.SharedInboxRoute);
Assert.AreEqual(0, result.Followings.Count);
Assert.AreEqual(0, result.FollowingsSyncStatus.Count);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task GetFollowers_NoId()
{
var dal = new FollowersPostgresDal(_settings);
await dal.GetFollowersAsync(default);
}
[TestMethod]
public async Task CreateAndGetFollower_NoSharedInbox()
{
@ -73,9 +107,10 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
};
var inboxRoute = "/myhandle/inbox";
string sharedInboxRoute = null;
var actorId = $"https://{host}/{acct}";
var dal = new FollowersPostgresDal(_settings);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetFollowerAsync(acct, host);
@ -83,6 +118,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(acct, result.Acct);
Assert.AreEqual(host, result.Host);
Assert.AreEqual(inboxRoute, result.InboxRoute);
Assert.AreEqual(actorId, result.ActorId);
Assert.AreEqual(sharedInboxRoute, result.SharedInboxRoute);
Assert.AreEqual(following.Length, result.Followings.Count);
Assert.AreEqual(following[0], result.Followings[0]);
@ -103,7 +139,8 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
var followingSync = new Dictionary<int, long>();
var inboxRoute = "/myhandle1/inbox";
var sharedInboxRoute = "/inbox";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
var actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
//User 2
acct = "myhandle2";
@ -111,7 +148,8 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
following = new[] { 2, 4, 5 };
inboxRoute = "/myhandle2/inbox";
sharedInboxRoute = "/inbox2";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
//User 2
acct = "myhandle3";
@ -119,7 +157,8 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
following = new[] { 1 };
inboxRoute = "/myhandle3/inbox";
sharedInboxRoute = "/inbox3";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetFollowersAsync(2);
Assert.AreEqual(2, result.Length);
@ -131,6 +170,43 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(0, result.Length);
}
[TestMethod]
public async Task GetAllFollowersAsync()
{
var dal = new FollowersPostgresDal(_settings);
//User 1
var acct = "myhandle1";
var host = "domain.ext";
var following = new[] { 1, 2, 3 };
var followingSync = new Dictionary<int, long>();
var inboxRoute = "/myhandle1/inbox";
var sharedInboxRoute = "/inbox";
var actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
//User 2
acct = "myhandle2";
host = "domain.ext";
following = new[] { 2, 4, 5 };
inboxRoute = "/myhandle2/inbox";
sharedInboxRoute = "/inbox2";
actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
//User 2
acct = "myhandle3";
host = "domain.ext";
following = new[] { 1 };
inboxRoute = "/myhandle3/inbox";
sharedInboxRoute = "/inbox3";
actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetAllFollowersAsync();
Assert.AreEqual(3, result.Length);
}
[TestMethod]
public async Task CountFollowersAsync()
{
@ -146,7 +222,8 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
var followingSync = new Dictionary<int, long>();
var inboxRoute = "/myhandle1/inbox";
var sharedInboxRoute = "/inbox";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
var actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
//User 2
acct = "myhandle2";
@ -154,7 +231,8 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
following = new[] { 2, 4, 5 };
inboxRoute = "/myhandle2/inbox";
sharedInboxRoute = "/inbox2";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
//User 2
acct = "myhandle3";
@ -162,7 +240,8 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
following = new[] { 1 };
inboxRoute = "/myhandle3/inbox";
sharedInboxRoute = "/inbox3";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
actorId = $"https://{host}/{acct}";
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
result = await dal.GetFollowersCountAsync();
Assert.AreEqual(3, result);
@ -182,9 +261,10 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
};
var inboxRoute = "/myhandle/inbox";
var sharedInboxRoute = "/inbox";
var actorId = $"https://{host}/{acct}";
var dal = new FollowersPostgresDal(_settings);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
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 };
@ -222,9 +302,10 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
};
var inboxRoute = "/myhandle/inbox";
var sharedInboxRoute = "/inbox";
var actorId = $"https://{host}/{acct}";
var dal = new FollowersPostgresDal(_settings);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetFollowerAsync(acct, host);
var updatedFollowing = new[] { 12, 19 };
@ -246,6 +327,27 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(updatedFollowingSync.First().Value, result.FollowingsSyncStatus.First().Value);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Update_NoFollower()
{
var dal = new FollowersPostgresDal(_settings);
await dal.UpdateFollowerAsync(null);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Update_NoFollowerId()
{
var follower = new Follower
{
Id = default
};
var dal = new FollowersPostgresDal(_settings);
await dal.UpdateFollowerAsync(follower);
}
[TestMethod]
public async Task CreateAndDeleteFollower_ById()
{
@ -260,9 +362,10 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
};
var inboxRoute = "/myhandle/inbox";
var sharedInboxRoute = "/inbox";
var actorId = $"https://{host}/{acct}";
var dal = new FollowersPostgresDal(_settings);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetFollowerAsync(acct, host);
Assert.IsNotNull(result);
@ -286,9 +389,10 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
};
var inboxRoute = "/myhandle/inbox";
var sharedInboxRoute = "/inbox";
var actorId = $"https://{host}/{acct}";
var dal = new FollowersPostgresDal(_settings);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, following, followingSync);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetFollowerAsync(acct, host);
Assert.IsNotNull(result);
@ -297,5 +401,29 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
result = await dal.GetFollowerAsync(acct, host);
Assert.IsNull(result);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Delete_NoFollowerId()
{
var dal = new FollowersPostgresDal(_settings);
await dal.DeleteFollowerAsync(default);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Delete_NoAcct()
{
var dal = new FollowersPostgresDal(_settings);
await dal.DeleteFollowerAsync(string.Empty, string.Empty);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Delete_NoHost()
{
var dal = new FollowersPostgresDal(_settings);
await dal.DeleteFollowerAsync("acct", string.Empty);
}
}
}

View File

@ -50,6 +50,24 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.IsTrue(result.Id > 0);
}
[TestMethod]
public async Task CreateAndGetUser_byId()
{
var acct = "myid";
var lastTweetId = 1548L;
var dal = new TwitterUserPostgresDal(_settings);
await dal.CreateTwitterUserAsync(acct, lastTweetId);
var result = await dal.GetTwitterUserAsync(acct);
var resultById = await dal.GetTwitterUserAsync(result.Id);
Assert.AreEqual(acct, resultById.Acct);
Assert.AreEqual(lastTweetId, resultById.LastTweetPostedId);
Assert.AreEqual(lastTweetId, resultById.LastTweetSynchronizedForAllFollowersId);
Assert.AreEqual(result.Id, resultById.Id);
}
[TestMethod]
public async Task CreateUpdateAndGetUser()
{
@ -75,10 +93,42 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Update_NoId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(default, default, default, DateTime.UtcNow);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Update_NoLastTweetPostedId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, default, default, DateTime.UtcNow);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Update_NoLastTweetSynchronizedForAllFollowersId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, 9556, default, DateTime.UtcNow);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Update_NoLastSync()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, 9556, 65, default);
}
[TestMethod]
public async Task CreateAndDeleteUser()
{
var acct = "myid";
var acct = "myacct";
var lastTweetId = 1548L;
var dal = new TwitterUserPostgresDal(_settings);
@ -93,7 +143,40 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
}
[TestMethod]
public async Task GetAllTwitterUsers()
[ExpectedException(typeof(ArgumentException))]
public async Task DeleteUser_NotAcct()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.DeleteTwitterUserAsync(string.Empty);
}
[TestMethod]
public async Task CreateAndDeleteUser_byId()
{
var acct = "myacct";
var lastTweetId = 1548L;
var dal = new TwitterUserPostgresDal(_settings);
await dal.CreateTwitterUserAsync(acct, lastTweetId);
var result = await dal.GetTwitterUserAsync(acct);
Assert.IsNotNull(result);
await dal.DeleteTwitterUserAsync(result.Id);
result = await dal.GetTwitterUserAsync(acct);
Assert.IsNull(result);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task DeleteUser_NotAcct_byId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.DeleteTwitterUserAsync(default(int));
}
[TestMethod]
public async Task GetAllTwitterUsers_Top()
{
var dal = new TwitterUserPostgresDal(_settings);
for (var i = 0; i < 1000; i++)
@ -147,6 +230,26 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.IsTrue(Math.Abs((acc.LastSync - oldest.ToUniversalTime()).TotalMilliseconds) < 1000);
}
[TestMethod]
public async Task GetAllTwitterUsers()
{
var dal = new TwitterUserPostgresDal(_settings);
for (var i = 0; i < 1000; i++)
{
var acct = $"myid{i}";
var lastTweetId = 1548L;
await dal.CreateTwitterUserAsync(acct, lastTweetId);
}
var result = await dal.GetAllTwitterUsersAsync();
Assert.AreEqual(1000, result.Length);
Assert.IsFalse(result[0].Id == default);
Assert.IsFalse(result[0].Acct == default);
Assert.IsFalse(result[0].LastTweetPostedId == default);
Assert.IsFalse(result[0].LastTweetSynchronizedForAllFollowersId == default);
}
[TestMethod]
public async Task CountTwitterUsers()
{

View File

@ -18,4 +18,8 @@
<ProjectReference Include="..\..\BirdsiteLive.Domain\BirdsiteLive.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Repository\" />
</ItemGroup>
</Project>

View File

@ -21,6 +21,7 @@ namespace BirdsiteLive.Domain.Tests.BusinessUseCases
var twitterName = "handle";
var followerInbox = "/user/testest";
var inbox = "/inbox";
var actorId = "actorUrl";
var follower = new Follower
{
@ -55,6 +56,7 @@ namespace BirdsiteLive.Domain.Tests.BusinessUseCases
It.Is<string>(y => y == domain),
It.Is<string>(y => y == followerInbox),
It.Is<string>(y => y == inbox),
It.Is<string>(y => y == actorId),
null,
null))
.Returns(Task.CompletedTask);
@ -80,7 +82,7 @@ namespace BirdsiteLive.Domain.Tests.BusinessUseCases
#endregion
var action = new ProcessFollowUser(followersDalMock.Object, twitterUserDalMock.Object);
await action.ExecuteAsync(username, domain, twitterName, followerInbox, inbox);
await action.ExecuteAsync(username, domain, twitterName, followerInbox, inbox, actorId);
#region Validations
followersDalMock.VerifyAll();
@ -97,7 +99,8 @@ namespace BirdsiteLive.Domain.Tests.BusinessUseCases
var twitterName = "handle";
var followerInbox = "/user/testest";
var inbox = "/inbox";
var actorId = "actorUrl";
var follower = new Follower
{
Id = 1,
@ -138,7 +141,7 @@ namespace BirdsiteLive.Domain.Tests.BusinessUseCases
#endregion
var action = new ProcessFollowUser(followersDalMock.Object, twitterUserDalMock.Object);
await action.ExecuteAsync(username, domain, twitterName, followerInbox, inbox);
await action.ExecuteAsync(username, domain, twitterName, followerInbox, inbox, actorId);
#region Validations
followersDalMock.VerifyAll();

View File

@ -0,0 +1,347 @@
using BirdsiteLive.Common.Settings;
using BirdsiteLive.Domain.Repository;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace BirdsiteLive.Domain.Tests.Repository
{
[TestClass]
public class ModerationRepositoryTests
{
#region GetModerationType
[TestMethod]
public void GetModerationType_Follower_WhiteListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersWhiteListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.WhiteListing ,repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.None, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
[TestMethod]
public void GetModerationType_TwitterAccount_WhiteListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
TwitterAccountsWhiteListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.None, repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.WhiteListing, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
[TestMethod]
public void GetModerationType_FollowerTwitterAccount_WhiteListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersWhiteListing = "@me@domain.ext",
TwitterAccountsWhiteListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.WhiteListing, repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.WhiteListing, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
[TestMethod]
public void GetModerationType_Follower_BlackListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersBlackListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.BlackListing, repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.None, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
[TestMethod]
public void GetModerationType_TwitterAccount_BlackListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
TwitterAccountsBlackListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.None, repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.BlackListing, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
[TestMethod]
public void GetModerationType_FollowerTwitterAccount_BlackListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersBlackListing = "@me@domain.ext",
TwitterAccountsBlackListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.BlackListing, repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.BlackListing, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
[TestMethod]
public void GetModerationType_Follower_BothListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersBlackListing = "@me@domain.ext",
FollowersWhiteListing = "@me@domain.ext",
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.WhiteListing, repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.None, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
[TestMethod]
public void GetModerationType_TwitterAccount_BothListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
TwitterAccountsBlackListing = "@me@domain.ext",
TwitterAccountsWhiteListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.None, repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.WhiteListing, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
[TestMethod]
public void GetModerationType_FollowerTwitterAccount_BothListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersBlackListing = "@me@domain.ext",
FollowersWhiteListing = "@me@domain.ext",
TwitterAccountsBlackListing = "@me@domain.ext",
TwitterAccountsWhiteListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModerationTypeEnum.WhiteListing, repo.GetModerationType(ModerationEntityTypeEnum.Follower));
Assert.AreEqual(ModerationTypeEnum.WhiteListing, repo.GetModerationType(ModerationEntityTypeEnum.TwitterAccount));
#endregion
}
#endregion
#region CheckStatus
[TestMethod]
public void CheckStatus_Follower_WhiteListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersWhiteListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.WhiteListed, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me@domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me2@domain.ext"));
#endregion
}
[TestMethod]
public void CheckStatus_Follower_WhiteListing_Instance_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersWhiteListing = "domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.WhiteListed, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me@domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.WhiteListed, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me2@domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me2@domain2.ext"));
#endregion
}
[TestMethod]
public void CheckStatus_Follower_WhiteListing_SubDomain_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersWhiteListing = "*.domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.WhiteListed, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me@s.domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.WhiteListed, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me2@s2.domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me2@domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me2@domain2.ext"));
#endregion
}
[TestMethod]
public void CheckStatus_TwitterAccount_WhiteListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
TwitterAccountsWhiteListing = "handle"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.WhiteListed, repo.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, "handle"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, "handle2"));
#endregion
}
[TestMethod]
public void CheckStatus_Follower_BlackListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersBlackListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.BlackListed, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me@domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me2@domain.ext"));
#endregion
}
[TestMethod]
public void CheckStatus_TwitterAccount_BlackListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
TwitterAccountsBlackListing = "handle"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.BlackListed, repo.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, "handle"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, "handle2"));
#endregion
}
[TestMethod]
public void CheckStatus_Follower_BothListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
FollowersWhiteListing = "@me@domain.ext",
FollowersBlackListing = "@me@domain.ext"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.WhiteListed, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me@domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@me2@domain.ext"));
#endregion
}
[TestMethod]
public void CheckStatus_TwitterAccount_BothListing_Test()
{
#region Stubs
var settings = new ModerationSettings
{
TwitterAccountsWhiteListing = "handle",
TwitterAccountsBlackListing = "handle"
};
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.WhiteListed, repo.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, "handle"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, "handle2"));
#endregion
}
[TestMethod]
public void CheckStatus_None_Test()
{
#region Stubs
var settings = new ModerationSettings();
#endregion
var repo = new ModerationRepository(settings);
#region Validations
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.Follower, "@handle@domain.ext"));
Assert.AreEqual(ModeratedTypeEnum.None, repo.CheckStatus(ModerationEntityTypeEnum.TwitterAccount, "handle"));
#endregion
}
#endregion
}
}

View File

@ -0,0 +1,149 @@
using System.Linq;
using BirdsiteLive.Domain.Tools;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace BirdsiteLive.Domain.Tests.Tools
{
[TestClass]
public class ModerationParserTests
{
[TestMethod]
public void Parse_Simple_Test()
{
#region Stubs
var entry = "test";
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.AreEqual("test", result.First());
#endregion
}
[TestMethod]
public void Parse_Null_Test()
{
#region Stubs
string entry = null;
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(0, result.Length);
#endregion
}
[TestMethod]
public void Parse_WhiteSpace_Test()
{
#region Stubs
var entry = " ";
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(0, result.Length);
#endregion
}
[TestMethod]
public void Parse_PipeSeparator_Test()
{
#region Stubs
var entry = "test|test2";
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(2, result.Length);
Assert.AreEqual("test", result[0]);
Assert.AreEqual("test2", result[1]);
#endregion
}
[TestMethod]
public void Parse_SemicolonSeparator_Test()
{
#region Stubs
var entry = "test;test2";
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(2, result.Length);
Assert.AreEqual("test", result[0]);
Assert.AreEqual("test2", result[1]);
#endregion
}
[TestMethod]
public void Parse_CommaSeparator_Test()
{
#region Stubs
var entry = "test,test2";
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(2, result.Length);
Assert.AreEqual("test", result[0]);
Assert.AreEqual("test2", result[1]);
#endregion
}
[TestMethod]
public void Parse_SemicolonSeparator_EmptyEntry_Test()
{
#region Stubs
var entry = "test;test2;";
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(2, result.Length);
Assert.AreEqual("test", result[0]);
Assert.AreEqual("test2", result[1]);
#endregion
}
[TestMethod]
public void Parse_SemicolonSeparator_WhiteSpace_Test()
{
#region Stubs
var entry = "test; test2";
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(2, result.Length);
Assert.AreEqual("test", result[0]);
Assert.AreEqual("test2", result[1]);
#endregion
}
[TestMethod]
public void Parse_SemicolonSeparator_EmptyEntry_WhiteSpace_Test()
{
#region Stubs
var entry = "test; test2; ";
#endregion
var result = ModerationParser.Parse(entry);
#region Validations
Assert.AreEqual(2, result.Length);
Assert.AreEqual("test", result[0]);
Assert.AreEqual("test2", result[1]);
#endregion
}
}
}

View File

@ -0,0 +1,78 @@
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Domain.Tools;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace BirdsiteLive.Domain.Tests.Tools
{
[TestClass]
public class ModerationRegexParserTests
{
[TestMethod]
public void Parse_TwitterAccount_Simple_Test()
{
#region Stubs
var pattern = "handle";
#endregion
var regex = ModerationRegexParser.Parse(ModerationEntityTypeEnum.TwitterAccount, pattern);
#region Validations
Assert.IsTrue(regex.IsMatch(pattern));
Assert.IsFalse(regex.IsMatch("handles"));
Assert.IsFalse(regex.IsMatch("andle"));
#endregion
}
[TestMethod]
public void Parse_Follower_Handle_Test()
{
#region Stubs
var pattern = "@handle@domain.ext";
#endregion
var regex = ModerationRegexParser.Parse(ModerationEntityTypeEnum.Follower, pattern);
#region Validations
Assert.IsTrue(regex.IsMatch(pattern));
Assert.IsFalse(regex.IsMatch("@handle2@domain.ext"));
Assert.IsFalse(regex.IsMatch("@handle@seb.domain.ext"));
#endregion
}
[TestMethod]
public void Parse_Follower_Domain_Test()
{
#region Stubs
var pattern = "domain.ext";
#endregion
var regex = ModerationRegexParser.Parse(ModerationEntityTypeEnum.Follower, pattern);
#region Validations
Assert.IsTrue(regex.IsMatch("@handle@domain.ext"));
Assert.IsTrue(regex.IsMatch("@handle2@domain.ext"));
Assert.IsFalse(regex.IsMatch("@handle2@domain2.ext"));
Assert.IsFalse(regex.IsMatch("@handle@seb.domain.ext"));
#endregion
}
[TestMethod]
public void Parse_Follower_SubDomains_Test()
{
#region Stubs
var pattern = "*.domain.ext";
#endregion
var regex = ModerationRegexParser.Parse(ModerationEntityTypeEnum.Follower, pattern);
#region Validations
Assert.IsTrue(regex.IsMatch("@handle2@sub.domain.ext"));
Assert.IsTrue(regex.IsMatch("@han@sub3.domain.ext"));
Assert.IsFalse(regex.IsMatch("@handle@domain.ext"));
Assert.IsFalse(regex.IsMatch("@handle2@.domain.ext"));
Assert.IsFalse(regex.IsMatch("@handle2@domain2.ext"));
Assert.IsFalse(regex.IsMatch("@handle@seb.domain2.ext"));
#endregion
}
}
}

View File

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain;
using BirdsiteLive.Moderation.Actions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Moderation.Tests.Actions
{
[TestClass]
public class RejectAllFollowingsActionTests
{
[TestMethod]
public async Task ProcessAsync()
{
#region Stubs
var follower = new Follower
{
Followings = new List<int>
{
24
},
Host = "host"
};
var settings = new InstanceSettings
{
Domain = "domain"
};
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(
It.Is<int>(y => y == 24)))
.ReturnsAsync(new SyncTwitterUser
{
Id = 24,
Acct = "acct"
});
var userServiceMock = new Mock<IUserService>(MockBehavior.Strict);
userServiceMock
.Setup(x => x.SendRejectFollowAsync(
It.Is<ActivityFollow>(y => y.type == "Follow"),
It.IsNotNull<string>()
))
.ReturnsAsync(true);
#endregion
var action = new RejectAllFollowingsAction(twitterUserDalMock.Object, userServiceMock.Object, settings);
await action.ProcessAsync(follower);
#region Validations
twitterUserDalMock.VerifyAll();
userServiceMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Exception()
{
#region Stubs
var follower = new Follower
{
Followings = new List<int>
{
24
},
Host = "host"
};
var settings = new InstanceSettings
{
Domain = "domain"
};
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(
It.Is<int>(y => y == 24)))
.ReturnsAsync(new SyncTwitterUser
{
Id = 24,
Acct = "acct"
});
var userServiceMock = new Mock<IUserService>(MockBehavior.Strict);
userServiceMock
.Setup(x => x.SendRejectFollowAsync(
It.Is<ActivityFollow>(y => y.type == "Follow"),
It.IsNotNull<string>()
))
.Throws(new Exception());
#endregion
var action = new RejectAllFollowingsAction(twitterUserDalMock.Object, userServiceMock.Object, settings);
await action.ProcessAsync(follower);
#region Validations
twitterUserDalMock.VerifyAll();
userServiceMock.VerifyAll();
#endregion
}
}
}

View File

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain;
using BirdsiteLive.Moderation.Actions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Moderation.Tests.Actions
{
[TestClass]
public class RejectFollowingActionTests
{
[TestMethod]
public async Task ProcessAsync()
{
#region Stubs
var follower = new Follower
{
Followings = new List<int>
{
24
},
Host = "host"
};
var settings = new InstanceSettings
{
Domain = "domain"
};
var twitterUser = new SyncTwitterUser
{
Id = 24,
Acct = "acct"
};
#endregion
#region Mocks
var userServiceMock = new Mock<IUserService>(MockBehavior.Strict);
userServiceMock
.Setup(x => x.SendRejectFollowAsync(
It.Is<ActivityFollow>(y => y.type == "Follow"),
It.IsNotNull<string>()
))
.ReturnsAsync(true);
#endregion
var action = new RejectFollowingAction(userServiceMock.Object, settings);
await action.ProcessAsync(follower, twitterUser);
#region Validations
userServiceMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Exception()
{
#region Stubs
var follower = new Follower
{
Followings = new List<int>
{
24
},
Host = "host"
};
var settings = new InstanceSettings
{
Domain = "domain"
};
var twitterUser = new SyncTwitterUser
{
Id = 24,
Acct = "acct"
};
#endregion
#region Mocks
var userServiceMock = new Mock<IUserService>(MockBehavior.Strict);
userServiceMock
.Setup(x => x.SendRejectFollowAsync(
It.Is<ActivityFollow>(y => y.type == "Follow"),
It.IsNotNull<string>()
))
.Throws(new Exception());
#endregion
var action = new RejectFollowingAction(userServiceMock.Object, settings);
await action.ProcessAsync(follower, twitterUser);
#region Validations
userServiceMock.VerifyAll();
#endregion
}
}
}

View File

@ -0,0 +1,111 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Moderation.Actions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Moderation.Tests.Actions
{
[TestClass]
public class RemoveFollowerActionTests
{
[TestMethod]
public async Task ProcessAsync_NoMoreFollowings()
{
#region Stubs
var follower = new Follower
{
Id = 12,
Followings = new List<int> { 1 }
};
#endregion
#region Mocks
var rejectAllFollowingsActionMock = new Mock<IRejectAllFollowingsAction>(MockBehavior.Strict);
rejectAllFollowingsActionMock
.Setup(x => x.ProcessAsync(
It.Is<Follower>(y => y.Id == follower.Id)))
.Returns(Task.CompletedTask);
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 RemoveFollowerAction(followersDalMock.Object, twitterUserDalMock.Object, rejectAllFollowingsActionMock.Object);
await action.ProcessAsync(follower);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
rejectAllFollowingsActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_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 rejectAllFollowingsActionMock = new Mock<IRejectAllFollowingsAction>(MockBehavior.Strict);
rejectAllFollowingsActionMock
.Setup(x => x.ProcessAsync(
It.Is<Follower>(y => y.Id == follower.Id)))
.Returns(Task.CompletedTask);
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 RemoveFollowerAction(followersDalMock.Object, twitterUserDalMock.Object, rejectAllFollowingsActionMock.Object);
await action.ProcessAsync(follower);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
rejectAllFollowingsActionMock.VerifyAll();
#endregion
}
}
}

View File

@ -0,0 +1,131 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Moderation.Actions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Moderation.Tests.Actions
{
[TestClass]
public class RemoveTwitterAccountActionTests
{
[TestMethod]
public async Task ProcessAsync_RemoveFollower()
{
#region Stubs
var twitter = new SyncTwitterUser
{
Id = 24,
Acct = "my-acct"
};
var followers = new List<Follower>
{
new Follower
{
Id = 48,
Followings = new List<int>{ 24 },
FollowingsSyncStatus = new Dictionary<int, long> { { 24, 1024 } }
}
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetFollowersAsync(
It.Is<int>(y => y == 24)))
.ReturnsAsync(followers.ToArray());
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 48)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.DeleteTwitterUserAsync(
It.Is<int>(y => y == 24)))
.Returns(Task.CompletedTask);
var rejectFollowingActionMock = new Mock<IRejectFollowingAction>(MockBehavior.Strict);
rejectFollowingActionMock
.Setup(x => x.ProcessAsync(
It.Is<Follower>(y => y.Id == 48),
It.Is<SyncTwitterUser>(y => y.Acct == twitter.Acct)))
.Returns(Task.CompletedTask);
#endregion
var action = new RemoveTwitterAccountAction(followersDalMock.Object, twitterUserDalMock.Object, rejectFollowingActionMock.Object);
await action.ProcessAsync(twitter);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
rejectFollowingActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_KeepFollower()
{
#region Stubs
var twitter = new SyncTwitterUser
{
Id = 24,
Acct = "my-acct"
};
var followers = new List<Follower>
{
new Follower
{
Id = 48,
Followings = new List<int>{ 24, 36 },
FollowingsSyncStatus = new Dictionary<int, long> { { 24, 1024 }, { 36, 24 } }
}
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetFollowersAsync(
It.Is<int>(y => y == 24)))
.ReturnsAsync(followers.ToArray());
followersDalMock
.Setup(x => x.UpdateFollowerAsync(
It.Is<Follower>(y => y.Id == 48
&& y.Followings.Count == 1
&& y.FollowingsSyncStatus.Count == 1
)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.DeleteTwitterUserAsync(
It.Is<int>(y => y == 24)))
.Returns(Task.CompletedTask);
var rejectFollowingActionMock = new Mock<IRejectFollowingAction>(MockBehavior.Strict);
rejectFollowingActionMock
.Setup(x => x.ProcessAsync(
It.Is<Follower>(y => y.Id == 48),
It.Is<SyncTwitterUser>(y => y.Acct == twitter.Acct)))
.Returns(Task.CompletedTask);
#endregion
var action = new RemoveTwitterAccountAction(followersDalMock.Object, twitterUserDalMock.Object, rejectFollowingActionMock.Object);
await action.ProcessAsync(twitter);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
rejectFollowingActionMock.VerifyAll();
#endregion
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Moq" Version="4.14.5" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BirdsiteLive.Moderation\BirdsiteLive.Moderation.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,106 @@
using System;
using System.Threading.Tasks;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Moderation.Processors;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Moderation.Tests
{
[TestClass]
public class ModerationPipelineTests
{
[TestMethod]
public async Task ApplyModerationSettingsAsync_None()
{
#region Mocks
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.GetModerationType(ModerationEntityTypeEnum.Follower))
.Returns(ModerationTypeEnum.None);
moderationRepositoryMock
.Setup(x => x.GetModerationType(ModerationEntityTypeEnum.TwitterAccount))
.Returns(ModerationTypeEnum.None);
var followerModerationProcessorMock = new Mock<IFollowerModerationProcessor>(MockBehavior.Strict);
var twitterAccountModerationProcessorMock = new Mock<ITwitterAccountModerationProcessor>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<ModerationPipeline>>(MockBehavior.Strict);
#endregion
var pipeline = new ModerationPipeline(moderationRepositoryMock.Object, followerModerationProcessorMock.Object, twitterAccountModerationProcessorMock.Object, loggerMock.Object);
await pipeline.ApplyModerationSettingsAsync();
#region Validations
moderationRepositoryMock.VerifyAll();
followerModerationProcessorMock.VerifyAll();
twitterAccountModerationProcessorMock.VerifyAll();
loggerMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ApplyModerationSettingsAsync_Process()
{
#region Mocks
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.GetModerationType(ModerationEntityTypeEnum.Follower))
.Returns(ModerationTypeEnum.WhiteListing);
moderationRepositoryMock
.Setup(x => x.GetModerationType(ModerationEntityTypeEnum.TwitterAccount))
.Returns(ModerationTypeEnum.BlackListing);
var followerModerationProcessorMock = new Mock<IFollowerModerationProcessor>(MockBehavior.Strict);
followerModerationProcessorMock
.Setup(x => x.ProcessAsync(
It.Is<ModerationTypeEnum>(y => y == ModerationTypeEnum.WhiteListing)))
.Returns(Task.CompletedTask);
var twitterAccountModerationProcessorMock = new Mock<ITwitterAccountModerationProcessor>(MockBehavior.Strict);
twitterAccountModerationProcessorMock
.Setup(x => x.ProcessAsync(
It.Is<ModerationTypeEnum>(y => y == ModerationTypeEnum.BlackListing)))
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<ModerationPipeline>>(MockBehavior.Strict);
#endregion
var pipeline = new ModerationPipeline(moderationRepositoryMock.Object, followerModerationProcessorMock.Object, twitterAccountModerationProcessorMock.Object, loggerMock.Object);
await pipeline.ApplyModerationSettingsAsync();
#region Validations
moderationRepositoryMock.VerifyAll();
followerModerationProcessorMock.VerifyAll();
twitterAccountModerationProcessorMock.VerifyAll();
loggerMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ApplyModerationSettingsAsync_Exception()
{
#region Mocks
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.GetModerationType(ModerationEntityTypeEnum.Follower))
.Throws(new Exception());
var followerModerationProcessorMock = new Mock<IFollowerModerationProcessor>(MockBehavior.Strict);
var twitterAccountModerationProcessorMock = new Mock<ITwitterAccountModerationProcessor>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<ModerationPipeline>>();
#endregion
var pipeline = new ModerationPipeline(moderationRepositoryMock.Object, followerModerationProcessorMock.Object, twitterAccountModerationProcessorMock.Object, loggerMock.Object);
await pipeline.ApplyModerationSettingsAsync();
#region Validations
moderationRepositoryMock.VerifyAll();
followerModerationProcessorMock.VerifyAll();
twitterAccountModerationProcessorMock.VerifyAll();
loggerMock.VerifyAll();
#endregion
}
}
}

View File

@ -0,0 +1,204 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Moderation.Processors;
using Castle.DynamicProxy.Generators.Emitters;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Moderation.Tests.Processors
{
[TestClass]
public class FollowerModerationProcessorTests
{
[TestMethod]
public async Task ProcessAsync_None()
{
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
var removeFollowerActionMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new FollowerModerationProcessor(followersDalMock.Object, moderationRepositoryMock.Object, removeFollowerActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.None);
#region Validations
followersDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeFollowerActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_WhiteListing_WhiteListed()
{
#region Stubs
var allFollowers = new List<Follower>
{
new Follower
{
Acct = "acct",
Host = "host"
}
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetAllFollowersAsync())
.ReturnsAsync(allFollowers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.CheckStatus(
It.Is<ModerationEntityTypeEnum>(y => y == ModerationEntityTypeEnum.Follower),
It.Is<string>(y => y == "@acct@host")))
.Returns(ModeratedTypeEnum.WhiteListed);
var removeFollowerActionMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new FollowerModerationProcessor(followersDalMock.Object, moderationRepositoryMock.Object, removeFollowerActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.WhiteListing);
#region Validations
followersDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeFollowerActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_WhiteListing_NotWhiteListed()
{
#region Stubs
var allFollowers = new List<Follower>
{
new Follower
{
Acct = "acct",
Host = "host"
}
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetAllFollowersAsync())
.ReturnsAsync(allFollowers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.CheckStatus(
It.Is<ModerationEntityTypeEnum>(y => y == ModerationEntityTypeEnum.Follower),
It.Is<string>(y => y == "@acct@host")))
.Returns(ModeratedTypeEnum.None);
var removeFollowerActionMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
removeFollowerActionMock
.Setup(x => x.ProcessAsync(
It.Is<Follower>(y => y.Acct == "acct")))
.Returns(Task.CompletedTask);
#endregion
var processor = new FollowerModerationProcessor(followersDalMock.Object, moderationRepositoryMock.Object, removeFollowerActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.WhiteListing);
#region Validations
followersDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeFollowerActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_BlackListing_BlackListed()
{
#region Stubs
var allFollowers = new List<Follower>
{
new Follower
{
Acct = "acct",
Host = "host"
}
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetAllFollowersAsync())
.ReturnsAsync(allFollowers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.CheckStatus(
It.Is<ModerationEntityTypeEnum>(y => y == ModerationEntityTypeEnum.Follower),
It.Is<string>(y => y == "@acct@host")))
.Returns(ModeratedTypeEnum.BlackListed);
var removeFollowerActionMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
removeFollowerActionMock
.Setup(x => x.ProcessAsync(
It.Is<Follower>(y => y.Acct == "acct")))
.Returns(Task.CompletedTask);
#endregion
var processor = new FollowerModerationProcessor(followersDalMock.Object, moderationRepositoryMock.Object, removeFollowerActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.BlackListing);
#region Validations
followersDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeFollowerActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_BlackListing_NotBlackListed()
{
#region Stubs
var allFollowers = new List<Follower>
{
new Follower
{
Acct = "acct",
Host = "host"
}
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetAllFollowersAsync())
.ReturnsAsync(allFollowers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.CheckStatus(
It.Is<ModerationEntityTypeEnum>(y => y == ModerationEntityTypeEnum.Follower),
It.Is<string>(y => y == "@acct@host")))
.Returns(ModeratedTypeEnum.None);
var removeFollowerActionMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new FollowerModerationProcessor(followersDalMock.Object, moderationRepositoryMock.Object, removeFollowerActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.BlackListing);
#region Validations
followersDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeFollowerActionMock.VerifyAll();
#endregion
}
}
}

View File

@ -0,0 +1,199 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Moderation.Processors;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Moderation.Tests.Processors
{
[TestClass]
public class TwitterAccountModerationProcessorTests
{
[TestMethod]
public async Task ProcessAsync_None()
{
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new TwitterAccountModerationProcessor(twitterUserDalMock.Object, moderationRepositoryMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.None);
#region Validations
twitterUserDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_WhiteListing_WhiteListed()
{
#region Stubs
var allUsers = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Acct = "acct"
}
};
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync())
.ReturnsAsync(allUsers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.CheckStatus(
It.Is<ModerationEntityTypeEnum>(y => y == ModerationEntityTypeEnum.TwitterAccount),
It.Is<string>(y => y == "acct")))
.Returns(ModeratedTypeEnum.WhiteListed);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new TwitterAccountModerationProcessor(twitterUserDalMock.Object, moderationRepositoryMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.WhiteListing);
#region Validations
twitterUserDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_WhiteListing_NotWhiteListed()
{
#region Stubs
var allUsers = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Acct = "acct"
}
};
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync())
.ReturnsAsync(allUsers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.CheckStatus(
It.Is<ModerationEntityTypeEnum>(y => y == ModerationEntityTypeEnum.TwitterAccount),
It.Is<string>(y => y == "acct")))
.Returns(ModeratedTypeEnum.None);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(
It.Is<SyncTwitterUser>(y => y.Acct == "acct")))
.Returns(Task.CompletedTask);
#endregion
var processor = new TwitterAccountModerationProcessor(twitterUserDalMock.Object, moderationRepositoryMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.WhiteListing);
#region Validations
twitterUserDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_BlackListing_BlackListed()
{
#region Stubs
var allUsers = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Acct = "acct"
}
};
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync())
.ReturnsAsync(allUsers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.CheckStatus(
It.Is<ModerationEntityTypeEnum>(y => y == ModerationEntityTypeEnum.TwitterAccount),
It.Is<string>(y => y == "acct")))
.Returns(ModeratedTypeEnum.BlackListed);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(
It.Is<SyncTwitterUser>(y => y.Acct == "acct")))
.Returns(Task.CompletedTask);
#endregion
var processor = new TwitterAccountModerationProcessor(twitterUserDalMock.Object, moderationRepositoryMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.BlackListing);
#region Validations
twitterUserDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_BlackListing_NotBlackListed()
{
#region Stubs
var allUsers = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Acct = "acct"
}
};
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync())
.ReturnsAsync(allUsers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
moderationRepositoryMock
.Setup(x => x.CheckStatus(
It.Is<ModerationEntityTypeEnum>(y => y == ModerationEntityTypeEnum.TwitterAccount),
It.Is<string>(y => y == "acct")))
.Returns(ModeratedTypeEnum.None);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new TwitterAccountModerationProcessor(twitterUserDalMock.Object, moderationRepositoryMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(ModerationTypeEnum.BlackListing);
#region Validations
twitterUserDalMock.VerifyAll();
moderationRepositoryMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
}
}

View File

@ -48,9 +48,9 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var processor = new RetrieveTwitterUsersProcessor(twitterUserDalMock.Object, maxUsersNumberProviderMock.Object, loggerMock.Object);
processor.WaitFactor = 10;
processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
var t = processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
await Task.Delay(50);
await Task.WhenAny(t, Task.Delay(50));
#region Validations
maxUsersNumberProviderMock.VerifyAll();
@ -95,10 +95,10 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var processor = new RetrieveTwitterUsersProcessor(twitterUserDalMock.Object, maxUsersNumberProviderMock.Object, loggerMock.Object);
processor.WaitFactor = 2;
processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
await Task.Delay(300);
var t = processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
await Task.WhenAny(t, Task.Delay(300));
#region Validations
maxUsersNumberProviderMock.VerifyAll();
twitterUserDalMock.VerifyAll();
@ -142,9 +142,14 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var processor = new RetrieveTwitterUsersProcessor(twitterUserDalMock.Object, maxUsersNumberProviderMock.Object, loggerMock.Object);
processor.WaitFactor = 2;
processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
var t = processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
var t2 = Task.Run(async () =>
{
while (buffer.Count < 11)
await Task.Delay(50);
});
await Task.Delay(200);
await Task.WhenAny(t, t2, Task.Delay(5000));
#region Validations
maxUsersNumberProviderMock.VerifyAll();
@ -181,9 +186,9 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var processor = new RetrieveTwitterUsersProcessor(twitterUserDalMock.Object, maxUsersNumberProviderMock.Object, loggerMock.Object);
processor.WaitFactor = 1;
processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
var t =processor.GetTwitterUsersAsync(buffer, CancellationToken.None);
await Task.Delay(50);
await Task.WhenAny(t, Task.Delay(50));
#region Validations
maxUsersNumberProviderMock.VerifyAll();

View File

@ -0,0 +1,50 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Pipeline.Contracts;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Pipeline.Tests
{
[TestClass]
public class StatusPublicationPipelineTests
{
[TestMethod]
public async Task ExecuteAsync_Test()
{
#region Stubs
var ct = new CancellationTokenSource(10);
#endregion
#region Mocks
var retrieveTwitterUsersProcessor = new Mock<IRetrieveTwitterUsersProcessor>(MockBehavior.Strict);
retrieveTwitterUsersProcessor
.Setup(x => x.GetTwitterUsersAsync(
It.IsAny<BufferBlock<SyncTwitterUser[]>>(),
It.IsAny<CancellationToken>()))
.Returns(Task.Delay(0));
var retrieveTweetsProcessor = new Mock<IRetrieveTweetsProcessor>(MockBehavior.Strict);
var retrieveFollowersProcessor = new Mock<IRetrieveFollowersProcessor>(MockBehavior.Strict);
var sendTweetsToFollowersProcessor = new Mock<ISendTweetsToFollowersProcessor>(MockBehavior.Strict);
var saveProgressionProcessor = new Mock<ISaveProgressionProcessor>(MockBehavior.Strict);
var logger = new Mock<ILogger<StatusPublicationPipeline>>();
#endregion
var pipeline = new StatusPublicationPipeline(retrieveTweetsProcessor.Object, retrieveTwitterUsersProcessor.Object, retrieveFollowersProcessor.Object, sendTweetsToFollowersProcessor.Object, saveProgressionProcessor.Object, logger.Object);
await pipeline.ExecuteAsync(ct.Token);
#region Validations
retrieveTwitterUsersProcessor.VerifyAll();
retrieveTweetsProcessor.VerifyAll();
retrieveFollowersProcessor.VerifyAll();
sendTweetsToFollowersProcessor.VerifyAll();
saveProgressionProcessor.VerifyAll();
logger.VerifyAll();
#endregion
}
}
}