フェディ返事
守矢諏訪子 <suwako@076.moe>、2026-11-11 16:26
コミット 8834e446cceb9125325c9c76fa0262121a0d274d
・親 a764d87
・ファイルを見る
5個のファイル、+79 -0
変更
route.php
| @@ -78,6 +78,9 @@ | ||
| 78 | 78 | $routes[] = Route::add('GET', 'ap/activities/create/{section}/{uuid}', |
| 79 | 79 | Fediverse::class.'@apactivity'); |
| 80 | 80 | $routes[] = Route::add('GET', 'ap/objects/{uuid}', Fediverse::class.'@apobject'); |
| 81 | + // 「{section}/{uuid}」より先に(「{uuid}/replies」もそれに一致するので) | |
| 82 | + $routes[] = Route::add('GET', 'ap/objects/{uuid}/replies', Fediverse::class.'@apreplies'); | |
| 83 | + $routes[] = Route::add('GET', 'ap/objects/{section}/{uuid}/replies', Fediverse::class.'@apreplies'); | |
| 81 | 84 | $routes[] = Route::add('GET', 'ap/objects/{section}/{uuid}', Fediverse::class.'@apobject'); |
| 82 | 85 | $routes[] = Route::add('POST', 'ap/inbox', Fediverse::class.'@apinbox'); |
| 83 | 86 | $routes[] = Route::add('GET', 'ap/actor', Fediverse::class.'@apactor'); |
| @@ -379,6 +379,31 @@ | ||
| 379 | 379 | } |
| 380 | 380 | |
| 381 | 381 | /** |
| 382 | + * 投稿への返信のコレクション(/ap/objects/{uuid}/replies) | |
| 383 | + * | |
| 384 | + * @param array $params パラメータ配列 | |
| 385 | + * @return void | |
| 386 | + */ | |
| 387 | + public function apreplies(array $params): void { | |
| 388 | + try { | |
| 389 | + header('Content-Type: application/activity+json'); | |
| 390 | + $ap = new Activitypub($this->federatedPosts()); | |
| 391 | + $json = $ap->getReplies($params['uuid'] ?? '', $params['section'] ?? 'blog'); | |
| 392 | + if ($json === '') { | |
| 393 | + header('HTTP/1.1 404 Not Found'); | |
| 394 | + echo json_encode(['error' => '見つかりません']); | |
| 395 | + exit; | |
| 396 | + } | |
| 397 | + echo $json; | |
| 398 | + exit; | |
| 399 | + } catch (\Exception $e) { | |
| 400 | + header('Content-Type: text/plain; charset=utf-8'); | |
| 401 | + echo 'フェディバースの作成に失敗: '.$e->getMessage(); | |
| 402 | + exit; | |
| 403 | + } | |
| 404 | + } | |
| 405 | + | |
| 406 | + /** | |
| 382 | 407 | * @param array $params パラメータ配列 |
| 383 | 408 | * @return void |
| 384 | 409 | */ |
| @@ -173,6 +173,31 @@ | ||
| 173 | 173 | } |
| 174 | 174 | |
| 175 | 175 | /** |
| 176 | + * 投稿への返信のコレクション(Noteの「replies」) | |
| 177 | + * Fediverseから受け取った返信(記事のコメントになった物)だけ。他のサーバーやbrowser.pub等がスレッドを表示するのに使う | |
| 178 | + * | |
| 179 | + * @param string $uuid 投稿のUUID | |
| 180 | + * @param string $section ブログのディレクトリ | |
| 181 | + * @return string JSONエンコードされたOrderedCollection、投稿が無ければ空文字 | |
| 182 | + */ | |
| 183 | + public function getReplies(string $uuid, string $section = 'blog'): string { | |
| 184 | + if (!ACTIVITYPUB_ENABLED) return ''; | |
| 185 | + $post = $this->findPublicPost($uuid, $section); | |
| 186 | + if ($post === null) return ''; | |
| 187 | + | |
| 188 | + $items = Comments::remoteReplies(($post['section'] ?? 'blog').'/'.$post['slug']); | |
| 189 | + $collection = [ | |
| 190 | + '@context' => 'https://www.w3.org/ns/activitystreams', | |
| 191 | + 'id' => "{$this->domain}/ap/objects/".self::postKey($post).'/replies', | |
| 192 | + 'type' => 'OrderedCollection', | |
| 193 | + 'totalItems' => count($items), | |
| 194 | + 'orderedItems' => $items, | |
| 195 | + ]; | |
| 196 | + | |
| 197 | + return json_encode($collection, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 198 | + } | |
| 199 | + | |
| 200 | + /** | |
| 176 | 201 | * アウトボックスデータを取得する(公開投稿のみ、新しい順) |
| 177 | 202 | * |
| 178 | 203 | * @return string JSONエンコードされたアウトボックスデータ |
| @@ -159,6 +159,22 @@ | ||
| 159 | 159 | } |
| 160 | 160 | |
| 161 | 161 | /** |
| 162 | + * Fediverseから受け取った、記事又はコメントへの直接の返信のID(古い順、削除済みを除く) | |
| 163 | + * ローカルのユーザーのコメントはActivityPubのオブジェクトではないので含まない | |
| 164 | + * | |
| 165 | + * @param string $thread スレッド | |
| 166 | + * @param string|null $parent コメントのID(記事への返信ならnull) | |
| 167 | + * @return array リモートの投稿のID | |
| 168 | + */ | |
| 169 | + public static function remoteReplies(string $thread, ?string $parent = null): array { | |
| 170 | + $out = []; | |
| 171 | + foreach (self::all($thread) as $c) { | |
| 172 | + if (empty($c['deleted']) && isset($c['apId']) && ($c['parent'] ?? null) === $parent) $out[] = $c['apId']; | |
| 173 | + } | |
| 174 | + return $out; | |
| 175 | + } | |
| 176 | + | |
| 177 | + /** | |
| 162 | 178 | * Fediverseの返信のIDからコメントを探す |
| 163 | 179 | * |
| 164 | 180 | * @param string $apId リモートの投稿のID |
| @@ -229,6 +229,16 @@ | ||
| 229 | 229 | $test->assertEquals(["comment:{$carol}", 'comment:alice1234'], array_slice($types('admin1234'), 0, 2), '管理者には全部のコメント'); |
| 230 | 230 | }); |
| 231 | 231 | |
| 232 | + $test->it('返信のコレクションはFediverseからの記事への直接の返信だけのはず', function($test) use ($ap, $post, $thread, $target): void { | |
| 233 | + $json = json_decode($ap->getReplies($post['uuid'], 'enblog'), true); | |
| 234 | + $test->assertEquals("{$target}/replies", $json['id']); | |
| 235 | + $test->assertEquals('OrderedCollection', $json['type']); | |
| 236 | + $test->assertEquals(['https://1.1.1.1/users/carol/statuses/1'], $json['orderedItems'], 'ローカルのコメントと返信への返信は含まない'); | |
| 237 | + $test->assertEquals(1, $json['totalItems']); | |
| 238 | + $test->assertEquals('', $ap->getReplies($post['uuid']), 'blogには無い'); | |
| 239 | + $test->assertEquals(['https://1.1.1.1/users/carol/statuses/7'], Comments::remoteReplies($thread, array_key_first(Comments::all($thread)))); | |
| 240 | + }); | |
| 241 | + | |
| 232 | 242 | $test->it('本人の編集と削除だけを反映するはず', function($test) use ($ap, $note, $thread, $carol): void { |
| 233 | 243 | $edited = $note('1', '', ['content' => '<p>編集しました</p>']); |
| 234 | 244 | $test->assertFalse($ap->handleComments(['type' => 'Update', 'actor' => 'https://1.1.1.1/users/dave', 'object' => $edited])); |