Added listmonk API call for published article
This commit is contained in:
parent
52065707df
commit
494f3c69a6
3 changed files with 351 additions and 76 deletions
52
admin.php
52
admin.php
|
|
@ -1141,6 +1141,7 @@ try {
|
||||||
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
|
||||||
$articleId = ensure_state_article_id($dir);
|
$articleId = ensure_state_article_id($dir);
|
||||||
$visibleNow = !empty($_POST['visible']);
|
$visibleNow = !empty($_POST['visible']);
|
||||||
|
$sendImmediately = !empty($_POST['send_immediately']);
|
||||||
$state = input_dir_state($dir);
|
$state = input_dir_state($dir);
|
||||||
$output = basename((string)($state['output'] ?? ''));
|
$output = basename((string)($state['output'] ?? ''));
|
||||||
$canShow = $output !== '' && is_file($config['videos_dir'].'/'.$output) && !input_dir_preview($dir);
|
$canShow = $output !== '' && is_file($config['videos_dir'].'/'.$output) && !input_dir_preview($dir);
|
||||||
|
|
@ -1153,7 +1154,7 @@ try {
|
||||||
if ($visibleNow && $canShow) {
|
if ($visibleNow && $canShow) {
|
||||||
if (is_file(__DIR__ . '/lib/send_push.php')) {
|
if (is_file(__DIR__ . '/lib/send_push.php')) {
|
||||||
include_once __DIR__ . '/lib/send_push.php';
|
include_once __DIR__ . '/lib/send_push.php';
|
||||||
try_send_show_notification($articleId ?: basename($dir), $dir, __DIR__, '/var/log/mvlog_notify.log');
|
try_send_show_notification($articleId ?: basename($dir), $dir, __DIR__, '/var/log/mvlog_notify.log', $sendImmediately);
|
||||||
} else {
|
} else {
|
||||||
error_log("[mvlog] send_push helper missing, cannot send notification for " . basename($dir) . "
|
error_log("[mvlog] send_push helper missing, cannot send notification for " . basename($dir) . "
|
||||||
", 3, '/var/log/mvlog_notify.log');
|
", 3, '/var/log/mvlog_notify.log');
|
||||||
|
|
@ -1906,17 +1907,65 @@ function initSwitchForms(){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureShowPromptStyles(){
|
||||||
|
if(document.getElementById('show-prompt-styles')) return;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'show-prompt-styles';
|
||||||
|
style.textContent = '#show-prompt-overlay{position:fixed;inset:0;background:rgba(17,24,39,.48);backdrop-filter:blur(1px);display:flex;align-items:center;justify-content:center;z-index:2995;}' +
|
||||||
|
'#show-prompt-overlay .panel{background:rgba(15,23,42,.96);color:#F3F4F6;border:1px solid rgba(255,255,255,.14);box-shadow:0 20px 60px rgba(0,0,0,.38);border-radius:16px;padding:22px 28px;min-width:min(360px,calc(100vw - 2rem));max-width:460px;}' +
|
||||||
|
'#show-prompt-overlay h3{margin:.1rem 0 .65rem;font-size:1.1rem;}' +
|
||||||
|
'#show-prompt-overlay p{margin:.45rem 0 .9rem;color:#A0A4AB;line-height:1.45;}' +
|
||||||
|
'#show-prompt-overlay .prompt-check{display:flex;align-items:center;gap:.55rem;margin:.8rem 0 1rem;font-weight:600;}' +
|
||||||
|
'#show-prompt-overlay .prompt-check input{display:inline-block;width:auto;margin:0;}' +
|
||||||
|
'#show-prompt-overlay .actions{display:flex;justify-content:flex-end;gap:.55rem;margin-top:.8rem;}';
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptSendEmailsImmediately(){
|
||||||
|
ensureShowPromptStyles();
|
||||||
|
return new Promise(function(resolve){
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.id = 'show-prompt-overlay';
|
||||||
|
overlay.innerHTML = '<div class="panel" role="dialog" aria-modal="true" aria-labelledby="show-prompt-title">' +
|
||||||
|
'<h3 id="show-prompt-title">Show article</h3>' +
|
||||||
|
'<p>This will announce the article and create the journal campaign.</p>' +
|
||||||
|
'<label class="prompt-check"><input type="checkbox" id="show-send-immediately"> <span>Send emails immediately</span></label>' +
|
||||||
|
'<div class="actions"><button type="button" id="show-prompt-ok">OK</button></div>' +
|
||||||
|
'</div>';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
const checkbox = overlay.querySelector('#show-send-immediately');
|
||||||
|
const ok = overlay.querySelector('#show-prompt-ok');
|
||||||
|
ok.addEventListener('click', function(){
|
||||||
|
const value = !!(checkbox && checkbox.checked);
|
||||||
|
overlay.remove();
|
||||||
|
resolve(value);
|
||||||
|
});
|
||||||
|
ok.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSwitchToggle(form, checkbox){
|
async function handleSwitchToggle(form, checkbox){
|
||||||
if(checkbox.disabled) return;
|
if(checkbox.disabled) return;
|
||||||
const previousState = checkbox.dataset.state === '1';
|
const previousState = checkbox.dataset.state === '1';
|
||||||
const data = new FormData(form);
|
const data = new FormData(form);
|
||||||
data.set('ajax', '1');
|
data.set('ajax', '1');
|
||||||
|
const useBusyOverlay = form.dataset.switch === 'show';
|
||||||
|
const isShowingNow = useBusyOverlay && checkbox.checked && !previousState;
|
||||||
|
let sendImmediately = false;
|
||||||
|
if(isShowingNow){
|
||||||
|
sendImmediately = await promptSendEmailsImmediately();
|
||||||
|
}
|
||||||
if(checkbox.checked){
|
if(checkbox.checked){
|
||||||
data.set(checkbox.name, '1');
|
data.set(checkbox.name, '1');
|
||||||
}else{
|
}else{
|
||||||
data.delete(checkbox.name);
|
data.delete(checkbox.name);
|
||||||
}
|
}
|
||||||
|
if(sendImmediately){
|
||||||
|
data.set('send_immediately', '1');
|
||||||
|
}
|
||||||
|
const busyLabel = checkbox.checked ? 'Showing article…' : 'Hiding article…';
|
||||||
checkbox.disabled = true;
|
checkbox.disabled = true;
|
||||||
|
if(useBusyOverlay) showThumbBusy(busyLabel);
|
||||||
try{
|
try{
|
||||||
const res = await fetch('admin.php', { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' }});
|
const res = await fetch('admin.php', { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' }});
|
||||||
const json = await res.json().catch(() => ({}));
|
const json = await res.json().catch(() => ({}));
|
||||||
|
|
@ -1929,6 +1978,7 @@ async function handleSwitchToggle(form, checkbox){
|
||||||
showToast(e.message || 'Switch update failed', false);
|
showToast(e.message || 'Switch update failed', false);
|
||||||
}finally{
|
}finally{
|
||||||
checkbox.disabled = false;
|
checkbox.disabled = false;
|
||||||
|
if(useBusyOverlay) setTimeout(hideThumbBusy, 120);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
141
lib/listmonk.php
Normal file
141
lib/listmonk.php
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// $title - title
|
||||||
|
// $articleUrl - permalink
|
||||||
|
// $excerpt - teaser
|
||||||
|
// $articleId - articleId
|
||||||
|
// $newsletterTitle - randomly chosen from header_titles.php
|
||||||
|
|
||||||
|
function listmonkApiRequest(string $base, string $user, string $pass, string $method, string $path, ?array $payload = null): array {
|
||||||
|
$json = $payload !== null ? json_encode($payload) : null;
|
||||||
|
$url = rtrim($base, '/') . $path;
|
||||||
|
|
||||||
|
if (function_exists('curl_init')) {
|
||||||
|
$ch = curl_init($url);
|
||||||
|
$headers = ['Content-Type: application/json'];
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_USERPWD => $user . ':' . $pass,
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_CUSTOMREQUEST => $method,
|
||||||
|
]);
|
||||||
|
if ($json !== null) {
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
if ($response === false) {
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
throw new Exception('Listmonk API request failed: ' . $error);
|
||||||
|
}
|
||||||
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
} else {
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => $method,
|
||||||
|
'header' => "Content-Type: application/json\r\nAuthorization: Basic " . base64_encode($user . ':' . $pass) . "\r\n",
|
||||||
|
'content' => $json ?? '',
|
||||||
|
'timeout' => 10,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$response = @file_get_contents($url, false, $context);
|
||||||
|
$code = 0;
|
||||||
|
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
|
||||||
|
$code = (int)$m[1];
|
||||||
|
}
|
||||||
|
if ($response === false) {
|
||||||
|
throw new Exception('Listmonk API request failed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($code < 200 || $code >= 300) {
|
||||||
|
throw new Exception('Listmonk API request failed. HTTP ' . $code . ': ' . $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string)$response, true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function listmonkCreateCampaign(
|
||||||
|
string $articleId,
|
||||||
|
string $articleUrl,
|
||||||
|
string $title,
|
||||||
|
string $excerpt,
|
||||||
|
string $newsletterTitle = 'Mistakes, Views & Luck',
|
||||||
|
bool $sendImmediately = false
|
||||||
|
): array {
|
||||||
|
$base = 'http://127.0.0.1:9000';
|
||||||
|
|
||||||
|
$user = 'mvlog';
|
||||||
|
$pass = '1VLUsqSyIQ4xboEd2tmQvgCjWJJk0F3Q';
|
||||||
|
|
||||||
|
$listId = 3; // MVLog Readers
|
||||||
|
$templateId = 5; // Your Listmonk template ID
|
||||||
|
|
||||||
|
$ogImageUrl = 'https://bubulescu.org/out-dir/' . rawurlencode($articleId) . '_og.jpg';
|
||||||
|
|
||||||
|
$safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||||
|
$safeExcerpt = nl2br(htmlspecialchars($excerpt, ENT_QUOTES, 'UTF-8'));
|
||||||
|
$safeArticleUrl = htmlspecialchars($articleUrl, ENT_QUOTES, 'UTF-8');
|
||||||
|
$safeOgImageUrl = htmlspecialchars($ogImageUrl, ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
$body = '
|
||||||
|
<a href="' . $safeArticleUrl . '">
|
||||||
|
<img
|
||||||
|
src="' . $safeOgImageUrl . '"
|
||||||
|
alt="' . $safeTitle . '"
|
||||||
|
style="display:block;width:100%;max-width:632px;height:auto;border-radius:18px;border:0;margin:0 auto 24px auto;"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="padding:0 24px 8px 24px;text-align:center;">
|
||||||
|
<h2 style="margin:0 0 12px 0;color:#ffffff;font-size:26px;line-height:1.25;">
|
||||||
|
' . $safeTitle . '
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p style="margin:0;color:#eeeeee;font-size:16px;line-height:1.6;">
|
||||||
|
' . $safeExcerpt . '
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="text-align:center;margin-top:28px;">
|
||||||
|
<a href="' . $safeArticleUrl . '"
|
||||||
|
style="display:inline-block;background:#d9822b;color:#111;text-decoration:none;font-weight:bold;padding:13px 22px;border-radius:999px;">
|
||||||
|
Read the story
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
';
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'name' => $newsletterTitle,
|
||||||
|
'subject' => 'Story published: ' . $title,
|
||||||
|
'lists' => [$listId],
|
||||||
|
'from_email' => 'MVLog <newsletter@bubulescu.org>',
|
||||||
|
'type' => 'regular',
|
||||||
|
'content_type' => 'html',
|
||||||
|
'body' => $body,
|
||||||
|
'template_id' => $templateId,
|
||||||
|
'messenger' => 'email',
|
||||||
|
'status' => 'draft',
|
||||||
|
'headers' => [
|
||||||
|
['Reply-To' => 'mvlog@bubulescu.org']
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$decoded = listmonkApiRequest($base, $user, $pass, 'POST', '/api/campaigns', $payload);
|
||||||
|
if ($sendImmediately) {
|
||||||
|
$campaignId = $decoded['data']['id'] ?? $decoded['id'] ?? null;
|
||||||
|
if (!$campaignId) {
|
||||||
|
throw new Exception('Listmonk campaign create succeeded but campaign ID was not returned.');
|
||||||
|
}
|
||||||
|
$sendResponse = listmonkApiRequest($base, $user, $pass, 'PUT', '/api/campaigns/' . rawurlencode((string)$campaignId) . '/status', [
|
||||||
|
'status' => 'running',
|
||||||
|
]);
|
||||||
|
$decoded['send_response'] = $sendResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* send_push.php
|
* send_push.php
|
||||||
* Helper function to send "Show enabled" web-push notifications from the webserver.
|
* 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
|
* Function: try_send_show_notification(string $articleRefOrJob, string $jobdir, string $mvlog_root = null, string $logfile = null, bool $sendImmediately = false): bool
|
||||||
*
|
*
|
||||||
* Returns true on successful send and cache update, false otherwise.
|
* Returns true on successful send and cache update, false otherwise.
|
||||||
*/
|
*/
|
||||||
|
|
@ -86,7 +86,81 @@ function mvlog_read_article_id(string $jobdir): string
|
||||||
return mvlog_article_id_is_valid($value) ? $value : '';
|
return mvlog_article_id_is_valid($value) ? $value : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = null, $logfile = null)
|
function mvlog_read_job_data_fields(string $jobdir): array
|
||||||
|
{
|
||||||
|
$data = ['title' => '', 'teaser' => '', 'description' => ''];
|
||||||
|
$file = rtrim($jobdir, '/') . '/data.txt';
|
||||||
|
if (!is_file($file) || !is_readable($file)) return $data;
|
||||||
|
|
||||||
|
$lines = @file($file, FILE_IGNORE_NEW_LINES);
|
||||||
|
if (!is_array($lines)) return $data;
|
||||||
|
|
||||||
|
$current = '';
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$trim = trim((string)$line);
|
||||||
|
if ($trim === '') {
|
||||||
|
if ($current === 'description') $data['description'] .= "\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^([A-Za-z_ -]+):\s*(.*)$/', $trim, $m)) {
|
||||||
|
$key = strtolower(trim((string)$m[1]));
|
||||||
|
$value = trim((string)$m[2]);
|
||||||
|
if ($key === 'title') { $data['title'] = $value; $current = 'title'; }
|
||||||
|
elseif (in_array($key, ['teaser', 'tagline', 'subtitle'], true)) { $data['teaser'] = $value; $current = 'teaser'; }
|
||||||
|
elseif (in_array($key, ['description', 'desc', 'text'], true)) { $data['description'] = $value; $current = 'description'; }
|
||||||
|
else { $current = ''; }
|
||||||
|
} elseif ($current === 'description') {
|
||||||
|
$data['description'] .= ($data['description'] !== '' ? "\n" : '') . $trim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($data as $k => $v) $data[$k] = trim((string)$v);
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mvlog_random_newsletter_title(string $mvlog_root, string $logfile): string
|
||||||
|
{
|
||||||
|
$titles_file = rtrim($mvlog_root, '/') . '/header_titles.php';
|
||||||
|
$titles = is_file($titles_file) && is_readable($titles_file) ? @require $titles_file : [];
|
||||||
|
if (!is_array($titles)) $titles = [];
|
||||||
|
|
||||||
|
$clean = [];
|
||||||
|
foreach ($titles as $title) {
|
||||||
|
$title = trim((string)$title);
|
||||||
|
if ($title !== '') $clean[] = $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$clean) {
|
||||||
|
error_log("[mvlog] header_titles.php missing/empty, using default newsletter title\n", 3, $logfile);
|
||||||
|
return 'Mistakes, Views & Luck';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clean[array_rand($clean)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mvlog_try_create_journal_campaign(string $articleId, string $articleTitle, string $articleUrl, string $articleExcerpt, string $newsletterTitle, bool $sendImmediately, string $mvlog_root, string $logfile): void
|
||||||
|
{
|
||||||
|
$helper = $mvlog_root . '/lib/listmonk.php';
|
||||||
|
if (!is_file($helper) || !is_readable($helper)) {
|
||||||
|
error_log("[mvlog] listmonk helper missing at $helper, skipping journal campaign\n", 3, $logfile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
include_once $helper;
|
||||||
|
if (!function_exists('listmonkCreateCampaign')) {
|
||||||
|
error_log("[mvlog] listmonkCreateCampaign missing in $helper, skipping journal campaign\n", 3, $logfile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listmonkCreateCampaign($articleId, $articleUrl, $articleTitle, $articleExcerpt, $newsletterTitle, $sendImmediately);
|
||||||
|
error_log("[mvlog] journal campaign created for $articleUrl\n", 3, $logfile);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log("[mvlog] journal campaign failed for $articleUrl: " . $e->getMessage() . "\n", 3, $logfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = null, $logfile = null, bool $sendImmediately = false)
|
||||||
{
|
{
|
||||||
if ($mvlog_root === null) $mvlog_root = dirname(__DIR__);
|
if ($mvlog_root === null) $mvlog_root = dirname(__DIR__);
|
||||||
if ($logfile === null) $logfile = '/var/log/mvlog_notify.log';
|
if ($logfile === null) $logfile = '/var/log/mvlog_notify.log';
|
||||||
|
|
@ -98,34 +172,18 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul
|
||||||
$articleRef = strtolower(trim((string)$articleRefOrJob));
|
$articleRef = strtolower(trim((string)$articleRefOrJob));
|
||||||
$articleId = mvlog_article_id_is_valid($articleRef) ? $articleRef : mvlog_read_article_id((string)$jobdir);
|
$articleId = mvlog_article_id_is_valid($articleRef) ? $articleRef : mvlog_read_article_id((string)$jobdir);
|
||||||
if ($articleId === '') {
|
if ($articleId === '') {
|
||||||
error_log("[mvlog] article id missing, skipping push for $job\n", 3, $logfile);
|
error_log("[mvlog] article id missing, skipping announcement for $job\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$identity = $articleId;
|
$identity = $articleId;
|
||||||
|
|
||||||
// Don't notify for hidden jobs
|
// Don't announce hidden jobs.
|
||||||
if (is_file($jobdir . '/.mvlog-hidden')) {
|
if (is_file($jobdir . '/.mvlog-hidden')) {
|
||||||
error_log("[mvlog] job $job hidden, skipping push\n", 3, $logfile);
|
error_log("[mvlog] job $job hidden, skipping announcement\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather subscriptions
|
// Read cache and check if already announced.
|
||||||
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 = [];
|
$cache = [];
|
||||||
if (is_file($cache_file)) {
|
if (is_file($cache_file)) {
|
||||||
$cache = json_decode((string)@file_get_contents($cache_file), true) ?: [];
|
$cache = json_decode((string)@file_get_contents($cache_file), true) ?: [];
|
||||||
|
|
@ -133,24 +191,11 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul
|
||||||
$shown = isset($cache['shown']) && is_array($cache['shown']) ? $cache['shown'] : [];
|
$shown = isset($cache['shown']) && is_array($cache['shown']) ? $cache['shown'] : [];
|
||||||
// Transition support: honor both new (ID) and legacy (job name) keys.
|
// Transition support: honor both new (ID) and legacy (job name) keys.
|
||||||
if (isset($shown[$identity]) || ($identity !== $job && isset($shown[$job]))) {
|
if (isset($shown[$identity]) || ($identity !== $job && isset($shown[$job]))) {
|
||||||
error_log("[mvlog] push already sent for $identity (job=$job), skipping\n", 3, $logfile);
|
error_log("[mvlog] announcement already sent for $identity (job=$job), skipping\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve push config consistently (same source model as push_config.php)
|
// Determine article title (used in push text and journal campaign).
|
||||||
$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);
|
$articleTitle = mvlog_nice_title((string)$job);
|
||||||
if (preg_match('/^\d{8}[_-](.+)$/', (string)$job, $m)) {
|
if (preg_match('/^\d{8}[_-](.+)$/', (string)$job, $m)) {
|
||||||
$articleTitle = mvlog_nice_title((string)$m[1]);
|
$articleTitle = mvlog_nice_title((string)$m[1]);
|
||||||
|
|
@ -173,8 +218,38 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul
|
||||||
|
|
||||||
if ($articleTitle === '') $articleTitle = mvlog_nice_title((string)$job);
|
if ($articleTitle === '') $articleTitle = mvlog_nice_title((string)$job);
|
||||||
|
|
||||||
$articleUrl = is_file($jobdir . '/.mvlog-permalink') ? './?id=' . rawurlencode($articleId) : './';
|
$jobData = mvlog_read_job_data_fields((string)$jobdir);
|
||||||
|
$journalTitle = (string)($jobData['title'] !== '' ? $jobData['title'] : $articleTitle);
|
||||||
|
$newsletterTitle = mvlog_random_newsletter_title($mvlog_root, $logfile);
|
||||||
|
$articleExcerpt = (string)($jobData['teaser'] !== '' ? $jobData['teaser'] : $jobData['description']);
|
||||||
|
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
|
||||||
|
if (mb_strlen($articleExcerpt) > 500) $articleExcerpt = rtrim(mb_substr($articleExcerpt, 0, 497)) . '…';
|
||||||
|
} elseif (strlen($articleExcerpt) > 500) {
|
||||||
|
$articleExcerpt = rtrim(substr($articleExcerpt, 0, 497)) . '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
$articleUrl = is_file($jobdir . '/.mvlog-permalink') ? './?id=' . rawurlencode($articleId) : './';
|
||||||
|
$journalUrl = 'https://bubulescu.org/' . (is_file($jobdir . '/.mvlog-permalink') ? '?id=' . rawurlencode($articleId) : '');
|
||||||
|
|
||||||
|
// Attempt web-push, but never let push failure prevent the journal/Listmonk campaign.
|
||||||
|
$pushOk = false;
|
||||||
|
$pushStatus = 'skipped';
|
||||||
|
$cfgSource = 'none';
|
||||||
|
|
||||||
|
$subs_raw = is_file($subs_file) ? @file_get_contents($subs_file) : false;
|
||||||
|
$subs = is_string($subs_raw) && trim($subs_raw) !== '' ? json_decode($subs_raw, true) : null;
|
||||||
|
if (!is_array($subs) || count($subs) === 0) {
|
||||||
|
error_log("[mvlog] subscriptions missing/empty/invalid, skipping web-push for $identity\n", 3, $logfile);
|
||||||
|
} else {
|
||||||
|
$cfg = mvlog_load_push_config($mvlog_root);
|
||||||
|
if (!$cfg) {
|
||||||
|
error_log("[mvlog] push config not found/invalid, skipping web-push for $identity\n", 3, $logfile);
|
||||||
|
} else {
|
||||||
|
$cfgSource = (string)($cfg['_source'] ?? 'unknown');
|
||||||
|
$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);
|
||||||
|
} else {
|
||||||
$payload = json_encode([
|
$payload = json_encode([
|
||||||
'title' => $articleTitle,
|
'title' => $articleTitle,
|
||||||
'body' => 'New unreliable article @ MVLog!',
|
'body' => 'New unreliable article @ MVLog!',
|
||||||
|
|
@ -182,50 +257,59 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul
|
||||||
'tag' => "mvlog-show-{$identity}",
|
'tag' => "mvlog-show-{$identity}",
|
||||||
], JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
// Call node send_push.js with subscriptions on stdin
|
|
||||||
$send_script = escapeshellarg($mvlog_root . '/send_push.js');
|
$send_script = escapeshellarg($mvlog_root . '/send_push.js');
|
||||||
$push_config_esc = escapeshellarg($tmp_push_config);
|
$push_config_esc = escapeshellarg($tmp_push_config);
|
||||||
$payload_esc = escapeshellarg((string)$payload);
|
$payload_esc = escapeshellarg((string)$payload);
|
||||||
$cmd = "node $send_script $push_config_esc $payload_esc";
|
$cmd = "node $send_script $push_config_esc $payload_esc";
|
||||||
|
|
||||||
$descriptorspec = [
|
$descriptorspec = [
|
||||||
0 => ['pipe', 'r'], // stdin
|
0 => ['pipe', 'r'],
|
||||||
1 => ['pipe', 'w'], // stdout
|
1 => ['pipe', 'w'],
|
||||||
2 => ['pipe', 'w'], // stderr
|
2 => ['pipe', 'w'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$proc = proc_open($cmd, $descriptorspec, $pipes);
|
$proc = proc_open($cmd, $descriptorspec, $pipes);
|
||||||
if (!is_resource($proc)) {
|
if (!is_resource($proc)) {
|
||||||
@unlink($tmp_push_config);
|
|
||||||
error_log("[mvlog] failed to start node send_push process for $identity\n", 3, $logfile);
|
error_log("[mvlog] failed to start node send_push process for $identity\n", 3, $logfile);
|
||||||
return false;
|
$pushStatus = 'failed-start';
|
||||||
}
|
} else {
|
||||||
|
fwrite($pipes[0], (string)$subs_raw);
|
||||||
// Write subscriptions to stdin
|
|
||||||
fwrite($pipes[0], $subs_raw);
|
|
||||||
fclose($pipes[0]);
|
fclose($pipes[0]);
|
||||||
|
|
||||||
$stdout = stream_get_contents($pipes[1]); fclose($pipes[1]);
|
$stdout = stream_get_contents($pipes[1]); fclose($pipes[1]);
|
||||||
$stderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
|
$stderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
|
||||||
|
|
||||||
$rc = proc_close($proc);
|
$rc = proc_close($proc);
|
||||||
@unlink($tmp_push_config);
|
if ($rc === 0) {
|
||||||
|
$pushOk = true;
|
||||||
if ($rc !== 0) {
|
$pushStatus = 'sent';
|
||||||
|
} else {
|
||||||
|
$pushStatus = 'failed';
|
||||||
error_log("[mvlog] send_push failed for $identity rc=$rc stdout=" . trim((string)$stdout) . " stderr=" . trim((string)$stderr) . "\n", 3, $logfile);
|
error_log("[mvlog] send_push failed for $identity rc=$rc stdout=" . trim((string)$stdout) . " stderr=" . trim((string)$stderr) . "\n", 3, $logfile);
|
||||||
return false;
|
}
|
||||||
|
}
|
||||||
|
@unlink($tmp_push_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// On success, record in cache['shown']
|
// Always attempt the journal/Listmonk campaign for the same eligible trigger.
|
||||||
|
mvlog_try_create_journal_campaign($articleId, $journalTitle, $journalUrl, $articleExcerpt, $newsletterTitle, $sendImmediately, $mvlog_root, $logfile);
|
||||||
|
|
||||||
|
// Record the announcement so the same article is not announced repeatedly.
|
||||||
if (!is_array($cache)) $cache = [];
|
if (!is_array($cache)) $cache = [];
|
||||||
|
unset($cache['sent']);
|
||||||
if (!isset($cache['shown']) || !is_array($cache['shown'])) $cache['shown'] = [];
|
if (!isset($cache['shown']) || !is_array($cache['shown'])) $cache['shown'] = [];
|
||||||
$cache['shown'][$identity] = [
|
$cache['shown'][$identity] = [
|
||||||
'sent_at' => date(DATE_ATOM),
|
'sent_at' => date(DATE_ATOM),
|
||||||
'job' => $job,
|
'job' => $job,
|
||||||
'article_id' => $articleId,
|
'article_id' => $articleId,
|
||||||
|
'title' => $journalTitle,
|
||||||
|
'newsletter_title' => $newsletterTitle,
|
||||||
|
'send_immediately' => $sendImmediately,
|
||||||
|
'push' => ($pushOk ? 'successful' : 'failed'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Atomic write: write to tmp then rename
|
|
||||||
$tmp = $cache_file . '.tmp.' . bin2hex(random_bytes(6));
|
$tmp = $cache_file . '.tmp.' . bin2hex(random_bytes(6));
|
||||||
$json = json_encode($cache, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
$json = json_encode($cache, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||||||
if (@file_put_contents($tmp, $json, LOCK_EX) === false) {
|
if (@file_put_contents($tmp, $json, LOCK_EX) === false) {
|
||||||
|
|
@ -239,6 +323,6 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log("[mvlog] show-notification sent: identity=$identity job=$job (cfg=" . ($cfg['_source'] ?? 'unknown') . ")\n", 3, $logfile);
|
error_log("[mvlog] announcement processed: identity=$identity job=$job push=$pushStatus (cfg=$cfgSource)\n", 3, $logfile);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue