fix(activitypub): Add quote policy fields to reply objects
All checks were successful
continuous-integration/drone/push Build is passing

- Mirror article quote policy/interactionPolicy logic for replies
- Add test coverage for reply quote policy fields
This commit is contained in:
Fabio Manganiello 2026-03-14 11:34:17 +01:00
commit 8c12db13da
Signed by: blacklight
GPG key ID: D90FBA7F76362774
2 changed files with 93 additions and 0 deletions

View file

@ -194,6 +194,28 @@ class ActivityPubRepliesMixin(ActivityPubPublishMixin):
activity_type = "Update" if self._is_published(url) else "Create" activity_type = "Update" if self._is_published(url) else "Create"
# Build quote policy (same logic as articles)
quote_control = None
quote_policy = None
interaction_policy = None
if config.activitypub_quote_control:
quote_control = {"quotePolicy": config.activitypub_quote_control}
quote_policy = config.activitypub_quote_control
if config.activitypub_quote_control == "public":
allowed = ["https://www.w3.org/ns/activitystreams#Public"]
elif config.activitypub_quote_control == "followers":
allowed = [self.handler.followers_url]
elif config.activitypub_quote_control == "following":
allowed = [self.handler.following_url]
elif config.activitypub_quote_control == "nobody":
allowed = []
else:
allowed = []
can_quote = {"automaticApproval": allowed}
interaction_policy = {"canQuote": can_quote}
obj = Object( obj = Object(
id=url, id=url,
type="Note", # Replies are Notes, not Articles type="Note", # Replies are Notes, not Articles
@ -208,6 +230,9 @@ class ActivityPubRepliesMixin(ActivityPubPublishMixin):
cc=cc_list, cc=cc_list,
tag=mention_tags + hashtag_tags, tag=mention_tags + hashtag_tags,
attachment=attachments, attachment=attachments,
quote_control=quote_control,
quote_policy=quote_policy,
interaction_policy=interaction_policy,
) )
obj.media_type = "text/html" obj.media_type = "text/html"

View file

@ -1752,6 +1752,74 @@ class ReplyMentionTest(unittest.TestCase):
finally: finally:
config.state_dir = orig_state_dir config.state_dir = orig_state_dir
@skip_if_no_pubby
def test_build_reply_object_includes_quote_policy(self):
"""build_reply_object should include quote policy fields like articles."""
from pubby import ActivityPubHandler
from madblog.activitypub import ActivityPubIntegration
from madblog.config import config
with tempfile.TemporaryDirectory() as tmpdir:
pages_dir = Path(tmpdir) / "pages"
pages_dir.mkdir()
replies_dir = Path(tmpdir) / "replies"
replies_dir.mkdir()
state_dir = Path(tmpdir) / "state"
state_dir.mkdir()
(replies_dir / "test-article").mkdir()
reply_file = replies_dir / "test-article" / "reply.md"
reply_file.write_text(
"[//]: # (reply-to: https://example.com/article/test-article)\n\n"
"Test reply content\n"
)
handler = MagicMock(spec=ActivityPubHandler)
handler.actor_path = "/ap/actor"
handler.followers_url = "https://example.com/ap/followers"
handler.following_url = "https://example.com/ap/following"
handler.storage = MagicMock()
handler.storage.get_interaction_by_object_id.return_value = None
orig_state_dir = config.state_dir
orig_quote_control = config.activitypub_quote_control
try:
config.state_dir = str(state_dir)
config.activitypub_quote_control = "public"
integration = ActivityPubIntegration(
handler=handler,
pages_dir=str(pages_dir),
base_url="https://example.com",
replies_dir=str(replies_dir),
)
url = integration.reply_file_to_url(str(reply_file))
actor_url = "https://example.com/ap/actor"
obj, _ = integration.build_reply_object(
str(reply_file), url, actor_url, allow_network=False
)
# Quote policy fields should be present
self.assertEqual(obj.quote_control, {"quotePolicy": "public"})
self.assertEqual(obj.quote_policy, "public")
self.assertEqual(
obj.interaction_policy,
{
"canQuote": {
"automaticApproval": [
"https://www.w3.org/ns/activitystreams#Public"
]
}
},
)
finally:
config.state_dir = orig_state_dir
config.activitypub_quote_control = orig_quote_control
class ReplyCollisionAvoidanceTest(unittest.TestCase): class ReplyCollisionAvoidanceTest(unittest.TestCase):
"""Test collision avoidance for reply delete/recreate scenarios.""" """Test collision avoidance for reply delete/recreate scenarios."""