mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-25 08:41:00 +00:00
a6922cb9d6
This removes the use of the EOL'd Bleach, and also integrates hashtag, mention and emoji searching into one single place.
282 lines
11 KiB
Python
282 lines
11 KiB
Python
from django import forms
|
|
from django.conf import settings
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
from django.utils.decorators import method_decorator
|
|
from django.views.generic import FormView
|
|
|
|
from activities.models import (
|
|
Post,
|
|
PostAttachment,
|
|
PostAttachmentStates,
|
|
PostStates,
|
|
TimelineEvent,
|
|
)
|
|
from core.files import blurhash_image, resize_image
|
|
from core.html import FediverseHtmlParser
|
|
from core.models import Config
|
|
from users.decorators import identity_required
|
|
|
|
|
|
@method_decorator(identity_required, name="dispatch")
|
|
class Compose(FormView):
|
|
|
|
template_name = "activities/compose.html"
|
|
|
|
class form_class(forms.Form):
|
|
text = forms.CharField(
|
|
widget=forms.Textarea(
|
|
attrs={
|
|
"autofocus": "autofocus",
|
|
"placeholder": "What's on your mind?",
|
|
},
|
|
)
|
|
)
|
|
visibility = forms.ChoiceField(
|
|
choices=[
|
|
(Post.Visibilities.public, "Public"),
|
|
(Post.Visibilities.local_only, "Local Only"),
|
|
(Post.Visibilities.unlisted, "Unlisted"),
|
|
(Post.Visibilities.followers, "Followers & Mentioned Only"),
|
|
(Post.Visibilities.mentioned, "Mentioned Only"),
|
|
],
|
|
)
|
|
content_warning = forms.CharField(
|
|
required=False,
|
|
label=Config.lazy_system_value("content_warning_text"),
|
|
widget=forms.TextInput(
|
|
attrs={
|
|
"placeholder": Config.lazy_system_value("content_warning_text"),
|
|
},
|
|
),
|
|
help_text="Optional - Post will be hidden behind this text until clicked",
|
|
)
|
|
reply_to = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.request = request
|
|
self.fields["text"].widget.attrs[
|
|
"_"
|
|
] = rf"""
|
|
init
|
|
-- Move cursor to the end of existing text
|
|
set my.selectionStart to my.value.length
|
|
end
|
|
|
|
on load or input
|
|
-- Unicode-aware counting to match Python
|
|
-- <LF> will be normalized as <CR><LF> in Django
|
|
set characters to Array.from(my.value.replaceAll('\n','\r\n').trim()).length
|
|
put {Config.system.post_length} - characters into #character-counter
|
|
|
|
if characters > {Config.system.post_length} then
|
|
set #character-counter's style.color to 'var(--color-text-error)'
|
|
add [@disabled=] to #post-button
|
|
else
|
|
set #character-counter's style.color to ''
|
|
remove @disabled from #post-button
|
|
end
|
|
"""
|
|
|
|
def clean_text(self):
|
|
text = self.cleaned_data.get("text")
|
|
# Check minimum interval
|
|
last_post = self.request.identity.posts.order_by("-created").first()
|
|
if (
|
|
last_post
|
|
and (timezone.now() - last_post.created).total_seconds()
|
|
< Config.system.post_minimum_interval
|
|
):
|
|
raise forms.ValidationError(
|
|
f"You must wait at least {Config.system.post_minimum_interval} seconds between posts"
|
|
)
|
|
if not text:
|
|
return text
|
|
# Check post length
|
|
length = len(text)
|
|
if length > Config.system.post_length:
|
|
raise forms.ValidationError(
|
|
f"Maximum post length is {Config.system.post_length} characters (you have {length})"
|
|
)
|
|
return text
|
|
|
|
def get_form(self, form_class=None):
|
|
return self.form_class(request=self.request, **self.get_form_kwargs())
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
if self.post_obj:
|
|
initial.update(
|
|
{
|
|
"reply_to": self.reply_to.pk if self.reply_to else "",
|
|
"visibility": self.post_obj.visibility,
|
|
"text": FediverseHtmlParser(self.post_obj.content).plain_text,
|
|
"content_warning": self.post_obj.summary,
|
|
}
|
|
)
|
|
else:
|
|
initial[
|
|
"visibility"
|
|
] = self.request.identity.config_identity.default_post_visibility
|
|
if self.reply_to:
|
|
initial["reply_to"] = self.reply_to.pk
|
|
if self.reply_to.visibility == Post.Visibilities.public:
|
|
initial["visibility"] = Post.Visibilities.unlisted
|
|
else:
|
|
initial["visibility"] = self.reply_to.visibility
|
|
initial["content_warning"] = self.reply_to.summary
|
|
# Build a set of mentions for the content to start as
|
|
mentioned = {self.reply_to.author}
|
|
mentioned.update(self.reply_to.mentions.all())
|
|
mentioned.discard(self.request.identity)
|
|
initial["text"] = "".join(
|
|
f"@{identity.handle} "
|
|
for identity in mentioned
|
|
if identity.username
|
|
)
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
# Gather any attachment objects now, they're not in the form proper
|
|
attachments = []
|
|
if "attachment" in self.request.POST:
|
|
attachments = PostAttachment.objects.filter(
|
|
pk__in=self.request.POST.getlist("attachment", [])
|
|
)
|
|
# Dispatch based on edit or not
|
|
if self.post_obj:
|
|
self.post_obj.edit_local(
|
|
content=form.cleaned_data["text"],
|
|
summary=form.cleaned_data.get("content_warning"),
|
|
visibility=form.cleaned_data["visibility"],
|
|
attachments=attachments,
|
|
)
|
|
self.post_obj.transition_perform(PostStates.edited)
|
|
else:
|
|
post = Post.create_local(
|
|
author=self.request.identity,
|
|
content=form.cleaned_data["text"],
|
|
summary=form.cleaned_data.get("content_warning"),
|
|
visibility=form.cleaned_data["visibility"],
|
|
reply_to=self.reply_to,
|
|
attachments=attachments,
|
|
)
|
|
# Add their own timeline event for immediate visibility
|
|
TimelineEvent.add_post(self.request.identity, post)
|
|
return redirect("/")
|
|
|
|
def dispatch(self, request, handle=None, post_id=None, *args, **kwargs):
|
|
self.post_obj = None
|
|
if handle and post_id:
|
|
# Make sure the request identity owns the post!
|
|
if handle != request.identity.handle:
|
|
raise PermissionDenied("Post author is not requestor")
|
|
|
|
self.post_obj = get_object_or_404(request.identity.posts, pk=post_id)
|
|
|
|
# Grab the reply-to post info now
|
|
self.reply_to = None
|
|
reply_to_id = request.POST.get("reply_to") or request.GET.get("reply_to")
|
|
if reply_to_id:
|
|
try:
|
|
self.reply_to = Post.objects.get(pk=reply_to_id)
|
|
except Post.DoesNotExist:
|
|
pass
|
|
# Keep going with normal rendering
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["reply_to"] = self.reply_to
|
|
if self.post_obj:
|
|
context["post"] = self.post_obj
|
|
return context
|
|
|
|
|
|
@method_decorator(identity_required, name="dispatch")
|
|
class ImageUpload(FormView):
|
|
"""
|
|
Handles image upload - returns a new input type hidden to embed in
|
|
the main form that references an orphaned PostAttachment
|
|
"""
|
|
|
|
template_name = "activities/_image_upload.html"
|
|
|
|
class form_class(forms.Form):
|
|
image = forms.ImageField(
|
|
widget=forms.FileInput(
|
|
attrs={
|
|
"_": f"""
|
|
on change
|
|
if me.files[0].size > {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB * 1024 ** 2}
|
|
add [@disabled=] to #upload
|
|
|
|
remove <ul.errorlist/>
|
|
make <ul.errorlist/> called errorlist
|
|
make <li/> called error
|
|
set size_in_mb to (me.files[0].size / 1024 / 1024).toFixed(2)
|
|
put 'File must be {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB}MB or less (actual: ' + size_in_mb + 'MB)' into error
|
|
put error into errorlist
|
|
put errorlist before me
|
|
else
|
|
remove @disabled from #upload
|
|
remove <ul.errorlist/>
|
|
end
|
|
end
|
|
"""
|
|
}
|
|
)
|
|
)
|
|
description = forms.CharField(required=False)
|
|
|
|
def clean_image(self):
|
|
value = self.cleaned_data["image"]
|
|
max_mb = settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB
|
|
max_bytes = max_mb * 1024 * 1024
|
|
if value.size > max_bytes:
|
|
# Erase the file from our data to stop trying to show it again
|
|
self.files = {}
|
|
raise forms.ValidationError(
|
|
f"File must be {max_mb}MB or less (actual: {value.size / 1024 ** 2:.2f})"
|
|
)
|
|
return value
|
|
|
|
def form_invalid(self, form):
|
|
return super().form_invalid(form)
|
|
|
|
def form_valid(self, form):
|
|
# Make a PostAttachment
|
|
main_file = resize_image(
|
|
form.cleaned_data["image"],
|
|
size=(2000, 2000),
|
|
cover=False,
|
|
)
|
|
thumbnail_file = resize_image(
|
|
form.cleaned_data["image"],
|
|
size=(400, 225),
|
|
cover=True,
|
|
)
|
|
attachment = PostAttachment.objects.create(
|
|
blurhash=blurhash_image(thumbnail_file),
|
|
mimetype="image/webp",
|
|
width=main_file.image.width,
|
|
height=main_file.image.height,
|
|
name=form.cleaned_data.get("description"),
|
|
state=PostAttachmentStates.fetched,
|
|
)
|
|
|
|
attachment.file.save(
|
|
main_file.name,
|
|
main_file,
|
|
)
|
|
attachment.thumbnail.save(
|
|
thumbnail_file.name,
|
|
thumbnail_file,
|
|
)
|
|
attachment.save()
|
|
# Return the response, with a hidden input plus a note
|
|
return render(
|
|
self.request, "activities/_image_uploaded.html", {"attachment": attachment}
|
|
)
|