|
|
|
|
@@ -16,6 +16,7 @@ class ActivityPub {
|
|
|
|
|
private string $desc;
|
|
|
|
|
private string $icon;
|
|
|
|
|
private array $posts = [];
|
|
|
|
|
private string $https;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* コンストラクタ
|
|
|
|
|
@@ -23,11 +24,13 @@ class ActivityPub {
|
|
|
|
|
* @param array $posts 投稿データの配列
|
|
|
|
|
*/
|
|
|
|
|
public function __construct(array $posts = []) {
|
|
|
|
|
$this->domain = $_SERVER['SERVER_NAME'];
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return;
|
|
|
|
|
$this->https = 'http'.((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 's' : '').'://';
|
|
|
|
|
$this->domain = $this->https.($_SERVER['SERVER_NAME'] ?? '127.0.0.1:8000');
|
|
|
|
|
$this->actor = FEDIINFO['actor'];
|
|
|
|
|
$this->actorNick = FEDIINFO['actorNick'];
|
|
|
|
|
$this->desc = FEDIINFO['desc'];
|
|
|
|
|
$this->icon = "https://{$this->domain}".FEDIINFO['icon'];
|
|
|
|
|
$this->icon = "{$this->domain}".FEDIINFO['icon'];
|
|
|
|
|
$this->posts = $posts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -38,6 +41,7 @@ class ActivityPub {
|
|
|
|
|
* @throws \Exception 公開鍵の読み込みに失敗した場合
|
|
|
|
|
*/
|
|
|
|
|
public function getActor(): string {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return '';
|
|
|
|
|
$pubkey = file_get_contents(FEDIINFO['pubkey']);
|
|
|
|
|
if ($pubkey === false) {
|
|
|
|
|
throw new \Exception('公開鍵の受取に失敗。パス:'.FEDIINFO['pubkey']);
|
|
|
|
|
@@ -48,7 +52,7 @@ class ActivityPub {
|
|
|
|
|
'https://www.w3.org/ns/activitystreams',
|
|
|
|
|
'https://w3id.org/security/v1',
|
|
|
|
|
],
|
|
|
|
|
'id' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'id' => "{$this->domain}/ap/actor",
|
|
|
|
|
'name' => $this->actorNick,
|
|
|
|
|
'summary' => $this->desc,
|
|
|
|
|
'manuallyApprovesFollowers' => false,
|
|
|
|
|
@@ -59,22 +63,21 @@ class ActivityPub {
|
|
|
|
|
],
|
|
|
|
|
'image' => [
|
|
|
|
|
'type' => 'Image',
|
|
|
|
|
'url' =>
|
|
|
|
|
"https://{$this->domain}/static/article/o_53803618dc1691.28179609.jpg",
|
|
|
|
|
'url' => "{$this->domain}/static/article/o_53803618dc1691.28179609.jpg",
|
|
|
|
|
'mediaType' => 'image/jpeg',
|
|
|
|
|
],
|
|
|
|
|
'type' => 'Person',
|
|
|
|
|
'url' => "https://{$this->domain}",
|
|
|
|
|
'url' => "{$this->domain}",
|
|
|
|
|
'preferredUsername' => $this->actor,
|
|
|
|
|
'inbox' => "https://{$this->domain}/ap/inbox",
|
|
|
|
|
'outbox' => "https://{$this->domain}/ap/outbox",
|
|
|
|
|
'followers' => "https://{$this->domain}/ap/followers",
|
|
|
|
|
'following' => "https://{$this->domain}/ap/following",
|
|
|
|
|
'inbox' => "{$this->domain}/ap/inbox",
|
|
|
|
|
'outbox' => "{$this->domain}/ap/outbox",
|
|
|
|
|
'followers' => "{$this->domain}/ap/followers",
|
|
|
|
|
'following' => "{$this->domain}/ap/following",
|
|
|
|
|
'published' => '2025-03-28T18:00:00Z',
|
|
|
|
|
'updated' => gmdate('c'),
|
|
|
|
|
'publicKey' => [
|
|
|
|
|
'id' => "https://{$this->domain}/ap/actor#main-key",
|
|
|
|
|
'owner' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'id' => "{$this->domain}/ap/actor#main-key",
|
|
|
|
|
'owner' => "{$this->domain}/ap/actor",
|
|
|
|
|
'publicKeyPem' => $pubkey,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
@@ -89,6 +92,7 @@ class ActivityPub {
|
|
|
|
|
* @return string JSONエンコードされたアクティビティデータ
|
|
|
|
|
*/
|
|
|
|
|
public function getActivity(string $uuid): string {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return '';
|
|
|
|
|
$items = [];
|
|
|
|
|
|
|
|
|
|
foreach ($this->posts as $post) {
|
|
|
|
|
@@ -107,28 +111,27 @@ class ActivityPub {
|
|
|
|
|
'toot' => 'http://joinmastodon.org/ns#',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'id' => "https://{$this->domain}/ap/activities/create/{$post['uuid']}",
|
|
|
|
|
'id' => "{$this->domain}/ap/activities/create/{$post['uuid']}",
|
|
|
|
|
'type' => 'Create',
|
|
|
|
|
'actor' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'actor' => "{$this->domain}/ap/actor",
|
|
|
|
|
'cc' => [
|
|
|
|
|
"https://{$this->domain}/ap/followers",
|
|
|
|
|
"{$this->domain}/ap/followers",
|
|
|
|
|
],
|
|
|
|
|
'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])),
|
|
|
|
|
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
|
|
|
|
'object' => [
|
|
|
|
|
'id' => "https://{$this->domain}/ap/objects/{$post['uuid']}",
|
|
|
|
|
'id' => "{$this->domain}/ap/objects/{$post['uuid']}",
|
|
|
|
|
'type' => 'Note',
|
|
|
|
|
'name' => $post['title'],
|
|
|
|
|
'attributedTo' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'attributedTo' => "{$this->domain}/ap/actor",
|
|
|
|
|
'cc' => [
|
|
|
|
|
"https://{$this->domain}/ap/followers",
|
|
|
|
|
"{$this->domain}/ap/followers",
|
|
|
|
|
],
|
|
|
|
|
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
|
|
|
|
'content' =>
|
|
|
|
|
$post['preview']."<br /><br /><a href=\"https://{$this->domain}/blog/{$post['slug']}\">読み続き</a>",
|
|
|
|
|
'url' => "https://{$this->domain}/blog/{$post['slug']}",
|
|
|
|
|
'content' => $post['preview']."<br /><br /><a href=\"{$this->domain}/blog/{$post['slug']}\">読み続き</a>",
|
|
|
|
|
'url' => "{$this->domain}/blog/{$post['slug']}",
|
|
|
|
|
'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])),
|
|
|
|
|
'replies' => "https://{$this->domain}/ap/objects/{$uuid}/replies",
|
|
|
|
|
'replies' => "{$this->domain}/ap/objects/{$uuid}/replies",
|
|
|
|
|
'sensitive' => false,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
@@ -141,7 +144,7 @@ class ActivityPub {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isset($post['thumbnail']) && $post['thumbnail'] != '') {
|
|
|
|
|
$imgurl = "https://technicalsuwako.moe/static/article/{$post['thumbnail']}";
|
|
|
|
|
$imgurl = "{$this->domain}/static/article/{$post['thumbnail']}";
|
|
|
|
|
$imgpath = ROOT."/public/static/article/{$post['thumbnail']}";
|
|
|
|
|
$imgraw = file_get_contents($imgpath);
|
|
|
|
|
|
|
|
|
|
@@ -167,6 +170,7 @@ class ActivityPub {
|
|
|
|
|
* @return string JSONエンコードされたアウトボックスデータ
|
|
|
|
|
*/
|
|
|
|
|
public function getOutbox(): string {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return '';
|
|
|
|
|
$items = [];
|
|
|
|
|
$counter = 0;
|
|
|
|
|
|
|
|
|
|
@@ -186,28 +190,27 @@ class ActivityPub {
|
|
|
|
|
'toot' => 'http://joinmastodon.org/ns#',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'id' => "https://{$this->domain}/ap/activities/create/{$uid}",
|
|
|
|
|
'id' => "{$this->domain}/ap/activities/create/{$uid}",
|
|
|
|
|
'type' => 'Create',
|
|
|
|
|
'actor' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'actor' => "{$this->domain}/ap/actor",
|
|
|
|
|
'cc' => [
|
|
|
|
|
"https://{$this->domain}/ap/followers",
|
|
|
|
|
"{$this->domain}/ap/followers",
|
|
|
|
|
],
|
|
|
|
|
'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])),
|
|
|
|
|
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
|
|
|
|
'object' => [
|
|
|
|
|
'id' => "https://{$this->domain}/ap/objects/{$uid}",
|
|
|
|
|
'id' => "{$this->domain}/ap/objects/{$uid}",
|
|
|
|
|
'type' => 'Note',
|
|
|
|
|
'name' => $post['title'],
|
|
|
|
|
'attributedTo' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'attributedTo' => "{$this->domain}/ap/actor",
|
|
|
|
|
'cc' => [
|
|
|
|
|
"https://{$this->domain}/ap/followers",
|
|
|
|
|
"{$this->domain}/ap/followers",
|
|
|
|
|
],
|
|
|
|
|
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
|
|
|
|
'content' =>
|
|
|
|
|
$post['preview']."<br /><br /><a href=\"https://{$this->domain}/blog/{$post['slug']}\">読み続き</a>",
|
|
|
|
|
'url' => "https://{$this->domain}/blog/{$post['slug']}",
|
|
|
|
|
'content' => $post['preview']."<br /><br /><a href=\"{$this->domain}/blog/{$post['slug']}\">読み続き</a>",
|
|
|
|
|
'url' => "{$this->domain}/blog/{$post['slug']}",
|
|
|
|
|
'published' => date("Y-m-d\TH:i:s.u\Z", strtotime($post['date'])),
|
|
|
|
|
'replies' => "https://{$this->domain}/ap/objects/{$uid}/replies",
|
|
|
|
|
'replies' => "{$this->domain}/ap/objects/{$uid}/replies",
|
|
|
|
|
'sensitive' => false,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
@@ -220,7 +223,7 @@ class ActivityPub {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isset($post['thumbnail']) && $post['thumbnail'] != '') {
|
|
|
|
|
$imgurl = "https://technicalsuwako.moe/static/article/{$post['thumbnail']}";
|
|
|
|
|
$imgurl = "{$this->domain}/static/article/{$post['thumbnail']}";
|
|
|
|
|
$imgpath = ROOT."/public/static/article/{$post['thumbnail']}";
|
|
|
|
|
$imgraw = file_get_contents($imgpath);
|
|
|
|
|
|
|
|
|
|
@@ -242,7 +245,7 @@ class ActivityPub {
|
|
|
|
|
'https://www.w3.org/ns/activitystreams',
|
|
|
|
|
'https://w3id.org/security/v1',
|
|
|
|
|
],
|
|
|
|
|
'id' => "https://{$this->domain}/ap/outbox",
|
|
|
|
|
'id' => "{$this->domain}/ap/outbox",
|
|
|
|
|
'type' => 'OrderedCollection',
|
|
|
|
|
'totalItems' => count($items),
|
|
|
|
|
'orderedItems' => $items,
|
|
|
|
|
@@ -257,13 +260,14 @@ class ActivityPub {
|
|
|
|
|
* @return string JSONエンコードされたWebFingerデータ
|
|
|
|
|
*/
|
|
|
|
|
public function getWebfinger(): string {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return '';
|
|
|
|
|
$webfinger = [
|
|
|
|
|
'subject' => "acct:{$this->actor}@{$this->domain}",
|
|
|
|
|
'links' => [
|
|
|
|
|
[
|
|
|
|
|
'rel' => 'self',
|
|
|
|
|
'type' => 'application/activity+json',
|
|
|
|
|
'href' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'href' => "{$this->domain}/ap/actor",
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
@@ -277,6 +281,7 @@ class ActivityPub {
|
|
|
|
|
* @return string JSONエンコードされたフォロワーのリスト
|
|
|
|
|
*/
|
|
|
|
|
public function getFollowers(): string {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return '';
|
|
|
|
|
$f = array_filter(explode("\n", file_get_contents(ROOT.'/data/followers.txt')));
|
|
|
|
|
|
|
|
|
|
$followers = [
|
|
|
|
|
@@ -284,7 +289,7 @@ class ActivityPub {
|
|
|
|
|
'https://www.w3.org/ns/activitystreams',
|
|
|
|
|
'https://w3id.org/security/v1',
|
|
|
|
|
],
|
|
|
|
|
'id' => "https://{$this->domain}/ap/followers",
|
|
|
|
|
'id' => "{$this->domain}/ap/followers",
|
|
|
|
|
'type' => 'OrderredCollection',
|
|
|
|
|
'totalItems' => count($f),
|
|
|
|
|
'orderedItems' => $f,
|
|
|
|
|
@@ -299,6 +304,7 @@ class ActivityPub {
|
|
|
|
|
* @return string JSONエンコードされたフォローリスト
|
|
|
|
|
*/
|
|
|
|
|
public function getFollowing(): string {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return '';
|
|
|
|
|
$f = array_filter(explode("\n", file_get_contents(ROOT.'/data/following.txt')));
|
|
|
|
|
|
|
|
|
|
$following = [
|
|
|
|
|
@@ -306,7 +312,7 @@ class ActivityPub {
|
|
|
|
|
'https://www.w3.org/ns/activitystreams',
|
|
|
|
|
'https://w3id.org/security/v1',
|
|
|
|
|
],
|
|
|
|
|
'id' => "https://{$this->domain}/ap/following",
|
|
|
|
|
'id' => "{$this->domain}/ap/following",
|
|
|
|
|
'type' => 'OrderredCollection',
|
|
|
|
|
'totalItems' => count($f),
|
|
|
|
|
'orderedItems' => $f,
|
|
|
|
|
@@ -322,6 +328,7 @@ class ActivityPub {
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function postInbox(array $activity): void {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return;
|
|
|
|
|
switch ($activity['type']) {
|
|
|
|
|
case 'Follow':
|
|
|
|
|
$this->acceptFollower($activity);
|
|
|
|
|
@@ -329,8 +336,7 @@ class ActivityPub {
|
|
|
|
|
default:
|
|
|
|
|
header('HTTP/1.1 501 Not Implemented');
|
|
|
|
|
header('Content-Type: application/activity+json');
|
|
|
|
|
echo json_encode(['error' =>
|
|
|
|
|
'未対応なアクティビティタイプ: '.$activity['type']]);
|
|
|
|
|
echo json_encode(['error' => '未対応なアクティビティタイプ: '.$activity['type']]);
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -346,14 +352,15 @@ class ActivityPub {
|
|
|
|
|
* @return string JSONエンコードされた更新アクティビティ
|
|
|
|
|
*/
|
|
|
|
|
public function update(): string {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return '';
|
|
|
|
|
$update = [
|
|
|
|
|
'@context' => [
|
|
|
|
|
'https://www.w3.org/ns/activitystreams',
|
|
|
|
|
'https://w3id.org/security/v1',
|
|
|
|
|
],
|
|
|
|
|
'id' => "https://{$this->domain}/ap/activities/update/".uuid(),
|
|
|
|
|
'id' => "{$this->domain}/ap/activities/update/".uuid(),
|
|
|
|
|
'type' => 'Update',
|
|
|
|
|
'actor' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'actor' => "{$this->domain}/ap/actor",
|
|
|
|
|
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
|
|
|
|
'object' => json_decode($this->getActor(), true),
|
|
|
|
|
];
|
|
|
|
|
@@ -368,14 +375,13 @@ class ActivityPub {
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function sendActorUpdate(array $params): void {
|
|
|
|
|
if (!ACTIVITYPUB_ENABLED) return;
|
|
|
|
|
$f = array_filter(explode("\n", file_get_contents(ROOT.'/data/followers.txt')));
|
|
|
|
|
$ap = new Activitypub();
|
|
|
|
|
$inboxes = implode("\n", $f);
|
|
|
|
|
$update = json_decode($ap->update(), true);
|
|
|
|
|
|
|
|
|
|
foreach ($f as $inbox) {
|
|
|
|
|
$this->sendActivity($inbox, $update);
|
|
|
|
|
}
|
|
|
|
|
foreach ($f as $inbox) $this->sendActivity($inbox, $update);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 機能性メソッド
|
|
|
|
|
@@ -410,9 +416,7 @@ class ActivityPub {
|
|
|
|
|
'Digest' => "SHA-256=$digest",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$stringToSign = "host: {$headers['Host']}\n"
|
|
|
|
|
."date: {$headers['Date']}\n"
|
|
|
|
|
."digest: {$headers['Digest']}";
|
|
|
|
|
$stringToSign = "host: {$headers['Host']}\n"."date: {$headers['Date']}\n"."digest: {$headers['Digest']}";
|
|
|
|
|
logger(\LogType::ActivityPub, "署名対象: {$stringToSign}");
|
|
|
|
|
|
|
|
|
|
if (!openssl_sign($stringToSign, $signature, $priv, OPENSSL_ALGO_SHA256)) {
|
|
|
|
|
@@ -425,18 +429,16 @@ class ActivityPub {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sigValue = base64_encode($signature);
|
|
|
|
|
$headers['Signature'] = "keyId=\"https://{$this->domain}/ap/actor#main-key\",";
|
|
|
|
|
$headers['Signature'] = "keyId=\"{$this->domain}/ap/actor#main-key\",";
|
|
|
|
|
$headers['Signature'] .= 'algorithm="rsa-sha256",';
|
|
|
|
|
$headers['Signature'] .= 'headers="host date digest",';
|
|
|
|
|
$headers['Signature'] .= 'signature="'.$sigValue.'"';
|
|
|
|
|
logger(\LogType::ActivityPub,
|
|
|
|
|
"署名: {$headers['Signature']}\n送信データ: {$body}");
|
|
|
|
|
logger(\LogType::ActivityPub, "署名: {$headers['Signature']}\n送信データ: {$body}");
|
|
|
|
|
|
|
|
|
|
$curl = new Curl($inboxUrl);
|
|
|
|
|
$curl->setMethod('POST')
|
|
|
|
|
->setPostRaw($body)
|
|
|
|
|
->setHeaders(array_map(fn($k, $v) => "$k: $v",
|
|
|
|
|
array_keys($headers), $headers))
|
|
|
|
|
->setHeaders(array_map(fn($k, $v) => "$k: $v", array_keys($headers), $headers))
|
|
|
|
|
->setCaInfo('/etc/ssl/cert.pem')
|
|
|
|
|
->setVerbose(true)
|
|
|
|
|
->setStderr(fopen(ROOT.'/log/ap_log.txt', 'a'));
|
|
|
|
|
@@ -447,8 +449,7 @@ class ActivityPub {
|
|
|
|
|
$err = $curl->getError();
|
|
|
|
|
|
|
|
|
|
var_dump(print_r($res));
|
|
|
|
|
logger(\LogType::ActivityPub,
|
|
|
|
|
"アクティビティは「{$inboxUrl}」に送信しました: HTTP {$code}");
|
|
|
|
|
logger(\LogType::ActivityPub, "アクティビティは「{$inboxUrl}」に送信しました: HTTP {$code}");
|
|
|
|
|
logger(\LogType::ActivityPub, "エラー: {$err}");
|
|
|
|
|
logger(\LogType::ActivityPub, "レスポンス: {$res}");
|
|
|
|
|
}
|
|
|
|
|
@@ -483,9 +484,9 @@ class ActivityPub {
|
|
|
|
|
'https://www.w3.org/ns/activitystreams',
|
|
|
|
|
'https://w3id.org/security/v1',
|
|
|
|
|
],
|
|
|
|
|
'id' => "https://{$this->domain}/ap/activities/".uniqid(),
|
|
|
|
|
'id' => "{$this->domain}/ap/activities/".uniqid(),
|
|
|
|
|
'type' => 'Accept',
|
|
|
|
|
'actor' => "https://{$this->domain}/ap/actor",
|
|
|
|
|
'actor' => "{$this->domain}/ap/actor",
|
|
|
|
|
'object' => $activity,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
@@ -506,9 +507,7 @@ class ActivityPub {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$followers = $this->getFollowersList();
|
|
|
|
|
if (!in_array($followerActor, $followers)) {
|
|
|
|
|
file_put_contents($file, "$followerActor\n", FILE_APPEND);
|
|
|
|
|
}
|
|
|
|
|
if (!in_array($followerActor, $followers)) file_put_contents($file, "$followerActor\n", FILE_APPEND);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -519,8 +518,7 @@ class ActivityPub {
|
|
|
|
|
private function getFollowersList(): array {
|
|
|
|
|
$file = ROOT.'/data/followers.txt';
|
|
|
|
|
$f = array_filter(explode("\n", file_get_contents($file)));
|
|
|
|
|
return file_exists($file)
|
|
|
|
|
? array_filter(explode("\n", file_get_contents($file))) : [];
|
|
|
|
|
return file_exists($file) ? array_filter(explode("\n", file_get_contents($file))) : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|