Add posting cooldown

If a user tries to post more than once in a certain amount of time, they
will be blocked from doing so until their cooldown is over. This
required a little bit of hacking to get the board and IP address set
*before* the validation checks were made, but it all appears to work.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2022-05-07 13:24:43 -07:00
parent 771dba1515
commit 3a35a35caf
3 changed files with 99 additions and 37 deletions

37
board/forms.py Normal file
View File

@@ -0,0 +1,37 @@
from django.forms import ModelForm
from board.models import Post
class PostForm(ModelForm):
"""
A form used for new threads for posts.
This requires the board and the IP address to be specified.
"""
class Meta:
model = Post
fields = ["subject", "name", "text", "image"]
def __init__(self, *args, board, ip, **kwargs):
super(PostForm, self).__init__(*args, **kwargs)
self.instance.board = board
self.instance.ip = ip
class ReplyForm(PostForm):
"""
A form used for replies to posts.
This requires the OP post, the reply post, the board, and the IP address be
specified.
"""
class Meta:
model = Post
fields = ["name", "text", "image"]
def __init__(self, *args, op, reply, **kwargs):
super(ReplyForm, self).__init__(*args, **kwargs)
self.instance.op = op
self.instance.reply = reply

View File

@@ -1,3 +1,4 @@
from datetime import timedelta
import os import os
from pathlib import Path from pathlib import Path
from django.db import models from django.db import models
@@ -43,6 +44,9 @@ class Board(models.Model):
max_pages = models.IntegerField(default=10) max_pages = models.IntegerField(default=10)
# Threads per page # Threads per page
threads_per_page = models.IntegerField(default=10) threads_per_page = models.IntegerField(default=10)
# The amount of time that users from the same IP address are allowed to make
# consecutive posts
post_cooldown = models.DurationField(default=timedelta(seconds=60))
@property @property
def threads(self): def threads(self):
@@ -139,11 +143,24 @@ class Post(models.Model):
) )
def clean(self): def clean(self):
# Image upload size check
if self.image and self.image.size > settings.MAX_UPLOAD_SIZE: if self.image and self.image.size > settings.MAX_UPLOAD_SIZE:
raise ValidationError( raise ValidationError(
"Image supplied is too large. Maximum image size is %(max)s", "Image supplied is too large. Maximum image size is %(max)s",
params={"max": settings.MAX_UPLOAD_SIZE}, params={"max": settings.MAX_UPLOAD_SIZE},
) )
# Rate limiting for posts
# BUG: if a user's last post is deleted, and it is their only post, they
# will not hit rate limit. This could probably be abused
last_post = self.board.post_set.filter(ip=self.ip).order_by("-created").first()
if last_post:
now = timezone.now()
delta = now - last_post.created
if delta < self.board.post_cooldown:
cooldown = self.board.post_cooldown - delta
raise ValidationError(
f"Please wait {int(cooldown.total_seconds())} seconds before posting again"
)
@receiver(signals.post_save, sender=Post) @receiver(signals.post_save, sender=Post)

View File

@@ -4,6 +4,7 @@ from django.shortcuts import render, get_object_or_404
from django.views.generic import DetailView from django.views.generic import DetailView
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from board.models import Post, Board from board.models import Post, Board
from board.forms import PostForm, ReplyForm
__all__ = ("BoardView", "PostView") __all__ = ("BoardView", "PostView")
@@ -18,70 +19,77 @@ def get_client_ip(request):
return ip return ip
class BoardView(CreateView): class CreatePostView(CreateView):
"""
Helper class that sets a few variables for posts. This should not be used by itself.
This class sets the following variables on GET and POST:
* self.board
"""
def get(self, request, *args, **kwargs):
self._set_board(kwargs["url"])
return super(CreatePostView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self._set_board(kwargs["url"])
return super(CreatePostView, self).post(request, *args, **kwargs)
def _set_board(self, board_url: str):
board = get_object_or_404(Board, url=board_url)
self.board = board
def get_form_kwargs(self):
kwargs = super(CreatePostView, self).get_form_kwargs()
kwargs["board"] = self.board
kwargs["ip"] = get_client_ip(self.request)
return kwargs
class BoardView(CreatePostView):
model = Post model = Post
fields = ["subject", "name", "text", "image"] form_class = PostForm
# fields = ["subject", "name", "text", "image"]
slug_field = "url" slug_field = "url"
slug_url_kwarg = "url" slug_url_kwarg = "url"
template_name = "board/board_detail.html" template_name = "board/board_detail.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
board_url = self.kwargs["url"] kwargs["board"] = self.board
board = get_object_or_404(Board, url=board_url)
kwargs["board"] = board
page = self.kwargs.get("page", 1) page = self.kwargs.get("page", 1)
if page not in range(1, board.max_pages + 1): if page not in range(1, self.board.max_pages + 1):
raise Http404() raise Http404()
start = (page - 1) * board.threads_per_page start = (page - 1) * self.board.threads_per_page
end = start + board.threads_per_page end = start + self.board.threads_per_page
kwargs["threads"] = board.threads.order_by("-last_bump")[start:end] kwargs["threads"] = self.board.threads.order_by("-last_bump")[start:end]
kwargs["page"] = page kwargs["page"] = page
kwargs["max_upload_size"] = settings.MAX_UPLOAD_SIZE kwargs["max_upload_size"] = settings.MAX_UPLOAD_SIZE
return super(BoardView, self).get_context_data(**kwargs) return super(BoardView, self).get_context_data(**kwargs)
def form_valid(self, form):
board_url = self.kwargs["url"]
board = get_object_or_404(Board, url=board_url)
form.instance.board = board class PostView(CreatePostView):
form.instance.ip = get_client_ip(self.request)
self.object = form.save()
return HttpResponseRedirect(self.get_success_url())
class PostView(CreateView):
model = Post model = Post
fields = ["name", "text", "image"] # fields = ["name", "text", "image"]
form_class = ReplyForm
slug_field = "url" slug_field = "url"
slug_url_kwarg = "url" slug_url_kwarg = "url"
template_name = "board/post_detail.html" template_name = "board/post_detail.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
board_url = self.kwargs["url"] kwargs["board"] = self.board
kwargs["board"] = get_object_or_404(Board, url=board_url)
post_id = self.kwargs["id"] post_id = self.kwargs["id"]
kwargs["post"] = get_object_or_404(Post, id=post_id) kwargs["post"] = get_object_or_404(Post, id=post_id)
kwargs["max_upload_size"] = settings.MAX_UPLOAD_SIZE kwargs["max_upload_size"] = settings.MAX_UPLOAD_SIZE
return super(PostView, self).get_context_data(**kwargs) return super(PostView, self).get_context_data(**kwargs)
def form_valid(self, form): def get_form_kwargs(self):
board_url = self.kwargs["url"] kwargs = super(PostView, self).get_form_kwargs()
board = get_object_or_404(Board, url=board_url)
post_id = self.kwargs["id"] post_id = self.kwargs["id"]
post = get_object_or_404(Post, id=post_id) post = get_object_or_404(Post, id=post_id)
kwargs["op"] = post
form.instance.board = board kwargs["reply"] = post
form.instance.ip = get_client_ip(self.request) return kwargs
form.instance.op = post
form.instance.reply = post
self.object = form.save()
return HttpResponseRedirect(post.get_absolute_url())