diff --git a/BSLManager.md b/BSLManager.md new file mode 100644 index 0000000..dc044b7 --- /dev/null +++ b/BSLManager.md @@ -0,0 +1,51 @@ +# BSLManager + +A CLI is provided in the Docker image so that admins can manage their instance. + +## Access to the CLI + +Since the CLI is packaged into the docker image, you'll have to open a shell from the container. To do so, list first your running containers: + +``` +docker ps +``` + +This should display you something equivalent to this: + +``` +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +3734c41af5a7 postgres:9.6 "docker-entrypoint.s…" 2 weeks ago Up 2 weeks 5432/tcp db_1 +be6870fe103e nicolasconstant/birdsitelive:latest "dotnet BirdsiteLive…" 6 weeks ago Up 2 weeks 443/tcp, 0.0.0.0:5000->80/tcp birdsitelive +``` + +Find the BSL container and keep the ID, here it's `be6870fe103e`. And you only need the three first char to identify it, so we'll be using `be6`. + +Then open a shell inside the container (change `be6` with your own id): + +``` +docker exec -it be6 /bin/bash +``` + +And you should now be inside the container, and all you have to do is calling the CLI: + +``` +./BSLManager +``` + +## Setting up the CLI + +The manager will ask you to provide information about the database and the instance. +Those must be same than the ones in the `docker-compose.yml` file. +Provide the information, review it and validate it. Then the CLI UI should shows up. + +## Using the CLI + +You can navigate between the sections with the arrows and tab keys. + +The **filter** permits to filter the list of users with a pattern. + +All users have their followings count provided next to them. +You can select any user by using the up/down arrow keys and hitting the `Enter` key, this will display more information about the user. +You can also remove a user and all their followings by hitting the `Del` key. You will be prompted by a confirmation message, and you'll be able to remove this user. + +Deleting users having a lots of followings can take some time: after the prompt has closed the process is still running and will update the list after that. Let the software do its thing and it will go through. diff --git a/Dockerfile b/Dockerfile index 7239ca2..11a4422 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,12 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:3.1-buster-slim AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +FROM mcr.microsoft.com/dotnet/sdk:3.1-buster AS publish COPY ./src/ ./src/ -RUN dotnet restore "/src/BirdsiteLive/BirdsiteLive.csproj" -RUN dotnet restore "/src/BSLManager/BSLManager.csproj" -RUN dotnet build "/src/BirdsiteLive/BirdsiteLive.csproj" -c Release -o /app/build - -FROM build AS publish RUN dotnet publish "/src/BirdsiteLive/BirdsiteLive.csproj" -c Release -o /app/publish RUN dotnet publish "/src/BSLManager/BSLManager.csproj" -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeAllContentForSelfExtract=true -c Release -o /app/publish diff --git a/README.md b/README.md index 31ef3fd..5d2be2c 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ You can find an official (and temporary) instance here: [beta.birdsite.live](htt I'm providing a [docker build](https://hub.docker.com/r/nicolasconstant/birdsitelive). To install it on your own server, please follow [those instructions](https://github.com/NicolasConstant/BirdsiteLive/blob/master/INSTALLATION.md). More [options](https://github.com/NicolasConstant/BirdsiteLive/blob/master/VARIABLES.md) are also available. +Also a [CLI](https://github.com/NicolasConstant/BirdsiteLive/blob/master/BSLManager.md) is available for adminitrative tasks. + ## License This project is licensed under the AGPLv3 License - see [LICENSE](https://github.com/NicolasConstant/BirdsiteLive/blob/master/LICENSE) for details. diff --git a/src/BSLManager/App.cs b/src/BSLManager/App.cs new file mode 100644 index 0000000..0e48262 --- /dev/null +++ b/src/BSLManager/App.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using BirdsiteLive.DAL.Contracts; +using BirdsiteLive.DAL.Models; +using BirdsiteLive.Moderation.Actions; +using BSLManager.Domain; +using BSLManager.Tools; +using Terminal.Gui; + +namespace BSLManager +{ + public class App + { + private readonly IFollowersDal _followersDal; + private readonly IRemoveFollowerAction _removeFollowerAction; + + private readonly FollowersListState _state = new FollowersListState(); + + #region Ctor + public App(IFollowersDal followersDal, IRemoveFollowerAction removeFollowerAction) + { + _followersDal = followersDal; + _removeFollowerAction = removeFollowerAction; + } + #endregion + + public void Run() + { + Application.Init(); + var top = Application.Top; + + // Creates the top-level window to show + var win = new Window("BSL Manager") + { + X = 0, + Y = 1, // Leave one row for the toplevel menu + + // By using Dim.Fill(), it will automatically resize without manual intervention + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + top.Add(win); + + // Creates a menubar, the item "New" has a help menu. + var menu = new MenuBar(new MenuBarItem[] + { + new MenuBarItem("_File", new MenuItem[] + { + new MenuItem("_Quit", "", () => + { + if (Quit()) top.Running = false; + }) + }), + //new MenuBarItem ("_Edit", new MenuItem [] { + // new MenuItem ("_Copy", "", null), + // new MenuItem ("C_ut", "", null), + // new MenuItem ("_Paste", "", null) + //}) + }); + top.Add(menu); + + static bool Quit() + { + var n = MessageBox.Query(50, 7, "Quit BSL Manager", "Are you sure you want to quit?", "Yes", "No"); + return n == 0; + } + + RetrieveUserList(); + + var list = new ListView(_state.GetDisplayableList()) + { + X = 1, + Y = 3, + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + list.KeyDown += _ => + { + if (_.KeyEvent.Key == Key.Enter) + { + OpenFollowerDialog(list.SelectedItem); + } + else if (_.KeyEvent.Key == Key.Delete + || _.KeyEvent.Key == Key.DeleteChar + || _.KeyEvent.Key == Key.Backspace + || _.KeyEvent.Key == Key.D) + { + OpenDeleteDialog(list.SelectedItem); + } + }; + + var listingFollowersLabel = new Label(1, 0, "Listing followers"); + var filterLabel = new Label("Filter: ") { X = 1, Y = 1 }; + var filterText = new TextField("") + { + X = Pos.Right(filterLabel), + Y = 1, + Width = 40 + }; + + filterText.KeyDown += _ => + { + var text = filterText.Text.ToString(); + if (_.KeyEvent.Key == Key.Enter && !string.IsNullOrWhiteSpace(text)) + { + _state.FilterBy(text); + ConsoleGui.RefreshUI(); + } + }; + + win.Add( + listingFollowersLabel, + filterLabel, + filterText, + list + ); + + Application.Run(); + } + + private void OpenFollowerDialog(int selectedIndex) + { + var close = new Button(3, 14, "Close"); + close.Clicked += () => Application.RequestStop(); + + var dialog = new Dialog("Info", 60, 18, close); + + var follower = _state.GetElementAt(selectedIndex); + + var name = new Label($"User: @{follower.Acct}@{follower.Host}") + { + X = 1, + Y = 1, + Width = Dim.Fill(), + Height = 1 + }; + var following = new Label($"Following Count: {follower.Followings.Count}") + { + X = 1, + Y = 3, + Width = Dim.Fill(), + Height = 1 + }; + var inbox = new Label($"Inbox: {follower.InboxRoute}") + { + X = 1, + Y = 4, + Width = Dim.Fill(), + Height = 1 + }; + var sharedInbox = new Label($"Shared Inbox: {follower.SharedInboxRoute}") + { + X = 1, + Y = 5, + Width = Dim.Fill(), + Height = 1 + }; + + dialog.Add(name); + dialog.Add(following); + dialog.Add(inbox); + dialog.Add(sharedInbox); + dialog.Add(close); + Application.Run(dialog); + } + + private void OpenDeleteDialog(int selectedIndex) + { + bool okpressed = false; + var ok = new Button(10, 14, "Yes"); + ok.Clicked += () => + { + Application.RequestStop(); + okpressed = true; + }; + + var cancel = new Button(3, 14, "No"); + cancel.Clicked += () => Application.RequestStop(); + + var dialog = new Dialog("Delete", 60, 18, cancel, ok); + + var follower = _state.GetElementAt(selectedIndex); + var name = new Label($"User: @{follower.Acct}@{follower.Host}") + { + X = 1, + Y = 1, + Width = Dim.Fill(), + Height = 1 + }; + var entry = new Label("Delete user and remove all their followings?") + { + X = 1, + Y = 3, + Width = Dim.Fill(), + Height = 1 + }; + dialog.Add(name); + dialog.Add(entry); + Application.Run(dialog); + + if (okpressed) + { + DeleteAndRemoveUser(selectedIndex); + } + } + + private void DeleteAndRemoveUser(int el) + { + Application.MainLoop.Invoke(async () => + { + try + { + var userToDelete = _state.GetElementAt(el); + + BasicLogger.Log($"Delete {userToDelete.Acct}@{userToDelete.Host}"); + await _removeFollowerAction.ProcessAsync(userToDelete); + BasicLogger.Log($"Remove user from list"); + _state.RemoveAt(el); + } + catch (Exception e) + { + BasicLogger.Log(e.Message); + } + + ConsoleGui.RefreshUI(); + }); + } + + private void RetrieveUserList() + { + Application.MainLoop.Invoke(async () => + { + var followers = await _followersDal.GetAllFollowersAsync(); + _state.Load(followers.ToList()); + ConsoleGui.RefreshUI(); + }); + } + } +} \ No newline at end of file diff --git a/src/BSLManager/BSLManager.csproj b/src/BSLManager/BSLManager.csproj index fafc29d..52e5cde 100644 --- a/src/BSLManager/BSLManager.csproj +++ b/src/BSLManager/BSLManager.csproj @@ -1,4 +1,4 @@ - + Exe @@ -6,7 +6,23 @@ + + + + + + + + + + + + + PreserveNewest + + + diff --git a/src/BSLManager/Bootstrapper.cs b/src/BSLManager/Bootstrapper.cs new file mode 100644 index 0000000..7375cd6 --- /dev/null +++ b/src/BSLManager/Bootstrapper.cs @@ -0,0 +1,94 @@ +using System; +using System.Net.Http; +using BirdsiteLive.Common.Settings; +using BirdsiteLive.Common.Structs; +using BirdsiteLive.DAL.Contracts; +using BirdsiteLive.DAL.Postgres.DataAccessLayers; +using BirdsiteLive.DAL.Postgres.Settings; +using Lamar; +using Lamar.Scanning.Conventions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace BSLManager +{ + public class Bootstrapper + { + private readonly DbSettings _dbSettings; + private readonly InstanceSettings _instanceSettings; + + #region Ctor + public Bootstrapper(DbSettings dbSettings, InstanceSettings instanceSettings) + { + _dbSettings = dbSettings; + _instanceSettings = instanceSettings; + } + #endregion + + public Container Init() + { + var container = new Container(x => + { + x.For().Use(x => _dbSettings); + + x.For().Use(x => _instanceSettings); + + if (string.Equals(_dbSettings.Type, DbTypes.Postgres, StringComparison.OrdinalIgnoreCase)) + { + var connString = $"Host={_dbSettings.Host};Username={_dbSettings.User};Password={_dbSettings.Password};Database={_dbSettings.Name}"; + var postgresSettings = new PostgresSettings + { + ConnString = connString + }; + x.For().Use(x => postgresSettings); + + x.For().Use().Singleton(); + x.For().Use().Singleton(); + x.For().Use().Singleton(); + } + else + { + throw new NotImplementedException($"{_dbSettings.Type} is not supported"); + } + + var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider(); + x.For().Use(_ => serviceProvider.GetService()); + + x.For(typeof(ILogger<>)).Use(typeof(DummyLogger<>)); + + x.Scan(_ => + { + _.Assembly("BirdsiteLive.Twitter"); + _.Assembly("BirdsiteLive.Domain"); + _.Assembly("BirdsiteLive.DAL"); + _.Assembly("BirdsiteLive.DAL.Postgres"); + _.Assembly("BirdsiteLive.Moderation"); + + _.TheCallingAssembly(); + + _.WithDefaultConventions(); + + _.LookForRegistries(); + }); + }); + return container; + } + + public class DummyLogger : ILogger + { + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + } + + public bool IsEnabled(LogLevel logLevel) + { + return false; + } + + public IDisposable BeginScope(TState state) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/src/BSLManager/Domain/FollowersListState.cs b/src/BSLManager/Domain/FollowersListState.cs new file mode 100644 index 0000000..f33acb8 --- /dev/null +++ b/src/BSLManager/Domain/FollowersListState.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using BirdsiteLive.DAL.Models; + +namespace BSLManager.Domain +{ + public class FollowersListState + { + private readonly List _filteredDisplayableUserList = new List(); + + private List _sourceUserList = new List(); + private List _filteredSourceUserList = new List(); + + public void Load(List followers) + { + _sourceUserList = followers.OrderByDescending(x => x.Followings.Count).ToList(); + + ResetLists(); + } + + private void ResetLists() + { + _filteredSourceUserList = _sourceUserList.ToList(); + + _filteredDisplayableUserList.Clear(); + + foreach (var follower in _sourceUserList) + { + var displayedUser = $"{GetFullHandle(follower)} ({follower.Followings.Count})"; + _filteredDisplayableUserList.Add(displayedUser); + } + } + + public List GetDisplayableList() + { + return _filteredDisplayableUserList; + } + + public void FilterBy(string pattern) + { + ResetLists(); + + if (!string.IsNullOrWhiteSpace(pattern)) + { + var elToRemove = _filteredSourceUserList + .Where(x => !GetFullHandle(x).Contains(pattern)) + .Select(x => x) + .ToList(); + + foreach (var el in elToRemove) + { + _filteredSourceUserList.Remove(el); + + var dElToRemove = _filteredDisplayableUserList.First(x => x.Contains(GetFullHandle(el))); + _filteredDisplayableUserList.Remove(dElToRemove); + } + } + } + + private string GetFullHandle(Follower follower) + { + return $"@{follower.Acct}@{follower.Host}"; + } + + public void RemoveAt(int index) + { + var displayableUser = _filteredDisplayableUserList[index]; + var sourceUser = _filteredSourceUserList[index]; + + _filteredDisplayableUserList.Remove(displayableUser); + + _filteredSourceUserList.Remove(sourceUser); + _sourceUserList.Remove(sourceUser); + } + + public Follower GetElementAt(int index) + { + return _filteredSourceUserList[index]; + } + } +} \ No newline at end of file diff --git a/src/BSLManager/Program.cs b/src/BSLManager/Program.cs index ff1a8bd..629ff25 100644 --- a/src/BSLManager/Program.cs +++ b/src/BSLManager/Program.cs @@ -1,7 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Text; +using System.Threading.Tasks; +using BirdsiteLive.Common.Settings; +using BirdsiteLive.DAL.Contracts; +using BSLManager.Tools; +using Microsoft.Extensions.Configuration; using NStack; using Terminal.Gui; @@ -9,111 +15,25 @@ namespace BSLManager { class Program { - static void Main(string[] args) + static async Task Main(string[] args) { Console.OutputEncoding = Encoding.Default; - Application.Init(); - var top = Application.Top; + var settingsManager = new SettingsManager(); + var settings = settingsManager.GetSettings(); - // Creates the top-level window to show - var win = new Window("BSL Manager") - { - X = 0, - Y = 1, // Leave one row for the toplevel menu + //var builder = new ConfigurationBuilder() + // .AddEnvironmentVariables(); + //var configuration = builder.Build(); - // By using Dim.Fill(), it will automatically resize without manual intervention - Width = Dim.Fill(), - Height = Dim.Fill() - }; + //var dbSettings = configuration.GetSection("Db").Get(); + //var instanceSettings = configuration.GetSection("Instance").Get(); - top.Add(win); + var bootstrapper = new Bootstrapper(settings.dbSettings, settings.instanceSettings); + var container = bootstrapper.Init(); - // Creates a menubar, the item "New" has a help menu. - var menu = new MenuBar(new MenuBarItem[] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_Quit", "", () => { if (Quit ()) top.Running = false; }) - }), - //new MenuBarItem ("_Edit", new MenuItem [] { - // new MenuItem ("_Copy", "", null), - // new MenuItem ("C_ut", "", null), - // new MenuItem ("_Paste", "", null) - //}) - }); - top.Add(menu); - - static bool Quit() - { - var n = MessageBox.Query(50, 7, "Quit BSL Manager", "Are you sure you want to quit?", "Yes", "No"); - return n == 0; - } - - var listData = new List(); - for (var i = 0; i < 100; i++) - { - listData.Add($"@User{i}@Instance.tld {i*3}"); - } - - var list = new ListView(listData) - { - X = 1, - Y = 2, - Width = Dim.Fill(), - Height = Dim.Fill() - }; - - list.KeyDown += _ => - { - if (_.KeyEvent.Key == Key.Enter) - { - var el = list.SelectedItem; - - bool okpressed = false; - var ok = new Button(10, 14, "Yes"); - ok.Clicked += () => - { - Application.RequestStop(); - okpressed = true; - }; - - var cancel = new Button(3, 14, "No"); - cancel.Clicked += () => Application.RequestStop(); - - var dialog = new Dialog("Delete", 60, 18, cancel, ok); - - var name = new Label($"User: {listData[el]}") - { - X = 1, - Y = 1, - Width = Dim.Fill(), - Height = 1 - }; - var entry = new Label("Delete user and remove all their followings?") - { - X = 1, - Y = 3, - Width = Dim.Fill(), - Height = 1 - }; - dialog.Add(name); - dialog.Add(entry); - Application.Run(dialog); - - if (okpressed) - { - listData.RemoveAt(el); - typeof(Application).GetMethod("TerminalResized", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, null); - } - } - }; - - // Add some controls, - win.Add( - new Label(1, 0, "Listing followers"), - list - ); - - Application.Run(); - } + var app = container.GetInstance(); + app.Run(); + } } } diff --git a/src/BSLManager/Tools/BasicLogger.cs b/src/BSLManager/Tools/BasicLogger.cs new file mode 100644 index 0000000..dbb9265 --- /dev/null +++ b/src/BSLManager/Tools/BasicLogger.cs @@ -0,0 +1,13 @@ +using System; +using System.IO; + +namespace BSLManager.Tools +{ + public static class BasicLogger + { + public static void Log(string log) + { + File.AppendAllLines($"Log-{Guid.NewGuid()}.txt", new []{ log }); + } + } +} \ No newline at end of file diff --git a/src/BSLManager/Tools/ConsoleGui.cs b/src/BSLManager/Tools/ConsoleGui.cs new file mode 100644 index 0000000..b6f7b6e --- /dev/null +++ b/src/BSLManager/Tools/ConsoleGui.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using Terminal.Gui; + +namespace BSLManager.Tools +{ + public static class ConsoleGui + { + public static void RefreshUI() + { + typeof(Application) + .GetMethod("TerminalResized", BindingFlags.Static | BindingFlags.NonPublic) + .Invoke(null, null); + } + } +} \ No newline at end of file diff --git a/src/BSLManager/Tools/SettingsManager.cs b/src/BSLManager/Tools/SettingsManager.cs new file mode 100644 index 0000000..5ff6314 --- /dev/null +++ b/src/BSLManager/Tools/SettingsManager.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using BirdsiteLive.Common.Settings; +using Newtonsoft.Json; +using Org.BouncyCastle.Asn1.IsisMtt.X509; + +namespace BSLManager.Tools +{ + public class SettingsManager + { + private const string LocalFileName = "ManagerSettings.json"; + + public (DbSettings dbSettings, InstanceSettings instanceSettings) GetSettings() + { + var localSettingsData = GetLocalSettingsFile(); + if (localSettingsData != null) return Convert(localSettingsData); + + Console.WriteLine("We need to set up the manager"); + Console.WriteLine("Please provide the following information as provided in the docker-compose file"); + + LocalSettingsData data; + do + { + data = GetDataFromUser(); + Console.WriteLine(); + Console.WriteLine("Please check if all is ok:"); + Console.WriteLine(); + Console.WriteLine($"Db Host: {data.DbHost}"); + Console.WriteLine($"Db Name: {data.DbName}"); + Console.WriteLine($"Db User: {data.DbUser}"); + Console.WriteLine($"Db Password: {data.DbPassword}"); + Console.WriteLine($"Instance Domain: {data.InstanceDomain}"); + Console.WriteLine(); + + string resp; + do + { + Console.WriteLine("Is it valid? (yes, no)"); + resp = Console.ReadLine()?.Trim().ToLowerInvariant(); + + if (resp == "n" || resp == "no") data = null; + + } while (resp != "y" && resp != "yes" && resp != "n" && resp != "no"); + + } while (data == null); + + SaveLocalSettings(data); + return Convert(data); + } + + private LocalSettingsData GetDataFromUser() + { + var data = new LocalSettingsData(); + + Console.WriteLine("Db Host:"); + data.DbHost = Console.ReadLine(); + + Console.WriteLine("Db Name:"); + data.DbName = Console.ReadLine(); + + Console.WriteLine("Db User:"); + data.DbUser = Console.ReadLine(); + + Console.WriteLine("Db Password:"); + data.DbPassword = Console.ReadLine(); + + Console.WriteLine("Instance Domain:"); + data.InstanceDomain = Console.ReadLine(); + + return data; + } + + private (DbSettings dbSettings, InstanceSettings instanceSettings) Convert(LocalSettingsData data) + { + var dbSettings = new DbSettings + { + Type = data.DbType, + Host = data.DbHost, + Name = data.DbName, + User = data.DbUser, + Password = data.DbPassword + }; + var instancesSettings = new InstanceSettings + { + Domain = data.InstanceDomain + }; + return (dbSettings, instancesSettings); + } + + private LocalSettingsData GetLocalSettingsFile() + { + try + { + if (!File.Exists(LocalFileName)) return null; + + var jsonContent = File.ReadAllText(LocalFileName); + var content = JsonConvert.DeserializeObject(jsonContent); + return content; + } + catch (Exception) + { + return null; + } + } + + private void SaveLocalSettings(LocalSettingsData data) + { + var jsonContent = JsonConvert.SerializeObject(data); + File.WriteAllText(LocalFileName, jsonContent); + } + } + + internal class LocalSettingsData + { + public string DbType { get; set; } = "postgres"; + public string DbHost { get; set; } + public string DbName { get; set; } + public string DbUser { get; set; } + public string DbPassword { get; set; } + + public string InstanceDomain { get; set; } + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs b/src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs index 99d72f6..d697351 100644 --- a/src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs +++ b/src/BirdsiteLive.Moderation/Processors/FollowerModerationProcessor.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using BirdsiteLive.DAL.Contracts; using BirdsiteLive.Domain.Repository; using BirdsiteLive.Moderation.Actions; @@ -38,7 +39,10 @@ namespace BirdsiteLive.Moderation.Processors if (type == ModerationTypeEnum.WhiteListing && status != ModeratedTypeEnum.WhiteListed || type == ModerationTypeEnum.BlackListing && status == ModeratedTypeEnum.BlackListed) + { + Console.WriteLine($"Remove {followerHandle}"); await _removeFollowerAction.ProcessAsync(follower); + } } } } diff --git a/src/BirdsiteLive.sln b/src/BirdsiteLive.sln index ba118c6..4b0cfc1 100644 --- a/src/BirdsiteLive.sln +++ b/src/BirdsiteLive.sln @@ -47,7 +47,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Moderation.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Common.Tests", "Tests\BirdsiteLive.Common.Tests\BirdsiteLive.Common.Tests.csproj", "{C69F7582-6050-44DC-BAAB-7C8F0BDA525C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BSLManager", "BSLManager\BSLManager.csproj", "{4A84D351-E91B-4E58-8E20-211F0F4991D7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BSLManager", "BSLManager\BSLManager.csproj", "{4A84D351-E91B-4E58-8E20-211F0F4991D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BSLManager.Tests", "Tests\BSLManager.Tests\BSLManager.Tests.csproj", "{D4457271-620E-465A-B08E-7FC63C99A2F6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -131,6 +133,10 @@ Global {4A84D351-E91B-4E58-8E20-211F0F4991D7}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A84D351-E91B-4E58-8E20-211F0F4991D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {4A84D351-E91B-4E58-8E20-211F0F4991D7}.Release|Any CPU.Build.0 = Release|Any CPU + {D4457271-620E-465A-B08E-7FC63C99A2F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4457271-620E-465A-B08E-7FC63C99A2F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4457271-620E-465A-B08E-7FC63C99A2F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4457271-620E-465A-B08E-7FC63C99A2F6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -153,6 +159,7 @@ Global {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} + {D4457271-620E-465A-B08E-7FC63C99A2F6} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {69E8DCAD-4C37-4010-858F-5F94E6FBABCE} diff --git a/src/BirdsiteLive/BirdsiteLive.csproj b/src/BirdsiteLive/BirdsiteLive.csproj index 08b6809..ac29f6e 100644 --- a/src/BirdsiteLive/BirdsiteLive.csproj +++ b/src/BirdsiteLive/BirdsiteLive.csproj @@ -4,7 +4,7 @@ netcoreapp3.1 d21486de-a812-47eb-a419-05682bb68856 Linux - 0.16.2 + 0.17.0 diff --git a/src/BirdsiteLive/Controllers/InboxController.cs b/src/BirdsiteLive/Controllers/InboxController.cs index a0d8748..db055cf 100644 --- a/src/BirdsiteLive/Controllers/InboxController.cs +++ b/src/BirdsiteLive/Controllers/InboxController.cs @@ -5,12 +5,22 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace BirdsiteLive.Controllers { [ApiController] public class InboxController : ControllerBase { + private readonly ILogger _logger; + + #region Ctor + public InboxController(ILogger logger) + { + _logger = logger; + } + #endregion + [Route("/inbox")] [HttpPost] public async Task Inbox() @@ -19,6 +29,8 @@ namespace BirdsiteLive.Controllers using (var reader = new StreamReader(Request.Body)) { var body = await reader.ReadToEndAsync(); + + _logger.LogTrace("Inbox: {Body}", body); //System.IO.File.WriteAllText($@"C:\apdebug\inbox\{Guid.NewGuid()}.json", body); } diff --git a/src/BirdsiteLive/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs index bf262fe..91b63c7 100644 --- a/src/BirdsiteLive/Controllers/UsersController.cs +++ b/src/BirdsiteLive/Controllers/UsersController.cs @@ -17,6 +17,7 @@ using BirdsiteLive.Twitter; using BirdsiteLive.Twitter.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; @@ -29,15 +30,17 @@ namespace BirdsiteLive.Controllers private readonly IUserService _userService; private readonly IStatusService _statusService; private readonly InstanceSettings _instanceSettings; + private readonly ILogger _logger; #region Ctor - public UsersController(ITwitterUserService twitterUserService, IUserService userService, IStatusService statusService, InstanceSettings instanceSettings, ITwitterTweetsService twitterTweetService) + public UsersController(ITwitterUserService twitterUserService, IUserService userService, IStatusService statusService, InstanceSettings instanceSettings, ITwitterTweetsService twitterTweetService, ILogger logger) { _twitterUserService = twitterUserService; _userService = userService; _statusService = statusService; _instanceSettings = instanceSettings; _twitterTweetService = twitterTweetService; + _logger = logger; } #endregion @@ -57,6 +60,8 @@ namespace BirdsiteLive.Controllers [Route("/users/{id}")] public IActionResult Index(string id) { + _logger.LogTrace("User Index: {Id}", id); + id = id.Trim(new[] { ' ', '@' }).ToLowerInvariant(); // Ensure valid username @@ -131,6 +136,8 @@ namespace BirdsiteLive.Controllers using (var reader = new StreamReader(Request.Body)) { var body = await reader.ReadToEndAsync(); + + _logger.LogTrace("User Inbox: {Body}", body); //System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body); var activity = ApDeserializer.ProcessActivity(body); diff --git a/src/Tests/BSLManager.Tests/BSLManager.Tests.csproj b/src/Tests/BSLManager.Tests/BSLManager.Tests.csproj new file mode 100644 index 0000000..033cfe1 --- /dev/null +++ b/src/Tests/BSLManager.Tests/BSLManager.Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + diff --git a/src/Tests/BSLManager.Tests/Domain/FollowersListStateTests.cs b/src/Tests/BSLManager.Tests/Domain/FollowersListStateTests.cs new file mode 100644 index 0000000..a0171a4 --- /dev/null +++ b/src/Tests/BSLManager.Tests/Domain/FollowersListStateTests.cs @@ -0,0 +1,307 @@ +using System.Collections.Generic; +using System.Linq; +using BirdsiteLive.DAL.Models; +using BSLManager.Domain; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BSLManager.Tests +{ + [TestClass] + public class FollowersListStateTests + { + [TestMethod] + public void FilterBy() + { + #region Stub + var followers = new List + { + new Follower + { + Id = 0, + Acct = "test", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 1, + Acct = "test", + Host = "host2", + Followings = new List() + }, + new Follower + { + Id = 2, + Acct = "user1", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 3, + Acct = "user2", + Host = "host1", + Followings = new List() + } + }; + #endregion + + var state = new FollowersListState(); + state.Load(followers); + + state.FilterBy("test"); + + #region Validate + Assert.AreEqual(2, state.GetDisplayableList().Count); + #endregion + } + + [TestMethod] + public void FilterBy_GetElement() + { + #region Stub + var followers = new List + { + new Follower + { + Id = 0, + Acct = "test", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 1, + Acct = "test", + Host = "host2", + Followings = new List() + }, + new Follower + { + Id = 2, + Acct = "user1", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 3, + Acct = "user2", + Host = "host1", + Followings = new List() + } + }; + #endregion + + var state = new FollowersListState(); + state.Load(followers); + + state.FilterBy("test"); + var el = state.GetElementAt(1); + + #region Validate + Assert.AreEqual(followers[1].Id, el.Id); + #endregion + } + + [TestMethod] + public void GetElement() + { + #region Stub + var followers = new List + { + new Follower + { + Id = 0, + Acct = "test", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 1, + Acct = "test", + Host = "host2", + Followings = new List() + }, + new Follower + { + Id = 2, + Acct = "user1", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 3, + Acct = "user2", + Host = "host1", + Followings = new List() + } + }; + #endregion + + var state = new FollowersListState(); + state.Load(followers); + + var el = state.GetElementAt(2); + + #region Validate + Assert.AreEqual(followers[2].Id, el.Id); + #endregion + } + + [TestMethod] + public void FilterBy_RemoveAt() + { + #region Stub + var followers = new List + { + new Follower + { + Id = 0, + Acct = "test", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 1, + Acct = "test", + Host = "host2", + Followings = new List() + }, + new Follower + { + Id = 2, + Acct = "user1", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 3, + Acct = "user2", + Host = "host1", + Followings = new List() + } + }; + #endregion + + var state = new FollowersListState(); + state.Load(followers.ToList()); + + state.FilterBy("test"); + state.RemoveAt(1); + + var list = state.GetDisplayableList(); + + #region Validate + Assert.AreEqual(1, list.Count); + Assert.IsTrue(list[0].Contains("@test@host1")); + #endregion + } + + [TestMethod] + public void RemoveAt() + { + #region Stub + var followers = new List + { + new Follower + { + Id = 0, + Acct = "test", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 1, + Acct = "test", + Host = "host2", + Followings = new List() + }, + new Follower + { + Id = 2, + Acct = "user1", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 3, + Acct = "user2", + Host = "host1", + Followings = new List() + } + }; + #endregion + + var state = new FollowersListState(); + state.Load(followers.ToList()); + + state.RemoveAt(1); + + var list = state.GetDisplayableList(); + + #region Validate + Assert.AreEqual(3, list.Count); + Assert.IsTrue(list[0].Contains("@test@host1")); + Assert.IsFalse(list[1].Contains("@test@host2")); + #endregion + } + + [TestMethod] + public void FilterBy_ResetFilter() + { + #region Stub + var followers = new List + { + new Follower + { + Id = 0, + Acct = "test", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 1, + Acct = "test", + Host = "host2", + Followings = new List() + }, + new Follower + { + Id = 2, + Acct = "user1", + Host = "host1", + Followings = new List() + }, + new Follower + { + Id = 3, + Acct = "user2", + Host = "host1", + Followings = new List() + } + }; + #endregion + + var state = new FollowersListState(); + state.Load(followers.ToList()); + + #region Validate + state.FilterBy("data"); + var list = state.GetDisplayableList(); + Assert.AreEqual(0, list.Count); + + state.FilterBy(string.Empty); + list = state.GetDisplayableList(); + Assert.AreEqual(4, list.Count); + #endregion + } + } +}