empty($n['read']))); } public static function markRead(string $username): void { if (self::unreadCount($username) === 0) return; Fedi::update(self::path("notifications/{$username}.json"), fn($l) => array_map(fn($n) => ['read' => true] + $n, $l)); } /** * 通知を追加する * * @param array $n type、from(username、又はactor・name・handle・url・icon)等。label・page・excerptが有れば表示にそのまま使う * @param callable|null $same 未読の同じ通知が有れば追加しない(fn(array $old): bool) */ public static function add(string $username, array $n, ?callable $same = null): void { if (!preg_match('/^[A-Za-z0-9]+$/', $username)) return; Fedi::update(self::path("notifications/{$username}.json"), function(array $l) use ($n, $same): array { if ($same !== null) foreach ($l as $old) if (empty($old['read']) && $same($old)) return $l; array_unshift($l, $n + ['at' => time(), 'read' => false]); return array_slice($l, 0, self::MAX); }); } //////// メール /** メールを待ち行列に入れる(MAILER_ENABLEDでなければ何もしない) */ public static function queueMail(string $to, string $subject, string $body, string $toName = ''): void { if (!MAILER_ENABLED || !filter_var($to, FILTER_VALIDATE_EMAIL)) return; Fedi::write(self::path('notifications/mail/'.Fedi::newId().'.json'), ['to' => $to, 'subject' => $subject, 'body' => $body, 'name' => $toName, 'tries' => 0]); } /** * 待っているメールを送る(cronのnotify) * * @param int $budget この秒数を超えたら次に回す */ public static function sendMail(int $budget = 50): Result { if (!MAILER_ENABLED) return Result::Success('メールは無効です。'); $start = time(); $sent = $failed = 0; $files = glob(self::path('notifications/mail/').'*.json') ?: []; sort($files); foreach ($files as $file) { if (time() - $start > $budget) break; // 同時に動いた別のcronと重ならない様に、名前を変えて取る $work = $file.'.work'; if (!@rename($file, $work)) continue; $m = Fedi::read($work, null); if (!is_array($m) || !is_string($m['to'] ?? null)) { @unlink($work); continue; } if (Mailer::deliver($m['to'], (string)$m['subject'], (string)$m['body'], (string)($m['name'] ?? '') ?: null)->isSuccess) { @unlink($work); $sent++; continue; } $failed++; $m['tries'] = (int)($m['tries'] ?? 0) + 1; if ($m['tries'] < self::MAIL_TRIES) Fedi::write($file, $m); @unlink($work); } return Result::Success("メール:送信{$sent}件、失敗{$failed}件", ['sent' => $sent, 'failed' => $failed]); } private static function path(string $rel): string { $path = rtrim(self::$dataDir ?? ROOT.'/data', '/').'/'.$rel; $dir = dirname($path); if (!is_dir($dir)) @mkdir($dir, 0700, true); return $path; } }