mirror of
https://github.com/jointakahe/takahe.git
synced 2024-05-19 17:48:10 +00:00
Compare commits
11 commits
c73478011a
...
6f7b8db3a2
Author | SHA1 | Date | |
---|---|---|---|
6f7b8db3a2 | |||
7c34ac78ed | |||
72eb6a6271 | |||
b2223ddf42 | |||
045a499ddf | |||
0fa48578f2 | |||
a09914beb2 | |||
54bddb0dd7 | |||
b3e67ffe3a | |||
42b0b5831a | |||
4c3cae337c |
|
@ -3,7 +3,7 @@
|
|||
A *beta* Fediverse server for microblogging. Not fully polished yet -
|
||||
we're still working towards a 1.0!
|
||||
|
||||
**Current version: [0.10.1](https://docs.jointakahe.org/en/latest/releases/0.10/)**
|
||||
**Current version: [0.11.0](https://docs.jointakahe.org/en/latest/releases/0.11/)**
|
||||
|
||||
Key features:
|
||||
|
||||
|
|
|
@ -471,6 +471,7 @@ class Post(StatorModel):
|
|||
"likes": self.stats.get("likes", 0) if self.stats else 0,
|
||||
"boosts": self.stats.get("boosts", 0) if self.stats else 0,
|
||||
"replies": self.stats.get("replies", 0) if self.stats else 0,
|
||||
"reactions": self.stats.get("reactions", {}) if self.stats else {},
|
||||
}
|
||||
|
||||
### Local creation/editing ###
|
||||
|
@ -610,12 +611,24 @@ class Post(StatorModel):
|
|||
"likes": self.interactions.filter(
|
||||
type=PostInteraction.Types.like,
|
||||
state__in=PostInteractionStates.group_active(),
|
||||
).count(),
|
||||
)
|
||||
.values("identity")
|
||||
.distinct()
|
||||
.count(), # This counts each user that's had any likes/reactions
|
||||
"boosts": self.interactions.filter(
|
||||
type=PostInteraction.Types.boost,
|
||||
state__in=PostInteractionStates.group_active(),
|
||||
).count(),
|
||||
"replies": Post.objects.filter(in_reply_to=self.object_uri).count(),
|
||||
"reactions": {
|
||||
row["value"] or "": row["count"]
|
||||
for row in self.interactions.filter(
|
||||
type=PostInteraction.Types.like,
|
||||
state__in=PostInteractionStates.group_active(),
|
||||
)
|
||||
.values("value")
|
||||
.annotate(count=models.Count("identity"))
|
||||
},
|
||||
}
|
||||
if save:
|
||||
self.save()
|
||||
|
|
|
@ -154,7 +154,7 @@ class PostInteraction(StatorModel):
|
|||
)
|
||||
|
||||
# Used to store any interaction extra text value like the vote
|
||||
# in the question/poll case
|
||||
# in the question/poll case, or the reaction
|
||||
value = models.CharField(max_length=50, blank=True, null=True)
|
||||
|
||||
# When the activity was originally created (as opposed to when we received it)
|
||||
|
@ -392,6 +392,7 @@ class PostInteraction(StatorModel):
|
|||
# Get the right type
|
||||
if data["type"].lower() == "like":
|
||||
type = cls.Types.like
|
||||
value = data.get("content") or data.get("_misskey_reaction")
|
||||
elif data["type"].lower() == "announce":
|
||||
type = cls.Types.boost
|
||||
elif (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from types import EllipsisType
|
||||
|
||||
from activities.models import (
|
||||
Post,
|
||||
|
@ -38,7 +39,7 @@ class PostService:
|
|||
def __init__(self, post: Post):
|
||||
self.post = post
|
||||
|
||||
def interact_as(self, identity: Identity, type: str):
|
||||
def interact_as(self, identity: Identity, type: str, value: str | None = None):
|
||||
"""
|
||||
Performs an interaction on this Post
|
||||
"""
|
||||
|
@ -46,28 +47,39 @@ class PostService:
|
|||
type=type,
|
||||
identity=identity,
|
||||
post=self.post,
|
||||
value=value,
|
||||
)[0]
|
||||
if interaction.state not in PostInteractionStates.group_active():
|
||||
interaction.transition_perform(PostInteractionStates.new)
|
||||
self.post.calculate_stats()
|
||||
|
||||
def uninteract_as(self, identity, type):
|
||||
def uninteract_as(self, identity, type, value: str | None | EllipsisType = ...):
|
||||
"""
|
||||
Undoes an interaction on this Post
|
||||
"""
|
||||
# Only search by value if it was actually given
|
||||
additional_fields = {}
|
||||
if value is not ...:
|
||||
additional_fields["value"] = value
|
||||
|
||||
for interaction in PostInteraction.objects.filter(
|
||||
type=type,
|
||||
identity=identity,
|
||||
post=self.post,
|
||||
**additional_fields,
|
||||
):
|
||||
interaction.transition_perform(PostInteractionStates.undone)
|
||||
|
||||
self.post.calculate_stats()
|
||||
|
||||
def like_as(self, identity: Identity):
|
||||
self.interact_as(identity, PostInteraction.Types.like)
|
||||
def like_as(self, identity: Identity, reaction: str | None = None):
|
||||
"""
|
||||
Add a Like to the post, including reactions.
|
||||
"""
|
||||
self.interact_as(identity, PostInteraction.Types.like, value=reaction)
|
||||
|
||||
def unlike_as(self, identity: Identity):
|
||||
self.uninteract_as(identity, PostInteraction.Types.like)
|
||||
def unlike_as(self, identity: Identity, reaction: str | None = None):
|
||||
self.uninteract_as(identity, PostInteraction.Types.like, value=reaction)
|
||||
|
||||
def boost_as(self, identity: Identity):
|
||||
self.interact_as(identity, PostInteraction.Types.boost)
|
||||
|
|
|
@ -219,7 +219,7 @@ class HttpSignature:
|
|||
body_bytes = b""
|
||||
# GET requests get implicit accept headers added
|
||||
if method == "get":
|
||||
headers["Accept"] = "application/ld+json"
|
||||
headers["Accept"] = "application/activity+json,application/ld+json"
|
||||
# Sign the headers
|
||||
signed_string = "\n".join(
|
||||
f"{name.lower()}: {value}" for name, value in headers.items()
|
||||
|
|
|
@ -172,3 +172,37 @@ We use `HTMX <https://htmx.org/>`_ for dynamically loading content, and
|
|||
`Hyperscript <https://hyperscript.org/>`_ for most interactions rather than raw
|
||||
JavaScript. If you can accomplish what you need with these tools, please use them
|
||||
rather than adding JS.
|
||||
|
||||
|
||||
Cutting a release
|
||||
-----------------
|
||||
|
||||
In order to make a release of Takahē, follow these steps:
|
||||
|
||||
* Create or update the release document (in ``/docs/releases``) for the
|
||||
release; major versions get their own document, minor releases get a
|
||||
subheading in the document for their major release.
|
||||
|
||||
* Go through the git commit history since the last release in order to write
|
||||
a reasonable summary of features.
|
||||
|
||||
* Be sure to include the little paragraphs at the end about contributing and
|
||||
the docker tag, and an Upgrade Notes section that at minimum mentions
|
||||
migrations and if they're normal or weird (even if there aren't any, it's
|
||||
nice to call that out).
|
||||
|
||||
* If it's a new doc, make sure you include it in ``docs/releases/index.rst``!
|
||||
|
||||
* Update the version number in ``/takahe/__init__.py``
|
||||
|
||||
* Update the version number in ``README.md``
|
||||
|
||||
* Make a commit containing these changes called ``Releasing 1.23.45``.
|
||||
|
||||
* Tag that commit with a tag in the format ``1.23.45``.
|
||||
|
||||
* Wait for the GitHub Actions to run and publish the docker images (around 20
|
||||
minutes as the ARM build is a bit slow)
|
||||
|
||||
* Post on the official account announcing the relase and linking to the
|
||||
now-published release notes.
|
||||
|
|
|
@ -1,21 +1,54 @@
|
|||
0.11
|
||||
====
|
||||
|
||||
*Released: Not Yet Released*
|
||||
*Released: 2024-02-05*
|
||||
|
||||
Notes TBD.
|
||||
This is largely a bugfix and catch up release.
|
||||
|
||||
Some highlights:
|
||||
|
||||
* Python 3.10 has been dropped. The new minimum Python version is 3.11
|
||||
* Jamie (`@astraluma@tacobelllabs.net <https://tacobelllabs.net/@astraluma>`_)
|
||||
has officially joined the project
|
||||
* If your S3 does not use TLS, you must use ``s3-insecure`` in your
|
||||
configuration
|
||||
* Takahē now supports unicode hashtags
|
||||
* Add a Maximum Media Attachments setting
|
||||
* Inverted the pruning command exit codes
|
||||
* Posts are no longer required to have text content
|
||||
|
||||
And some interoperability bugs:
|
||||
|
||||
* Fixed a bug with GoToSocial
|
||||
* Attempted to fix follows from Misskey family
|
||||
* Correctly handle when a federated report doesn't have content
|
||||
|
||||
In additions, there's many bugfixes and minor changes, including:
|
||||
|
||||
* Several JSON handling improvements
|
||||
* Post pruning now has a random element to it
|
||||
* More specific loggers
|
||||
* Don't make local identities stale
|
||||
* Don't try to unmute when there's no expiration
|
||||
* Don't try to WebFinger local users
|
||||
* Synchronize follow accepting and profile fetching
|
||||
* Perform some basic domain validity
|
||||
* Correctly reject more operations when the identity is deleted
|
||||
* Post edit fanouts for likers/boosters
|
||||
|
||||
|
||||
If you'd like to help with code, design, or other areas, see
|
||||
:doc:`/contributing` to see how to get in touch.
|
||||
|
||||
You can download images from `Docker Hub <https://hub.docker.com/r/jointakahe/takahe>`_,
|
||||
or use the image name ``jointakahe/takahe:0.11``.
|
||||
|
||||
|
||||
Upgrade Notes
|
||||
-------------
|
||||
|
||||
VAPID keys and Push notifications
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Migrations
|
||||
~~~~~~~~~~
|
||||
|
||||
Takahē now supports push notifications if you supply a valid VAPID keypair as
|
||||
the ``TAKAHE_VAPID_PUBLIC_KEY`` and ``TAKAHE_VAPID_PRIVATE_KEY`` environment
|
||||
variables. You can generate a keypair via `https://web-push-codelab.glitch.me/`_.
|
||||
|
||||
Note that users of apps may need to sign out and in again to their accounts for
|
||||
the app to notice that it can now do push notifications. Some apps, like Elk,
|
||||
may cache the fact your server didn't support it for a while.
|
||||
There are new database migrations; they are backwards-compatible and should
|
||||
not present any major database load.
|
||||
|
|
|
@ -7,6 +7,7 @@ Versions
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
0.11
|
||||
0.10
|
||||
0.9
|
||||
0.8
|
||||
|
|
15
docs/releases/next.rst
Normal file
15
docs/releases/next.rst
Normal file
|
@ -0,0 +1,15 @@
|
|||
|
||||
|
||||
Upgrade Notes
|
||||
-------------
|
||||
|
||||
VAPID keys and Push notifications
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Takahē now supports push notifications if you supply a valid VAPID keypair as
|
||||
the ``TAKAHE_VAPID_PUBLIC_KEY`` and ``TAKAHE_VAPID_PRIVATE_KEY`` environment
|
||||
variables. You can generate a keypair via `https://web-push-codelab.glitch.me/`_.
|
||||
|
||||
Note that users of apps may need to sign out and in again to their accounts for
|
||||
the app to notice that it can now do push notifications. Some apps, like Elk,
|
||||
may cache the fact your server didn't support it for a while.
|
|
@ -1 +1 @@
|
|||
__version__ = "0.10.1"
|
||||
__version__ = "0.11.0"
|
||||
|
|
|
@ -78,10 +78,16 @@
|
|||
<i class="fa-solid fa-reply"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.replies|default:"0" }}</span>
|
||||
</a>
|
||||
<a title="Likes" class="no-action">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.likes|default:"0" }}</span>
|
||||
</a>
|
||||
{% for reaction, count in post.stats_with_defaults.reactions.items %}
|
||||
<a title="Reaction {{reaction}}" class="no-action">
|
||||
{% if reaction %}
|
||||
<span>{{reaction}}</span>
|
||||
{% else %}
|
||||
<i class="fa-solid fa-star"></i>
|
||||
{% endif %}
|
||||
<span class="like-count">{{count}}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
<a title="Boosts" class="no-action">
|
||||
<i class="fa-solid fa-retweet"></i>
|
||||
<span class="like-count">{{ post.stats_with_defaults.boosts|default:"0" }}</span>
|
||||
|
|
364
tests/activities/models/test_reactions.py
Normal file
364
tests/activities/models/test_reactions.py
Normal file
|
@ -0,0 +1,364 @@
|
|||
import pytest
|
||||
|
||||
from activities.models import Post, TimelineEvent
|
||||
from activities.services import PostService
|
||||
from users.models import Identity, InboxMessage
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("local", [True, False])
|
||||
@pytest.mark.parametrize("reaction", ["\U0001F607"])
|
||||
def test_react_notification(
|
||||
identity: Identity,
|
||||
other_identity: Identity,
|
||||
remote_identity: Identity,
|
||||
stator,
|
||||
local: bool,
|
||||
reaction: str,
|
||||
):
|
||||
"""
|
||||
Ensures that a reaction of a local Post notifies its author.
|
||||
|
||||
This mostly ensures that basic reaction flows happen.
|
||||
"""
|
||||
post = Post.create_local(author=identity, content="I love birds!")
|
||||
if local:
|
||||
PostService(post).like_as(other_identity, reaction)
|
||||
else:
|
||||
message = {
|
||||
"id": "test",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": reaction,
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
interactor = other_identity if local else remote_identity
|
||||
|
||||
# Run stator thrice - to receive the post, make fanouts and then process them
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
|
||||
# Verify we got an event
|
||||
event = TimelineEvent.objects.filter(
|
||||
type=TimelineEvent.Types.liked, identity=identity
|
||||
).first()
|
||||
assert event
|
||||
assert event.subject_identity == interactor
|
||||
assert event.subject_post_interaction.value == reaction
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("local", [True, False])
|
||||
@pytest.mark.parametrize("reaction", ["\U0001F607"])
|
||||
def test_react_duplicate(
|
||||
identity: Identity,
|
||||
other_identity: Identity,
|
||||
remote_identity: Identity,
|
||||
stator,
|
||||
local: bool,
|
||||
reaction: str,
|
||||
):
|
||||
"""
|
||||
Ensures that if we receive the same reaction from the same actor multiple times,
|
||||
only one notification and interaction are produced.
|
||||
"""
|
||||
post = Post.create_local(author=identity, content="I love birds!")
|
||||
for _ in range(3):
|
||||
if local:
|
||||
PostService(post).like_as(other_identity, reaction)
|
||||
else:
|
||||
message = {
|
||||
"id": "test",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": reaction,
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
interactor = other_identity if local else remote_identity
|
||||
|
||||
# Running stator 3 times for each interaction. Not sure what's the right number.
|
||||
for _ in range(9):
|
||||
stator.run_single_cycle()
|
||||
|
||||
# Verify we got an event
|
||||
events = TimelineEvent.objects.filter(
|
||||
type=TimelineEvent.Types.liked, identity=identity
|
||||
).all()
|
||||
|
||||
assert len(events) == 1
|
||||
(event,) = events
|
||||
|
||||
assert event.subject_identity == interactor
|
||||
assert event.subject_post_interaction.value == reaction
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("local", [True, False])
|
||||
@pytest.mark.parametrize("reaction", ["\U0001F607"])
|
||||
def test_react_undo(
|
||||
identity: Identity,
|
||||
other_identity: Identity,
|
||||
remote_identity: Identity,
|
||||
stator,
|
||||
local: bool,
|
||||
reaction: str,
|
||||
):
|
||||
"""
|
||||
Ensures basic un-reacting.
|
||||
"""
|
||||
post = Post.create_local(author=identity, content="I love birds!")
|
||||
if local:
|
||||
PostService(post).like_as(other_identity, reaction)
|
||||
else:
|
||||
message = {
|
||||
"id": "test",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": reaction,
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
# Run stator thrice - to receive the post, make fanouts and then process them
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
|
||||
# Verify we got an event
|
||||
events = TimelineEvent.objects.filter(
|
||||
type=TimelineEvent.Types.liked, identity=identity
|
||||
).all()
|
||||
assert len(events) == 1
|
||||
|
||||
if local:
|
||||
PostService(post).unlike_as(other_identity, reaction)
|
||||
else:
|
||||
message = {
|
||||
"id": "test/undo",
|
||||
"type": "Undo",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": {
|
||||
"id": "test",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": reaction,
|
||||
},
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
# Run stator thrice - to receive the post, make fanouts and then process them
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
|
||||
# Verify the event was removed.
|
||||
events = TimelineEvent.objects.filter(
|
||||
type=TimelineEvent.Types.liked, identity=identity
|
||||
).all()
|
||||
assert len(events) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("local", [True, False])
|
||||
def test_react_undo_mismatched(
|
||||
identity: Identity,
|
||||
other_identity: Identity,
|
||||
remote_identity: Identity,
|
||||
stator,
|
||||
local: bool,
|
||||
):
|
||||
"""
|
||||
Ensures that un-reacting deletes the right reaction.
|
||||
"""
|
||||
post = Post.create_local(author=identity, content="I love birds!")
|
||||
if local:
|
||||
PostService(post).like_as(other_identity, "foo")
|
||||
else:
|
||||
message = {
|
||||
"id": "test",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": "foo",
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
# Run stator thrice - to receive the post, make fanouts and then process them
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
|
||||
# Verify we got an event
|
||||
events = TimelineEvent.objects.filter(
|
||||
type=TimelineEvent.Types.liked, identity=identity
|
||||
).all()
|
||||
assert len(events) == 1
|
||||
|
||||
if local:
|
||||
PostService(post).unlike_as(other_identity, "bar")
|
||||
else:
|
||||
message = {
|
||||
"id": "test/undo",
|
||||
"type": "Undo",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": {
|
||||
# AstraLuma: I'm actually unsure if this test should use the same or different ID.
|
||||
"id": "test2",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": "bar",
|
||||
},
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
# Run stator thrice - to receive the post, make fanouts and then process them
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
|
||||
# Verify the event was removed.
|
||||
events = TimelineEvent.objects.filter(
|
||||
type=TimelineEvent.Types.liked, identity=identity
|
||||
).all()
|
||||
assert len(events) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("local", [True, False])
|
||||
@pytest.mark.parametrize("reaction", ["\U0001F607"])
|
||||
def test_react_stats(
|
||||
identity: Identity,
|
||||
other_identity: Identity,
|
||||
remote_identity: Identity,
|
||||
stator,
|
||||
local: bool,
|
||||
reaction: str,
|
||||
):
|
||||
"""
|
||||
Checks basic post stats generation
|
||||
"""
|
||||
post = Post.create_local(author=identity, content="I love birds!")
|
||||
if local:
|
||||
PostService(post).like_as(other_identity, reaction)
|
||||
else:
|
||||
message = {
|
||||
"id": "test",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": reaction,
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
# Run stator thrice - to receive the post, make fanouts and then process them
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
|
||||
post.refresh_from_db()
|
||||
|
||||
assert "reactions" in post.stats
|
||||
assert post.stats["reactions"] == {reaction: 1}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("local", [True, False])
|
||||
def test_react_stats_multiple(
|
||||
identity: Identity,
|
||||
other_identity: Identity,
|
||||
remote_identity: Identity,
|
||||
stator,
|
||||
local: bool,
|
||||
):
|
||||
"""
|
||||
Ensures that multiple reactions get aggregated correctly.
|
||||
|
||||
Basically, if the same person leaves multiple reactions, aggregate all of them into one Like.
|
||||
"""
|
||||
post = Post.create_local(author=identity, content="I love birds!")
|
||||
for i, reaction in enumerate("abc"):
|
||||
if local:
|
||||
PostService(post).like_as(other_identity, reaction)
|
||||
else:
|
||||
message = {
|
||||
"id": f"test{i}",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": reaction,
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
# Run stator thrice - to receive the post, make fanouts and then process them
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
stator.run_single_cycle()
|
||||
|
||||
post.refresh_from_db()
|
||||
|
||||
assert post.stats["reactions"] == {"a": 1, "b": 1, "c": 1}
|
||||
assert post.stats["likes"] == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("local", [True, False])
|
||||
def test_react_stats_mixed(
|
||||
identity: Identity,
|
||||
other_identity: Identity,
|
||||
remote_identity: Identity,
|
||||
stator,
|
||||
local: bool,
|
||||
):
|
||||
"""
|
||||
Ensures that mixed Likes and Reactions get aggregated
|
||||
"""
|
||||
post = Post.create_local(author=identity, content="I love birds!")
|
||||
for i, reaction in enumerate("abc"):
|
||||
if local:
|
||||
PostService(post).like_as(other_identity, reaction)
|
||||
else:
|
||||
message = {
|
||||
"id": f"test{i}",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
"content": reaction,
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
if local:
|
||||
PostService(post).like_as(other_identity)
|
||||
else:
|
||||
message = {
|
||||
"id": "test",
|
||||
"type": "Like",
|
||||
"actor": remote_identity.actor_uri,
|
||||
"object": post.object_uri,
|
||||
}
|
||||
InboxMessage.objects.create(message=message)
|
||||
|
||||
# Run stator thrice - to receive the post, make fanouts and then process them
|
||||
for _ in range(4):
|
||||
stator.run_single_cycle()
|
||||
|
||||
post.refresh_from_db()
|
||||
|
||||
assert post.stats["reactions"] == {"a": 1, "b": 1, "c": 1, "": 1}
|
||||
assert post.stats["likes"] == 1
|
||||
|
||||
|
||||
# TODO: Test that multiple reactions can be added and deleted correctly
|
||||
|
||||
# TODO: How should plain likes and reactions from the same source be handled?
|
||||
# Specifically if we receive an unlike without a specific reaction.
|
||||
|
||||
# Hm, If Misskey is single-reaction, will it send Like interactions for changes
|
||||
# in reaction? Then we're expected to overwrite that users previous interaction
|
||||
# rather than create a new one.
|
Loading…
Reference in a new issue