Some cleanup around editing
This commit is contained in:
parent
6c7ddedd34
commit
8e9e3ecf69
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
12
core/html.py
12
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("<br>", "\n").replace("</p>", "\n")
|
||||
# Remove all other HTML and return
|
||||
cleaner = bleach.Cleaner(tags=[], strip=True, filters=[])
|
||||
return cleaner.clean(post_html).strip()
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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"
|
||||
)
|
Loading…
Reference in New Issue