Browse Source

Merge pull request #94 from NicolasConstant/develop

0.15.0 PR
master
Nicolas Constant 2 years ago
committed by GitHub
parent
commit
27312dd3c4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 3131 additions and 109 deletions
  1. +1
    -1
      .github/workflows/dotnet-core.yml
  2. +76
    -1
      VARIABLES.md
  3. +0
    -1
      src/BirdsiteLive.ActivityPub/ApDeserializer.cs
  4. +10
    -0
      src/BirdsiteLive.Common/Settings/ModerationSettings.cs
  5. +3
    -3
      src/BirdsiteLive.Domain/BusinessUseCases/ProcessFollowUser.cs
  6. +148
    -0
      src/BirdsiteLive.Domain/Repository/ModerationRepository.cs
  7. +1
    -4
      src/BirdsiteLive.Domain/StatusService.cs
  8. +23
    -0
      src/BirdsiteLive.Domain/Tools/ModerationParser.cs
  9. +28
    -0
      src/BirdsiteLive.Domain/Tools/ModerationRegexParser.cs
  10. +77
    -39
      src/BirdsiteLive.Domain/UserService.cs
  11. +52
    -0
      src/BirdsiteLive.Moderation/Actions/RejectAllFollowingsAction.cs
  12. +44
    -0
      src/BirdsiteLive.Moderation/Actions/RejectFollowingAction.cs
  13. +51
    -0
      src/BirdsiteLive.Moderation/Actions/RemoveFollowerAction.cs
  14. +57
    -0
      src/BirdsiteLive.Moderation/Actions/RemoveTwitterAccountAction.cs
  15. +16
    -0
      src/BirdsiteLive.Moderation/BirdsiteLive.Moderation.csproj
  16. +61
    -0
      src/BirdsiteLive.Moderation/ModerationPipeline.cs
  17. +45
    -0
      src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs
  18. +45
    -0
      src/BirdsiteLive.Moderation/Processors/TwitterAccountModerationProcessor.cs
  19. +14
    -0
      src/BirdsiteLive.sln
  20. +2
    -1
      src/BirdsiteLive/BirdsiteLive.csproj
  21. +59
    -0
      src/BirdsiteLive/Component/NodeInfoViewComponent.cs
  22. +58
    -0
      src/BirdsiteLive/Controllers/AboutController.cs
  23. +19
    -4
      src/BirdsiteLive/Controllers/DebugingController.cs
  24. +1
    -3
      src/BirdsiteLive/Controllers/UsersController.cs
  25. +7
    -3
      src/BirdsiteLive/Controllers/WellKnownController.cs
  26. +53
    -0
      src/BirdsiteLive/Services/CachedStatisticsService.cs
  27. +5
    -1
      src/BirdsiteLive/Services/FederationService.cs
  28. +5
    -1
      src/BirdsiteLive/Startup.cs
  29. +27
    -0
      src/BirdsiteLive/Views/About/Blacklisting.cshtml
  30. +30
    -0
      src/BirdsiteLive/Views/About/Index.cshtml
  31. +27
    -0
      src/BirdsiteLive/Views/About/Whitelisting.cshtml
  32. +7
    -0
      src/BirdsiteLive/Views/Debuging/Index.cshtml
  33. +22
    -0
      src/BirdsiteLive/Views/Shared/Components/NodeInfo/Default.cshtml
  34. +6
    -0
      src/BirdsiteLive/Views/Shared/_Layout.cshtml
  35. +12
    -1
      src/BirdsiteLive/appsettings.json
  36. +71
    -0
      src/BirdsiteLive/wwwroot/css/pattern.css
  37. +14
    -6
      src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs
  38. +20
    -5
      src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs
  39. +42
    -4
      src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs
  40. +2
    -1
      src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs
  41. +3
    -0
      src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs
  42. +1
    -0
      src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs
  43. +3
    -3
      src/Tests/BirdsiteLive.Cryptography.Tests/MagicKeyTests.cs
  44. +141
    -13
      src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/FollowersPostgresDalTests.cs
  45. +105
    -2
      src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/TwitterUserPostgresDalTests.cs
  46. +4
    -0
      src/Tests/BirdsiteLive.Domain.Tests/BirdsiteLive.Domain.Tests.csproj
  47. +6
    -3
      src/Tests/BirdsiteLive.Domain.Tests/BusinessUseCases/ProcessFollowUserTests.cs
  48. +347
    -0
      src/Tests/BirdsiteLive.Domain.Tests/Repository/ModerationRepositoryTests.cs
  49. +149
    -0
      src/Tests/BirdsiteLive.Domain.Tests/Tools/ModerationParserTests.cs
  50. +78
    -0
      src/Tests/BirdsiteLive.Domain.Tests/Tools/ModerationRegexParserTests.cs
  51. +114
    -0
      src/Tests/BirdsiteLive.Moderation.Tests/Actions/RejectAllFollowingsActionTests.cs
  52. +103
    -0
      src/Tests/BirdsiteLive.Moderation.Tests/Actions/RejectFollowingActionTests.cs
  53. +111
    -0
      src/Tests/BirdsiteLive.Moderation.Tests/Actions/RemoveFollowerActionTests.cs
  54. +131
    -0
      src/Tests/BirdsiteLive.Moderation.Tests/Actions/RemoveTwitterAccountActionTests.cs
  55. +21
    -0
      src/Tests/BirdsiteLive.Moderation.Tests/BirdsiteLive.Moderation.Tests.csproj
  56. +106
    -0
      src/Tests/BirdsiteLive.Moderation.Tests/ModerationPipelineTests.cs
  57. +204
    -0
      src/Tests/BirdsiteLive.Moderation.Tests/Processors/FollowerModerationProcessorTests.cs
  58. +199
    -0
      src/Tests/BirdsiteLive.Moderation.Tests/Processors/TwitterAccountModerationProcessorTests.cs
  59. +14
    -9
      src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RetrieveTwitterUsersProcessorTests.cs
  60. +50
    -0
      src/Tests/BirdsiteLive.Pipeline.Tests/StatusPublicationPipelineTests.cs

