'actorがありません']]; // ローカルのアクターを名乗るリモートのアクティビティは受け付けない if (!$local && Fedi::isLocal($actor)) return [403, ['error' => '不正なactor']]; if (Actor::isDomainBlocked($actor)) return [403, ['error' => 'ブロックされています']]; try { return match ($type) { 'Follow' => self::follow($activity, $actor), 'Accept' => self::accept($activity, $actor, true), 'Reject' => self::accept($activity, $actor, false), 'Undo' => self::undo($activity, $actor), 'Create' => self::create($activity, $actor, $local), 'Update' => self::update($activity, $actor), 'Delete' => self::delete($activity, $actor), 'Like' => self::like($activity, $actor), 'Announce' => self::announce($activity, $actor, $local), 'Block' => self::block($activity, $actor), 'Flag' => self::flag($activity, $actor), default => [202, ['status' => '未対応なアクティビティタイプ: '.$type]], }; } catch (\Throwable $e) { logger(\LogType::ActivityPub, "受付ボックスの処理に失敗({$type}):".$e->getMessage()); return [500, ['error' => '処理に失敗']]; } } ////// private static function follow(array $a, string $actor): array { $target = Fedi::id($a['object'] ?? null); if ($target === Fedi::blogActorUrl()) return (new Activitypub())->handleInbox($a); $username = $target ? Fedi::localUsername($target) : null; if ($username === null || Fedi::localActor($username) === null) return [404, ['error' => 'アカウントが見つかりません']]; if (Actor::isBlocked($username, $actor)) { Social::rejectFollow($username, $a); return [202, ['status' => '拒否しました']]; } if (Actor::isFollowedBy($username, $actor)) { // 既にフォロワー:もう一度Acceptを返す(相手が状態を失った場合) Social::acceptFollow($username, $a); return [202, ['status' => 'OK']]; } if (Actor::settings($username)['locked']) { Fedi::update(Fedi::userPath($username, 'requests.json'), fn($l) => [$actor => ['activity' => $a, 'at' => time()]] + $l); Actor::notify($username, 'follow_request', $actor); return [202, ['status' => '承認待ち']]; } Social::acceptFollow($username, $a); return [202, ['status' => 'OK']]; } /** * 自分が送ったFollowへのAccept・Reject * Rejectはフォロワーを外された場合(Followが自分の場合)もある */ private static function accept(array $a, string $actor, bool $accepted): array { $obj = $a['object'] ?? null; $followId = Fedi::id($obj); $follower = is_array($obj) ? Fedi::id($obj['actor'] ?? null) : null; foreach (Social::localUsernames() as $u) { $me = Fedi::actorUrl($u); $f = Actor::following($u)[$actor] ?? null; if ($f === null) continue; if ($follower !== null && $follower !== $me) continue; if ($follower === null && $followId !== $f['id']) continue; Fedi::update(Fedi::userPath($u, 'following.json'), function($l) use ($actor, $accepted) { if ($accepted) $l[$actor]['state'] = 'accepted'; else unset($l[$actor]); return $l; }); // 相手の最近の投稿をホームに入れる(取得はcronで。ここで待たせると相手のサーバーが再送する) if ($accepted) Delivery::queueBackfill($actor); } return [202, ['status' => 'OK']]; } private static function undo(array $a, string $actor): array { $inner = $a['object'] ?? null; if (is_string($inner)) $inner = self::findOwnActivity($inner, $actor); if (!is_array($inner)) return [202, ['status' => '無視しました']]; // 他人のアクティビティは取り消せない if (Fedi::id($inner['actor'] ?? null) !== $actor) return [202, ['status' => '無視しました']]; switch ($inner['type'] ?? '') { case 'Follow': $target = Fedi::id($inner['object'] ?? null); if ($target === Fedi::blogActorUrl()) return (new Activitypub())->handleInbox($a); $username = $target ? Fedi::localUsername($target) : null; if ($username !== null) { Fedi::update(Fedi::userPath($username, 'followers.json'), function($l) use ($actor) { unset($l[$actor]); return $l; }); Fedi::update(Fedi::userPath($username, 'requests.json'), function($l) use ($actor) { unset($l[$actor]); return $l; }); } break; case 'Like': self::modifyLocalNote(Fedi::id($inner['object'] ?? null), function($n) use ($actor) { unset($n['likes'][$actor]); return $n; }); break; case 'Announce': $obj = Fedi::id($inner['object'] ?? null); self::modifyLocalNote($obj, function($n) use ($actor) { unset($n['boosts'][$actor]); return $n; }); if ($obj) Timeline::remove($obj, $actor); break; } return [202, ['status' => 'OK']]; } private static function create(array $a, string $actor, bool $local): array { $obj = $a['object'] ?? null; if (is_string($obj)) $obj = Remote::object($obj); if (!is_array($obj) || !in_array($obj['type'] ?? '', ['Note', 'Article', 'Question', 'Page'], true)) return [202, ['status' => '無視しました']]; if (Fedi::id($obj['attributedTo'] ?? null) !== $actor) return [403, ['error' => '作者が違います']]; $id = Fedi::id($obj['id'] ?? null); if ($id === null || Fedi::domainOf($id) !== Fedi::domainOf($actor)) return [403, ['error' => '不正なID']]; $addressed = self::addressedLocalUsers($obj); $parent = Fedi::id($obj['inReplyTo'] ?? null); $parentIds = $parent ? Fedi::localNoteId($parent) : null; if (!$local) { // 誰にも関係の無い投稿は保存しない(スパム対策) $followers = self::localFollowersOf($actor); if (empty($addressed) && empty($followers) && $parentIds === null) return [202, ['status' => '無視しました']]; $obj['_receivedAt'] = time(); Remote::store($obj); Timeline::add($obj); // 表示用にアクターをキャッシュする(1日1回まで取得) Remote::actor($actor); } // 返信と通知 if ($parentIds !== null) { Notes::modify($parentIds[1], function($n) use ($id) { if (!in_array($id, $n['replies'] ?? [], true)) $n['replies'][] = $id; return $n; }); if ($parentIds[0] !== Fedi::localUsername($actor)) Actor::notify($parentIds[0], 'reply', $actor, $id); } foreach ($addressed as $u) { if ($parentIds !== null && $u === $parentIds[0]) continue; if (Fedi::localUsername($actor) === $u || Actor::isBlocked($u, $actor)) continue; Actor::notify($u, 'mention', $actor, $id); } return [202, ['status' => 'OK']]; } private static function update(array $a, string $actor): array { $obj = $a['object'] ?? null; if (!is_array($obj)) return [202, ['status' => '無視しました']]; if (Remote::isActor($obj)) { if (($obj['id'] ?? null) === $actor) Remote::cacheActor($obj); return [202, ['status' => 'OK']]; } $id = Fedi::id($obj['id'] ?? null); if ($id === null || Fedi::isLocal($id)) return [202, ['status' => 'OK']]; $old = Remote::stored($id); if (!$old || Fedi::id($old['attributedTo'] ?? null) !== $actor || Fedi::id($obj['attributedTo'] ?? null) !== $actor) { return [202, ['status' => '無視しました']]; } $obj['_receivedAt'] = $old['_receivedAt'] ?? time(); Remote::store($obj); Timeline::refresh($obj); return [202, ['status' => 'OK']]; } private static function delete(array $a, string $actor): array { $id = Fedi::id($a['object'] ?? null); if ($id === null) return [202, ['status' => '無視しました']]; if ($id === $actor) { // アカウントの削除:全部から消す $blog = (new Activitypub())->handleInbox($a); foreach (Social::localUsernames() as $u) { foreach (['followers.json', 'following.json', 'requests.json'] as $f) { Fedi::update(Fedi::userPath($u, $f), function($l) use ($actor) { unset($l[$actor]); return $l; }); } } foreach (Timeline::entries() as $e) { if ($e['author'] === $actor && !$e['local']) Remote::forget($e['object']); } Timeline::removeActor($actor); Remote::forgetActor($actor); return [202, ['status' => 'OK']]; } $stored = Remote::stored($id); if ($stored && Fedi::id($stored['attributedTo'] ?? null) === $actor) { Remote::forget($id); Timeline::remove($id); $parent = Fedi::id($stored['inReplyTo'] ?? null); self::modifyLocalNote($parent, function($n) use ($id) { $n['replies'] = array_values(array_diff($n['replies'] ?? [], [$id])); return $n; }); } return [202, ['status' => 'OK']]; } private static function like(array $a, string $actor): array { $obj = Fedi::id($a['object'] ?? null); $ids = $obj ? Fedi::localNoteId($obj) : null; if ($ids === null) return [202, ['status' => '無視しました']]; if (Actor::isBlocked($ids[0], $actor)) return [202, ['status' => '無視しました']]; $id = Fedi::id($a['id'] ?? null) ?? ''; self::modifyLocalNote($obj, function($n) use ($actor, $id) { $n['likes'][$actor] = $id; return $n; }); if (Fedi::localUsername($actor) !== $ids[0]) Actor::notify($ids[0], 'like', $actor, $obj); return [202, ['status' => 'OK']]; } private static function announce(array $a, string $actor, bool $local): array { $objId = Fedi::id($a['object'] ?? null); if ($objId === null) return [202, ['status' => '無視しました']]; $ids = Fedi::localNoteId($objId); $id = Fedi::id($a['id'] ?? null) ?? ''; if ($ids !== null && !Actor::isBlocked($ids[0], $actor)) { self::modifyLocalNote($objId, function($n) use ($actor, $id) { $n['boosts'][$actor] = $id; return $n; }); if (Fedi::localUsername($actor) !== $ids[0]) Actor::notify($ids[0], 'boost', $actor, $objId); } // フォローしている人のブーストはタイムラインに載せる if (!$local && !empty(self::localFollowersOf($actor))) { // 埋め込まれたオブジェクトは偽装出来るので、IDから取得する $obj = Remote::object($objId); if ($obj && in_array(Fedi::visibility(Fedi::ids($obj['to'] ?? []), Fedi::ids($obj['cc'] ?? [])), ['public', 'unlisted'], true)) { if (!Fedi::isLocal($objId)) Remote::store($obj); Timeline::add($obj, $actor, strtotime((string)($a['published'] ?? '')) ?: time()); } } return [202, ['status' => 'OK']]; } private static function block(array $a, string $actor): array { $target = Fedi::id($a['object'] ?? null); $username = $target ? Fedi::localUsername($target) : null; if ($username === null) return [202, ['status' => '無視しました']]; foreach (['followers.json', 'following.json', 'requests.json'] as $f) { Fedi::update(Fedi::userPath($username, $f), function($l) use ($actor) { unset($l[$actor]); return $l; }); } return [202, ['status' => 'OK']]; } private static function flag(array $a, string $actor): array { $objects = Fedi::ids($a['object'] ?? []); if (empty($objects)) return [202, ['status' => '無視しました']]; Fedi::update(Fedi::path('reports.json'), function($l) use ($a, $actor, $objects) { array_unshift($l, ['id' => Fedi::newId(), 'from' => $actor, 'actor' => $objects[0], 'objects' => array_slice($objects, 1), 'content' => mb_substr(Html::text((string)($a['content'] ?? '')), 0, 1000), 'at' => time(), 'remote' => true, 'resolved' => false]); return array_slice($l, 0, 500); }); return [202, ['status' => 'OK']]; } ////// /** * このアクターをフォローしているローカルのユーザー */ public static function localFollowersOf(string $actor): array { $out = []; foreach (Social::localUsernames() as $u) { if (Actor::isFollowing($u, $actor)) $out[] = $u; } return $out; } /** * 投稿の宛先(to・cc・メンション)に含まれるローカルのユーザー */ private static function addressedLocalUsers(array $obj): array { $targets = array_merge(Fedi::ids($obj['to'] ?? []), Fedi::ids($obj['cc'] ?? [])); foreach ($obj['tag'] ?? [] as $t) { if (is_array($t) && ($t['type'] ?? '') === 'Mention' && is_string($t['href'] ?? null)) $targets[] = $t['href']; } $out = []; foreach (array_unique($targets) as $t) { $u = Fedi::localUsername($t); if ($u !== null && Fedi::localActor($u) !== null) $out[] = $u; } return array_values(array_unique($out)); } private static function modifyLocalNote(?string $objectId, callable $fn): void { $ids = $objectId ? Fedi::localNoteId($objectId) : null; if ($ids) Notes::modify($ids[1], $fn); } /** * UndoのobjectがIDだけの場合:このアクターの以前のアクティビティを探す(いいね・ブースト・フォロー) */ private static function findOwnActivity(string $id, string $actor): ?array { foreach (Timeline::entries() as $e) { if (!$e['local'] || $e['type'] !== 'note') continue; $ids = Fedi::localNoteId($e['object']); $n = $ids ? Notes::get($ids[1]) : null; if (!$n) continue; if (($n['likes'][$actor] ?? null) === $id) return ['type' => 'Like', 'actor' => $actor, 'object' => $e['object']]; if (($n['boosts'][$actor] ?? null) === $id) return ['type' => 'Announce', 'actor' => $actor, 'object' => $e['object']]; } return null; } }