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.
"""
LOCAL_IDENTITY = True
REMOTE_IDENTITY = False
fan_out = await instance.afetch_full()
match (fan_out.type, fan_out.identity.local):
# 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()
# Make a timeline event directly
# 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
case (FanOut.Types.post, REMOTE_IDENTITY):
case (FanOut.Types.post, False):
post = await fan_out.subject_post.afetch_full()
# Sign it and send it
await post.author.signed_request(
@ -60,7 +58,7 @@ class FanOutStates(StateGraph):
)
# 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()
# Sign it and send it
await post.author.signed_request(
@ -70,7 +68,7 @@ class FanOutStates(StateGraph):
)
# 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()
if fan_out.identity.local:
# Remove all timeline events mentioning it
@ -80,7 +78,7 @@ class FanOutStates(StateGraph):
).adelete()
# 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()
# Send it to the remote inbox
await post.author.signed_request(
@ -90,7 +88,7 @@ class FanOutStates(StateGraph):
)
# Handle local boosts/likes
case (FanOut.Types.interaction, LOCAL_IDENTITY):
case (FanOut.Types.interaction, True):
interaction = await fan_out.subject_post_interaction.afetch_full()
# Make a timeline event directly
await sync_to_async(TimelineEvent.add_post_interaction)(
@ -99,7 +97,7 @@ class FanOutStates(StateGraph):
)
# 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()
# Send it to the remote inbox
await interaction.identity.signed_request(
@ -109,7 +107,7 @@ class FanOutStates(StateGraph):
)
# 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()
# Delete any local timeline events
@ -119,7 +117,7 @@ class FanOutStates(StateGraph):
)
# 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()
# Send an undo to the remote inbox
await interaction.identity.signed_request(

View file

@ -1,5 +1,5 @@
import re
from typing import Dict, Iterable, Optional
from typing import Dict, Iterable, Optional, Set
import httpx
import urlman
@ -244,6 +244,12 @@ class Post(StatorModel):
"""
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 def afetch_full(self):
@ -256,7 +262,7 @@ class Post(StatorModel):
.aget(pk=self.pk)
)
### Local creation ###
### Local creation/editing ###
@classmethod
def create_local(
@ -269,21 +275,7 @@ class Post(StatorModel):
) -> "Post":
with transaction.atomic():
# Find mentions in this post
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)
mentions = cls.mentions_from_content(content, author)
if reply_to:
mentions.add(reply_to.author)
# Maintain local-only for replies
@ -307,6 +299,41 @@ class Post(StatorModel):
post.save()
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) ###
def to_ap(self) -> Dict:

View file

@ -2,7 +2,6 @@ from django import forms
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse
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, TemplateView, View
@ -13,6 +12,7 @@ from activities.models import (
PostStates,
TimelineEvent,
)
from core.html import html_to_plaintext
from core.ld import canonicalise
from core.models import Config
from users.decorators import identity_required
@ -218,7 +218,7 @@ class Compose(FormView):
"id": self.post_obj.id,
"reply_to": self.reply_to.pk if self.reply_to else "",
"visibility": self.post_obj.visibility,
"text": self.post_obj.content,
"text": html_to_plaintext(self.post_obj.content),
"content_warning": self.post_obj.summary,
}
)
@ -236,11 +236,11 @@ class Compose(FormView):
post_id = form.cleaned_data.get("id")
if post_id:
post = get_object_or_404(self.request.identity.posts, pk=post_id)
post.edited = timezone.now()
post.content = form.cleaned_data["text"]
post.summary = form.cleaned_data.get("content_warning")
post.visibility = form.cleaned_data["visibility"]
post.save()
post.edit_local(
content=form.cleaned_data["text"],
summary=form.cleaned_data.get("content_warning"),
visibility=form.cleaned_data["visibility"],
)
# Should there be a timeline event for edits?
# 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])
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()
print(f"{self.handled} tasks processed so far")
print("Running cleaning and scheduling")
await self.run_cleanup()
await self.run_scheduling()
self.remove_completed_tasks()
await self.fetch_and_process_tasks()
@ -75,7 +75,7 @@ class StatorRunner:
print("Complete")
return self.handled
async def run_cleanup(self):
async def run_scheduling(self):
"""
Do any transition cleanup tasks
"""

View file

@ -18,7 +18,7 @@
{% include "forms/_field.html" with field=form.visibility %}
</fieldset>
<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>
</form>
{% endblock %}

View file

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