diff --git a/src/BirdsiteLive.Moderation/Actions/RemoveFollowerAction.cs b/src/BirdsiteLive.Moderation/Actions/RemoveFollowerAction.cs
new file mode 100644
index 0000000..8f0261a
--- /dev/null
+++ b/src/BirdsiteLive.Moderation/Actions/RemoveFollowerAction.cs
@@ -0,0 +1,44 @@
+using System.Linq;
+using System.Threading.Tasks;
+using BirdsiteLive.DAL.Contracts;
+using BirdsiteLive.DAL.Models;
+
+namespace BirdsiteLive.Moderation.Actions
+{
+ public interface IRemoveFollowerAction
+ {
+ Task ProcessAsync(Follower follower);
+ }
+
+ public class RemoveFollowerAction : IRemoveFollowerAction
+ {
+ private readonly IFollowersDal _followersDal;
+ private readonly ITwitterUserDal _twitterUserDal;
+
+ #region Ctor
+ public RemoveFollowerAction(IFollowersDal followersDal, ITwitterUserDal twitterUserDal)
+ {
+ _followersDal = followersDal;
+ _twitterUserDal = twitterUserDal;
+ }
+ #endregion
+
+ public async Task ProcessAsync(Follower follower)
+ {
+ // Perform undo following to user instance
+ // TODO: Insert ActivityPub magic here
+
+ // 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Moderation/Actions/RemoveTwitterAccountAction.cs b/src/BirdsiteLive.Moderation/Actions/RemoveTwitterAccountAction.cs
new file mode 100644
index 0000000..cfadd15
--- /dev/null
+++ b/src/BirdsiteLive.Moderation/Actions/RemoveTwitterAccountAction.cs
@@ -0,0 +1,55 @@
+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;
+
+ #region Ctor
+ public RemoveTwitterAccountAction(IFollowersDal followersDal, ITwitterUserDal twitterUserDal)
+ {
+ _followersDal = followersDal;
+ _twitterUserDal = twitterUserDal;
+ }
+ #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
+ // TODO: Insert ActivityPub magic here
+
+ // 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(twitterUser.Acct);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Moderation/BirdsiteLive.Moderation.csproj b/src/BirdsiteLive.Moderation/BirdsiteLive.Moderation.csproj
new file mode 100644
index 0000000..b7bbeea
--- /dev/null
+++ b/src/BirdsiteLive.Moderation/BirdsiteLive.Moderation.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BirdsiteLive.Moderation/ModerationPipeline.cs b/src/BirdsiteLive.Moderation/ModerationPipeline.cs
new file mode 100644
index 0000000..bc92a17
--- /dev/null
+++ b/src/BirdsiteLive.Moderation/ModerationPipeline.cs
@@ -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 _logger;
+
+ #region Ctor
+ public ModerationPipeline(IModerationRepository moderationRepository, IFollowerModerationProcessor followerModerationProcessor, ITwitterAccountModerationProcessor twitterAccountModerationProcessor, ILogger 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);
+ }
+ }
+}
diff --git a/src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs b/src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs
new file mode 100644
index 0000000..18c9b14
--- /dev/null
+++ b/src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs
@@ -0,0 +1,44 @@
+using System.Threading.Tasks;
+using BirdsiteLive.DAL.Contracts;
+using BirdsiteLive.DAL.Models;
+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)
+ {
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.Moderation/Processors/TwitterAccountModerationProcessor.cs b/src/BirdsiteLive.Moderation/Processors/TwitterAccountModerationProcessor.cs
new file mode 100644
index 0000000..3c267bb
--- /dev/null
+++ b/src/BirdsiteLive.Moderation/Processors/TwitterAccountModerationProcessor.cs
@@ -0,0 +1,43 @@
+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)
+ {
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BirdsiteLive.sln b/src/BirdsiteLive.sln
index 0a35bf6..d345e68 100644
--- a/src/BirdsiteLive.sln
+++ b/src/BirdsiteLive.sln
@@ -39,7 +39,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Domain.Tests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Pipeline.Tests", "Tests\BirdsiteLive.Pipeline.Tests\BirdsiteLive.Pipeline.Tests.csproj", "{BF51CA81-5A7A-46F8-B4FB-861C6BE59298}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.DAL.Tests", "Tests\BirdsiteLive.DAL.Tests\BirdsiteLive.DAL.Tests.csproj", "{5A1E3EB5-6CBB-470D-8A0D-10F8C18353D5}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.DAL.Tests", "Tests\BirdsiteLive.DAL.Tests\BirdsiteLive.DAL.Tests.csproj", "{5A1E3EB5-6CBB-470D-8A0D-10F8C18353D5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.Moderation", "BirdsiteLive.Moderation\BirdsiteLive.Moderation.csproj", "{4BE541AC-8A93-4FA3-98AC-956CC2D5B748}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -107,6 +109,10 @@ 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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -126,6 +132,7 @@ 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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {69E8DCAD-4C37-4010-858F-5F94E6FBABCE}
diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs
index 39c07cb..07e1578 100644
--- a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs
+++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs
@@ -84,6 +84,11 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
+ public async Task GetAllFollowersAsync()
+ {
+ throw new NotImplementedException();
+ }
+
public async Task UpdateFollowerAsync(Follower follower)
{
if (follower == default) throw new ArgumentException("follower");
diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs
index afbf7d1..847ae2c 100644
--- a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs
+++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs
@@ -49,6 +49,11 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
+ public async Task DeleteTwitterUserAsync(int id)
+ {
+ throw new NotImplementedException();
+ }
+
public async Task GetTwitterUsersCountAsync()
{
var query = $"SELECT COUNT(*) FROM {_settings.TwitterUserTableName}";
@@ -75,6 +80,11 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
+ public async Task GetAllTwitterUsersAsync()
+ {
+ throw new NotImplementedException();
+ }
+
public async Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, DateTime lastSync)
{
if(id == default) throw new ArgumentException("id");
diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs
index 23cf2b2..6d20ce4 100644
--- a/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs
+++ b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs
@@ -10,6 +10,7 @@ namespace BirdsiteLive.DAL.Contracts
Task CreateFollowerAsync(string acct, string host, string inboxRoute, string sharedInboxRoute, int[] followings = null,
Dictionary followingSyncStatus = null);
Task GetFollowersAsync(int followedUserId);
+ Task GetAllFollowersAsync();
Task UpdateFollowerAsync(Follower follower);
Task DeleteFollowerAsync(int id);
Task DeleteFollowerAsync(string acct, string host);
diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs
index 1fa8127..3af2aa4 100644
--- a/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs
+++ b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs
@@ -9,8 +9,10 @@ namespace BirdsiteLive.DAL.Contracts
Task CreateTwitterUserAsync(string acct, long lastTweetPostedId);
Task GetTwitterUserAsync(string acct);
Task GetAllTwitterUsersAsync(int maxNumber);
+ Task GetAllTwitterUsersAsync();
Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, DateTime lastSync);
Task DeleteTwitterUserAsync(string acct);
+ Task DeleteTwitterUserAsync(int id);
Task GetTwitterUsersCountAsync();
}
}
\ No newline at end of file