PHP Classes

File: scripts/echomail_stats.php

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   scripts/echomail_stats.php   Download  
File: scripts/echomail_stats.php
Role: Example script
Content type: text/plain
Description: Example script
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 12,383 bytes
 

Contents

Class file image Download
#!/usr/bin/env php <?php /** * Report echomail traffic over a configurable period. * * Usage: * php scripts/echomail_stats.php * php scripts/echomail_stats.php --days=30 * php scripts/echomail_stats.php --from=2026-03-01 --to=2026-03-31 * php scripts/echomail_stats.php --domain=fidonet * php scripts/echomail_stats.php --area=GENERAL * php scripts/echomail_stats.php --top=25 */ require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../src/functions.php'; use BinktermPHP\Database; const DEFAULT_DAYS = 30; const DEFAULT_TOP = 50; function printUsage(): void { echo "Usage: php scripts/echomail_stats.php [options]\n\n"; echo "Options:\n"; echo " --days=N Look back N days (default: 30)\n"; echo " --from=YYYY-MM-DD Start date (overrides --days)\n"; echo " --to=YYYY-MM-DD End date (used with --from, defaults to now)\n"; echo " --domain=NAME Limit to a single echomail domain\n"; echo " --area=TAG Limit to a single echo area tag\n"; echo " --top=N Show top N areas in the ranked table (default: 50)\n"; echo " --sort=FIELD Sort area table by: msgs, threads, replies, actdays,\n"; echo " msgsday, lastmsg, size (default: msgs)\n"; echo " --help Show this help message\n"; } function parseArgs(array $argv): array { $args = []; foreach (array_slice($argv, 1) as $arg) { if (!str_starts_with($arg, '--')) { continue; } $arg = substr($arg, 2); if (strpos($arg, '=') !== false) { [$key, $value] = explode('=', $arg, 2); $args[$key] = $value; } else { $args[$arg] = true; } } return $args; } function parseDateArgument(string $value, bool $endOfDay = false): DateTimeImmutable { $suffix = $endOfDay ? '23:59:59' : '00:00:00'; $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', trim($value) . ' ' . $suffix, new DateTimeZone('UTC')); if (!$date) { throw new RuntimeException("Invalid date: {$value}"); } return $date; } function buildWindow(array $args): array { $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); if (!empty($args['from'])) { $from = parseDateArgument((string)$args['from']); $to = !empty($args['to']) ? parseDateArgument((string)$args['to'], true) : $now; } else { $days = isset($args['days']) ? (int)$args['days'] : DEFAULT_DAYS; if ($days <= 0) { throw new RuntimeException('--days must be greater than 0'); } $from = $now->sub(new DateInterval('P' . $days . 'D')); $to = $now; } if ($from >= $to) { throw new RuntimeException('Start time must be earlier than end time'); } return [$from, $to]; } function buildFilters(array $args): array { $where = []; $params = []; if (!empty($args['domain'])) { $where[] = "LOWER(COALESCE(ea.domain, '')) = LOWER(?)"; $params[] = trim((string)$args['domain']); } if (!empty($args['area'])) { $where[] = 'LOWER(ea.tag) = LOWER(?)'; $params[] = trim((string)$args['area']); } return [$where, $params]; } function fetchOverview(DateTimeImmutable $from, DateTimeImmutable $to, array $where, array $params): array { $db = Database::getInstance()->getPdo(); $sql = " SELECT COUNT(*) AS total_messages, COUNT(*) FILTER (WHERE em.reply_to_id IS NULL) AS total_threads, COUNT(DISTINCT em.echoarea_id) AS active_areas, COUNT(DISTINCT COALESCE(ea.domain, '')) AS active_domains FROM echomail em INNER JOIN echoareas ea ON ea.id = em.echoarea_id WHERE em.date_received >= ? AND em.date_received < ? "; if ($where !== []) { $sql .= ' AND ' . implode(' AND ', $where); } $stmt = $db->prepare($sql); $stmt->execute(array_merge([ $from->format('Y-m-d H:i:sP'), $to->format('Y-m-d H:i:sP'), ], $params)); return $stmt->fetch(PDO::FETCH_ASSOC) ?: []; } /** * Fetch total echomail storage in bytes across all messages (not date-filtered). * Sums the octet length of the primary text columns: message_text and kludge_lines. */ function fetchTotalStorage(array $where, array $params): int { $db = Database::getInstance()->getPdo(); $sql = " SELECT COALESCE(SUM( octet_length(COALESCE(em.message_text, '')) + octet_length(COALESCE(em.kludge_lines, '')) ), 0) AS total_bytes FROM echomail em INNER JOIN echoareas ea ON ea.id = em.echoarea_id "; if ($where !== []) { $sql .= ' WHERE ' . implode(' AND ', $where); } $stmt = $db->prepare($sql); $stmt->execute($params); $row = $stmt->fetch(PDO::FETCH_ASSOC); return (int)($row['total_bytes'] ?? 0); } /** * Resolve a user-supplied sort key to a SQL ORDER BY clause. * * @return string Safe, whitelisted ORDER BY expression */ function resolveSortOrder(string $sort): string { return match (strtolower(trim($sort))) { 'threads' => 'threads DESC, messages DESC, ea.tag ASC', 'replies' => 'replies DESC, messages DESC, ea.tag ASC', 'actdays' => 'active_days DESC, messages DESC, ea.tag ASC', 'msgsday' => 'messages DESC, active_days DESC, ea.tag ASC', 'lastmsg' => 'last_received DESC NULLS LAST, ea.tag ASC', 'size' => 'storage_bytes DESC, messages DESC, ea.tag ASC', default => 'messages DESC, threads DESC, ea.tag ASC', // 'msgs' and unknown values }; } function fetchAreaStats(DateTimeImmutable $from, DateTimeImmutable $to, array $where, array $params, int $top, string $sort = 'msgs'): array { $db = Database::getInstance()->getPdo(); $sql = " SELECT ea.tag, COALESCE(ea.domain, '') AS domain, COUNT(*) AS messages, COUNT(*) FILTER (WHERE em.reply_to_id IS NULL) AS threads, COUNT(*) FILTER (WHERE em.reply_to_id IS NOT NULL) AS replies, COUNT(DISTINCT DATE(em.date_received AT TIME ZONE 'UTC')) AS active_days, MAX(em.date_received) AS last_received, COALESCE(SUM( octet_length(COALESCE(em.message_text, '')) + octet_length(COALESCE(em.kludge_lines, '')) ), 0) AS storage_bytes FROM echomail em INNER JOIN echoareas ea ON ea.id = em.echoarea_id WHERE em.date_received >= ? AND em.date_received < ? "; if ($where !== []) { $sql .= ' AND ' . implode(' AND ', $where); } $orderBy = resolveSortOrder($sort); $sql .= " GROUP BY ea.id, ea.tag, ea.domain ORDER BY {$orderBy} LIMIT ? "; $stmt = $db->prepare($sql); $execParams = array_merge([ $from->format('Y-m-d H:i:sP'), $to->format('Y-m-d H:i:sP'), ], $params, [$top]); $stmt->execute($execParams); return $stmt->fetchAll(PDO::FETCH_ASSOC); } function fetchDailyStats(DateTimeImmutable $from, DateTimeImmutable $to, array $where, array $params): array { $db = Database::getInstance()->getPdo(); $sql = " SELECT DATE(em.date_received AT TIME ZONE 'UTC') AS day, COUNT(*) AS messages, COUNT(*) FILTER (WHERE em.reply_to_id IS NULL) AS threads FROM echomail em INNER JOIN echoareas ea ON ea.id = em.echoarea_id WHERE em.date_received >= ? AND em.date_received < ? "; if ($where !== []) { $sql .= ' AND ' . implode(' AND ', $where); } $sql .= " GROUP BY day ORDER BY day ASC "; $stmt = $db->prepare($sql); $stmt->execute(array_merge([ $from->format('Y-m-d H:i:sP'), $to->format('Y-m-d H:i:sP'), ], $params)); return $stmt->fetchAll(PDO::FETCH_ASSOC); } function formatTimestamp(?string $value): string { if ($value === null || $value === '') { return '-'; } try { return (new DateTimeImmutable($value))->format('Y-m-d'); } catch (Throwable $e) { return $value; } } function formatAreaName(array $row): string { $label = (string)$row['tag']; $domain = trim((string)($row['domain'] ?? '')); if ($domain !== '') { $label .= '@' . $domain; } return $label; } function formatBytes(int $bytes): string { if ($bytes >= 1024 * 1024 * 1024) { return number_format($bytes / (1024 * 1024 * 1024), 2) . ' GB'; } if ($bytes >= 1024 * 1024) { return number_format($bytes / (1024 * 1024), 2) . ' MB'; } if ($bytes >= 1024) { return number_format($bytes / 1024, 2) . ' KB'; } return $bytes . ' B'; } function printOverview(array $overview, DateTimeImmutable $from, DateTimeImmutable $to, int $totalStorageBytes): void { $days = max(1, (int)$from->diff($to)->days); $messages = (int)($overview['total_messages'] ?? 0); $threads = (int)($overview['total_threads'] ?? 0); echo "Echomail Traffic Report\n"; echo "From: " . $from->format('Y-m-d H:i:s T') . "\n"; echo "To: " . $to->format('Y-m-d H:i:s T') . "\n"; echo "Days: {$days}\n"; echo "\n"; echo "Total messages : {$messages}\n"; echo "New threads : {$threads}\n"; echo "Replies : " . max(0, $messages - $threads) . "\n"; echo "Active areas : " . (int)($overview['active_areas'] ?? 0) . "\n"; echo "Active domains : " . (int)($overview['active_domains'] ?? 0) . "\n"; echo "Msgs / day : " . number_format($messages / $days, 1) . "\n"; echo "Threads / day : " . number_format($threads / $days, 1) . "\n"; echo "Total storage : " . formatBytes($totalStorageBytes) . " (all-time, message text)\n"; echo "\n"; } function printAreaTable(array $rows, int $days): void { echo "By Echo Area\n"; echo str_repeat('-', 106) . "\n"; echo sprintf("%-28s %8s %8s %8s %8s %10s %10s %10s\n", 'Area', 'Msgs', 'Threads', 'Replies', 'ActDays', 'Msgs/Day', 'Last Msg', 'Size'); echo str_repeat('-', 106) . "\n"; if ($rows === []) { echo "No echomail activity found in the selected window.\n\n"; return; } foreach ($rows as $row) { $messages = (int)$row['messages']; echo sprintf( "%-28s %8d %8d %8d %8d %10.1f %10s %10s\n", mb_strimwidth(formatAreaName($row), 0, 28, '?'), $messages, (int)$row['threads'], (int)$row['replies'], (int)$row['active_days'], $messages / max(1, $days), formatTimestamp($row['last_received'] ?? null), formatBytes((int)($row['storage_bytes'] ?? 0)) ); } echo "\n"; } function printDailyTable(array $rows): void { echo "Daily Totals\n"; echo str_repeat('-', 40) . "\n"; echo sprintf("%-12s %10s %10s\n", 'Day', 'Messages', 'Threads'); echo str_repeat('-', 40) . "\n"; if ($rows === []) { echo "No daily traffic to report.\n\n"; return; } foreach ($rows as $row) { echo sprintf( "%-12s %10d %10d\n", (string)$row['day'], (int)$row['messages'], (int)$row['threads'] ); } echo "\n"; } $args = parseArgs($argv); if (isset($args['help'])) { printUsage(); exit(0); } try { [$from, $to] = buildWindow($args); [$where, $params] = buildFilters($args); $top = isset($args['top']) ? (int)$args['top'] : DEFAULT_TOP; if ($top <= 0) { throw new RuntimeException('--top must be greater than 0'); } $sort = isset($args['sort']) ? (string)$args['sort'] : 'msgs'; $overview = fetchOverview($from, $to, $where, $params); $areaRows = fetchAreaStats($from, $to, $where, $params, $top, $sort); $dailyRows = fetchDailyStats($from, $to, $where, $params); $totalStorageBytes = fetchTotalStorage($where, $params); $days = max(1, (int)$from->diff($to)->days); printOverview($overview, $from, $to, $totalStorageBytes); printAreaTable($areaRows, $days); printDailyTable($dailyRows); exit(0); } catch (Throwable $e) { fwrite(STDERR, 'Error: ' . $e->getMessage() . "\n"); exit(1); }