diff --git a/board/admin.py b/board/admin.py
index 691ee72..9207029 100644
--- a/board/admin.py
+++ b/board/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin
from django.utils.safestring import mark_safe
-from board.models import Board, Post, ReportReason, ReportRecord
+from board.models import Ban, Board, Post, RangeBan, ReportReason, ReportRecord
@admin.register(Board)
@@ -45,3 +45,13 @@ class ReportRecordAdmin(admin.ModelAdmin):
html += f"
{obj.post.text}
"
html += ""
return mark_safe(html)
+
+
+@admin.register(RangeBan)
+class RangeBanAdmin(admin.ModelAdmin):
+ pass
+
+
+@admin.register(Ban)
+class BanAdmin(admin.ModelAdmin):
+ pass
diff --git a/board/models.py b/board/models.py
index 1666895..5b09283 100644
--- a/board/models.py
+++ b/board/models.py
@@ -264,32 +264,50 @@ def report_created(sender, instance, created, **kwargs):
# board =
-class RangeBan(models.Model):
- # Starting IP address of the ban
- start = models.GenericIPAddressField()
- # Ending IP address of the ban
- end = models.GenericIPAddressField()
- # The reason for this ban
- ban_reason = models.TextField(blank=False)
- # The time that this ban was created.
- created = models.DateTimeField(auto_now_add=True)
- # Expiration date of this ban. If it is null, it is permanent.
- expires = models.DateTimeField(null=True)
-
-
-class Ban(models.Model):
- # IP address of the ban
- ip = models.GenericIPAddressField()
+class BanCommon(models.Model):
# The reason for this ban
ban_reason = models.TextField(blank=False)
# Board that this ban is for. If null, then all boards.
# Even though this is nullable, we just want to delete all reports for a
# board if that board is deleted.
- board = models.ForeignKey("Board", on_delete=models.CASCADE, null=True)
+ board = models.ForeignKey("Board", on_delete=models.CASCADE, null=True, blank=True)
# The time that this ban was created.
created = models.DateTimeField(auto_now_add=True)
# Expiration date of this ban. If it is null, it is permanent.
- expires = models.DateTimeField(null=True)
+ expires = models.DateTimeField(null=True, blank=True)
+
+ class Meta:
+ abstract = True
+
+
+class RangeBan(BanCommon):
+ # Starting IP address of the ban
+ start = models.GenericIPAddressField()
+ # Ending IP address of the ban
+ end = models.GenericIPAddressField()
+
+ def clean(self):
+ import ipaddress
+
+ start = ipaddress.ip_address(self.start)
+ end = ipaddress.ip_address(self.end)
+ if start.version != end.version:
+ raise ValidationError(
+ _("Start and end IP address must be the same protocol (IPv4 or IPv6)")
+ )
+ if start >= end:
+ raise ValidationError(_("Start IP must be lower than end IP"))
+
+ def __str__(self):
+ return f"Range ban for {self.start}-{self.end}"
+
+
+class Ban(BanCommon):
+ # IP address of the ban
+ ip = models.GenericIPAddressField(unique=True)
+
+ def __str__(self):
+ return f"Ban for {self.ip}"
class BanTemplate(models.Model):
diff --git a/board/templates/board/banned.html b/board/templates/board/banned.html
new file mode 100644
index 0000000..c8dd723
--- /dev/null
+++ b/board/templates/board/banned.html
@@ -0,0 +1,81 @@
+{% extends "board/base.html" %}
+
+{% block title %}
+ {% if bans %}
+ {% with title="Banned" %}
+ {{ block.super }}
+ {% endwith %}
+ {% else %}
+ {% with title="Not banned" %}
+ {{ block.super }}
+ {% endwith %}
+ {% endif %}
+{% endblock title %}
+
+{% block extrastyle %}
+{{block.super}}
+
+{% endblock extrastyle %}
+
+{% block content %}
+
+
+
+ {% if bans %}
+
+
You are banned.
+
+
+
+ You have been banned, and you are not allowed to post until your
+ bans have expired.
+
+ {% if ban.expires %}
+ This ban will expire on {{ban.expires|date:"M dS"}} at {{ban.expires|date:"P e"}}
+ {% else %}
+ This ban will not expire.
+ {% endif %}
+
+
+
+ {% endfor %}
+ {% else %}
+
+
You are not banned.
+
+
+
You have no bans recorded for your IP address.
+
+ {% endif %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/board/templates/board/base.html b/board/templates/board/base.html
index c16db4d..74064ce 100644
--- a/board/templates/board/base.html
+++ b/board/templates/board/base.html
@@ -7,9 +7,11 @@
{% block title %}{% if title %}{{title}}{% else %}Index{% endif %}{% endblock %}
+ {% block extrastyle %}{% endblock %}
+ {% block extrajs %}{% endblock %}
diff --git a/board/urls.py b/board/urls.py
index 5085fd4..c584be4 100644
--- a/board/urls.py
+++ b/board/urls.py
@@ -16,6 +16,11 @@ urlpatterns = [
TemplateView.as_view(template_name="board/report_success.html"),
name="report_success",
),
+ path(
+ "banned",
+ BannedView.as_view(),
+ name="banned",
+ ),
]
# TODO - make this conditional so we can serve images up with whatever server we want
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/board/utils.py b/board/utils.py
new file mode 100644
index 0000000..44cd221
--- /dev/null
+++ b/board/utils.py
@@ -0,0 +1,26 @@
+from board.models import Ban, RangeBan
+import ipaddress
+
+
+def get_client_ip(request):
+ "Get the IP address of a client-side request. Shamelessly copy/pasted from StackOverflow."
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(",")[0]
+ else:
+ ip = request.META.get("REMOTE_ADDR")
+ return ip
+
+
+def get_ip_bans(ip: str) -> list:
+ bans = list(Ban.objects.filter(ip=ip))
+
+ ip_addr = ipaddress.ip_address(ip)
+ for rangeban in RangeBan.objects.all():
+ start = ipaddress.ip_address(rangeban.start)
+ end = ipaddress.ip_address(rangeban.end)
+ if ip_addr.version != start.version or ip_addr.version != end.version:
+ continue
+ if start <= ip_addr <= end: # type: ignore
+ bans += [rangeban]
+ return bans
diff --git a/board/views.py b/board/views.py
index a42b856..3ecaab7 100644
--- a/board/views.py
+++ b/board/views.py
@@ -1,23 +1,27 @@
from django.conf import settings
from django.http import Http404, HttpResponseRedirect
-from django.shortcuts import render, get_object_or_404
+from django.shortcuts import get_object_or_404
from django.views.generic import DetailView
+from django.views.generic.base import TemplateView
from django.views.generic.edit import CreateView
from django.urls import reverse, reverse_lazy
-from board.models import Post, Board, Report, ReportRecord
from board.forms import PostForm, ReplyForm, ReportForm
+from board.models import Ban, Post, Board, Report
+from board.utils import get_client_ip, get_ip_bans
-__all__ = ("BoardView", "PostView", "ReportView")
+__all__ = ("BannedView", "BoardView", "PostView", "ReportView")
-def get_client_ip(request):
- "Get the IP address of a client-side request. Shamelessly copy/pasted from StackOverflow."
- x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
- if x_forwarded_for:
- ip = x_forwarded_for.split(",")[0]
- else:
- ip = request.META.get("REMOTE_ADDR")
- return ip
+class BannedView(TemplateView):
+ template_name = "board/banned.html"
+
+ def get_context_data(self, **kwargs):
+ context = super(TemplateView, self).get_context_data(**kwargs)
+ ip = get_client_ip(self.request)
+ bans = get_ip_bans(ip)
+ context["bans"] = bans
+ context["ip"] = ip
+ return context
class CreatePostView(CreateView):
@@ -46,6 +50,20 @@ class CreatePostView(CreateView):
kwargs["ip"] = get_client_ip(self.request)
return kwargs
+ def dispatch(self, request, *args, **kwargs):
+ self._set_board(kwargs["url"])
+ if request.method == "POST":
+ ip = get_client_ip(request)
+ bans = [
+ ban
+ for ban in get_ip_bans(ip)
+ if ban.board == self.board or not ban.board
+ ]
+ if bans:
+ return HttpResponseRedirect(reverse("board:banned"))
+
+ return super(CreatePostView, self).dispatch(request, *args, **kwargs)
+
class BoardView(CreatePostView):
model = Post