From 3a35a35caf8cd92a38f3ece14411886ea04d0e3b Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Sat, 7 May 2022 13:24:43 -0700 Subject: [PATCH] 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 --- board/forms.py | 37 ++++++++++++++++++++++ board/models.py | 17 ++++++++++ board/views.py | 82 +++++++++++++++++++++++++++---------------------- 3 files changed, 99 insertions(+), 37 deletions(-) create mode 100644 board/forms.py diff --git a/board/forms.py b/board/forms.py new file mode 100644 index 0000000..1dc56b4 --- /dev/null +++ b/board/forms.py @@ -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 diff --git a/board/models.py b/board/models.py index d574de7..25b6b73 100644 --- a/board/models.py +++ b/board/models.py @@ -1,3 +1,4 @@ +from datetime import timedelta import os from pathlib import Path from django.db import models @@ -43,6 +44,9 @@ class Board(models.Model): max_pages = models.IntegerField(default=10) # Threads per page 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 def threads(self): @@ -139,11 +143,24 @@ class Post(models.Model): ) def clean(self): + # Image upload size check if self.image and self.image.size > settings.MAX_UPLOAD_SIZE: raise ValidationError( "Image supplied is too large. Maximum image size is %(max)s", 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) diff --git a/board/views.py b/board/views.py index 8e22761..3dcf4b7 100644 --- a/board/views.py +++ b/board/views.py @@ -4,6 +4,7 @@ from django.shortcuts import render, get_object_or_404 from django.views.generic import DetailView from django.views.generic.edit import CreateView from board.models import Post, Board +from board.forms import PostForm, ReplyForm __all__ = ("BoardView", "PostView") @@ -18,70 +19,77 @@ def get_client_ip(request): 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 - fields = ["subject", "name", "text", "image"] + form_class = PostForm + # fields = ["subject", "name", "text", "image"] slug_field = "url" slug_url_kwarg = "url" template_name = "board/board_detail.html" def get_context_data(self, **kwargs): - board_url = self.kwargs["url"] - board = get_object_or_404(Board, url=board_url) - kwargs["board"] = board + kwargs["board"] = self.board 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() - start = (page - 1) * board.threads_per_page - end = start + board.threads_per_page - kwargs["threads"] = board.threads.order_by("-last_bump")[start:end] + start = (page - 1) * self.board.threads_per_page + end = start + self.board.threads_per_page + kwargs["threads"] = self.board.threads.order_by("-last_bump")[start:end] kwargs["page"] = page kwargs["max_upload_size"] = settings.MAX_UPLOAD_SIZE - 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 - form.instance.ip = get_client_ip(self.request) - - self.object = form.save() - return HttpResponseRedirect(self.get_success_url()) - - -class PostView(CreateView): +class PostView(CreatePostView): model = Post - fields = ["name", "text", "image"] + # fields = ["name", "text", "image"] + form_class = ReplyForm slug_field = "url" slug_url_kwarg = "url" template_name = "board/post_detail.html" def get_context_data(self, **kwargs): - board_url = self.kwargs["url"] - kwargs["board"] = get_object_or_404(Board, url=board_url) + kwargs["board"] = self.board post_id = self.kwargs["id"] kwargs["post"] = get_object_or_404(Post, id=post_id) kwargs["max_upload_size"] = settings.MAX_UPLOAD_SIZE return super(PostView, self).get_context_data(**kwargs) - def form_valid(self, form): - board_url = self.kwargs["url"] - board = get_object_or_404(Board, url=board_url) - + def get_form_kwargs(self): + kwargs = super(PostView, self).get_form_kwargs() post_id = self.kwargs["id"] post = get_object_or_404(Post, id=post_id) - - form.instance.board = board - form.instance.ip = get_client_ip(self.request) - form.instance.op = post - form.instance.reply = post - - self.object = form.save() - - return HttpResponseRedirect(post.get_absolute_url()) + kwargs["op"] = post + kwargs["reply"] = post + return kwargs