master
ブランチ master
LittleBeast / src / Std / Lib / Fedi / Delivery.php
9.5 KB、231行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, and/or sell copies of the Software, subject to the following conditions:
10
11 1. **Origin Attribution**:
12 - You must not misrepresent the origin of the Software; you must not claim
13 you created the original Software.
14 - If the Software is used in a product, you must either:
15 a. Provide clear attribution in the product's documentation, user interface,
16 or other visible areas, **OR**
17 b. Pay the original developers a fee they specify in writing.
18 2. **Usage Restriction**:
19 - The Software, or any derivative works, dependencies, or libraries
20 incorporating it, must not be used for censorship or to suppress freedom of
21 speech, expression, or creativity. Prohibited uses include, but are not
22 limited to:
23 - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
24 ideas, or objective reality.
25 - Tools or systems designed to restrict access to information or
26 artistic works.
27 3. **Notice Preservation**:
28 - This license and the above copyright notice must remain intact in all copies
29 of the source code.
30
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Lib\Fedi;
39
40use Std\Lib\Activitypub;
41use Std\Lib\Curl;
42
43/**
44 * 配信の待ち行列(data/ap/queue/)
45 *
46 * 送るアクティビティは一旦ファイルにして、cronのapdeliverが毎分送る(Webのリクエストを待たせない為)。
47 * 失敗したら間隔を空けて再試行し、MAX_TRIES回で諦める。
48 * 宛先がこのサイトなら、HTTPを使わずに直接受付ボックスの処理を呼ぶ。
49 */
50class Delivery {
51 public const MAX_TRIES = 8;
52 /** 再試行までの秒数(回数毎) */
53 private const BACKOFF = [60, 300, 1800, 7200, 21600, 43200, 86400, 86400];
54
55 /**
56 * アクティビティを送る(ローカルの宛先はすぐに処理し、リモートは待ち行列に入れる)
57 *
58 * @param string $from 送るユーザー(「gitler:所有者/名前」はGitlerのリポジトリ)
59 * @param array $activity アクティビティ
60 * @param array $inboxes 受付ボックスのURL
61 * @return void
62 */
63 public static function enqueue(string $from, array $activity, array $inboxes): void {
64 $inboxes = array_values(array_unique(array_filter($inboxes, 'is_string')));
65 $local = false;
66 foreach ($inboxes as $inbox) {
67 if (Fedi::isLocal($inbox)) {
68 $local = true;
69 continue;
70 }
71 if (Actor::isDomainBlocked($inbox)) continue;
72 $job = ['from' => $from, 'inbox' => $inbox, 'activity' => $activity, 'tries' => 0, 'next' => time()];
73 Fedi::write(Fedi::path('queue/'.Fedi::newId().'.json'), $job);
74 }
75 // ローカルの宛先は1回だけ処理する(共有の受付ボックスなので、受け取る全員に届く)
76 if ($local) Inbox::handle($activity, true);
77 }
78
79 /**
80 * アクティビティの宛先の受付ボックス(to・cc・フォロワー)
81 *
82 * @param string $from 送るユーザー
83 * @param array $activity アクティビティ
84 * @param array $extra 他に送るアクター(返信先の投稿者等)
85 * @return array 受付ボックス(sharedInboxがあればそちら、重複なし)
86 */
87 public static function audience(string $from, array $activity, array $extra = []): array {
88 $obj = is_array($activity['object'] ?? null) ? $activity['object'] : [];
89 $targets = array_merge(
90 Fedi::ids($activity['to'] ?? []), Fedi::ids($activity['cc'] ?? []),
91 Fedi::ids($obj['to'] ?? []), Fedi::ids($obj['cc'] ?? []), $extra
92 );
93 $followersUrl = Fedi::actorUrl($from).'/followers';
94 $inboxes = [];
95 foreach (array_unique($targets) as $t) {
96 if ($t === Fedi::PUBLIC || $t === Fedi::actorUrl($from)) continue;
97 if ($t === $followersUrl) {
98 foreach (Actor::followers($from) as $actor => $f) {
99 $inbox = $f['sharedInbox'] ?? $f['inbox'] ?? null;
100 if ($inbox) $inboxes[] = $inbox;
101 }
102 continue;
103 }
104 if (str_ends_with($t, '/followers')) continue; // 他人のフォロワーには送れない
105 $doc = Remote::actor($t);
106 if (!$doc) continue;
107 $inbox = Remote::inbox($doc);
108 if ($inbox) $inboxes[] = $inbox;
109 }
110 return array_values(array_unique($inboxes));
111 }
112
113 /**
114 * 送る時刻になったアクティビティを送る(cronのapdeliver)
115 *
116 * @param int $budget この秒数を超えたら次の実行に回す
117 * @return \Result data:sent, failed, dropped
118 */
119 public static function run(int $budget = 50): \Result {
120 if (!Fedi::enabled()) return \Result::Error('マルチユーザーのFediverseは無効です。');
121 $lock = fopen(Fedi::path('queue.lock'), 'c');
122 if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) return \Result::Success('前の配信がまだ終わっていません。');
123
124 $start = time();
125 $sent = $failed = $dropped = $filled = 0;
126 try {
127 $files = glob(Fedi::path('queue/').'*.json') ?: [];
128 sort($files);
129 foreach ($files as $file) {
130 if (time() - $start > $budget) break;
131 $job = Fedi::read($file, null);
132 if (!$job) {
133 @unlink($file);
134 continue;
135 }
136 if (($job['next'] ?? 0) > time()) continue;
137
138 if (static::send($job['from'], $job['inbox'], $job['activity'])) {
139 @unlink($file);
140 $sent++;
141 continue;
142 }
143 $job['tries']++;
144 if ($job['tries'] >= self::MAX_TRIES) {
145 logger(\LogType::ActivityPub, "配信を諦めました:{$job['activity']['type']} → {$job['inbox']}");
146 @unlink($file);
147 $dropped++;
148 continue;
149 }
150 $job['next'] = time() + self::BACKOFF[$job['tries'] - 1];
151 Fedi::write($file, $job);
152 $failed++;
153 }
154 $filled = static::runBackfill($budget - (time() - $start));
155 } finally {
156 flock($lock, LOCK_UN);
157 fclose($lock);
158 }
159
160 $data = ['sent' => $sent, 'failed' => $failed, 'dropped' => $dropped, 'backfilled' => $filled];
161 return \Result::Success("配信:成功{$sent}件、失敗{$failed}件、諦めた{$dropped}件、取得した投稿{$filled}件", $data);
162 }
163
164 /**
165 * フォローが承認された相手の最近の投稿を、次の配信の時に取得する
166 */
167 public static function queueBackfill(string $actor): void {
168 if (Fedi::isLocal($actor)) return;
169 Fedi::update(Fedi::path('backfill.json'), fn($l) => $l + [$actor => time()]);
170 }
171
172 /**
173 * 待っている相手の投稿を取得する(1人1回だけ。失敗しても、新しい投稿はこれから届く)
174 *
175 * @param int $budget 使って良い秒数
176 * @return int タイムラインに入れた件数
177 */
178 protected static function runBackfill(int $budget): int {
179 $start = time();
180 $n = 0;
181 foreach (array_keys(Fedi::read(Fedi::path('backfill.json'))) as $actor) {
182 if (time() - $start > $budget) break;
183 Fedi::update(Fedi::path('backfill.json'), function($l) use ($actor) {
184 unset($l[$actor]);
185 return $l;
186 });
187 $n += Social::backfill((string)$actor);
188 }
189 return $n;
190 }
191
192 /** 待ち行列の件数 */
193 public static function pending(): int {
194 return count(glob(Fedi::path('queue/').'*.json') ?: []);
195 }
196
197 /**
198 * 署名して送る
199 *
200 * @param string $from 送るユーザー(「gitler:所有者/名前」はGitlerのリポジトリ)
201 * @param string $inbox 受付ボックス
202 * @param array $activity アクティビティ
203 * @return bool 2xxならtrue
204 */
205 protected static function send(string $from, string $inbox, array $activity): bool {
206 if (!Remote::isSafeUrl($inbox)) return false;
207 // Gitlerのリポジトリ(「gitler:所有者/名前」)はリポジトリの鍵
208 [$key, $keyId] = str_starts_with($from, 'gitler:') ? (\Std\Lib\Gitler\Federation::signer(substr($from, 7)) ?? [null, ''])
209 : [Actor::privateKey($from), Actor::keyId($from)];
210 if ($key === null) return false;
211
212 $body = json_encode($activity, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
213 $headers = (new Activitypub())->signRequest($inbox, $body, $key, 'post', $keyId);
214 if ($headers === null) return false;
215
216 $curl = new Curl($inbox);
217 $curl->setMethod('POST')
218 ->setPostRaw($body)
219 ->setHeaders($headers)
220 ->setFollowRedirects(false)
221 ->setTimeout(15)
222 ->setCaInfo('/etc/ssl/cert.pem');
223 $res = $curl->execute();
224 $code = $curl->getResponseCode();
225 $ok = $res->isSuccess && $code >= 200 && $code < 300;
226 // 410(もう無い)は再試行しても無駄
227 if ($code === 410) return true;
228 if (!$ok) logger(\LogType::ActivityPub, "{$activity['type']}を「{$inbox}」に送れません:HTTP {$code} {$curl->getError()}");
229 return $ok;
230 }
231}