Some cleanup around editing

This commit is contained in:
Andrew Godwin 2022-11-27 12:09:08 -07:00
parent 6c7ddedd34
commit 8e9e3ecf69
8 changed files with 91 additions and 39 deletions

View File

@ -17,14 +17,12 @@ class FanOutStates(StateGraph):
""" """
Sends the fan-out to the right inbox. Sends the fan-out to the right inbox.
""" """
LOCAL_IDENTITY = True
REMOTE_IDENTITY = False
fan_out = await instance.afetch_full() fan_out = await instance.afetch_full()
match (fan_out.type, fan_out.identity.local): match (fan_out.type, fan_out.identity.local):
# Handle creating/updating local posts # Handle creating/updating local posts
case (FanOut.Types.post | FanOut.Types.post_edited, LOCAL_IDENTITY): case ((FanOut.Types.post | FanOut.Types.post_edited), True):
post = await fan_out.subject_post.afetch_full() post = await fan_out.subject_post.afetch_full()
# Make a timeline event directly # Make a timeline event directly
# If it's a reply, we only add it if we follow at least one # If it's a reply, we only add it if we follow at least one
@ -50,7 +48,7 @@ class FanOutStates(StateGraph):
) )
# Handle sending remote posts create # Handle sending remote posts create
case (FanOut.Types.post, REMOTE_IDENTITY): case (FanOut.Types.post, False):
post = await fan_out.subject_post.afetch_full() post = await fan_out.subject_post.afetch_full()
# Sign it and send it # Sign it and send it
await post.author.signed_request( await post.author.signed_request(
@ -60,7 +58,7 @@ class FanOutStates(StateGraph):
) )
# Handle sending remote posts update # Handle sending remote posts update
case (FanOut.Types.post_edited, REMOTE_IDENTITY): case (FanOut.Types.post_edited, False):
post = await fan_out.subject_post.afetch_full() post = await fan_out.subject_post.afetch_full()
# Sign it and send it # Sign it and send it
await post.author.signed_request( await post.author.signed_request(
@ -70,7 +68,7 @@ class FanOutStates(StateGraph):
) )
# Handle deleting local posts # Handle deleting local posts
case (FanOut.Types.post_deleted, LOCAL_IDENTITY): case (FanOut.Types.post_deleted, True):
post = await fan_out.subject_post.afetch_full() post = await fan_out.subject_post.afetch_full()
if fan_out.identity.local: if fan_out.identity.local:
# Remove all timeline events mentioning it # Remove all timeline events mentioning it
@ -80,7 +78,7 @@ class FanOutStates(StateGraph):
).adelete() ).adelete()
# Handle sending remote post deletes # Handle sending remote post deletes
case (FanOut.Types.post_deleted, REMOTE_IDENTITY): case (FanOut.Types.post_deleted, False):
post = await fan_out.subject_post.afetch_full() post = await fan_out.subject_post.afetch_full()
# Send it to the remote inbox # Send it to the remote inbox
await post.author.signed_request( await post.author.signed_request(
@ -90,7 +88,7 @@ class FanOutStates(StateGraph):
) )
# Handle local boosts/likes # Handle local boosts/likes
case (FanOut.Types.interaction, LOCAL_IDENTITY): case (FanOut.Types.interaction, True):
interaction = await fan_out.subject_post_interaction.afetch_full() interaction = await fan_out.subject_post_interaction.afetch_full()
# Make a timeline event directly # Make a timeline event directly
await sync_to_async(TimelineEvent.add_post_interaction)( await sync_to_async(TimelineEvent.add_post_interaction)(
@ -99,7 +97,7 @@ class FanOutStates(StateGraph):
) )
# Handle sending remote boosts/likes # Handle sending remote boosts/likes
case (FanOut.Types.interaction, REMOTE_IDENTITY): case (FanOut.Types.interaction, False):
interaction = await fan_out.subject_post_interaction.afetch_full() interaction = await fan_out.subject_post_interaction.afetch_full()
# Send it to the remote inbox # Send it to the remote inbox
await interaction.identity.signed_request( await interaction.identity.signed_request(
@ -109,7 +107,7 @@ class FanOutStates(StateGraph):
) )
# Handle undoing local boosts/likes # Handle undoing local boosts/likes
case (FanOut.Types.undo_interaction, LOCAL_IDENTITY): # noqa:F841 case (FanOut.Types.undo_interaction, True): # noqa:F841
interaction = await fan_out.subject_post_interaction.afetch_full() interaction = await fan_out.subject_post_interaction.afetch_full()
# Delete any local timeline events # Delete any local timeline events
@ -119,7 +117,7 @@ class FanOutStates(StateGraph):
) )
# Handle sending remote undoing boosts/likes # Handle sending remote undoing boosts/likes
case (FanOut.Types.undo_interaction, REMOTE_IDENTITY): # noqa:F841 case (FanOut.Types.undo_interaction, False): # noqa:F841
interaction = await fan_out.subject_post_interaction.afetch_full() interaction = await fan_out.subject_post_interaction.afetch_full()
# Send an undo to the remote inbox # Send an undo to the remote inbox
await interaction.identity.signed_request( await interaction.identity.signed_request(

View File

@ -1,5 +1,5 @@
import re import re
from typing import Dict, Iterable, Optional from typing import Dict, Iterable, Optional, Set
import httpx import httpx
import urlman import urlman
@ -244,6 +244,12 @@ class Post(StatorModel):
""" """
return self.linkify_mentions(sanitize_post(self.content)) return self.linkify_mentions(sanitize_post(self.content))
def safe_content_plain(self):
"""
Returns the content formatted as plain text
"""
return self.linkify_mentions(sanitize_post(self.content))
### Async helpers ### ### Async helpers ###
async def afetch_full(self): async def afetch_full(self):
@ -256,7 +262,7 @@ class Post(StatorModel):
.aget(pk=self.pk) .aget(pk=self.pk)
) )
### Local creation ### ### Local creation/editing ###
@classmethod @classmethod
def create_local( def create_local(
@ -269,21 +275,7 @@ class Post(StatorModel):
) -> "Post": ) -> "Post":
with transaction.atomic(): with transaction.atomic():
# Find mentions in this post # Find mentions in this post
mention_hits = cls.mention_regex.findall(content) mentions = cls.mentions_from_content(content, author)
mentions = set()
for precursor, handle in mention_hits:
if "@" in handle:
username, domain = handle.split("@", 1)
else:
username = handle
domain = author.domain_id
identity = Identity.by_username_and_domain(
username=username,
domain=domain,
fetch=True,
)
if identity is not None:
mentions.add(identity)
if reply_to: if reply_to:
mentions.add(reply_to.author) mentions.add(reply_to.author)
# Maintain local-only for replies # Maintain local-only for replies
@ -307,6 +299,41 @@ class Post(StatorModel):
post.save() post.save()
return post return post
def edit_local(
self,
content: str,
summary: Optional[str] = None,
visibility: int = Visibilities.public,
):
with transaction.atomic():
# Strip all HTML and apply linebreaks filter
self.content = linebreaks_filter(strip_html(content))
self.summary = summary or None
self.sensitive = bool(summary)
self.visibility = visibility
self.edited = timezone.now()
self.mentions.set(self.mentions_from_content(content, self.author))
self.save()
@classmethod
def mentions_from_content(cls, content, author) -> Set[Identity]:
mention_hits = cls.mention_regex.findall(content)
mentions = set()
for precursor, handle in mention_hits:
if "@" in handle:
username, domain = handle.split("@", 1)
else:
username = handle
domain = author.domain_id
identity = Identity.by_username_and_domain(
username=username,
domain=domain,
fetch=True,
)
if identity is not None:
mentions.add(identity)
return mentions
### ActivityPub (outbound) ### ### ActivityPub (outbound) ###
def to_ap(self) -> Dict: def to_ap(self) -> Dict:

