Merge pull request #928 from mouse-reeve/blocklist

Allow admins to upload domain blocklists
This commit is contained in:
Mouse Reeve 2021-04-12 15:23:51 -07:00 committed by GitHub
commit 20cb173150
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 175 additions and 12 deletions

View file

@ -32,27 +32,27 @@ Federation makes it possible to have small, self-determining communities, in con
### Features ### Features
Since the project is still in its early stages, the features are growing every day, and there is plenty of room for suggestions and ideas. Open an [issue](https://github.com/mouse-reeve/bookwyrm/issues) to get the conversation going! Since the project is still in its early stages, the features are growing every day, and there is plenty of room for suggestions and ideas. Open an [issue](https://github.com/mouse-reeve/bookwyrm/issues) to get the conversation going!
- Posting about books - Posting about books
- Compose reviews, with or without ratings, which are aggregated in the book page - Compose reviews, with or without ratings, which are aggregated in the book page
- Compose other kinds of statuses about books, such as: - Compose other kinds of statuses about books, such as:
- Comments on a book - Comments on a book
- Quotes or excerpts - Quotes or excerpts
- Reply to statuses - Reply to statuses
- View aggregate reviews of a book across connected BookWyrm instances - View aggregate reviews of a book across connected BookWyrm instances
- Differentiate local and federated reviews and rating in your activity feed - Differentiate local and federated reviews and rating in your activity feed
- Track reading activity - Track reading activity
- Shelve books on default "to-read," "currently reading," and "read" shelves - Shelve books on default "to-read," "currently reading," and "read" shelves
- Create custom shelves - Create custom shelves
- Store started reading/finished reading dates, as well as progress updates along the way - Store started reading/finished reading dates, as well as progress updates along the way
- Update followers about reading activity (optionally, and with granular privacy controls) - Update followers about reading activity (optionally, and with granular privacy controls)
- Create lists of books which can be open to submissions from anyone, curated, or only edited by the creator - Create lists of books which can be open to submissions from anyone, curated, or only edited by the creator
- Federation with ActivityPub - Federation with ActivityPub
- Broadcast and receive user statuses and activity - Broadcast and receive user statuses and activity
- Share book data between instances to create a networked database of metadata - Share book data between instances to create a networked database of metadata
- Identify shared books across instances and aggregate related content - Identify shared books across instances and aggregate related content
- Follow and interact with users across BookWyrm instances - Follow and interact with users across BookWyrm instances
- Inter-operate with non-BookWyrm ActivityPub services (currently, Mastodon is supported) - Inter-operate with non-BookWyrm ActivityPub services (currently, Mastodon is supported)
- Granular privacy controls - Granular privacy controls
- Private, followers-only, and public privacy levels for posting, shelves, and lists - Private, followers-only, and public privacy levels for posting, shelves, and lists
- Option for users to manually approve followers - Option for users to manually approve followers
- Allow blocking and flagging for moderation - Allow blocking and flagging for moderation

View file

@ -26,7 +26,7 @@
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="id_csv_field">{% trans "Data file:" %}</label> <label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
{{ import_form.csv_file }} {{ import_form.csv_file }}
</div> </div>
</div> </div>

View file

@ -9,6 +9,19 @@
{% block panel %} {% block panel %}
<div class="tabs">
<ul>
{% url 'settings-import-blocklist' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Import block list" %}</a>
</li>
{% url 'settings-add-federated-server' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Add server" %}</a>
</li>
</ul>
</div>
<form method="POST" action="{% url 'settings-add-federated-server' %}"> <form method="POST" action="{% url 'settings-add-federated-server' %}">
{% csrf_token %} {% csrf_token %}
<div class="columns"> <div class="columns">
@ -24,8 +37,8 @@
<label class="label" for="id_status">{% trans "Status:" %}</label> <label class="label" for="id_status">{% trans "Status:" %}</label>
<div class="select"> <div class="select">
<select name="status" class="" id="id_status"> <select name="status" class="" id="id_status">
<option value="federated" {% if form.status.value == "federated" or not form.status.value %}selected=""{% endif %}>{% trans "Federated" %}</option> <option value="federated" {% if form.status.value == "federated" %}selected=""{% endif %}>{% trans "Federated" %}</option>
<option value="blocked" {% if form.status.value == "blocked" %}selected{% endif %}>{% trans "Blocked" %}</option> <option value="blocked" {% if form.status.value == "blocked" or not form.status.value %}selected{% endif %}>{% trans "Blocked" %}</option>
</select> </select>
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/admin_layout.html' %}
{% block title %}{{ server.server_name }}{% endblock %} {% block title %}{{ server.server_name }}{% endblock %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% block header %} {% block header %}
{{ server.server_name }} {{ server.server_name }}
@ -81,7 +82,7 @@
</div> </div>
</header> </header>
{% if server.notes %} {% if server.notes %}
<p id="hide-edit-notes">{{ server.notes }}</p> <p id="hide-edit-notes">{{ server.notes|to_markdown|safe }}</p>
{% endif %} {% endif %}
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes"> <form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes">
{% csrf_token %} {% csrf_token %}

View file

@ -5,7 +5,7 @@
{% block header %}{% trans "Federated Servers" %}{% endblock %} {% block header %}{% trans "Federated Servers" %}{% endblock %}
{% block edit-button %} {% block edit-button %}
<a href="{% url 'settings-add-federated-server' %}"> <a href="{% url 'settings-import-blocklist' %}">
<span class="icon icon-plus" title="{% trans 'Add server' %}"> <span class="icon icon-plus" title="{% trans 'Add server' %}">
<span class="is-sr-only">{% trans "Add server" %}</span> <span class="is-sr-only">{% trans "Add server" %}</span>
</span> </span>

View file

@ -0,0 +1,67 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}
{% block title %}{% trans "Add server" %}{% endblock %}
{% block header %}
{% trans "Import Blocklist" %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to server list" %}</a>
{% endblock %}
{% block panel %}
<div class="tabs">
<ul>
{% url 'settings-import-blocklist' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Import block list" %}</a>
</li>
{% url 'settings-add-federated-server' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Add server" %}</a>
</li>
</ul>
</div>
{% if succeeded and not failed %}
<p class="notification is-primary">{% trans "Success!" %}</p>
{% elif succeeded or failed %}
<div class="block content">
{% if succeeded %}
<p>{% trans "Successfully blocked:" %} {{ succeeded }}</p>
{% endif %}
<p>{% trans "Failed:" %}</p>
<ul>
{% for item in failed %}
<li>
<pre>
{{ item }}
</pre>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="POST" action="{% url 'settings-import-blocklist' %}" enctype="multipart/form-data">
{% csrf_token %}
<div class="field">
<label class="label" for="id_file">JSON data:</label>
<aside class="help">
Expects a json file in the format provided by <a href="https://fediblock.org/" target="_blank" rel=”noopener”>FediBlock</a>, with a list of entries that have <code>instance</code> and <code>url</code> fields. For example:
<pre>
[
{
"instance": "example.server.com",
"url": "https://link.to.more/info"
},
...
]
</pre>
</aside>
<input type="file" name="json_file" required="" id="id_file">
</div>
<button type="submit" class="button is-primary">{% trans "Import" %}</button>
</form>
{% endblock %}

View file

@ -1,5 +1,7 @@
""" test for app action functionality """ """ test for app action functionality """
import json
from unittest.mock import patch from unittest.mock import patch
from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -127,3 +129,39 @@ class FederationViews(TestCase):
self.assertEqual(server.server_name, "remote.server") self.assertEqual(server.server_name, "remote.server")
self.assertEqual(server.application_type, "coolsoft") self.assertEqual(server.application_type, "coolsoft")
self.assertEqual(server.status, "blocked") 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")

View file

@ -83,6 +83,11 @@ urlpatterns = [
views.AddFederatedServer.as_view(), views.AddFederatedServer.as_view(),
name="settings-add-federated-server", name="settings-add-federated-server",
), ),
re_path(
r"^settings/federation/import/?$",
views.ImportServerBlocklist.as_view(),
name="settings-import-blocklist",
),
re_path( re_path(
r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites" r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites"
), ),

View file

@ -5,7 +5,8 @@ from .block import Block, unblock
from .books import Book, EditBook, ConfirmEditBook, Editions from .books import Book, EditBook, ConfirmEditBook, Editions
from .books import upload_cover, add_description, switch_edition, resolve_book from .books import upload_cover, add_description, switch_edition, resolve_book
from .directory import Directory 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 .federation import block_server, unblock_server
from .feed import DirectMessage, Feed, Replies, Status from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow from .follow import follow, unfollow

View file

@ -1,6 +1,8 @@
""" manage federated servers """ """ manage federated servers """
import json
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -63,6 +65,42 @@ class AddFederatedServer(View):
return redirect("settings-federated-server", server.id) 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(login_required, name="dispatch")
@method_decorator( @method_decorator(
permission_required("bookwyrm.control_federation", raise_exception=True), permission_required("bookwyrm.control_federation", raise_exception=True),