Merge branch 'latest' of forge.dmz.skyfritt.net:stopplidelsen/innhold into latest

This commit is contained in:
Ruben Solvang 2026-01-24 22:45:56 +01:00
commit 959386a365

View file

@ -7,6 +7,7 @@
* - View failed email sends from SMTP log * - View failed email sends from SMTP log
* - View unconfirmed signatures from petition CSVs * - View unconfirmed signatures from petition CSVs
* - Retry sending emails with rate limiting (250/hour max) * - Retry sending emails with rate limiting (250/hour max)
* - Mark entries as ignored (for malformed emails etc.)
* *
* Usage: php custom/petition-cli.php * Usage: php custom/petition-cli.php
*/ */
@ -20,6 +21,7 @@ define('BASE_DIR', __DIR__);
define('DATA_DIR', BASE_DIR . '/data'); define('DATA_DIR', BASE_DIR . '/data');
define('PETITIONS_DIR', DATA_DIR . '/petitions'); define('PETITIONS_DIR', DATA_DIR . '/petitions');
define('SMTP_LOG', DATA_DIR . '/smtp-log.csv'); define('SMTP_LOG', DATA_DIR . '/smtp-log.csv');
define('IGNORE_LIST', DATA_DIR . '/petition-ignore.csv');
// Rate limit: 250 emails/hour = 1 email per 14.4 seconds, using 15 seconds // Rate limit: 250 emails/hour = 1 email per 14.4 seconds, using 15 seconds
define('EMAIL_DELAY_SECONDS', 15); define('EMAIL_DELAY_SECONDS', 15);
@ -49,14 +51,93 @@ function getPetitionFiles(): array {
}, $files); }, $files);
} }
/**
* Load ignore list
*/
function loadIgnoreList(): array {
if (!file_exists(IGNORE_LIST)) {
return [];
}
$ignored = [];
$fp = fopen(IGNORE_LIST, 'r');
if (!$fp) {
return [];
}
// Skip header
fgetcsv($fp, null, ',', '"', '');
// CSV format: timestamp, type, petition_id, email, reason
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[3])) {
// Key format: type|petition_id|email
$key = ($row[1] ?? '') . '|' . ($row[2] ?? '') . '|' . strtolower($row[3]);
$ignored[$key] = [
'timestamp' => (int)$row[0],
'type' => $row[1],
'petition_id' => $row[2],
'email' => $row[3],
'reason' => $row[4] ?? ''
];
}
}
fclose($fp);
return $ignored;
}
/**
* Add entry to ignore list
*/
function addToIgnoreList(string $type, string $petitionId, string $email, string $reason): bool {
$isNewFile = !file_exists(IGNORE_LIST);
$fp = fopen(IGNORE_LIST, 'a');
if (!$fp) {
return false;
}
if (flock($fp, LOCK_EX)) {
if ($isNewFile) {
fputcsv($fp, ['timestamp', 'type', 'petition_id', 'email', 'reason'], ',', '"', '');
}
fputcsv($fp, [
time(),
$type,
$petitionId,
$email,
$reason
], ',', '"', '');
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
fclose($fp);
return false;
}
/**
* Check if entry is ignored
*/
function isIgnored(array $ignoreList, string $type, string $petitionId, string $email): bool {
$key = $type . '|' . $petitionId . '|' . strtolower($email);
return isset($ignoreList[$key]);
}
/** /**
* Read failed emails from SMTP log * Read failed emails from SMTP log
*/ */
function getFailedEmails(): array { function getFailedEmails(bool $excludeIgnored = true): array {
if (!file_exists(SMTP_LOG)) { if (!file_exists(SMTP_LOG)) {
return []; return [];
} }
$ignoreList = $excludeIgnored ? loadIgnoreList() : [];
$failed = []; $failed = [];
$fp = fopen(SMTP_LOG, 'r'); $fp = fopen(SMTP_LOG, 'r');
if (!$fp) { if (!$fp) {
@ -69,13 +150,20 @@ function getFailedEmails(): array {
// CSV format: timestamp, type, petition_id, email, status, error_message // CSV format: timestamp, type, petition_id, email, status, error_message
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[4]) && $row[4] === 'failed') { if (isset($row[4]) && $row[4] === 'failed') {
$failed[] = [ $entry = [
'timestamp' => (int)$row[0], 'timestamp' => (int)$row[0],
'type' => $row[1], 'type' => $row[1],
'petition_id' => $row[2], 'petition_id' => $row[2],
'email' => $row[3], 'email' => $row[3],
'error' => $row[5] ?? '' 'error' => $row[5] ?? ''
]; ];
// Skip if ignored
if ($excludeIgnored && isIgnored($ignoreList, $entry['type'], $entry['petition_id'], $entry['email'])) {
continue;
}
$failed[] = $entry;
} }
} }
@ -101,12 +189,14 @@ function getFailedEmails(): array {
/** /**
* Get unconfirmed signatures from a petition CSV * Get unconfirmed signatures from a petition CSV
*/ */
function getUnconfirmedSignatures(string $petitionId): array { function getUnconfirmedSignatures(string $petitionId, bool $excludeIgnored = true): array {
$csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv'; $csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv';
if (!file_exists($csvPath)) { if (!file_exists($csvPath)) {
return []; return [];
} }
$ignoreList = $excludeIgnored ? loadIgnoreList() : [];
$unconfirmed = []; $unconfirmed = [];
$fp = fopen($csvPath, 'r'); $fp = fopen($csvPath, 'r');
if (!$fp) { if (!$fp) {
@ -120,7 +210,7 @@ function getUnconfirmedSignatures(string $petitionId): array {
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[6]) && $row[6] === 'pending') { if (isset($row[6]) && $row[6] === 'pending') {
$tokenCreated = isset($row[8]) ? (int)$row[8] : (int)$row[0]; $tokenCreated = isset($row[8]) ? (int)$row[8] : (int)$row[0];
$unconfirmed[] = [ $entry = [
'timestamp' => (int)$row[0], 'timestamp' => (int)$row[0],
'email' => $row[1], 'email' => $row[1],
'firstname' => $row[2], 'firstname' => $row[2],
@ -131,6 +221,13 @@ function getUnconfirmedSignatures(string $petitionId): array {
'token_created' => $tokenCreated, 'token_created' => $tokenCreated,
'petition_id' => $petitionId 'petition_id' => $petitionId
]; ];
// Skip if ignored
if ($excludeIgnored && isIgnored($ignoreList, 'unconfirmed', $petitionId, $entry['email'])) {
continue;
}
$unconfirmed[] = $entry;
} }
} }
@ -145,10 +242,10 @@ function getUnconfirmedSignatures(string $petitionId): array {
/** /**
* Get all unconfirmed signatures from all petitions * Get all unconfirmed signatures from all petitions
*/ */
function getAllUnconfirmedSignatures(): array { function getAllUnconfirmedSignatures(bool $excludeIgnored = true): array {
$all = []; $all = [];
foreach (getPetitionFiles() as $petitionId) { foreach (getPetitionFiles() as $petitionId) {
$all = array_merge($all, getUnconfirmedSignatures($petitionId)); $all = array_merge($all, getUnconfirmedSignatures($petitionId, $excludeIgnored));
} }
// Sort by timestamp descending // Sort by timestamp descending
@ -268,6 +365,7 @@ function printColor(string $text, string $color): void {
'green' => "\033[32m", 'green' => "\033[32m",
'yellow' => "\033[33m", 'yellow' => "\033[33m",
'blue' => "\033[34m", 'blue' => "\033[34m",
'cyan' => "\033[36m",
'reset' => "\033[0m" 'reset' => "\033[0m"
]; ];
@ -302,7 +400,8 @@ function showMenu(): void {
echo " 2) Vis ubekreftede signaturer\n"; echo " 2) Vis ubekreftede signaturer\n";
echo " 3) Send e-post pa nytt til mislykkede\n"; echo " 3) Send e-post pa nytt til mislykkede\n";
echo " 4) Send bekreftelse pa nytt til ubekreftede\n"; echo " 4) Send bekreftelse pa nytt til ubekreftede\n";
echo " 5) Avslutt\n"; echo " 5) Marker oppforinger som ignorert\n";
echo " 6) Avslutt\n";
echo "\n"; echo "\n";
} }
@ -537,6 +636,153 @@ function resendToUnconfirmed(): void {
printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green'); printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green');
} }
/**
* Interactive ignore marking
*/
function markAsIgnored(): void {
echo "\n";
printColor("Marker oppforinger som ignorert\n", 'cyan');
echo "\n";
echo "Velg liste:\n";
echo " 1) Mislykkede e-poster fra SMTP-logg\n";
echo " 2) Ubekreftede signaturer\n";
echo " 3) Tilbake\n";
echo "\n";
$choice = prompt('Valg: ');
if ($choice === '1') {
markFailedAsIgnored();
} elseif ($choice === '2') {
markUnconfirmedAsIgnored();
}
}
/**
* Mark failed emails as ignored (one by one)
*/
function markFailedAsIgnored(): void {
$failed = getFailedEmails();
if (empty($failed)) {
printColor("\nIngen mislykkede e-poster a markere.\n", 'green');
return;
}
echo "\n";
printColor("Ga gjennom mislykkede e-poster en etter en.\n", 'cyan');
echo "Trykk 'j' for a ignorere, 'n' for a beholde, 's' for a hoppe over resten.\n\n";
$ignored = 0;
$kept = 0;
foreach ($failed as $i => $entry) {
$num = $i + 1;
$total = count($failed);
$date = formatDate($entry['timestamp']);
$type = $entry['type'] === 'confirmation' ? 'Bekreftelse' : 'Takk';
echo str_repeat('-', 60) . "\n";
echo "[{$num}/{$total}]\n";
echo " E-post: {$entry['email']}\n";
echo " Type: {$type}\n";
echo " Kampanje: {$entry['petition_id']}\n";
echo " Dato: {$date}\n";
echo " Feil: " . ($entry['error'] ?: '(ukjent)') . "\n";
$response = strtolower(prompt("\nIgnorer denne? [j/n/s]: "));
if ($response === 's' || $response === 'stopp' || $response === 'stop') {
echo "Avslutter gjennomgang.\n";
break;
}
if ($response === 'j' || $response === 'ja' || $response === 'y' || $response === 'yes') {
$reason = prompt("Grunn (valgfritt): ");
if (addToIgnoreList($entry['type'], $entry['petition_id'], $entry['email'], $reason)) {
printColor("Markert som ignorert.\n", 'green');
$ignored++;
} else {
printColor("Kunne ikke lagre til ignorliste.\n", 'red');
}
} else {
echo "Beholdt.\n";
$kept++;
}
}
echo "\n";
printColor("Ferdig: {$ignored} ignorert, {$kept} beholdt\n", 'green');
}
/**
* Mark unconfirmed signatures as ignored (one by one)
*/
function markUnconfirmedAsIgnored(): void {
$unconfirmed = getAllUnconfirmedSignatures();
// Include expired for review
$currentTime = time();
if (empty($unconfirmed)) {
printColor("\nIngen ubekreftede signaturer a markere.\n", 'green');
return;
}
echo "\n";
printColor("Ga gjennom ubekreftede signaturer en etter en.\n", 'cyan');
echo "Trykk 'j' for a ignorere, 'n' for a beholde, 's' for a hoppe over resten.\n\n";
$ignored = 0;
$kept = 0;
foreach ($unconfirmed as $i => $sig) {
$num = $i + 1;
$total = count($unconfirmed);
$date = formatDate($sig['timestamp']);
$name = $sig['firstname'] . ' ' . $sig['surname'];
$daysLeft = 30 - floor(($currentTime - $sig['token_created']) / 86400);
$expired = $daysLeft < 0;
echo str_repeat('-', 60) . "\n";
echo "[{$num}/{$total}]";
if ($expired) {
printColor(" (UTLOPT)", 'red');
}
echo "\n";
echo " E-post: {$sig['email']}\n";
echo " Navn: {$name}\n";
echo " Kampanje: {$sig['petition_id']}\n";
echo " Dato: {$date}\n";
if (!$expired) {
echo " Utloper: om {$daysLeft} dager\n";
}
$response = strtolower(prompt("\nIgnorer denne? [j/n/s]: "));
if ($response === 's' || $response === 'stopp' || $response === 'stop') {
echo "Avslutter gjennomgang.\n";
break;
}
if ($response === 'j' || $response === 'ja' || $response === 'y' || $response === 'yes') {
$reason = prompt("Grunn (valgfritt): ");
if (addToIgnoreList('unconfirmed', $sig['petition_id'], $sig['email'], $reason)) {
printColor("Markert som ignorert.\n", 'green');
$ignored++;
} else {
printColor("Kunne ikke lagre til ignorliste.\n", 'red');
}
} else {
echo "Beholdt.\n";
$kept++;
}
}
echo "\n";
printColor("Ferdig: {$ignored} ignorert, {$kept} beholdt\n", 'green');
}
// Main loop // Main loop
function main(): void { function main(): void {
// Check requirements // Check requirements
@ -568,13 +814,16 @@ function main(): void {
resendToUnconfirmed(); resendToUnconfirmed();
break; break;
case '5': case '5':
markAsIgnored();
break;
case '6':
case 'q': case 'q':
case 'quit': case 'quit':
case 'exit': case 'exit':
echo "Ha det!\n"; echo "Ha det!\n";
exit(0); exit(0);
default: default:
printColor("Ugyldig valg. Velg 1-5.\n", 'red'); printColor("Ugyldig valg. Velg 1-6.\n", 'red');
} }
} }
} }