View File

@ -2,7 +2,6 @@ from django import forms
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import FormView, TemplateView, View from django.views.generic import FormView, TemplateView, View
@ -13,6 +12,7 @@ from activities.models import (
PostStates, PostStates,
TimelineEvent, TimelineEvent,
) )
from core.html import html_to_plaintext
from core.ld import canonicalise from core.ld import canonicalise
from core.models import Config from core.models import Config
from users.decorators import identity_required from users.decorators import identity_required
@ -218,7 +218,7 @@ class Compose(FormView):
"id": self.post_obj.id, "id": self.post_obj.id,
"reply_to": self.reply_to.pk if self.reply_to else "", "reply_to": self.reply_to.pk if self.reply_to else "",
"visibility": self.post_obj.visibility, "visibility": self.post_obj.visibility,
"text": self.post_obj.content, "text": html_to_plaintext(self.post_obj.content),
"content_warning": self.post_obj.summary, "content_warning": self.post_obj.summary,
} }
) )
@ -236,11 +236,11 @@ class Compose(FormView):
post_id = form.cleaned_data.get("id") post_id = form.cleaned_data.get("id")
if post_id: if post_id:
post = get_object_or_404(self.request.identity.posts, pk=post_id) post = get_object_or_404(self.request.identity.posts, pk=post_id)
post.edited = timezone.now() post.edit_local(
post.content = form.cleaned_data["text"] content=form.cleaned_data["text"],
post.summary = form.cleaned_data.get("content_warning") summary=form.cleaned_data.get("content_warning"),
post.visibility = form.cleaned_data["visibility"] visibility=form.cleaned_data["visibility"],
post.save() )
# Should there be a timeline event for edits? # Should there be a timeline event for edits?
# E.g. "@user edited #123" # E.g. "@user edited #123"

View File

@ -38,3 +38,15 @@ def strip_html(post_html: str) -> str:
""" """
cleaner = bleach.Cleaner(tags=[], strip=True, filters=[LinkifyFilter]) cleaner = bleach.Cleaner(tags=[], strip=True, filters=[LinkifyFilter])
return mark_safe(cleaner.clean(post_html)) return mark_safe(cleaner.clean(post_html))
def html_to_plaintext(post_html: str) -> str:
"""
Tries to do the inverse of the linebreaks filter.
"""
# TODO: Handle HTML entities
# Remove all newlines, then replace br with a newline and /p with two (one comes from bleach)
post_html = post_html.replace("\n", "").replace("<br>", "\n").replace("</p>", "\n")
# Remove all other HTML and return
cleaner = bleach.Cleaner(tags=[], strip=True, filters=[])
return cleaner.clean(post_html).strip()

View File

@ -52,7 +52,7 @@ class StatorRunner:
Config.system = await Config.aload_system() Config.system = await Config.aload_system()
print(f"{self.handled} tasks processed so far") print(f"{self.handled} tasks processed so far")
print("Running cleaning and scheduling") print("Running cleaning and scheduling")
await self.run_cleanup() await self.run_scheduling()
self.remove_completed_tasks() self.remove_completed_tasks()
await self.fetch_and_process_tasks() await self.fetch_and_process_tasks()
@ -75,7 +75,7 @@ class StatorRunner:
print("Complete") print("Complete")
return self.handled return self.handled
async def run_cleanup(self): async def run_scheduling(self):
""" """
Do any transition cleanup tasks Do any transition cleanup tasks
""" """

View File

@ -18,7 +18,7 @@
{% include "forms/_field.html" with field=form.visibility %} {% include "forms/_field.html" with field=form.visibility %}
</fieldset> </fieldset>
<div class="buttons"> <div class="buttons">
<button>{% if form.id %}Edit{% elif config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button> <button>{% if form.id.value %}Edit{% elif config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -4,11 +4,11 @@
{% block content %} {% block content %}
<h1>Delete this post?</h1> <h1>Delete this post?</h1>
{% include "activities/_mini_post.html" %}
<form action="." method="POST"> <form action="." method="POST">
{% csrf_token %} {% csrf_token %}
<a class="button" onclick="history.back()">Cancel</a> <a class="button" onclick="history.back()">Cancel</a>
<button class="delete">Delete</button> <button class="delete">Delete</button>
</form> </form>
{% include "activities/_post.html" %}
{% endblock %} {% endblock %}

15
tests/core/test_html.py Normal file
View File

@ -0,0 +1,15 @@
from core.html import html_to_plaintext
def test_html_to_plaintext():
assert html_to_plaintext("<p>Hi!</p>") == "Hi!"
assert html_to_plaintext("<p>Hi!<br>There</p>") == "Hi!\nThere"
assert (
html_to_plaintext("<p>Hi!</p>\n\n<p>How are you?</p>") == "Hi!\n\nHow are you?"
)
assert (
html_to_plaintext("<p>Hi!</p>\n\n<p>How are<br> you?</p><p>today</p>")
== "Hi!\n\nHow are\n you?\n\ntoday"
)