diff --git a/.github/changelog/1562-from-description b/.github/changelog/1562-from-description new file mode 100644 index 000000000..acf5ca947 --- /dev/null +++ b/.github/changelog/1562-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Incoming interactions create an Announce activity so other instances get notified about it. diff --git a/includes/collection/class-interactions.php b/includes/collection/class-interactions.php index 545272ccd..10c5c3815 100644 --- a/includes/collection/class-interactions.php +++ b/includes/collection/class-interactions.php @@ -330,8 +330,9 @@ public static function activity_to_comment( $activity ) { 'comment_date' => \get_date_from_gmt( \gmdate( 'Y-m-d H:i:s', \strtotime( $date ) ) ), 'comment_date_gmt' => \gmdate( 'Y-m-d H:i:s', \strtotime( $date ) ), 'comment_meta' => array( - 'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ), - 'protocol' => 'activitypub', + 'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ), + 'protocol' => 'activitypub', + '_activitypub_activity' => $activity, ), ); diff --git a/includes/scheduler/class-comment.php b/includes/scheduler/class-comment.php index 09535f335..87d145d0d 100644 --- a/includes/scheduler/class-comment.php +++ b/includes/scheduler/class-comment.php @@ -7,6 +7,8 @@ namespace Activitypub\Scheduler; +use Activitypub\Activity\Activity; +use Activitypub\Collection\Actors; use Activitypub\Comment as Comment_Utils; use function Activitypub\add_to_outbox; @@ -45,9 +47,12 @@ public static function schedule_comment_activity( $new_status, $old_status, $com } $comment = get_comment( $comment ); + if ( ! $comment ) { + return; + } - // Federate only comments that are written by a registered user. - if ( ! $comment || ! $comment->user_id ) { + if ( ! $comment->user_id ) { + self::maybe_announce_interaction( $new_status, $old_status, $comment ); return; } @@ -81,6 +86,54 @@ public static function schedule_comment_activity( $new_status, $old_status, $com add_to_outbox( $comment, $type, $comment->user_id ); } + /** + * Announce an interaction. + * + * When a comment is received from another ActivityPub instance and approved, + * this method creates an Announce activity so the blog's followers are notified + * about the interaction. + * + * @param string $new_status The new comment status. + * @param string $old_status The old comment status. + * @param \WP_Comment $comment The comment object. + */ + public static function maybe_announce_interaction( $new_status, $old_status, $comment ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return; + } + + if ( 'approved' !== $new_status || 'approved' === $old_status ) { + return; + } + + if ( ! Comment_Utils::was_received( $comment ) ) { + return; + } + + // Get activity from comment meta. + $activity = \get_comment_meta( $comment->comment_ID, '_activitypub_activity', true ); + + // Validate activity structure. + if ( ! $activity || ! \is_array( $activity ) ) { + return; + } + + // Ensure object exists in the activity. + if ( empty( $activity['object'] ) || ! \is_array( $activity['object'] ) ) { + return; + } + + // Create the Announce activity. + $announce = new Activity(); + $announce->set_type( 'Announce' ); + $announce->set_actor( Actors::BLOG_USER_ID ); + $announce->set_object( $activity ); + + // Add to outbox with error handling. + add_to_outbox( $announce, null, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + } + /** * Schedule Comment Activities on insert. * diff --git a/package-lock.json b/package-lock.json index 50e2b659a..1fb208cb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,6 +122,7 @@ "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.25.7", @@ -2098,6 +2099,7 @@ "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2138,6 +2140,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2161,6 +2164,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2295,6 +2299,7 @@ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -4050,6 +4055,7 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -4073,6 +4079,7 @@ "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" }, @@ -4086,6 +4093,7 @@ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, @@ -4568,6 +4576,7 @@ "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" @@ -4595,6 +4604,7 @@ "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", @@ -4623,6 +4633,7 @@ "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -4994,6 +5005,7 @@ "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.56.1" }, @@ -5941,6 +5953,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -6193,8 +6206,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6302,6 +6314,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -6648,6 +6661,7 @@ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -6659,6 +6673,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -6874,6 +6889,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -8269,6 +8285,7 @@ "integrity": "sha512-wPfA0BGnfyMLsiOhkxac2eNXTmcHH5r8nEnfSxf2bDQWRUcbQ2Laae53N0NplGmI79wsBSOXRfMCibM+gyq2rg==", "dev": true, "license": "GPL-2.0-or-later", + "peer": true, "dependencies": { "@inquirer/prompts": "^7.2.0", "chalk": "^4.0.0", @@ -9309,6 +9326,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9402,6 +9420,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -9619,7 +9638,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -10464,6 +10482,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -12261,7 +12280,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -12320,7 +12338,8 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1507524.tgz", "integrity": "sha512-OjaNE7qpk6GRTXtqQjAE5bGx6+c4F1zZH0YXtpZQLM92HNXx4zMAaqlKhP4T52DosG6hDW8gPMNhGOF8xbwk/w==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/diff": { "version": "4.0.2", @@ -12399,8 +12418,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -12983,6 +13001,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -13039,6 +13058,7 @@ "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -16732,6 +16752,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -18432,7 +18453,8 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz", "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/ws": { "version": "8.18.3", @@ -18722,7 +18744,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -19616,6 +19637,7 @@ "integrity": "sha512-cuXAJJB1Rdqz0UO6w524matlBqDBjcNt7Ru+RDIu4y6RI1gVqiWBnylrK8sPRk81gGBA0X8hJbDXolVOoTc+sA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^6.12.6", "ajv-errors": "^1.0.1", @@ -20771,6 +20793,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -21547,6 +21570,7 @@ "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -21569,6 +21593,7 @@ "integrity": "sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -21598,7 +21623,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -21614,7 +21638,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -21627,8 +21650,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/process-nextick-args": { "version": "2.0.1", @@ -21991,6 +22013,7 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -22064,6 +22087,7 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -22100,6 +22124,7 @@ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -22339,7 +22364,8 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -22922,6 +22948,7 @@ "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -23027,6 +23054,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -24563,6 +24591,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -24898,6 +24927,7 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -25673,6 +25703,7 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -26320,6 +26351,7 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -26428,6 +26460,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -26508,6 +26541,7 @@ "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -27194,6 +27228,7 @@ "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lib0": "^0.2.99" }, diff --git a/tests/phpunit/tests/includes/scheduler/class-test-comment.php b/tests/phpunit/tests/includes/scheduler/class-test-comment.php index 2a204031f..2d70e9d9a 100644 --- a/tests/phpunit/tests/includes/scheduler/class-test-comment.php +++ b/tests/phpunit/tests/includes/scheduler/class-test-comment.php @@ -225,4 +225,251 @@ public function test_no_delete_activity_for_non_federated_comment() { $this->assertEmpty( $outbox_posts, 'Should not create Delete activity for non-federated comment deletion' ); } + + /** + * Test that no announce is created when not in Blog and User mode. + * + * @covers ::maybe_announce_interaction + */ + public function test_no_announce_when_not_in_blog_and_user_mode() { + $post_id = self::factory()->post->create( array( 'post_author' => self::$user_id ) ); + + // Set actor mode to User only mode. + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ); + + $activity = array( + 'type' => 'Create', + 'actor' => 'https://example.com/users/testuser', + 'object' => array( + 'type' => 'Note', + 'content' => 'Test comment content', + ), + ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 0, + 'comment_meta' => array( + 'protocol' => 'activitypub', + '_activitypub_activity' => $activity, + ), + ) + ); + + // Get count of Announce activities before approval. + $before_count = \count( + \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'numberposts' => -1, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Announce', + ), + ), + ) + ) + ); + + \wp_set_comment_status( $comment_id, 'approve' ); + + // Get count after approval. + $after_count = \count( + \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'numberposts' => -1, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Announce', + ), + ), + ) + ) + ); + + $this->assertEquals( $before_count, $after_count, 'Should not create Announce when not in Blog and User mode' ); + + \wp_delete_comment( $comment_id, true ); + } + + /** + * Test that no announce is created for non-received comments. + * + * @covers ::maybe_announce_interaction + */ + public function test_no_announce_for_non_received_comments() { + $post_id = self::factory()->post->create( array( 'post_author' => self::$user_id ) ); + + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + // Create a comment WITHOUT ActivityPub protocol meta (not received). + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 0, + ) + ); + + $before_count = \count( + \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'numberposts' => -1, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Announce', + ), + ), + ) + ) + ); + + \wp_set_comment_status( $comment_id, 'approve' ); + + $after_count = \count( + \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'numberposts' => -1, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Announce', + ), + ), + ) + ) + ); + + $this->assertEquals( $before_count, $after_count, 'Should not create Announce for non-received comments' ); + + \wp_delete_comment( $comment_id, true ); + } + + /** + * Test that no announce is created when activity data is missing. + * + * @covers ::maybe_announce_interaction + */ + public function test_no_announce_when_activity_data_missing() { + $post_id = self::factory()->post->create( array( 'post_author' => self::$user_id ) ); + + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + // Create a comment with protocol meta but no activity data. + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 0, + 'comment_meta' => array( + 'protocol' => 'activitypub', + ), + ) + ); + + $before_count = \count( + \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'numberposts' => -1, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Announce', + ), + ), + ) + ) + ); + + \wp_set_comment_status( $comment_id, 'approve' ); + + $after_count = \count( + \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'numberposts' => -1, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Announce', + ), + ), + ) + ) + ); + + $this->assertEquals( $before_count, $after_count, 'Should not create Announce when activity data is missing' ); + + \wp_delete_comment( $comment_id, true ); + } + + /** + * Test that no announce is created when activity is malformed. + * + * @covers ::maybe_announce_interaction + */ + public function test_no_announce_when_activity_malformed() { + $post_id = self::factory()->post->create( array( 'post_author' => self::$user_id ) ); + + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + // Create a comment with malformed activity (missing 'object' field). + $malformed_activity = array( + 'type' => 'Create', + 'actor' => 'https://example.com/users/testuser', + ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 0, + 'comment_meta' => array( + 'protocol' => 'activitypub', + '_activitypub_activity' => $malformed_activity, + ), + ) + ); + + $before_count = \count( + \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'numberposts' => -1, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Announce', + ), + ), + ) + ) + ); + + \wp_set_comment_status( $comment_id, 'approve' ); + + $after_count = \count( + \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'numberposts' => -1, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => '_activitypub_activity_type', + 'value' => 'Announce', + ), + ), + ) + ) + ); + + $this->assertEquals( $before_count, $after_count, 'Should not create Announce when activity is malformed' ); + + \wp_delete_comment( $comment_id, true ); + } }