+ 1
- 1
.github/workflows/dotnet-core.yml 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}}

+ 76
- 1
VARIABLES.md 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.

+ 0
- 1
src/BirdsiteLive.ActivityPub/ApDeserializer.cs View File

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


+ 10
- 0
src/BirdsiteLive.Common/Settings/ModerationSettings.cs 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; }
}
}

+ 3
- 3
src/BirdsiteLive.Domain/BusinessUseCases/ProcessFollowUser.cs 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);
}


+ 148
- 0
src/BirdsiteLive.Domain/Repository/ModerationRepository.cs 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
}
}

+ 1
- 4
src/BirdsiteLive.Domain/StatusService.cs 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,


+ 23
- 0
src/BirdsiteLive.Domain/Tools/ModerationParser.cs 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();
}
}
}

+ 28
- 0
src/BirdsiteLive.Domain/Tools/ModerationRegexParser.cs 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}$");
}
}
}

+ 77
- 39
src/BirdsiteLive.Domain/UserService.cs 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)
{


+ 52
- 0
src/BirdsiteLive.Moderation/Actions/RejectAllFollowingsAction.cs 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) { }
}
}
}
}

+ 44
- 0
src/BirdsiteLive.Moderation/Actions/RejectFollowingAction.cs 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) { }
}
}
}

+ 51
- 0
src/BirdsiteLive.Moderation/Actions/RemoveFollowerAction.cs 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);
}
}
}

+ 57
- 0
src/BirdsiteLive.Moderation/Actions/RemoveTwitterAccountAction.cs 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);
}
}
}

+ 16
- 0
src/BirdsiteLive.Moderation/BirdsiteLive.Moderation.csproj 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>

+ 61
- 0
src/BirdsiteLive.Moderation/ModerationPipeline.cs 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);
}
}
}

+ 45
- 0
src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs 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);
}
}
}
}

+ 45
- 0
src/BirdsiteLive.Moderation/Processors/TwitterAccountModerationProcessor.cs 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);
}
}
}
}

+ 14
- 0
src/BirdsiteLive.sln 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


+ 2
- 1
src/BirdsiteLive/BirdsiteLive.csproj 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" />


+ 59
- 0
src/BirdsiteLive/Component/NodeInfoViewComponent.cs 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; }
}
}

+ 58
- 0
src/BirdsiteLive/Controllers/AboutController.cs 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; }
}
}

+ 19
- 4
src/BirdsiteLive/Controllers/DebugingController.cs 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


+ 1
- 3
src/BirdsiteLive/Controllers/UsersController.cs 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();


+ 7
- 3
src/BirdsiteLive/Controllers/WellKnownController.cs 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],


+ 53
- 0
src/BirdsiteLive/Services/CachedStatisticsService.cs 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; }
}
}

+ 5
- 1
src/BirdsiteLive/Services/FederationService.cs 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


+ 5
- 1
src/BirdsiteLive/Startup.cs 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();


+ 27
- 0
src/BirdsiteLive/Views/About/Blacklisting.cshtml 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>

+ 30
- 0
src/BirdsiteLive/Views/About/Index.cshtml 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>

+ 27
- 0
src/BirdsiteLive/Views/About/Whitelisting.cshtml 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>

+ 7
- 0
src/BirdsiteLive/Views/Debuging/Index.cshtml 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>

+ 22
- 0
src/BirdsiteLive/Views/Shared/Components/NodeInfo/Default.cshtml 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>

+ 6
- 0
src/BirdsiteLive/Views/Shared/_Layout.cshtml 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>*@


+ 12
- 1
src/BirdsiteLive/appsettings.json 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
}
}

+ 71
- 0
src/BirdsiteLive/wwwroot/css/pattern.css View File

@ -0,0 +1,71 @@
.container-nodeinfo {
line-height: 30px;
text-align: center;
}
.wrapper-nodeinfo {
border-bottom: 1px solid #dee2e6;
}
.