mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-22 15:21:01 +00:00
Some cleanup around editing
This commit is contained in:
parent
6c7ddedd34
commit
8e9e3ecf69
8 changed files with 91 additions and 39 deletions
|
@ -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(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
|
|
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])
|
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()
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
15
tests/core/test_html.py
Normal 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"
|
||||||
|
)
|
Loading…
Reference in a new issue