242 lines
8.5 KiB
PHP
242 lines
8.5 KiB
PHP
<?php
|
|
/**
|
|
* send_push.php
|
|
* Helper function to send "Show enabled" web-push notifications from the webserver.
|
|
*
|
|
* Function: try_send_show_notification(string $articleRefOrJob, string $jobdir, string $mvlog_root = null, string $logfile = null): bool
|
|
*
|
|
* Returns true on successful send and cache update, false otherwise.
|
|
*/
|
|
|
|
function mvlog_load_push_config(string $mvlog_root): ?array
|
|
{
|
|
$candidates = [
|
|
'/etc/mvlog/push.php',
|
|
'/etc/mvlog/push.json',
|
|
$mvlog_root . '/push.json',
|
|
(getenv('HOME') !== false ? getenv('HOME') . '/.config/mvlog/push.json' : null),
|
|
];
|
|
|
|
foreach ($candidates as $candidate) {
|
|
if (!$candidate || !is_file($candidate) || !is_readable($candidate)) continue;
|
|
|
|
$cfg = null;
|
|
if (str_ends_with($candidate, '.php')) {
|
|
$loaded = @require $candidate;
|
|
if (is_array($loaded)) $cfg = $loaded;
|
|
} else {
|
|
$decoded = json_decode((string)@file_get_contents($candidate), true);
|
|
if (is_array($decoded)) $cfg = $decoded;
|
|
}
|
|
|
|
if (!is_array($cfg)) continue;
|
|
|
|
$public = (string)($cfg['public_key'] ?? $cfg['vapidPublicKey'] ?? '');
|
|
$private = (string)($cfg['private_key'] ?? $cfg['vapidPrivateKey'] ?? '');
|
|
$subject = (string)($cfg['subject'] ?? $cfg['vapidSubject'] ?? '');
|
|
|
|
if ($public !== '' && $private !== '') {
|
|
return [
|
|
'subject' => $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);
|
|
$identity = $articleId !== '' ? $articleId : $job;
|
|
|
|
// 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);
|
|
|
|
$filterUrl = 'https://bubulescu.org/mvlog/?' . http_build_query([
|
|
'q' => $articleTitle,
|
|
]);
|
|
|
|
$payload = json_encode([
|
|
'title' => $articleTitle,
|
|
'body' => 'New aticle @ MVL!',
|
|
'url' => $filterUrl,
|
|
'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;
|
|
}
|