summaryrefslogtreecommitdiffstats
path: root/activities/views/compose.py
blob: e9092f7abf59979ddc8f6ece76e640fa6fdbe5ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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.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 html_to_plaintext
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",
                    "maxlength": Config.lazy_system_value("post_length"),
                    "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, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.fields["text"].widget.attrs[
                "_"
            ] = f"""
                on load or input
                set characters to my.value.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")
            if not text:
                return text
            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_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": html_to_plaintext(self.post_obj.content),
                    "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
                # 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
                )
        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()
        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")
            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}
        )