From 99203168632edb8bcc603068180e72f0306d1e48 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Fri, 5 Feb 2021 01:12:54 -0500 Subject: [PATCH] first iteration of logic to apply moderation policy --- .../Actions/RemoveFollowerAction.cs | 44 +++++++++++++ .../Actions/RemoveTwitterAccountAction.cs | 55 +++++++++++++++++ .../BirdsiteLive.Moderation.csproj | 16 +++++ .../ModerationPipeline.cs | 61 +++++++++++++++++++ .../Processors/FollowerModerationProcessor.cs | 44 +++++++++++++ .../TwitterAccountModerationProcessor.cs | 43 +++++++++++++ src/BirdsiteLive.sln | 9 ++- .../DataAccessLayers/FollowersPostgresDal.cs | 5 ++ .../TwitterUserPostgresDal.cs | 10 +++ .../Contracts/IFollowersDal.cs | 1 + .../Contracts/ITwitterUserDal.cs | 2 + 11 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 src/BirdsiteLive.Moderation/Actions/RemoveFollowerAction.cs create mode 100644 src/BirdsiteLive.Moderation/Actions/RemoveTwitterAccountAction.cs create mode 100644 src/BirdsiteLive.Moderation/BirdsiteLive.Moderation.csproj create mode 100644 src/BirdsiteLive.Moderation/ModerationPipeline.cs create mode 100644 src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs create mode 100644 src/BirdsiteLive.Moderation/Processors/TwitterAccountModerationProcessor.cs 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