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:
37
board/forms.py
Normal file
37
board/forms.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
|
||||||
|
|||||||
Reference in New Issue
Block a user