$subject, 'public_key' => $public, 'private_key' => $private, '_source' => $candidate, ]; } } return null; } function mvlog_write_temp_push_json(array $cfg): ?string { $tmp = tempnam(sys_get_temp_dir(), 'mvlog-push-'); if ($tmp === false) return null; $payload = json_encode([ 'subject' => (string)($cfg['subject'] ?? ''), 'public_key' => (string)($cfg['public_key'] ?? ''), 'private_key' => (string)($cfg['private_key'] ?? ''), ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (!is_string($payload) || @file_put_contents($tmp, $payload, LOCK_EX) === false) { @unlink($tmp); return null; } @chmod($tmp, 0600); return $tmp; } function mvlog_nice_title(string $s): string { return trim(ucwords(str_replace(['_', '-'], ' ', $s))); } function mvlog_article_id_is_valid(string $id): bool { return (bool)preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id); } function mvlog_read_article_id(string $jobdir): string { $path = rtrim($jobdir, '/') . '/.mvlog-id'; if (!is_file($path)) return ''; $value = strtolower(trim((string)@file_get_contents($path))); return mvlog_article_id_is_valid($value) ? $value : ''; } function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = null, $logfile = null) { if ($mvlog_root === null) $mvlog_root = dirname(__DIR__); if ($logfile === null) $logfile = '/var/log/mvlog_notify.log'; $subs_file = $mvlog_root . '/cache/push_subscriptions.json'; $cache_file = $mvlog_root . '/cache/push_notifications.json'; $job = basename((string)$jobdir); $articleRef = strtolower(trim((string)$articleRefOrJob)); $articleId = mvlog_article_id_is_valid($articleRef) ? $articleRef : mvlog_read_article_id((string)$jobdir); if ($articleId === '') { error_log("[mvlog] article id missing, skipping push for $job\n", 3, $logfile); return false; } $identity = $articleId; // Don't notify for hidden jobs if (is_file($jobdir . '/.mvlog-hidden')) { error_log("[mvlog] job $job hidden, skipping push\n", 3, $logfile); return false; } // Gather subscriptions if (!is_file($subs_file)) { error_log("[mvlog] subscriptions file missing, skipping push for $identity\n", 3, $logfile); return false; } $subs_raw = @file_get_contents($subs_file); if (!is_string($subs_raw) || trim($subs_raw) === '') { error_log("[mvlog] subscriptions empty, skipping push for $identity\n", 3, $logfile); return false; } $subs = json_decode($subs_raw, true); if (!is_array($subs) || count($subs) === 0) { error_log("[mvlog] subscriptions invalid/empty, skipping push for $identity\n", 3, $logfile); return false; } // Read cache and check if already shown $cache = []; if (is_file($cache_file)) { $cache = json_decode((string)@file_get_contents($cache_file), true) ?: []; } $shown = isset($cache['shown']) && is_array($cache['shown']) ? $cache['shown'] : []; // Transition support: honor both new (ID) and legacy (job name) keys. if (isset($shown[$identity]) || ($identity !== $job && isset($shown[$job]))) { error_log("[mvlog] push already sent for $identity (job=$job), skipping\n", 3, $logfile); return false; } // Resolve push config consistently (same source model as push_config.php) $cfg = mvlog_load_push_config($mvlog_root); if (!$cfg) { error_log("[mvlog] push config not found/invalid, skipping push for $identity\n", 3, $logfile); return false; } $tmp_push_config = mvlog_write_temp_push_json($cfg); if (!$tmp_push_config) { error_log("[mvlog] failed to create temp push config for $identity\n", 3, $logfile); return false; } // Determine article title (used both in notification text and filtered URL). $articleTitle = mvlog_nice_title((string)$job); if (preg_match('/^\d{8}[_-](.+)$/', (string)$job, $m)) { $articleTitle = mvlog_nice_title((string)$m[1]); } $state_file = $jobdir . '/.movmaker-state.json'; if (is_file($state_file)) { $state = json_decode((string)@file_get_contents($state_file), true) ?: []; if (!empty($state['output']) && is_string($state['output'])) { $out = pathinfo($state['output'], PATHINFO_FILENAME); if ($out) { if (preg_match('/^\d{8}[_-](.+)$/', (string)$out, $m)) { $articleTitle = mvlog_nice_title((string)$m[1]); } else { $articleTitle = mvlog_nice_title((string)$out); } } } } if ($articleTitle === '') $articleTitle = mvlog_nice_title((string)$job); $articleUrl = is_file($jobdir . '/.mvlog-permalink') ? './?id=' . rawurlencode($articleId) : './'; $payload = json_encode([ 'title' => $articleTitle, 'body' => 'New unreliable article @ MVLog!', 'url' => $articleUrl, 'tag' => "mvlog-show-{$identity}", ], JSON_UNESCAPED_UNICODE); // Call node send_push.js with subscriptions on stdin $send_script = escapeshellarg($mvlog_root . '/send_push.js'); $push_config_esc = escapeshellarg($tmp_push_config); $payload_esc = escapeshellarg((string)$payload); $cmd = "node $send_script $push_config_esc $payload_esc"; $descriptorspec = [ 0 => ['pipe', 'r'], // stdin 1 => ['pipe', 'w'], // stdout 2 => ['pipe', 'w'], // stderr ]; $proc = proc_open($cmd, $descriptorspec, $pipes); if (!is_resource($proc)) { @unlink($tmp_push_config); error_log("[mvlog] failed to start node send_push process for $identity\n", 3, $logfile); return false; } // Write subscriptions to stdin fwrite($pipes[0], $subs_raw); fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); $rc = proc_close($proc); @unlink($tmp_push_config); if ($rc !== 0) { error_log("[mvlog] send_push failed for $identity rc=$rc stdout=" . trim((string)$stdout) . " stderr=" . trim((string)$stderr) . "\n", 3, $logfile); return false; } // On success, record in cache['shown'] if (!is_array($cache)) $cache = []; if (!isset($cache['shown']) || !is_array($cache['shown'])) $cache['shown'] = []; $cache['shown'][$identity] = [ 'sent_at' => date(DATE_ATOM), 'job' => $job, 'article_id' => $articleId, ]; // Atomic write: write to tmp then rename $tmp = $cache_file . '.tmp.' . bin2hex(random_bytes(6)); $json = json_encode($cache, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"; if (@file_put_contents($tmp, $json, LOCK_EX) === false) { error_log("[mvlog] failed to write cache temp file for $identity\n", 3, $logfile); return false; } @chmod($tmp, 0664); if (!@rename($tmp, $cache_file)) { @unlink($tmp); error_log("[mvlog] failed to atomically write cache file for $identity\n", 3, $logfile); return false; } error_log("[mvlog] show-notification sent: identity=$identity job=$job (cfg=" . ($cfg['_source'] ?? 'unknown') . ")\n", 3, $logfile); return true; }