summaryrefslogtreecommitdiffstats
path: root/activities
diff options
context:
space:
mode:
authorAndrew Godwin2022-11-16 06:53:39 -0700
committerAndrew Godwin2022-11-16 13:53:40 -0700
commit495e955378d62dc439c4c210785e5d401bc77f64 (patch)
tree859813b06314f387470295e752d1f1b3828830a7 /activities
parent906ed2f27c9105dbd78f416930f1aa2b49497567 (diff)
downloadtakahe-495e955378d62dc439c4c210785e5d401bc77f64.tar.gz
takahe-495e955378d62dc439c4c210785e5d401bc77f64.tar.bz2
takahe-495e955378d62dc439c4c210785e5d401bc77f64.zip
Tag and visibility handling
Diffstat (limited to 'activities')
-rw-r--r--activities/migrations/0005_post_hashtags_alter_fanout_type_and_more.py48
-rw-r--r--activities/models/post.py90
-rw-r--r--activities/models/timeline_event.py11
-rw-r--r--activities/views/posts.py49
-rw-r--r--activities/views/timelines.py11
5 files changed, 169 insertions, 40 deletions
diff --git a/activities/migrations/0005_post_hashtags_alter_fanout_type_and_more.py b/activities/migrations/0005_post_hashtags_alter_fanout_type_and_more.py
new file mode 100644
index 0000000..07d5cca
--- /dev/null
+++ b/activities/migrations/0005_post_hashtags_alter_fanout_type_and_more.py
@@ -0,0 +1,48 @@
+# Generated by Django 4.1.3 on 2022-11-16 20:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "activities",
+ "0004_rename_authored_post_published_alter_fanout_type_and_more",
+ ),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="post",
+ name="hashtags",
+ field=models.JSONField(default=[]),
+ ),
+ migrations.AlterField(
+ model_name="fanout",
+ name="type",
+ field=models.CharField(
+ choices=[
+ ("post", "Post"),
+ ("interaction", "Interaction"),
+ ("undo_interaction", "Undo Interaction"),
+ ],
+ max_length=100,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="timelineevent",
+ name="type",
+ field=models.CharField(
+ choices=[
+ ("post", "Post"),
+ ("boost", "Boost"),
+ ("mentioned", "Mentioned"),
+ ("liked", "Liked"),
+ ("followed", "Followed"),
+ ("boosted", "Boosted"),
+ ],
+ max_length=100,
+ ),
+ ),
+ ]
diff --git a/activities/models/post.py b/activities/models/post.py
index 22e6412..4896e58 100644
--- a/activities/models/post.py
+++ b/activities/models/post.py
@@ -8,7 +8,7 @@ from django.utils import timezone
from activities.models.fan_out import FanOut
from activities.models.timeline_event import TimelineEvent
from core.html import sanitize_post
-from core.ld import canonicalise, format_ld_date, parse_ld_date
+from core.ld import canonicalise, format_ld_date, get_list, parse_ld_date
from stator.models import State, StateField, StateGraph, StatorModel
from users.models.follow import Follow
from users.models.identity import Identity
@@ -25,7 +25,32 @@ class PostStates(StateGraph):
"""
Creates all needed fan-out objects for a new Post.
"""
- await instance.afan_out()
+ post = await instance.afetch_full()
+ # Non-local posts should not be here
+ if not post.local:
+ raise ValueError("Trying to run handle_new on a non-local post!")
+ # Build list of targets - mentions always included
+ targets = set()
+ async for mention in post.mentions.all():
+ targets.add(mention)
+ # Then, if it's not mentions only, also deliver to followers
+ if post.visibility != Post.Visibilities.mentioned:
+ async for follower in post.author.inbound_follows.select_related("source"):
+ targets.add(follower.source)
+ # Fan out to each one
+ for follow in targets:
+ await FanOut.objects.acreate(
+ identity=follow,
+ type=FanOut.Types.post,
+ subject_post=post,
+ )
+ # And one for themselves if they're local
+ if post.author.local:
+ await FanOut.objects.acreate(
+ identity_id=post.author_id,
+ type=FanOut.Types.post,
+ subject_post=post,
+ )
return cls.fanned_out
@@ -91,6 +116,9 @@ class Post(StatorModel):
blank=True,
)
+ # Hashtags in the post
+ hashtags = models.JSONField(default=[])
+
# When the post was originally created (as opposed to when we received it)
published = models.DateTimeField(default=timezone.now)
@@ -133,7 +161,11 @@ class Post(StatorModel):
@classmethod
def create_local(
- cls, author: Identity, content: str, summary: Optional[str] = None
+ cls,
+ author: Identity,
+ content: str,
+ summary: Optional[str] = None,
+ visibility: int = Visibilities.public,
) -> "Post":
with transaction.atomic():
post = cls.objects.create(
@@ -142,6 +174,7 @@ class Post(StatorModel):
summary=summary or None,
sensitive=bool(summary),
local=True,
+ visibility=visibility,
)
post.object_uri = post.urls.object_uri
post.url = post.urls.view_nice
@@ -150,29 +183,6 @@ class Post(StatorModel):
### ActivityPub (outbound) ###
- async def afan_out(self):
- """
- Creates FanOuts for a new post
- """
- # Send a copy to all people who follow this user
- post = await self.afetch_full()
- async for follow in post.author.inbound_follows.select_related(
- "source", "target"
- ):
- if follow.source.local or follow.target.local:
- await FanOut.objects.acreate(
- identity_id=follow.source_id,
- type=FanOut.Types.post,
- subject_post=post,
- )
- # And one for themselves if they're local
- if post.author.local:
- await FanOut.objects.acreate(
- identity_id=post.author_id,
- type=FanOut.Types.post,
- subject_post=post,
- )
-
def to_ap(self) -> Dict:
"""
Returns the AP JSON for this object
@@ -185,7 +195,7 @@ class Post(StatorModel):
"content": self.safe_content,
"to": "as:Public",
"as:sensitive": self.sensitive,
- "url": self.urls.view_nice if self.local else self.url,
+ "url": str(self.urls.view_nice if self.local else self.url),
}
if self.summary:
value["summary"] = self.summary
@@ -236,8 +246,24 @@ class Post(StatorModel):
post.url = data.get("url", None)
post.published = parse_ld_date(data.get("published", None))
# TODO: to
- # TODO: mentions
- # TODO: visibility
+ # Mentions and hashtags
+ post.hashtags = []
+ for tag in get_list(data, "tag"):
+ if tag["type"].lower() == "mention":
+ mention_identity = Identity.by_actor_uri(tag["href"], create=True)
+ post.mentions.add(mention_identity)
+ elif tag["type"].lower() == "as:hashtag":
+ post.hashtags.append(tag["name"].lstrip("#"))
+ else:
+ raise ValueError(f"Unknown tag type {tag['type']}")
+ # Visibility and to
+ # (a post is public if it's ever to/cc as:Public, otherwise we
+ # regard it as unlisted for now)
+ targets = get_list(data, "to") + get_list(data, "cc")
+ post.visibility = Post.Visibilities.unlisted
+ for target in targets:
+ if target.lower() == "as:public":
+ post.visibility = Post.Visibilities.public
post.save()
return post
@@ -275,9 +301,13 @@ class Post(StatorModel):
raise ValueError("Create actor does not match its Post object", data)
# Create it
post = cls.by_ap(data["object"], create=True, update=True)
- # Make timeline events as appropriate
+ # Make timeline events for followers
for follow in Follow.objects.filter(target=post.author, source__local=True):
TimelineEvent.add_post(follow.source, post)
+ # Make timeline events for mentions if they're local
+ for mention in post.mentions.all():
+ if mention.local:
+ TimelineEvent.add_mentioned(mention, post)
# Force it into fanned_out as it's not ours
post.transition_perform(PostStates.fanned_out)
diff --git a/activities/models/timeline_event.py b/activities/models/timeline_event.py
index 29dec19..368fdad 100644
--- a/activities/models/timeline_event.py
+++ b/activities/models/timeline_event.py
@@ -82,6 +82,17 @@ class TimelineEvent(models.Model):
)[0]
@classmethod
+ def add_mentioned(cls, identity, post):
+ """
+ Adds a mention of identity by post
+ """
+ return cls.objects.get_or_create(
+ identity=identity,
+ type=cls.Types.mentioned,
+ subject_post=post,
+ )[0]
+
+ @classmethod
def add_post_interaction(cls, identity, interaction):
"""
Adds a boost/like to the timeline if it's not there already.
diff --git a/activities/views/posts.py b/activities/views/posts.py
index ece7cf3..3ee35cc 100644
--- a/activities/views/posts.py
+++ b/activities/views/posts.py
@@ -1,13 +1,15 @@
+from django import forms
from django.shortcuts import get_object_or_404, redirect, render
+from django.template.defaultfilters import linebreaks_filter
from django.utils.decorators import method_decorator
-from django.views.generic import TemplateView, View
+from django.views.generic import FormView, TemplateView, View
-from activities.models import PostInteraction, PostInteractionStates
+from activities.models import Post, PostInteraction, PostInteractionStates
from users.decorators import identity_required
from users.shortcuts import by_handle_or_404
-class Post(TemplateView):
+class Individual(TemplateView):
template_name = "activities/post.html"
@@ -100,3 +102,44 @@ class Boost(View):
},
)
return redirect(post.urls.view)
+
+
+@method_decorator(identity_required, name="dispatch")
+class Compose(FormView):
+
+ template_name = "activities/compose.html"
+
+ class form_class(forms.Form):
+ text = forms.CharField(
+ widget=forms.Textarea(
+ attrs={
+ "placeholder": "What's on your mind?",
+ },
+ )
+ )
+ visibility = forms.ChoiceField(
+ choices=[
+ (Post.Visibilities.public, "Public"),
+ (Post.Visibilities.unlisted, "Unlisted"),
+ (Post.Visibilities.followers, "Followers & Mentioned Only"),
+ (Post.Visibilities.mentioned, "Mentioned Only"),
+ ],
+ )
+ content_warning = forms.CharField(
+ required=False,
+ widget=forms.TextInput(
+ attrs={
+ "placeholder": "Content Warning",
+ },
+ ),
+ help_text="Optional - Post will be hidden behind this text until clicked",
+ )
+
+ def form_valid(self, form):
+ Post.create_local(
+ author=self.request.identity,
+ content=linebreaks_filter(form.cleaned_data["text"]),
+ summary=form.cleaned_data.get("content_warning"),
+ visibility=form.cleaned_data["visibility"],
+ )
+ return redirect("/")
diff --git a/activities/views/timelines.py b/activities/views/timelines.py
index c59c3b6..45a0c30 100644
--- a/activities/views/timelines.py
+++ b/activities/views/timelines.py
@@ -95,12 +95,9 @@ class Notifications(TemplateView):
def get_context_data(self):
context = super().get_context_data()
- context["events"] = (
- TimelineEvent.objects.filter(
- identity=self.request.identity,
- )
- .exclude(type__in=[TimelineEvent.Types.post, TimelineEvent.Types.boost])
- .select_related("subject_post", "subject_post__author", "subject_identity")
- )
+ context["events"] = TimelineEvent.objects.filter(
+ identity=self.request.identity,
+ type__in=[TimelineEvent.Types.mentioned, TimelineEvent.Types.boosted],
+ ).select_related("subject_post", "subject_post__author", "subject_identity")
context["current_page"] = "notifications"
return context