Repository: bookwyrm-social/bookwyrm

Issue:
Instance blocklists
**Is your feature request related to a problem? Please describe.**
As an admin, I'd like to specify instances that my instance won't federate with.

**Describe the solution you'd like**
The instance should not show any remote content of those instances and, as far as it's technically possible with AP, deny those instances access to users, posts, etc.

**Describe alternatives you've considered**
Possibly this can be done via nginx config? Idk.

**Additional context**

 It would be nice to have a user interface similar to the Mastodon one where one can adds notes on why instances are suspended. 
 
![image](https://user-images.githubusercontent.com/66644064/112718955-86d2e800-8ef6-11eb-956c-9ecee2771529.png)
![image](https://user-images.githubusercontent.com/66644064/112718985-ac5ff180-8ef6-11eb-83f0-1ba06c599a11.png)

Additionally, it would be nice to have a possibility to ex- and import blocklists, maybe in this format: https://fediblock.org/#api


Discussion:
**Workaround**: Mastodon, Pleroma and Friendica send the URL of the instance as part of their User-Agent when they make requests. For example:

|  Software | User-Agent                                                     |
|----------:|:---------------------------------------------------------------|
|  Mastodon | http.rb/4.4.1 (Mastodon/3.3.0; +https://example.com/)          |
| Friendica | Friendica 'Red Hot Poker' 2021.03-rc-1412; https://example.com |
|   Pleroma | Pleroma 2.3.0; https://example.com <email@example.com>         |

You could make a file like this:

```nginx
map $http_user_agent $blockedagent {
    default 0;
    ~*example\.com 1;
}
```

include it in your nginx config (`include myfile;`) and then write this into your `server`-block:

```nginx
if ($blockedagent) {
    return 403;
}
```

Resolving patch:
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -83,6 +83,11 @@
         views.AddFederatedServer.as_view(),
         name="settings-add-federated-server",
     ),
+    re_path(
+        r"^settings/federation/import/?$",
+        views.ImportServerBlocklist.as_view(),
+        name="settings-import-blocklist",
+    ),
     re_path(
         r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites"
     ),
diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py
--- a/bookwyrm/views/__init__.py
+++ b/bookwyrm/views/__init__.py
@@ -5,7 +5,8 @@
 from .books import Book, EditBook, ConfirmEditBook, Editions
 from .books import upload_cover, add_description, switch_edition, resolve_book
 from .directory import Directory
-from .federation import Federation, FederatedServer, AddFederatedServer
+from .federation import Federation, FederatedServer
+from .federation import AddFederatedServer, ImportServerBlocklist
 from .federation import block_server, unblock_server
 from .feed import DirectMessage, Feed, Replies, Status
 from .follow import follow, unfollow
diff --git a/bookwyrm/views/federation.py b/bookwyrm/views/federation.py
--- a/bookwyrm/views/federation.py
+++ b/bookwyrm/views/federation.py
@@ -1,6 +1,8 @@
 """ manage federated servers """
+import json
 from django.contrib.auth.decorators import login_required, permission_required
 from django.core.paginator import Paginator
+from django.db import transaction
 from django.shortcuts import get_object_or_404, redirect
 from django.template.response import TemplateResponse
 from django.utils.decorators import method_decorator
@@ -63,6 +65,42 @@ def post(self, request):
         return redirect("settings-federated-server", server.id)
 
 
+@method_decorator(login_required, name="dispatch")
+@method_decorator(
+    permission_required("bookwyrm.control_federation", raise_exception=True),
+    name="dispatch",
+)
+class ImportServerBlocklist(View):
+    """ manually add a server """
+
+    def get(self, request):
+        """ add server form """
+        return TemplateResponse(request, "settings/server_blocklist.html")
+
+    def post(self, request):
+        """ add a server from the admin panel """
+        json_data = json.load(request.FILES["json_file"])
+        failed = []
+        success_count = 0
+        for item in json_data:
+            server_name = item.get("instance")
+            if not server_name:
+                failed.append(item)
+                continue
+            info_link = item.get("url")
+
+            with transaction.atomic():
+                server, _ = models.FederatedServer.objects.get_or_create(
+                    server_name=server_name,
+                )
+                server.notes = info_link
+                server.save()
+                server.block()
+            success_count += 1
+        data = {"failed": failed, "succeeded": success_count}
+        return TemplateResponse(request, "settings/server_blocklist.html", data)
+
+
 @method_decorator(login_required, name="dispatch")
 @method_decorator(
     permission_required("bookwyrm.control_federation", raise_exception=True),


Test patch:
diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py
--- a/bookwyrm/tests/views/test_federation.py
+++ b/bookwyrm/tests/views/test_federation.py
@@ -1,5 +1,7 @@
 """ test for app action functionality """
+import json
 from unittest.mock import patch
+from django.core.files.uploadedfile import SimpleUploadedFile
 from django.template.response import TemplateResponse
 from django.test import TestCase
 from django.test.client import RequestFactory
@@ -127,3 +129,39 @@ def test_add_view_post_create(self):
         self.assertEqual(server.server_name, "remote.server")
         self.assertEqual(server.application_type, "coolsoft")
         self.assertEqual(server.status, "blocked")
+
+    def test_import_blocklist(self):
+        """ load a json file with a list of servers to block """
+        server = models.FederatedServer.objects.create(server_name="hi.there.com")
+        self.remote_user.federated_server = server
+        self.remote_user.save()
+
+        data = [
+            {"instance": "server.name", "url": "https://explanation.url"},  # new server
+            {"instance": "hi.there.com", "url": "https://explanation.url"},  # existing
+            {"a": "b"},  # invalid
+        ]
+        json.dump(data, open("file.json", "w"))
+
+        view = views.ImportServerBlocklist.as_view()
+        request = self.factory.post(
+            "",
+            {
+                "json_file": SimpleUploadedFile(
+                    "file.json", open("file.json", "rb").read()
+                )
+            },
+        )
+        request.user = self.local_user
+        request.user.is_superuser = True
+
+        view(request)
+        server.refresh_from_db()
+        self.remote_user.refresh_from_db()
+
+        self.assertEqual(models.FederatedServer.objects.count(), 2)
+        self.assertEqual(server.status, "blocked")
+        self.assertFalse(self.remote_user.is_active)
+        created = models.FederatedServer.objects.get(server_name="server.name")
+        self.assertEqual(created.status, "blocked")
+        self.assertEqual(created.notes, "https://explanation.url")