1284 lines
42 KiB
PHP
1284 lines
42 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Zentrale Telegram-Cron-Datei fuer wiederkehrende Aufgaben.
|
|
*
|
|
* Empfohlener Cron:
|
|
* jede Minute aufrufen
|
|
*
|
|
* Beispiel Windows Aufgabenplanung:
|
|
* C:\laragon\bin\php\php-8.4.5-nts-Win32-vs17-x64\php.exe
|
|
* C:\Users\ich\source\repos\skyview.astronomiemuseum.de\scripts\telegram\telegram_cron.php
|
|
*/
|
|
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', '1');
|
|
set_time_limit(0);
|
|
|
|
const IMPORT_RUN_SOURCE = 'telegram-cron';
|
|
|
|
$rootDir = dirname(__DIR__, 2);
|
|
$stateFile = __DIR__ . DIRECTORY_SEPARATOR . 'telegram_cron_state.json';
|
|
$dbConfig = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php';
|
|
$telegramConfig = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'telegram.php';
|
|
require_once $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'telegram_message_footer.php';
|
|
$filterColorConfigPath = $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'filter_colors.json';
|
|
|
|
$dsn = sprintf(
|
|
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
|
$dbConfig['host'],
|
|
(int) ($dbConfig['port'] ?? 3306),
|
|
$dbConfig['dbname'],
|
|
$dbConfig['charset'] ?? 'utf8mb4'
|
|
);
|
|
|
|
$pdo = new PDO(
|
|
$dsn,
|
|
$dbConfig['user'],
|
|
$dbConfig['pass'],
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
]
|
|
);
|
|
|
|
ensureImportRunsTableExists($pdo);
|
|
|
|
$lockHandle = openCronLock();
|
|
if ($lockHandle === null) {
|
|
writeMessage('Telegram-Cron laeuft bereits. Dieser Start wird beendet.', true);
|
|
exit(1);
|
|
}
|
|
|
|
$timezoneBerlin = new DateTimeZone('Europe/Berlin');
|
|
$timezoneUtc = new DateTimeZone('UTC');
|
|
$nowBerlin = new DateTimeImmutable('now', $timezoneBerlin);
|
|
$nowUtc = $nowBerlin->setTimezone($timezoneUtc);
|
|
|
|
$tasks = [
|
|
[
|
|
'key' => 'send_tcrb_latest_v',
|
|
'label' => 'Letzten T-CrB-Wert mit Filter V senden',
|
|
'enabled' => true,
|
|
'schedule' => 'every-minute',
|
|
'handler' => static function (PDO $pdo, array $telegramConfig, DateTimeImmutable $nowBerlin) use ($stateFile): array {
|
|
$latestObservation = fetchLatestTcrbObservationByFilter($pdo, 'V');
|
|
if ($latestObservation === null) {
|
|
return [
|
|
'sent' => 0,
|
|
'updated' => 0,
|
|
'message' => 'Keine T-CrB-Beobachtung mit Filter V gefunden.',
|
|
];
|
|
}
|
|
|
|
$state = loadCronState($stateFile);
|
|
$taskKey = 'send_tcrb_latest_v';
|
|
$currentSignature = buildTcrbObservationSignature($latestObservation);
|
|
$lastSignature = (string) (($state[$taskKey]['last_signature'] ?? ''));
|
|
|
|
if ($currentSignature !== '' && $currentSignature === $lastSignature) {
|
|
return [
|
|
'sent' => 0,
|
|
'updated' => 0,
|
|
'message' => 'Kein neuer V-Messpunkt seit der letzten Telegram-Nachricht.',
|
|
];
|
|
}
|
|
|
|
$recipients = fetchTelegramRecipients($pdo);
|
|
if ($recipients === []) {
|
|
return [
|
|
'sent' => 0,
|
|
'updated' => 0,
|
|
'message' => 'Keine verbundenen Telegram-Empfaenger vorhanden.',
|
|
];
|
|
}
|
|
|
|
$messageText = buildTcrbLatestVMessage($latestObservation, $nowBerlin);
|
|
$chartObservations = fetchRecentTcrbObservationsByFilterWindow($pdo, 'V', '-2 months');
|
|
$chartSvg = buildTcrbVChartSvg($chartObservations, $latestObservation);
|
|
$chartPackage = createTelegramChartPackage($chartSvg);
|
|
$sentCount = 0;
|
|
$chartSentCount = 0;
|
|
|
|
try {
|
|
foreach ($recipients as $recipient) {
|
|
$chatId = trim((string) ($recipient['telegram_chat_id'] ?? ''));
|
|
if ($chatId === '') {
|
|
continue;
|
|
}
|
|
|
|
$messageSent = telegramSendBotMessage($telegramConfig, $chatId, $messageText);
|
|
$chartSent = false;
|
|
if (($chartPackage['kind'] ?? '') === 'photo') {
|
|
$chartSent = telegramSendBotPhoto(
|
|
$telegramConfig,
|
|
$chatId,
|
|
(string) $chartPackage['path'],
|
|
'T CrB V-Diagramm'
|
|
);
|
|
} elseif (($chartPackage['kind'] ?? '') === 'document') {
|
|
$chartSent = telegramSendBotDocument(
|
|
$telegramConfig,
|
|
$chatId,
|
|
(string) $chartPackage['path'],
|
|
'T CrB V-Diagramm'
|
|
);
|
|
}
|
|
|
|
if ($chartSent) {
|
|
$chartSentCount++;
|
|
}
|
|
|
|
if ($messageSent || $chartSent) {
|
|
$sentCount++;
|
|
}
|
|
}
|
|
} finally {
|
|
cleanupTelegramChartPackage($chartPackage);
|
|
}
|
|
|
|
if ($sentCount < 1) {
|
|
return [
|
|
'sent' => 0,
|
|
'updated' => 0,
|
|
'message' => 'Telegram-Versand konnte keinen Empfaenger erfolgreich erreichen.',
|
|
];
|
|
}
|
|
|
|
$state[$taskKey] = [
|
|
'last_signature' => $currentSignature,
|
|
'last_sent_at' => $nowBerlin->format(DateTimeInterface::ATOM),
|
|
'last_observed_at_utc' => (string) ($latestObservation['observed_at_utc'] ?? ''),
|
|
'last_filter' => 'V',
|
|
];
|
|
saveCronState($stateFile, $state);
|
|
|
|
return [
|
|
'sent' => $sentCount,
|
|
'updated' => 0,
|
|
'message' => sprintf(
|
|
'T-CrB V-Messpunkt %s an %d Telegram-Empfaenger gesendet, Diagramm an %d.',
|
|
(string) ($latestObservation['observed_at_utc'] ?? 'unbekannt'),
|
|
$sentCount,
|
|
$chartSentCount
|
|
),
|
|
];
|
|
},
|
|
],
|
|
[
|
|
'key' => 'cleanup_expired_link_tokens',
|
|
'label' => 'Abgelaufene Telegram-Verknuepfungscodes aufraeumen',
|
|
'enabled' => true,
|
|
'schedule' => 'every-15-minutes',
|
|
'handler' => static function (PDO $pdo, array $telegramConfig, DateTimeImmutable $nowBerlin): array {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE `app_users`
|
|
SET
|
|
`telegram_link_token` = NULL,
|
|
`telegram_link_token_expires_at` = NULL,
|
|
`updated_at` = NOW()
|
|
WHERE `telegram_link_token` IS NOT NULL
|
|
AND `telegram_link_token` <> ''
|
|
AND `telegram_link_token_expires_at` IS NOT NULL
|
|
AND `telegram_link_token_expires_at` < NOW()
|
|
");
|
|
$stmt->execute();
|
|
|
|
return [
|
|
'sent' => 0,
|
|
'updated' => (int) $stmt->rowCount(),
|
|
'message' => 'Abgelaufene Verbindungscodes bereinigt.',
|
|
];
|
|
},
|
|
],
|
|
[
|
|
'key' => 'daily_morning_report_placeholder',
|
|
'label' => 'Morgendlichen Telegram-Bericht vorbereiten',
|
|
'enabled' => false,
|
|
'schedule' => 'daily-08:00',
|
|
'handler' => static function (PDO $pdo, array $telegramConfig, DateTimeImmutable $nowBerlin): array {
|
|
return [
|
|
'sent' => 0,
|
|
'updated' => 0,
|
|
'message' => 'Platzhalter fuer den spaeteren Morgenbericht.',
|
|
];
|
|
},
|
|
],
|
|
[
|
|
'key' => 'daily_evening_report_placeholder',
|
|
'label' => 'Abendlichen Telegram-Bericht vorbereiten',
|
|
'enabled' => false,
|
|
'schedule' => 'daily-19:30',
|
|
'handler' => static function (PDO $pdo, array $telegramConfig, DateTimeImmutable $nowBerlin): array {
|
|
return [
|
|
'sent' => 0,
|
|
'updated' => 0,
|
|
'message' => 'Platzhalter fuer den spaeteren Abendbericht.',
|
|
];
|
|
},
|
|
],
|
|
[
|
|
'key' => 'connected_users_heartbeat_placeholder',
|
|
'label' => 'Verbundene Telegram-Nutzer pruefen',
|
|
'enabled' => false,
|
|
'schedule' => 'hourly-minute-05',
|
|
'handler' => static function (PDO $pdo, array $telegramConfig, DateTimeImmutable $nowBerlin): array {
|
|
$count = (int) $pdo->query("
|
|
SELECT COUNT(*)
|
|
FROM `app_users`
|
|
WHERE `telegram_chat_id` IS NOT NULL
|
|
AND `telegram_chat_id` <> ''
|
|
")->fetchColumn();
|
|
|
|
return [
|
|
'sent' => 0,
|
|
'updated' => 0,
|
|
'message' => 'Aktuell verbundene Telegram-Nutzer: ' . $count,
|
|
];
|
|
},
|
|
],
|
|
];
|
|
|
|
$runId = startImportRun($pdo, IMPORT_RUN_SOURCE, null);
|
|
$recordsReceived = count($tasks);
|
|
$recordsInserted = 0;
|
|
$recordsUpdated = 0;
|
|
$messages = [];
|
|
|
|
try {
|
|
foreach ($tasks as $task) {
|
|
if (($task['enabled'] ?? false) !== true) {
|
|
$messages[] = sprintf('[%s] deaktiviert', (string) $task['key']);
|
|
continue;
|
|
}
|
|
|
|
$schedule = (string) ($task['schedule'] ?? '');
|
|
if (!isTaskDue($schedule, $nowBerlin)) {
|
|
$messages[] = sprintf('[%s] noch nicht faellig (%s)', (string) $task['key'], $schedule);
|
|
continue;
|
|
}
|
|
|
|
$messages[] = sprintf('[%s] gestartet (%s)', (string) $task['key'], (string) ($task['label'] ?? $task['key']));
|
|
|
|
$handler = $task['handler'] ?? null;
|
|
if (!is_callable($handler)) {
|
|
throw new RuntimeException('Task-Handler ist nicht aufrufbar: ' . (string) $task['key']);
|
|
}
|
|
|
|
$result = $handler($pdo, $telegramConfig, $nowBerlin);
|
|
$recordsInserted += (int) ($result['sent'] ?? 0);
|
|
$recordsUpdated += (int) ($result['updated'] ?? 0);
|
|
$messages[] = sprintf(
|
|
'[%s] ok - %s',
|
|
(string) $task['key'],
|
|
trim((string) ($result['message'] ?? 'ohne Rueckmeldung'))
|
|
);
|
|
}
|
|
|
|
finishImportRun(
|
|
$pdo,
|
|
$runId,
|
|
$recordsReceived,
|
|
$recordsInserted,
|
|
$recordsUpdated,
|
|
'success',
|
|
implode("\n", $messages)
|
|
);
|
|
|
|
writeMessage('Telegram-Cron abgeschlossen.');
|
|
foreach ($messages as $message) {
|
|
writeMessage($message);
|
|
}
|
|
} catch (Throwable $exception) {
|
|
finishImportRun(
|
|
$pdo,
|
|
$runId,
|
|
$recordsReceived,
|
|
$recordsInserted,
|
|
$recordsUpdated,
|
|
'error',
|
|
$exception->getMessage()
|
|
);
|
|
|
|
writeMessage('Telegram-Cron mit Fehler beendet: ' . $exception->getMessage(), true);
|
|
exit(1);
|
|
} finally {
|
|
flock($lockHandle, LOCK_UN);
|
|
fclose($lockHandle);
|
|
}
|
|
|
|
function ensureImportRunsTableExists(PDO $pdo): void
|
|
{
|
|
$pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS `app_import_runs` (
|
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
`source` varchar(32) NOT NULL,
|
|
`source_url` varchar(500) DEFAULT NULL,
|
|
`started_at` datetime NOT NULL,
|
|
`finished_at` datetime DEFAULT NULL,
|
|
`records_received` int(11) NOT NULL DEFAULT 0,
|
|
`records_inserted` int(11) NOT NULL DEFAULT 0,
|
|
`records_updated` int(11) NOT NULL DEFAULT 0,
|
|
`status` varchar(32) NOT NULL DEFAULT 'success',
|
|
`message` text DEFAULT NULL,
|
|
PRIMARY KEY (`id`),
|
|
KEY `idx_app_import_runs_source` (`source`),
|
|
KEY `idx_app_import_runs_started_at` (`started_at`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
");
|
|
}
|
|
|
|
function openCronLock(): mixed
|
|
{
|
|
$lockFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'skyview_telegram_cron.lock';
|
|
$handle = fopen($lockFile, 'c+');
|
|
if ($handle === false) {
|
|
return null;
|
|
}
|
|
|
|
if (!flock($handle, LOCK_EX | LOCK_NB)) {
|
|
fclose($handle);
|
|
return null;
|
|
}
|
|
|
|
return $handle;
|
|
}
|
|
|
|
function startImportRun(PDO $pdo, string $source, ?string $sourceUrl): int
|
|
{
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO `app_import_runs` (
|
|
`source`,
|
|
`source_url`,
|
|
`started_at`,
|
|
`status`,
|
|
`message`
|
|
) VALUES (
|
|
:source,
|
|
:source_url,
|
|
NOW(),
|
|
'running',
|
|
NULL
|
|
)
|
|
");
|
|
$stmt->execute([
|
|
':source' => $source,
|
|
':source_url' => $sourceUrl,
|
|
]);
|
|
|
|
return (int) $pdo->lastInsertId();
|
|
}
|
|
|
|
function finishImportRun(
|
|
PDO $pdo,
|
|
int $runId,
|
|
int $recordsReceived,
|
|
int $recordsInserted,
|
|
int $recordsUpdated,
|
|
string $status,
|
|
?string $message
|
|
): void {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE `app_import_runs`
|
|
SET
|
|
`finished_at` = NOW(),
|
|
`records_received` = :records_received,
|
|
`records_inserted` = :records_inserted,
|
|
`records_updated` = :records_updated,
|
|
`status` = :status,
|
|
`message` = :message
|
|
WHERE `id` = :id
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([
|
|
':records_received' => $recordsReceived,
|
|
':records_inserted' => $recordsInserted,
|
|
':records_updated' => $recordsUpdated,
|
|
':status' => $status,
|
|
':message' => $message,
|
|
':id' => $runId,
|
|
]);
|
|
}
|
|
|
|
function fetchLatestTcrbObservationByFilter(PDO $pdo, string $filterCode): ?array
|
|
{
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
`id`,
|
|
`observed_at_utc`,
|
|
`jd`,
|
|
`magnitude`,
|
|
`magnitude_error`,
|
|
`filter_code`,
|
|
`observer_code`
|
|
FROM `aavso_tcrb_observations`
|
|
WHERE `filter_code` = :filter_code
|
|
ORDER BY `observed_at_utc` DESC, `id` DESC
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([':filter_code' => $filterCode]);
|
|
$row = $stmt->fetch();
|
|
|
|
return is_array($row) ? $row : null;
|
|
}
|
|
|
|
function fetchRecentTcrbObservationsByFilterWindow(PDO $pdo, string $filterCode, string $windowSpec = '-2 months'): array
|
|
{
|
|
$windowStartUtc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))
|
|
->modify($windowSpec)
|
|
->format('Y-m-d H:i:s');
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
`id`,
|
|
`observed_at_utc`,
|
|
`jd`,
|
|
`magnitude`,
|
|
`magnitude_error`,
|
|
`filter_code`,
|
|
`observer_code`
|
|
FROM `aavso_tcrb_observations`
|
|
WHERE `filter_code` = :filter_code
|
|
AND `observed_at_utc` >= :window_start_utc
|
|
ORDER BY `observed_at_utc` ASC, `id` ASC
|
|
");
|
|
$stmt->execute([
|
|
':filter_code' => $filterCode,
|
|
':window_start_utc' => $windowStartUtc,
|
|
]);
|
|
$rows = $stmt->fetchAll();
|
|
|
|
if ($rows === []) {
|
|
$fallbackStmt = $pdo->prepare("
|
|
SELECT
|
|
`id`,
|
|
`observed_at_utc`,
|
|
`jd`,
|
|
`magnitude`,
|
|
`magnitude_error`,
|
|
`filter_code`,
|
|
`observer_code`
|
|
FROM `aavso_tcrb_observations`
|
|
WHERE `filter_code` = :filter_code
|
|
ORDER BY `observed_at_utc` DESC, `id` DESC
|
|
LIMIT 30
|
|
");
|
|
$fallbackStmt->execute([
|
|
':filter_code' => $filterCode,
|
|
]);
|
|
$rows = $fallbackStmt->fetchAll();
|
|
usort($rows, static function (array $a, array $b): int {
|
|
return strcmp((string) ($a['observed_at_utc'] ?? ''), (string) ($b['observed_at_utc'] ?? ''));
|
|
});
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
function fetchTelegramRecipients(PDO $pdo): array
|
|
{
|
|
$stmt = $pdo->query("
|
|
SELECT `id`, `telegram_chat_id`, `telegram_username`
|
|
FROM `app_users`
|
|
WHERE `telegram_chat_id` IS NOT NULL
|
|
AND `telegram_chat_id` <> ''
|
|
AND `telegram_tcrb_v_enabled` = 1
|
|
AND `is_active` = 1
|
|
ORDER BY `id` ASC
|
|
");
|
|
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
function buildTcrbLatestVMessage(array $observation, DateTimeImmutable $nowBerlin): string
|
|
{
|
|
$observedUtc = trim((string) ($observation['observed_at_utc'] ?? ''));
|
|
$observedText = formatGermanDateTime($observedUtc);
|
|
$magnitude = formatDecimal($observation['magnitude'] ?? null, 3);
|
|
$magnitudeError = $observation['magnitude_error'] !== null
|
|
? ' ± ' . formatDecimal($observation['magnitude_error'], 3)
|
|
: '';
|
|
$jd = formatDecimal($observation['jd'] ?? null, 6);
|
|
$observerCode = trim((string) ($observation['observer_code'] ?? ''));
|
|
$nextPossibleEruptionDate = calculateNextPossibleTcrbEruptionDate($nowBerlin);
|
|
|
|
$lines = [
|
|
'SkyViews TCrBotti mit dem Update',
|
|
'',
|
|
'Magnitude: ' . $magnitude . $magnitudeError,
|
|
'Zeitpunkt: ' . $observedText,
|
|
'JD: ' . $jd,
|
|
'Nächster möglicher Ausbruch:',
|
|
$nextPossibleEruptionDate,
|
|
];
|
|
|
|
|
|
return implode("\n", $lines);
|
|
}
|
|
|
|
function calculateNextPossibleTcrbEruptionDate(DateTimeImmutable $referenceDate): string
|
|
{
|
|
$eruptionIntervalDays = 227.5687;
|
|
$lastEruptionUtc = new DateTimeImmutable('1946-02-09 00:00:00', new DateTimeZone('UTC'));
|
|
$referenceUtc = $referenceDate->setTimezone(new DateTimeZone('UTC'));
|
|
$secondsPerDay = 86400;
|
|
$eruptionIntervalSeconds = (int) round($eruptionIntervalDays * $secondsPerDay);
|
|
$elapsedSecondsSinceLastEruption = max(0, $referenceUtc->getTimestamp() - $lastEruptionUtc->getTimestamp());
|
|
$elapsedIntervals = (int) floor($elapsedSecondsSinceLastEruption / $eruptionIntervalSeconds);
|
|
$nextPossibleEruptionUtc = $lastEruptionUtc->modify('+' . (($elapsedIntervals + 1) * $eruptionIntervalSeconds) . ' seconds');
|
|
|
|
return $nextPossibleEruptionUtc->format('d.m.Y');
|
|
}
|
|
|
|
function formatGermanDateTime(?string $value): string
|
|
{
|
|
if ($value === null || trim($value) === '') {
|
|
return 'unbekannt';
|
|
}
|
|
|
|
try {
|
|
$date = new DateTimeImmutable($value, new DateTimeZone('UTC'));
|
|
} catch (Exception) {
|
|
return $value;
|
|
}
|
|
|
|
return $date->format('d.m.Y H:i:s') . ' UTC';
|
|
}
|
|
|
|
function buildTcrbVChartSvg(array $observations, array $latestObservation): string
|
|
{
|
|
$filterColors = loadFilterColors();
|
|
$vColor = (string) ($filterColors['V'] ?? '#92FF00');
|
|
$vStrokeColor = darkenHexColor($vColor, 0.35);
|
|
$vHighlightColor = lightenHexColor($vColor, 0.35);
|
|
$vHighlightStrokeColor = lightenHexColor($vColor, 0.6);
|
|
|
|
$width = 1200;
|
|
$height = 700;
|
|
$paddingLeft = 90;
|
|
$paddingRight = 40;
|
|
$paddingTop = 80;
|
|
$paddingBottom = 90;
|
|
$plotWidth = $width - $paddingLeft - $paddingRight;
|
|
$plotHeight = $height - $paddingTop - $paddingBottom;
|
|
$plotBottomY = $paddingTop + $plotHeight;
|
|
$plotRightX = $paddingLeft + $plotWidth;
|
|
|
|
if (count($observations) < 2) {
|
|
$observations = [$latestObservation];
|
|
}
|
|
|
|
$timestamps = [];
|
|
$magnitudes = [];
|
|
foreach ($observations as $row) {
|
|
$timestamps[] = strtotime((string) ($row['observed_at_utc'] ?? ''));
|
|
$magnitudes[] = (float) ($row['magnitude'] ?? 0.0);
|
|
}
|
|
|
|
$timestamps = array_values(array_filter($timestamps, static fn ($value): bool => is_int($value) || is_float($value)));
|
|
if ($timestamps === []) {
|
|
$timestamps = [time() - 3600, time()];
|
|
}
|
|
|
|
$minX = (float) min($timestamps);
|
|
$maxX = (float) max($timestamps);
|
|
if ($maxX <= $minX) {
|
|
$maxX = $minX + 3600.0;
|
|
}
|
|
|
|
$minY = min($magnitudes);
|
|
$maxY = max($magnitudes);
|
|
if ($maxY <= $minY) {
|
|
$maxY = $minY + 0.5;
|
|
}
|
|
|
|
$paddingMag = max(0.06, ($maxY - $minY) * 0.10);
|
|
$axisMinY = $minY - $paddingMag;
|
|
$axisMaxY = $maxY + $paddingMag;
|
|
|
|
$scaleX = static function (float $timestamp) use ($minX, $maxX, $paddingLeft, $plotWidth): float {
|
|
return $paddingLeft + (($timestamp - $minX) / ($maxX - $minX)) * $plotWidth;
|
|
};
|
|
|
|
$scaleY = static function (float $magnitude) use ($axisMinY, $axisMaxY, $paddingTop, $plotHeight): float {
|
|
$ratio = ($magnitude - $axisMinY) / ($axisMaxY - $axisMinY);
|
|
return $paddingTop + $ratio * $plotHeight;
|
|
};
|
|
|
|
$gridLines = [];
|
|
for ($i = 0; $i <= 4; $i++) {
|
|
$gridMag = $axisMinY + (($axisMaxY - $axisMinY) / 4) * $i;
|
|
$y = $scaleY($gridMag);
|
|
$gridLines[] = sprintf(
|
|
'<line x1="%1$.2f" y1="%2$.2f" x2="%3$.2f" y2="%2$.2f" stroke="#243445" stroke-width="1" stroke-dasharray="6 6" />',
|
|
$paddingLeft,
|
|
$y,
|
|
$paddingLeft + $plotWidth
|
|
);
|
|
$gridLines[] = sprintf(
|
|
'<text x="%1$.2f" y="%2$.2f" fill="#dbe7f2" font-size="20" text-anchor="end" dominant-baseline="middle">%3$s</text>',
|
|
$paddingLeft - 14,
|
|
$y,
|
|
htmlspecialchars(number_format($gridMag, 2, '.', ''), ENT_QUOTES, 'UTF-8')
|
|
);
|
|
}
|
|
|
|
$pathParts = [];
|
|
$errorBars = [];
|
|
$pointsSvg = [];
|
|
|
|
foreach ($observations as $index => $row) {
|
|
$timestamp = strtotime((string) ($row['observed_at_utc'] ?? ''));
|
|
if ($timestamp === false) {
|
|
continue;
|
|
}
|
|
|
|
$magnitude = (float) ($row['magnitude'] ?? 0.0);
|
|
$x = $scaleX((float) $timestamp);
|
|
$y = $scaleY($magnitude);
|
|
$pathParts[] = sprintf('%s %.2f %.2f', $index === 0 ? 'M' : 'L', $x, $y);
|
|
|
|
$error = isset($row['magnitude_error']) && $row['magnitude_error'] !== null
|
|
? (float) $row['magnitude_error']
|
|
: 0.0;
|
|
if ($error > 0) {
|
|
$yTop = $scaleY($magnitude - $error);
|
|
$yBottom = $scaleY($magnitude + $error);
|
|
$errorBars[] = sprintf(
|
|
'<line x1="%1$.2f" y1="%2$.2f" x2="%1$.2f" y2="%3$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />' .
|
|
'<line x1="%4$.2f" y1="%2$.2f" x2="%5$.2f" y2="%2$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />' .
|
|
'<line x1="%4$.2f" y1="%3$.2f" x2="%5$.2f" y2="%3$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />',
|
|
$x,
|
|
$yTop,
|
|
$yBottom,
|
|
$x - 7,
|
|
$x + 7,
|
|
htmlspecialchars($vHighlightColor, ENT_QUOTES, 'UTF-8')
|
|
);
|
|
}
|
|
|
|
$isLatest = (string) ($row['observed_at_utc'] ?? '') === (string) ($latestObservation['observed_at_utc'] ?? '')
|
|
&& (string) ($row['jd'] ?? '') === (string) ($latestObservation['jd'] ?? '');
|
|
|
|
$pointsSvg[] = sprintf(
|
|
'<circle cx="%1$.2f" cy="%2$.2f" r="%3$.2f" fill="%4$s" stroke="%5$s" stroke-width="%6$.2f" />',
|
|
$x,
|
|
$y,
|
|
$isLatest ? 7.0 : 4.2,
|
|
$isLatest ? $vHighlightColor : $vColor,
|
|
$isLatest ? $vHighlightStrokeColor : $vHighlightColor,
|
|
$isLatest ? 2.6 : 1.4
|
|
);
|
|
}
|
|
|
|
$firstLabel = date('d.m.Y H:i', (int) $minX) . ' UTC';
|
|
$lastLabel = date('d.m.Y H:i', (int) $maxX) . ' UTC';
|
|
$latestMagnitude = number_format((float) ($latestObservation['magnitude'] ?? 0.0), 3, '.', '');
|
|
$latestError = isset($latestObservation['magnitude_error']) && $latestObservation['magnitude_error'] !== null
|
|
? ' +/- ' . number_format((float) $latestObservation['magnitude_error'], 3, '.', '')
|
|
: '';
|
|
$latestUtc = htmlspecialchars((string) ($latestObservation['observed_at_utc'] ?? ''), ENT_QUOTES, 'UTF-8');
|
|
$gridSvg = implode("\n ", $gridLines);
|
|
$errorBarsSvg = implode("\n ", $errorBars);
|
|
$pathSvg = implode(' ', $pathParts);
|
|
$pointsSvgMarkup = implode("\n ", $pointsSvg);
|
|
|
|
return sprintf(
|
|
<<<'SVG'
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">
|
|
<defs>
|
|
<linearGradient id="bg" x1="0%%" y1="0%%" x2="0%%" y2="100%%">
|
|
<stop offset="0%%" stop-color="#07111d"/>
|
|
<stop offset="100%%" stop-color="#0f2133"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<rect width="%d" height="%d" fill="url(#bg)"/>
|
|
<rect x="%d" y="%d" width="%d" height="%d" rx="16" fill="#11263b" stroke="#2f4d68" stroke-width="1.5"/>
|
|
<text x="%d" y="44" fill="#f8fafc" font-size="30" font-weight="700">T CrB Lichtkurve - Filter V</text>
|
|
<text x="%d" y="70" fill="#cbd5e1" font-size="19">letzte 2 Monate</text>
|
|
<text x="%d" y="%d" fill="#cbd5e1" font-size="18">%s</text>
|
|
<text x="%d" y="%d" fill="#cbd5e1" font-size="18" text-anchor="end">%s</text>
|
|
<text x="%d" y="48" fill="%s" font-size="22" text-anchor="end">Aktuell: %s%s</text>
|
|
<text x="%d" y="74" fill="#cbd5e1" font-size="18" text-anchor="end">%s UTC</text>
|
|
%s
|
|
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#88a4bf" stroke-width="1.6"/>
|
|
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#88a4bf" stroke-width="1.6"/>
|
|
%s
|
|
<path d="%s" fill="none" stroke="%s" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
|
|
%s
|
|
</svg>
|
|
SVG,
|
|
$width,
|
|
$height,
|
|
$width,
|
|
$height,
|
|
$width,
|
|
$height,
|
|
$paddingLeft,
|
|
$paddingTop,
|
|
$plotWidth,
|
|
$plotHeight,
|
|
$paddingLeft,
|
|
$paddingLeft,
|
|
$paddingLeft,
|
|
$height - 28,
|
|
htmlspecialchars($firstLabel, ENT_QUOTES, 'UTF-8'),
|
|
$width - $paddingRight,
|
|
$height - 28,
|
|
htmlspecialchars($lastLabel, ENT_QUOTES, 'UTF-8'),
|
|
$width - $paddingRight,
|
|
htmlspecialchars($vHighlightColor, ENT_QUOTES, 'UTF-8'),
|
|
htmlspecialchars($latestMagnitude, ENT_QUOTES, 'UTF-8'),
|
|
htmlspecialchars($latestError, ENT_QUOTES, 'UTF-8'),
|
|
$width - $paddingRight,
|
|
$latestUtc,
|
|
$gridSvg,
|
|
$paddingLeft,
|
|
$paddingTop,
|
|
$paddingLeft,
|
|
$plotBottomY,
|
|
$paddingLeft,
|
|
$plotBottomY,
|
|
$plotRightX,
|
|
$plotBottomY,
|
|
$errorBarsSvg,
|
|
$pathSvg,
|
|
htmlspecialchars($vStrokeColor, ENT_QUOTES, 'UTF-8'),
|
|
$pointsSvgMarkup
|
|
);
|
|
}
|
|
|
|
function loadFilterColors(): array
|
|
{
|
|
static $colors = null;
|
|
global $filterColorConfigPath;
|
|
|
|
if (is_array($colors)) {
|
|
return $colors;
|
|
}
|
|
|
|
$colors = [];
|
|
if (!is_string($filterColorConfigPath) || !is_file($filterColorConfigPath)) {
|
|
return $colors;
|
|
}
|
|
|
|
$raw = file_get_contents($filterColorConfigPath);
|
|
$decoded = is_string($raw) ? json_decode($raw, true) : null;
|
|
if (!is_array($decoded)) {
|
|
return $colors;
|
|
}
|
|
|
|
foreach ($decoded as $filterCode => $config) {
|
|
if (is_array($config) && !empty($config['color'])) {
|
|
$colors[(string) $filterCode] = (string) $config['color'];
|
|
}
|
|
}
|
|
|
|
return $colors;
|
|
}
|
|
|
|
function darkenHexColor(string $hexColor, float $amount): string
|
|
{
|
|
return adjustHexColor($hexColor, -abs($amount));
|
|
}
|
|
|
|
function lightenHexColor(string $hexColor, float $amount): string
|
|
{
|
|
return adjustHexColor($hexColor, abs($amount));
|
|
}
|
|
|
|
function adjustHexColor(string $hexColor, float $amount): string
|
|
{
|
|
$hex = ltrim(trim($hexColor), '#');
|
|
if (!preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
|
|
return '#92FF00';
|
|
}
|
|
|
|
$channels = [
|
|
hexdec(substr($hex, 0, 2)),
|
|
hexdec(substr($hex, 2, 2)),
|
|
hexdec(substr($hex, 4, 2)),
|
|
];
|
|
|
|
foreach ($channels as $index => $value) {
|
|
if ($amount >= 0) {
|
|
$channels[$index] = (int) round($value + ((255 - $value) * $amount));
|
|
} else {
|
|
$channels[$index] = (int) round($value * (1 + $amount));
|
|
}
|
|
$channels[$index] = max(0, min(255, $channels[$index]));
|
|
}
|
|
|
|
return sprintf('#%02X%02X%02X', $channels[0], $channels[1], $channels[2]);
|
|
}
|
|
|
|
function buildTcrbObservationSignature(array $observation): string
|
|
{
|
|
return implode('|', [
|
|
(string) ($observation['observed_at_utc'] ?? ''),
|
|
(string) ($observation['jd'] ?? ''),
|
|
(string) ($observation['magnitude'] ?? ''),
|
|
(string) ($observation['magnitude_error'] ?? ''),
|
|
(string) ($observation['filter_code'] ?? ''),
|
|
(string) ($observation['observer_code'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
function loadCronState(string $stateFile): array
|
|
{
|
|
if (!is_file($stateFile)) {
|
|
return [];
|
|
}
|
|
|
|
$raw = @file_get_contents($stateFile);
|
|
if ($raw === false || trim($raw) === '') {
|
|
return [];
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
function saveCronState(string $stateFile, array $state): void
|
|
{
|
|
$json = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
if ($json === false) {
|
|
throw new RuntimeException('Cron-Statusdatei konnte nicht als JSON gespeichert werden.');
|
|
}
|
|
|
|
if (@file_put_contents($stateFile, $json . PHP_EOL, LOCK_EX) === false) {
|
|
throw new RuntimeException('Cron-Statusdatei konnte nicht geschrieben werden: ' . $stateFile);
|
|
}
|
|
}
|
|
|
|
function createTelegramChartPackage(string $svgContent): array
|
|
{
|
|
$directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'skyview_telegram_charts';
|
|
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
|
throw new RuntimeException('Temp-Verzeichnis fuer Telegram-Diagramme konnte nicht erstellt werden.');
|
|
}
|
|
|
|
$baseName = 'tcrb_v_latest_chart_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4));
|
|
$svgPath = $directory . DIRECTORY_SEPARATOR . $baseName . '.svg';
|
|
if (@file_put_contents($svgPath, $svgContent) === false) {
|
|
throw new RuntimeException('Diagrammdatei konnte nicht geschrieben werden: ' . $svgPath);
|
|
}
|
|
|
|
$pngPath = $directory . DIRECTORY_SEPARATOR . $baseName . '.png';
|
|
if (convertSvgToPng($svgPath, $pngPath)) {
|
|
return [
|
|
'kind' => 'photo',
|
|
'path' => $pngPath,
|
|
'cleanup' => [$svgPath, $pngPath],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'kind' => 'document',
|
|
'path' => $svgPath,
|
|
'cleanup' => [$svgPath],
|
|
];
|
|
}
|
|
|
|
function cleanupTelegramChartPackage(array $chartPackage): void
|
|
{
|
|
$files = is_array($chartPackage['cleanup'] ?? null) ? $chartPackage['cleanup'] : [];
|
|
foreach ($files as $file) {
|
|
if (is_string($file) && $file !== '') {
|
|
@unlink($file);
|
|
}
|
|
}
|
|
}
|
|
|
|
function convertSvgToPng(string $svgPath, string $pngPath): bool
|
|
{
|
|
if (!is_file($svgPath)) {
|
|
return false;
|
|
}
|
|
|
|
$commands = [
|
|
sprintf('rsvg-convert -o %s %s', escapeshellarg($pngPath), escapeshellarg($svgPath)),
|
|
sprintf('magick %s %s', escapeshellarg($svgPath), escapeshellarg($pngPath)),
|
|
sprintf('convert %s %s', escapeshellarg($svgPath), escapeshellarg($pngPath)),
|
|
];
|
|
|
|
foreach ($commands as $command) {
|
|
if (!function_exists('shell_exec')) {
|
|
continue;
|
|
}
|
|
|
|
@shell_exec($command . ' 2>&1');
|
|
if (is_file($pngPath) && filesize($pngPath) > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function telegramHttpPostJson(string $url, array $payload): array
|
|
{
|
|
$jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
if ($jsonPayload === false) {
|
|
return [
|
|
'ok' => false,
|
|
'error' => 'Die Telegram-Anfrage konnte nicht als JSON kodiert werden.',
|
|
'body' => null,
|
|
];
|
|
}
|
|
|
|
if (DIRECTORY_SEPARATOR === '\\' && function_exists('shell_exec')) {
|
|
$urlBase64 = base64_encode($url);
|
|
$payloadBase64 = base64_encode($jsonPayload);
|
|
$powerShellScript = str_replace(
|
|
['__URL_BASE64__', '__PAYLOAD_BASE64__'],
|
|
[$urlBase64, $payloadBase64],
|
|
<<<'PS'
|
|
$ErrorActionPreference = 'Stop'
|
|
$url = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('__URL_BASE64__'))
|
|
$body = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('__PAYLOAD_BASE64__'))
|
|
try {
|
|
$response = Invoke-RestMethod -Method Post -Uri $url -ContentType 'application/json' -Body $body
|
|
[PSCustomObject]@{
|
|
ok = $true
|
|
body = ($response | ConvertTo-Json -Depth 20 -Compress)
|
|
error = ''
|
|
} | ConvertTo-Json -Compress
|
|
} catch {
|
|
$stream = $null
|
|
$responseBody = ''
|
|
if ($_.Exception.Response) {
|
|
try {
|
|
$stream = $_.Exception.Response.GetResponseStream()
|
|
if ($stream) {
|
|
$reader = New-Object System.IO.StreamReader($stream)
|
|
$responseBody = $reader.ReadToEnd()
|
|
}
|
|
} catch {
|
|
} finally {
|
|
if ($stream) {
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
}
|
|
[PSCustomObject]@{
|
|
ok = $false
|
|
body = $responseBody
|
|
error = $_.Exception.Message
|
|
} | ConvertTo-Json -Compress
|
|
}
|
|
PS
|
|
);
|
|
$encodedCommandBinary = function_exists('iconv')
|
|
? iconv('UTF-8', 'UTF-16LE', $powerShellScript)
|
|
: $powerShellScript;
|
|
$encodedCommand = base64_encode($encodedCommandBinary === false ? $powerShellScript : $encodedCommandBinary);
|
|
$command = 'powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ' . $encodedCommand;
|
|
$rawOutput = shell_exec($command);
|
|
|
|
if ($rawOutput !== null && trim($rawOutput) !== '') {
|
|
$decodedShell = json_decode(trim($rawOutput), true);
|
|
if (is_array($decodedShell)) {
|
|
return [
|
|
'ok' => (bool) ($decodedShell['ok'] ?? false),
|
|
'error' => $decodedShell['error'] ?? null,
|
|
'body' => $decodedShell['body'] ?? null,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (function_exists('curl_init')) {
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Content-Length: ' . strlen($jsonPayload),
|
|
],
|
|
CURLOPT_POSTFIELDS => $jsonPayload,
|
|
CURLOPT_TIMEOUT => 20,
|
|
]);
|
|
|
|
$body = curl_exec($ch);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
return [
|
|
'ok' => $body !== false,
|
|
'error' => $body === false ? $error : null,
|
|
'body' => $body !== false ? $body : null,
|
|
];
|
|
}
|
|
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'POST',
|
|
'header' => implode("\r\n", [
|
|
'Content-Type: application/json',
|
|
'Content-Length: ' . strlen($jsonPayload),
|
|
]),
|
|
'content' => $jsonPayload,
|
|
'timeout' => 20,
|
|
'ignore_errors' => true,
|
|
],
|
|
]);
|
|
|
|
$body = @file_get_contents($url, false, $context);
|
|
|
|
return [
|
|
'ok' => $body !== false,
|
|
'error' => $body === false ? 'HTTP-Request fehlgeschlagen.' : null,
|
|
'body' => $body !== false ? $body : null,
|
|
];
|
|
}
|
|
|
|
function telegramHttpPostMultipart(string $url, array $fields, string $fileField, string $filePath, ?string $fileName = null): array
|
|
{
|
|
if (!is_file($filePath)) {
|
|
return [
|
|
'ok' => false,
|
|
'error' => 'Datei fuer Telegram-Versand nicht gefunden: ' . $filePath,
|
|
'body' => null,
|
|
];
|
|
}
|
|
|
|
if (DIRECTORY_SEPARATOR === '\\' && function_exists('shell_exec')) {
|
|
$urlBase64 = base64_encode($url);
|
|
$filePathBase64 = base64_encode($filePath);
|
|
$fileNameBase64 = base64_encode((string) ($fileName ?? basename($filePath)));
|
|
$fieldsBase64 = base64_encode(json_encode($fields, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}');
|
|
$fileFieldBase64 = base64_encode($fileField);
|
|
|
|
$powerShellScript = str_replace(
|
|
['__URL_BASE64__', '__FILE_PATH_BASE64__', '__FILE_NAME_BASE64__', '__FIELDS_BASE64__', '__FILE_FIELD_BASE64__'],
|
|
[$urlBase64, $filePathBase64, $fileNameBase64, $fieldsBase64, $fileFieldBase64],
|
|
<<<'PS'
|
|
$ErrorActionPreference = 'Stop'
|
|
$url = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('__URL_BASE64__'))
|
|
$filePath = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('__FILE_PATH_BASE64__'))
|
|
$fileName = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('__FILE_NAME_BASE64__'))
|
|
$fileField = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('__FILE_FIELD_BASE64__'))
|
|
$fieldsJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('__FIELDS_BASE64__'))
|
|
$fieldsObject = $fieldsJson | ConvertFrom-Json
|
|
$form = @{}
|
|
foreach ($property in $fieldsObject.PSObject.Properties) {
|
|
$form[$property.Name] = [string] $property.Value
|
|
}
|
|
$form[$fileField] = Get-Item -LiteralPath $filePath
|
|
try {
|
|
$response = Invoke-RestMethod -Method Post -Uri $url -Form $form
|
|
[PSCustomObject]@{
|
|
ok = $true
|
|
body = ($response | ConvertTo-Json -Depth 20 -Compress)
|
|
error = ''
|
|
} | ConvertTo-Json -Compress
|
|
} catch {
|
|
[PSCustomObject]@{
|
|
ok = $false
|
|
body = ''
|
|
error = $_.Exception.Message
|
|
} | ConvertTo-Json -Compress
|
|
}
|
|
PS
|
|
);
|
|
$encodedCommandBinary = function_exists('iconv')
|
|
? iconv('UTF-8', 'UTF-16LE', $powerShellScript)
|
|
: $powerShellScript;
|
|
$encodedCommand = base64_encode($encodedCommandBinary === false ? $powerShellScript : $encodedCommandBinary);
|
|
$command = 'powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ' . $encodedCommand;
|
|
$rawOutput = shell_exec($command);
|
|
|
|
if ($rawOutput !== null && trim($rawOutput) !== '') {
|
|
$decodedShell = json_decode(trim($rawOutput), true);
|
|
if (is_array($decodedShell)) {
|
|
return [
|
|
'ok' => (bool) ($decodedShell['ok'] ?? false),
|
|
'error' => $decodedShell['error'] ?? null,
|
|
'body' => $decodedShell['body'] ?? null,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (function_exists('curl_init')) {
|
|
$payload = $fields;
|
|
$extension = strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION));
|
|
$mimeType = match ($extension) {
|
|
'png' => 'image/png',
|
|
'jpg', 'jpeg' => 'image/jpeg',
|
|
'svg' => 'image/svg+xml',
|
|
default => 'application/octet-stream',
|
|
};
|
|
$payload[$fileField] = new CURLFile($filePath, $mimeType, $fileName ?? basename($filePath));
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_TIMEOUT => 30,
|
|
]);
|
|
|
|
$body = curl_exec($ch);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
return [
|
|
'ok' => $body !== false,
|
|
'error' => $body === false ? $error : null,
|
|
'body' => $body !== false ? $body : null,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'ok' => false,
|
|
'error' => 'Multipart-Versand ist ohne curl nicht verfuegbar.',
|
|
'body' => null,
|
|
];
|
|
}
|
|
|
|
function telegramSendBotMessage(array $telegramConfig, string $chatId, string $messageText): bool
|
|
{
|
|
$botToken = trim((string) ($telegramConfig['bot_token'] ?? ''));
|
|
if ($botToken === '' || $chatId === '' || $messageText === '') {
|
|
return false;
|
|
}
|
|
|
|
$response = telegramHttpPostJson(
|
|
'https://api.telegram.org/bot' . $botToken . '/sendMessage',
|
|
[
|
|
'chat_id' => $chatId,
|
|
'text' => appendTelegramServiceFooter($messageText),
|
|
]
|
|
);
|
|
|
|
return (bool) ($response['ok'] ?? false);
|
|
}
|
|
|
|
function telegramSendBotDocument(array $telegramConfig, string $chatId, string $filePath, string $caption = ''): bool
|
|
{
|
|
$botToken = trim((string) ($telegramConfig['bot_token'] ?? ''));
|
|
if ($botToken === '' || $chatId === '' || !is_file($filePath)) {
|
|
return false;
|
|
}
|
|
|
|
$response = telegramHttpPostMultipart(
|
|
'https://api.telegram.org/bot' . $botToken . '/sendDocument',
|
|
[
|
|
'chat_id' => $chatId,
|
|
'caption' => appendTelegramServiceFooter($caption),
|
|
],
|
|
'document',
|
|
$filePath,
|
|
basename($filePath)
|
|
);
|
|
|
|
return (bool) ($response['ok'] ?? false);
|
|
}
|
|
|
|
function telegramSendBotPhoto(array $telegramConfig, string $chatId, string $filePath, string $caption = ''): bool
|
|
{
|
|
$botToken = trim((string) ($telegramConfig['bot_token'] ?? ''));
|
|
if ($botToken === '' || $chatId === '' || !is_file($filePath)) {
|
|
return false;
|
|
}
|
|
|
|
$response = telegramHttpPostMultipart(
|
|
'https://api.telegram.org/bot' . $botToken . '/sendPhoto',
|
|
[
|
|
'chat_id' => $chatId,
|
|
'caption' => appendTelegramServiceFooter($caption),
|
|
],
|
|
'photo',
|
|
$filePath,
|
|
basename($filePath)
|
|
);
|
|
|
|
return (bool) ($response['ok'] ?? false);
|
|
}
|
|
|
|
function formatDecimal(mixed $value, int $decimals): string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return '—';
|
|
}
|
|
|
|
if (!is_numeric((string) $value)) {
|
|
return (string) $value;
|
|
}
|
|
|
|
return number_format((float) $value, $decimals, '.', '');
|
|
}
|
|
|
|
function isTaskDue(string $schedule, DateTimeImmutable $nowBerlin): bool
|
|
{
|
|
if ($schedule === 'every-run') {
|
|
return true;
|
|
}
|
|
|
|
if ($schedule === 'every-minute') {
|
|
return true;
|
|
}
|
|
|
|
if (preg_match('/^every-(\d+)-minutes$/', $schedule, $matches)) {
|
|
$interval = max(1, (int) $matches[1]);
|
|
return ((int) $nowBerlin->format('i')) % $interval === 0;
|
|
}
|
|
|
|
if (preg_match('/^hourly-minute-(\d{2})$/', $schedule, $matches)) {
|
|
return $nowBerlin->format('i') === $matches[1];
|
|
}
|
|
|
|
if (preg_match('/^daily-(\d{2}):(\d{2})$/', $schedule, $matches)) {
|
|
return $nowBerlin->format('H') === $matches[1]
|
|
&& $nowBerlin->format('i') === $matches[2];
|
|
}
|
|
|
|
if (preg_match('/^weekday-(mon|tue|wed|thu|fri|sat|sun)-(\d{2}):(\d{2})$/', $schedule, $matches)) {
|
|
$weekdayMap = [
|
|
'mon' => '1',
|
|
'tue' => '2',
|
|
'wed' => '3',
|
|
'thu' => '4',
|
|
'fri' => '5',
|
|
'sat' => '6',
|
|
'sun' => '7',
|
|
];
|
|
|
|
return $nowBerlin->format('N') === ($weekdayMap[$matches[1]] ?? '')
|
|
&& $nowBerlin->format('H') === $matches[2]
|
|
&& $nowBerlin->format('i') === $matches[3];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function writeMessage(string $message, bool $isError = false): void
|
|
{
|
|
$prefix = '[' . (new DateTimeImmutable('now'))->format('Y-m-d H:i:s') . '] ';
|
|
$defaultStream = $isError ? 'php://stderr' : 'php://stdout';
|
|
|
|
if ($isError && defined('STDERR')) {
|
|
fwrite(STDERR, $prefix . $message . PHP_EOL);
|
|
return;
|
|
}
|
|
|
|
if (!$isError && defined('STDOUT')) {
|
|
fwrite(STDOUT, $prefix . $message . PHP_EOL);
|
|
return;
|
|
}
|
|
|
|
$stream = @fopen($defaultStream, 'wb');
|
|
if ($stream === false) {
|
|
error_log($prefix . $message);
|
|
return;
|
|
}
|
|
|
|
fwrite($stream, $prefix . $message . PHP_EOL);
|
|
fclose($stream);
|
|
}
|