From 8e9e3ecf6935db84bbc731252f592795675de685 Mon Sep 17 00:00:00 2001
From: Andrew Godwin
Date: Sun, 27 Nov 2022 12:09:08 -0700
Subject: [PATCH] Some cleanup around editing
---
activities/models/fan_out.py | 20 ++++-----
activities/models/post.py | 61 +++++++++++++++++++--------
activities/views/posts.py | 14 +++---
core/html.py | 12 ++++++
stator/runner.py | 4 +-
templates/activities/compose.html | 2 +-
templates/activities/post_delete.html | 2 +-
tests/core/test_html.py | 15 +++++++
8 files changed, 91 insertions(+), 39 deletions(-)
create mode 100644 tests/core/test_html.py
diff --git a/activities/models/fan_out.py b/activities/models/fan_out.py
index a86e30a..14f52a4 100644
--- a/activities/models/fan_out.py
+++ b/activities/models/fan_out.py
@@ -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(
diff --git a/activities/models/post.py b/activities/models/post.py
index 23194b3..f504fcb 100644
--- a/activities/models/post.py
+++ b/activities/models/post.py
@@ -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:
diff --git a/activities/views/posts.py b/activities/views/posts.py
index 5d7b0c9..083df30 100644
--- a/activities/views/posts.py
+++ b/activities/views/posts.py
@@ -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"
diff --git a/core/html.py b/core/html.py
index 3230284..dfb7beb 100644
--- a/core/html.py
+++ b/core/html.py
@@ -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("
", "\n").replace("
", "\n")
+ # Remove all other HTML and return
+ cleaner = bleach.Cleaner(tags=[], strip=True, filters=[])
+ return cleaner.clean(post_html).strip()
diff --git a/stator/runner.py b/stator/runner.py
index c78437d..5cc0091 100644
--- a/stator/runner.py
+++ b/stator/runner.py
@@ -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
"""
diff --git a/templates/activities/compose.html b/templates/activities/compose.html
index e705f97..4809177 100644
--- a/templates/activities/compose.html
+++ b/templates/activities/compose.html
@@ -18,7 +18,7 @@
{% include "forms/_field.html" with field=form.visibility %}
-
+
{% endblock %}
diff --git a/templates/activities/post_delete.html b/templates/activities/post_delete.html
index 1566399..47d283c 100644
--- a/templates/activities/post_delete.html
+++ b/templates/activities/post_delete.html
@@ -4,11 +4,11 @@
{% block content %}
Delete this post?
+ {% include "activities/_mini_post.html" %}
- {% include "activities/_post.html" %}
{% endblock %}
diff --git a/tests/core/test_html.py b/tests/core/test_html.py
new file mode 100644
index 0000000..012a0ce
--- /dev/null
+++ b/tests/core/test_html.py
@@ -0,0 +1,15 @@
+from core.html import html_to_plaintext
+
+
+def test_html_to_plaintext():
+
+ assert html_to_plaintext("Hi!
") == "Hi!"
+ assert html_to_plaintext("Hi!
There
") == "Hi!\nThere"
+ assert (
+ html_to_plaintext("Hi!
\n\nHow are you?
") == "Hi!\n\nHow are you?"
+ )
+
+ assert (
+ html_to_plaintext("Hi!
\n\nHow are
you?
today
")
+ == "Hi!\n\nHow are\n you?\n\ntoday"
+ )