Merge branch 'latest' of forge.dmz.skyfritt.net:stopplidelsen/innhold into latest
This commit is contained in:
commit
959386a365
1 changed files with 257 additions and 8 deletions
|
|
@ -7,6 +7,7 @@
|
|||
* - View failed email sends from SMTP log
|
||||
* - View unconfirmed signatures from petition CSVs
|
||||
* - Retry sending emails with rate limiting (250/hour max)
|
||||
* - Mark entries as ignored (for malformed emails etc.)
|
||||
*
|
||||
* Usage: php custom/petition-cli.php
|
||||
*/
|
||||
|
|
@ -20,6 +21,7 @@ define('BASE_DIR', __DIR__);
|
|||
define('DATA_DIR', BASE_DIR . '/data');
|
||||
define('PETITIONS_DIR', DATA_DIR . '/petitions');
|
||||
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
|
||||
define('EMAIL_DELAY_SECONDS', 15);
|
||||
|
|
@ -49,14 +51,93 @@ function getPetitionFiles(): array {
|
|||
}, $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
|
||||
*/
|
||||
function getFailedEmails(): array {
|
||||
function getFailedEmails(bool $excludeIgnored = true): array {
|
||||
if (!file_exists(SMTP_LOG)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ignoreList = $excludeIgnored ? loadIgnoreList() : [];
|
||||
|
||||
$failed = [];
|
||||
$fp = fopen(SMTP_LOG, 'r');
|
||||
if (!$fp) {
|
||||
|
|
@ -69,13 +150,20 @@ function getFailedEmails(): array {
|
|||
// CSV format: timestamp, type, petition_id, email, status, error_message
|
||||
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
|
||||
if (isset($row[4]) && $row[4] === 'failed') {
|
||||
$failed[] = [
|
||||
$entry = [
|
||||
'timestamp' => (int)$row[0],
|
||||
'type' => $row[1],
|
||||
'petition_id' => $row[2],
|
||||
'email' => $row[3],
|
||||
'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
|
||||
*/
|
||||
function getUnconfirmedSignatures(string $petitionId): array {
|
||||
function getUnconfirmedSignatures(string $petitionId, bool $excludeIgnored = true): array {
|
||||
$csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv';
|
||||
if (!file_exists($csvPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ignoreList = $excludeIgnored ? loadIgnoreList() : [];
|
||||
|
||||
$unconfirmed = [];
|
||||
$fp = fopen($csvPath, 'r');
|
||||
if (!$fp) {
|
||||
|
|
@ -120,7 +210,7 @@ function getUnconfirmedSignatures(string $petitionId): array {
|
|||
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
|
||||
if (isset($row[6]) && $row[6] === 'pending') {
|
||||
$tokenCreated = isset($row[8]) ? (int)$row[8] : (int)$row[0];
|
||||
$unconfirmed[] = [
|
||||
$entry = [
|
||||
'timestamp' => (int)$row[0],
|
||||
'email' => $row[1],
|
||||
'firstname' => $row[2],
|
||||
|
|
@ -131,6 +221,13 @@ function getUnconfirmedSignatures(string $petitionId): array {
|
|||
'token_created' => $tokenCreated,
|
||||
'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
|
||||
*/
|
||||
function getAllUnconfirmedSignatures(): array {
|
||||
function getAllUnconfirmedSignatures(bool $excludeIgnored = true): array {
|
||||
$all = [];
|
||||
foreach (getPetitionFiles() as $petitionId) {
|
||||
$all = array_merge($all, getUnconfirmedSignatures($petitionId));
|
||||
$all = array_merge($all, getUnconfirmedSignatures($petitionId, $excludeIgnored));
|
||||
}
|
||||
|
||||
// Sort by timestamp descending
|
||||
|
|
@ -268,6 +365,7 @@ function printColor(string $text, string $color): void {
|
|||
'green' => "\033[32m",
|
||||
'yellow' => "\033[33m",
|
||||
'blue' => "\033[34m",
|
||||
'cyan' => "\033[36m",
|
||||
'reset' => "\033[0m"
|
||||
];
|
||||
|
||||
|
|
@ -302,7 +400,8 @@ function showMenu(): void {
|
|||
echo " 2) Vis ubekreftede signaturer\n";
|
||||
echo " 3) Send e-post pa nytt til mislykkede\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";
|
||||
}
|
||||
|
||||
|
|
@ -537,6 +636,153 @@ function resendToUnconfirmed(): void {
|
|||
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
|
||||
function main(): void {
|
||||
// Check requirements
|
||||
|
|
@ -568,13 +814,16 @@ function main(): void {
|
|||
resendToUnconfirmed();
|
||||
break;
|
||||
case '5':
|
||||
markAsIgnored();
|
||||
break;
|
||||
case '6':
|
||||
case 'q':
|
||||
case 'quit':
|
||||
case 'exit':
|
||||
echo "Ha det!\n";
|
||||
exit(0);
|
||||
default:
|
||||
printColor("Ugyldig valg. Velg 1-5.\n", 'red');
|
||||
printColor("Ugyldig valg. Velg 1-6.\n", 'red');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue