migrate to Twig

This commit is contained in:
Christopher Teutsch 2023-01-20 18:54:40 +01:00
parent 302d41d09d
commit 0bc2c726f5
5 changed files with 2239 additions and 225 deletions

View File

@ -2,6 +2,11 @@
"require": {
"johngrogg/ics-parser": "^3",
"symfony/http-client": "^6.2",
"symfony/http-kernel": "^6.2"
"symfony/http-kernel": "^6.2",
"twig/twig": "^3.5",
"twig/intl-extra": "^3.5"
},
"require-dev": {
"vimeo/psalm": "^5.4"
}
}

1995
composer.lock generated

File diff suppressed because it is too large Load Diff

347
index.php
View File

@ -1,86 +1,50 @@
<?php
require_once 'vendor/autoload.php';
use ICal\ICal;
use Symfony\Component\HttpClient\CachingHttpClient;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpKernel\HttpCache\Store;
use Twig\TwigFilter;
$store = new Store('/tmp/dorf.jetzt/');
$client = HttpClient::create();
$client = new CachingHttpClient($client, $store, array('default_ttl' => 60, 'allow_revalidate' => true));
const INVALID_UAS = [
"AhrefsBot",
"Googlebot",
"Yahoo",
"Go-http-client/",
"bingbot",
"CheckMarkNetwork",
"SemrushBot",
"BingPreview",
"facebookexternalhit",
"hetrix.tools",
];
static $DATE_FORMAT = 'd.m.Y H:i';
static $VISITORS_FILE = '/opt/dorf.jetzt_visitors';
static $DORF_IN_LOCKDOWN = false;
static $DORF_VIRTUAL_EVENTS = true;
static $ICAL_URL = 'https://chaosdorf.de/~derf/cccd_all.ics';
$state_map = array(
'closed' => (object) array(
'state_string' => 'Das Dorf ist gerade <em>geschlossen</em>.',
'svg' => 'lock',
'color' => 'red',
),
'maybe_open' => (object) array(
'state_string' => 'Das Dorf ist gerade <em>vielleicht geöffnet</em>: </p><p>Der Clubraum ist offen, aber es findet keine Veranstaltung statt.</p><p>
Der Status kann sich also kurzfristig ändern.',
'svg' => 'done',
'color' => 'brown',
),
'open' => (object) array(
'state_string' => 'Das Dorf ist gerade <em>geöffnet</em>.</p><p>
Komm gerne vorbei.',
'svg' => 'done',
'color' => 'green',
),
'private' => (object) array(
'state_string' => 'Das Dorf ist gerade <em>privat</em>: </p><p>Es sind Leute da, aber der Clubraum ist nicht geöffnet.</p><p>
Komm gerne vorbei (aber frag lieber vorher, wie lange noch Leute da sind).',
'svg' => 'lock',
'color' => 'fdd835',
),
'unknown' => (object) array(
'state_string' => 'Der Status vom Dorf ist gerade <em>unbekannt</em>',
'svg' => 'warning',
'color' => 'orange',
),
'error' => (object) array(
'state_string' => 'Der Server konnte den Status vom Dorf nicht abrufen.',
'svg' => 'error',
'color' => 'blue',
),
);
$hash_to_state = array(
const HASH_TO_STATE = [
'bff0167ed8aba031c49122ef4046cf1b' => 'closed',
'd8ec899c69283bc775952a767db9d5f5' => 'maybe_open',
'2c2672c641425e5b2acd6ee74f39ae60' => 'open',
'66aece8ae27ffd3a656d42005fa3efbd' => 'private',
'86c75c0ad413b06ff8291673162d0b64' => 'unknown',
'0' => 'error',
);
if ($DORF_VIRTUAL_EVENTS || ! $DORF_IN_LOCKDOWN){
try {
$response = $client->request('GET', 'https://chaosdorf.de/raumstatus/status.png');
$hash = md5($response->getContent());
} catch (\Exception $e){
$hash = 0;
];
function hasValidUa(): bool
{
if (isset($_SERVER['HTTP_USER_AGENT'])) {
if (in_array(true, array_map(fn ($ua) => str_contains($_SERVER['HTTP_USER_AGENT'], $ua), INVALID_UAS))) {
return false;
}
$state = $hash_to_state[$hash];
$ical = new ICal(false, array(
'defaultSpan' => 2,
'defaultTimeZone' => 'Europe/Berlin',
'defaultWeekStart' => 'MO',
'filterDaysBefore' => '1',
));
$ical->initUrl($ICAL_URL, $acceptLanguage = 'de');
$events = $ical->eventsFromInterval('2 week');
$first_event = $events[0];
$events = array_slice($events, 1);
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])){
$locale = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
setlocale(LC_TIME, $locale);
return true;
}
return false;
}
function format_event($event): string{
/**
* @param object $event
*/
function format_event($event): string
{
global $DATE_FORMAT, $ical;
$startdate_loop = $ical->iCalDateToDateTime($event->dtstart_array[3]);
$startdate_str = $startdate_loop->format($DATE_FORMAT);
@ -92,126 +56,137 @@ function format_event($event): string{
}
return $startdate_str . ' &ndash; ' . $enddate_str;
}
$v = file_get_contents($VISITORS_FILE);
?>
<!DOCTYPE html>
<html lang="de">
<head>
<title>Was geht im Dorf.jetzt?</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="assets/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link href="assets/font/Material-Icons.css" rel="stylesheet">
<meta charset="UTF-8" />
</head>
<body>
<div class="container">
<nav class="navbar">
<h1 class="text-center">Was geht im Dorf.jetzt?</h1><hr />
</nav>
<?php if($DORF_IN_LOCKDOWN): ?>
<div class="card mb-3 text-center">
<div class="card-body">
<h5 class="card-title">Das Chaosdorf ist geschlossen</h5>
<img src="assets/svg/ic_lock_48px.svg" style="filter:url(assets/css/filter.svg#red);height:16rem;width:auto;" />
<p class="card-text font-weight-normal">Das Chaosdorf öffnet vorsichtig wieder.</p>
<p class="card-text font-weight-normal">Veranstaltungen finden teilweise hybrid oder online-only statt.</p>
<p class="card-text font-weight-normal">Einwählen zu den Veranstaltungen könnt ihr euch hier: <a href="https://wiki.chaosdorf.de/VirtualSpace">VirtualSpace</a></p>
</div>
</div>
/**
* @return array<int, array<string, DateTime|string|bool>>
*/
function prepare_events(array $events): array{
global $ical;
$returns = [];
foreach ($events as $event){
$start = DateTimeImmutable::createFromMutable($ical->iCalDateToDateTime($event->dtstart_array[3]));
$end = $start->add(new DateInterval($event->duration));
$returns[] = [
'summary' => $event->summary,
'url' => $event->url,
'start' => $start,
'end' => $end,
];
}
return $returns;
}
<?php else: ?>
<div class="card mb-3 text-center">
<div class="card-body">
<h5 class="card-title">Türstatus</h5>
<img src="assets/svg/ic_<?= $state_map[$state]->svg ?>_48px.svg" style="filter:url(assets/css/filter.svg#<?= $state_map[$state]->color ?>);height:16rem;width:auto;" />
<p class="card-text"><?= $state_map[$state]->state_string ?></p>
<div class="btn-wrapper">
<a href="https://wiki.chaosdorf.de/Raumstatus" class="btn btn-lg btn-block btn-primary">What's this?</a>
</div>
</div>
</div>
<?php endif ?>
<?php if($DORF_VIRTUAL_EVENTS || !$DORF_IN_LOCKDOWN): ?>
<div class="card mb-3 text-center">
<div class="card-body">
<h5 class="card-title">Events</h5>
<?php if (!empty($events)) : ?>
<h6 class="card-subtitle mb-2 text-muted">Nächste Veranstaltung</h6>
<p class="card-text">
<h5 class="font-weight-bold"><?= htmlspecialchars($first_event->summary, ENT_QUOTES, 'UTF-8') ?></h5>
<span class="font-weight-normal"><?= format_event($first_event) ?></span>
<span class="font-weight-normal"><a href="<?= $first_event->url ?>">Details zur Veranstaltung</a></span>
</p>
<h5 class="card-title">Danach:</h5>
<table class="table">
<tbody>
<?php foreach ($events as $event) : ?>
<tr>
<th scope="row"><?=format_event($event)?></th>
<td><?= htmlspecialchars($event->summary) ?></td>
<td><a href="<?= $event->url ?>">Details</a></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<a href="https://wiki.chaosdorf.de/Chaosdorf_Wiki:Current_events" class="btn btn-lg btn-block btn-primary">Event-Kalender</a>
<?php else: ?>
<h6 class="card-subtitle mb-2 text-muted">Aktuell keine Veranstaltungen.</h6>
<p class="card-text">
<span class="font-weight-normal">
Es stehen aktuell keine Veranstaltungen an.
</span>
</p>
<?php endif ?>
</div>
</div>
<?php endif ?>
</div>
<footer class="page-footer font-small text-center">
<div>
Du bist Besucher #<?= $v ?>
</div>
<div>
<a href="https://git.iwonder.name/iwonder/dorf.jetzt">Sieh dir den Code hier an</a>
</div>
<div>
Melde Fehler bitte an <a href="mailto:help@dorf.jetzt">help@dorf.jetzt</a>
</div>
<div>
Dies ist kein offizieller Auftritt des <a href="https://chaosdorf.de">Chaos Computer Club Düsseldorf / Chaosdorf e.V.</a>.
</div>
</footer>
<?php
// These are actually not needed, see https://getbootstrap.com/docs/4.0/getting-started/introduction/#js
// <script src="assets/js/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
// <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
// <script src="assets/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
?>
</body>
</html>
<?php
$store = new Store('/tmp/dorf.jetzt/http_cache');
$client = HttpClient::create();
$client = new CachingHttpClient($client, $store, ['default_ttl' => 60, 'allow_revalidate' => true]);
static $DATE_FORMAT = 'd.m.Y H:i';
static $VISITORS_FILE = '/opt/dorf.jetzt_visitors';
static $DORF_IN_LOCKDOWN = false;
static $DORF_VIRTUAL_EVENTS = true;
static $ICAL_URL = 'https://chaosdorf.de/~derf/cccd_all.ics';
$state_map = [
'closed' => (object) [
'state_string' => 'Das Dorf ist gerade <em>geschlossen</em>.',
'svg' => 'lock',
'color' => 'red',
],
'maybe_open' => (object) [
'state_string' => 'Das Dorf ist gerade <em>vielleicht geöffnet</em>: </p><p>Der Clubraum ist offen, aber es findet keine Veranstaltung statt.</p><p>
Der Status kann sich also kurzfristig ändern.',
'svg' => 'done',
'color' => 'brown',
],
'open' => (object) [
'state_string' => 'Das Dorf ist gerade <em>geöffnet</em>.</p><p>
Komm gerne vorbei.',
'svg' => 'done',
'color' => 'green',
],
'private' => (object) [
'state_string' => 'Das Dorf ist gerade <em>privat</em>: </p><p>Es sind Leute da, aber der Clubraum ist nicht geöffnet.</p><p>
Komm gerne vorbei (aber frag lieber vorher, wie lange noch Leute da sind).',
'svg' => 'lock',
'color' => 'fdd835',
],
'unknown' => (object) [
'state_string' => 'Der Status vom Dorf ist gerade <em>unbekannt</em>',
'svg' => 'warning',
'color' => 'orange',
],
'error' => (object) [
'state_string' => 'Der Server konnte den Status vom Dorf nicht abrufen.',
'svg' => 'error',
'color' => 'blue',
],
];
$hash_to_state = [
'bff0167ed8aba031c49122ef4046cf1b' => 'closed',
'd8ec899c69283bc775952a767db9d5f5' => 'maybe_open',
'2c2672c641425e5b2acd6ee74f39ae60' => 'open',
'66aece8ae27ffd3a656d42005fa3efbd' => 'private',
'86c75c0ad413b06ff8291673162d0b64' => 'unknown',
'0' => 'error',
];
$state = 'error';
if ($DORF_VIRTUAL_EVENTS || !$DORF_IN_LOCKDOWN) {
try {
$response = $client->request('GET', 'https://chaosdorf.de/raumstatus/status.png');
$hash = md5($response->getContent());
} catch (\Exception $e) {
$hash = 0;
}
$state = $hash_to_state[$hash];
$ical = new ICal(false, [
'defaultSpan' => 2,
'defaultTimeZone' => 'Europe/Berlin',
'defaultWeekStart' => 'MO',
'filterDaysBefore' => '1',
]);
$ical->initUrl($ICAL_URL, $acceptLanguage = 'de');
$events = $ical->eventsFromInterval('2 week');
// $first_event = $events[0];
// $events = array_slice($events, 1);
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$locale = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
setlocale(LC_TIME, $locale);
}
}
$visitors = file_get_contents($VISITORS_FILE);
if (is_string($visitors)) {
$visitors = intval($visitors);
} else {
$visitors = 0;
}
$state_obj = $state_map[$state];
$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader, [
//'cache' => '/tmp/dorf.jetzt/twig_cache'
]);
function formatEndDt(DateTime|DateTimeImmutable $end, DateTime|DateTimeImmutable $start, string $tz = 'Europe/Berlin'): string{
$daySame = $end->setTimeZone(new DateTimeZone($tz))->format('d.m.Y') == $start->setTimeZone(new DateTimeZone($tz))->format('d.m.Y');
$endIsMidnight = $end->setTimeZone(new DateTimeZone($tz))->format('H:i') == '00:00' && $start->setTimeZone(new DateTimeZone($tz))->format('H:i') != '00:00';
if ($daySame || $endIsMidnight){
return $end->format('H:i');
} else {
return $end->format('d.m.Y H:i');
}
}
$twig->addFilter(new TwigFilter('end_datetime', 'formatEndDt'));
//$twig->addExtension(new \Twig\Extra\Intl\IntlExtension());
$twig->getExtension(\Twig\Extension\CoreExtension::class)->setTimezone('Europe/Berlin');
$render_evts = prepare_events($events);
echo($twig->render("Main.twig", [
'visitors' => $visitors,
'state_svg' => $state_obj->svg,
'state_color' => $state_obj->color,
'state_string' => $state_obj->state_string,
'events' => $render_evts,
]));
/* Initialising values */
if(!$v) $v=0;
$count_visit = true;
/* List of bots I found in various access logs */
$invalid_uas = array(
"AhrefsBot",
"Googlebot",
"Yahoo",
"Go-http-client/",
"bingbot",
"CheckMarkNetwork",
"SemrushBot",
"BingPreview",
"facebookexternalhit",
"hetrix.tools",
);
foreach ($invalid_uas as $ua){
if(stripos($_SERVER['HTTP_USER_AGENT'], $ua) !== FALSE){
$count_visit = false;
}
}
if ($count_visit === TRUE){
$v++;
file_put_contents($VISITORS_FILE, $v);
if (hasValidUa()) {
$visitors++;
file_put_contents($VISITORS_FILE, strval($visitors));
}

15
psalm.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<psalm
errorLevel="3"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<file name="index.php" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

78
templates/Main.twig Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>Was geht im Dorf.jetzt?</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="assets/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="preload" as="style" href="assets/css/filter.svg" />
<link href="assets/font/Material-Icons.css" rel="stylesheet">
<meta charset="UTF-8" />
</head>
<body>
<div class="container">
<h1 class="text-center">Was geht im Dorf.jetzt?</h1>
<div class="card mb-3 text-center">
<div class="card-body">
<h5 class="card-title">Türstatus</h5>
<img src="assets/svg/ic_{{ state_svg }}_48px.svg" style="filter:url(assets/css/filter.svg#{{ state_color }});height:16rem;width:auto;" />
<p class="card-text">{{ state_string|raw }}</p>
<div class="btn-wrapper">
<a href="https://wiki.chaosdorf.de/Raumstatus" class="btn btn-lg btn-block btn-primary">Was bedeutet das?</a>
</div>
</div>
</div>
<div class="card mb-3 text-center">
<div class="card-body">
<h5 class="card-title">Events</h5>
{% if events|length > 0 %}
<h6 class="card-subtitle mb-2 text-muted">Nächste Veranstaltung</h6>
<p class="card-text">
<h5 class="font-weight-bold">{{ events[0].summary|e }}</h5>
<span class="font-weight-normal">{{ events[0].start|date('d.m.Y H:i', 'Europe/Berlin') }} &ndash; {{ events[0].end|end_datetime(events[0].start, 'Europe/Berlin') }}</span>
<span class="font-weight-normal"><a href="{{ events[0].url }}">Details zur Veranstaltung</a></span>
</p>
<h5 class="card-title">Danach:</h5>
<table class="table">
<tbody>
{% for event in events %}
{% if loop.index > 0 %}
<tr>
<th scope="row"> {{ event.start|date('d.m.Y H:i', 'Europe/Berlin') }} &ndash; {{ event.end|end_datetime(event.start, 'Europe/Berlin') }}</th>
<td>{{ event.summary|e }}</td>
<td><a href="{{ event.url }}">Details</a></td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<a href="https://wiki.chaosdorf.de/Chaosdorf_Wiki:Current_events" class="btn btn-lg btn-block btn-primary">Event-Kalender</a>
{% else %}
<h6 class="card-subtitle mb-2 text-muted">Aktuell keine Veranstaltungen.</h6>
<p class="card-text">
<span class="font-weight-normal">
Es stehen aktuell keine Veranstaltungen an.
</span>
</p>
{% endif %}
</div>
</div>
</div>
<footer class="page-footer font-small text-center">
<div>
Du bist Besucher #{{ visitors }}
</div>
<div>
<a href="https://git.iwonder.name/iwonder/dorf.jetzt">Sieh dir den Code hier an</a>
</div>
<div>
Melde Fehler bitte an <a href="mailto:help@dorf.jetzt">help@dorf.jetzt</a>
</div>
<div>
Dies ist kein offizieller Auftritt des <a href="https://chaosdorf.de">Chaos Computer Club Düsseldorf / Chaosdorf e.V.</a>.
</div>
</footer>
</body>
</html>