<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>dev.ebroda.de</title>
    <link>https://dev.ebroda.de/</link>
    <description>Recent content on dev.ebroda.de</description>
    <generator>Hugo - gohugo.io</generator>
    <language>en</language>
    <contact>dev@ebroda.de</contact>
    <copyright>Eike Broda &middot; dev@ebroda.de</copyright>
    
        <atom:link href="https://dev.ebroda.de/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>Blitzfotos im Gewittervideo finden</title>
      <link>https://dev.ebroda.de/posts/2021_gewitter_fotos/</link>
      <pubDate>Fri, 04 Jun 2021 23:13:00 +0000</pubDate>
      <author>Eike Broda</author>
      <guid>https://dev.ebroda.de/posts/2021_gewitter_fotos/</guid>
      <description></description>
      
      <content>&lt;p&gt;&lt;img src=&#34;https://dev.ebroda.de/images/2021/2021_blitz_65.jpg&#34; alt=&#34;Blitz Blitz&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Gerade sah der Himmel sehr interessant aus. Des öfteren waren leichte Blitze zu sehen. Also die Spiegelreflex rausgeholt
und ein Video vom Himmel gemacht. Mit dem Plan da später dann mal nach Fotos von Blitzen am Himmel zu gucken. Das ganze
habe ich dann wenig später auch umgesetzt. Also erstmal das Video in viele Einzelbilder konvertieren. Dabei hilft ffmpeg,
die Wunderwaffe für alles, was um Video geht. Einfach mal ffmpeg + (englische Beschreibung, was ihr machen wollt) googlen
und sicherlich gibt es bei StackOverflow einen entsprechenden Post dazu. In dem Fall führte dann
&lt;a href=&#34;https://stackoverflow.com/q/40088222&#34;&gt;diese Antwort&lt;/a&gt; ans Ziel und hatte auch noch den netten Tipp, das doch besser
mit JPGs zu machen. Okay, dann also mal los. Das Video kommt direkt von der SD-Karte:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ffmpeg -i /media/dev/nnnn-nnnn/DCIM/101CANON/MVI_1121.MP4 -vf fps=30 -qscale:v 2 out%d.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Okay, nun haben wir einen Ordner voll mit jpg-Bildern. Die kann man jetzt in einem Bildbetrachter, z.B. &lt;code&gt;eog&lt;/code&gt;,
durchschauen und dann den Blitz suchen. Ein guter Viewer schafft es zwar im Daumenkino-Modus, trotzdem muss man noch
einiges an Fotos durchschauen. Bei 30 Frames, die die EOS macht, sind das bei 10 Minuten immerhin 60 * 30 * 10 = 18.000
Fotos. Das muss doch besser gehen. Und eine intuitive Idee war: Gucken wir doch mal, wie hell das Bild insgesamt ist.
Durch den Blitz muss das doch heller werden. Und wieder &lt;a href=&#34;https://stackoverflow.com/a/3498247&#34;&gt;StackOverflow&lt;/a&gt; und schon
kann mittels Variante 4 die Helligkeit bestimmt werden. Dann mal ein Foto anschauen, wie hell das so im Mittel ist ohne
Blitz und das mit ein bisschen Puffer als Grenzwert nehmen. Hier ist es die 16 geworden. Da einfach mal ein bisschen
mit rumprobieren. Hängt stark von den Aufnahmen ab und wie hell insgesamt die Bilder sind.
Zusätzlich noch etwas Logik um alle Dateien in einem Verzeichnis aufzulisten, fertig ist
Variante 1 des Skripts. Die speichert dann auch alle gefundenen Bilder in einer Liste und gibt die am Ende
leerzeichen-getrennt zurück, sodass alle interessanten Bilder einfach mittels &lt;code&gt;cp&lt;/code&gt; oder &lt;code&gt;mv&lt;/code&gt; in ein extra
Verzeichnis verschoben werden können.&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;color:#60a0b0;font-style:italic&#34;&gt;#! /usr/bin/python3&lt;/span&gt;
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;math&lt;/span&gt;

&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;os&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; listdir
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;os.path&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; isfile, join
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;PIL&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; Image, ImageStat


&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06287e&#34;&gt;brightness&lt;/span&gt;(im_file):
   im &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; Image&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#007020&#34;&gt;open&lt;/span&gt;(im_file)
   stat &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; ImageStat&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;Stat(im)
   r,g,b &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; stat&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;rms
   &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;return&lt;/span&gt; math&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;sqrt(&lt;span style=&#34;color:#40a070&#34;&gt;0.241&lt;/span&gt;&lt;span style=&#34;color:#666&#34;&gt;*&lt;/span&gt;(r&lt;span style=&#34;color:#666&#34;&gt;**&lt;/span&gt;&lt;span style=&#34;color:#40a070&#34;&gt;2&lt;/span&gt;) &lt;span style=&#34;color:#666&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#40a070&#34;&gt;0.691&lt;/span&gt;&lt;span style=&#34;color:#666&#34;&gt;*&lt;/span&gt;(g&lt;span style=&#34;color:#666&#34;&gt;**&lt;/span&gt;&lt;span style=&#34;color:#40a070&#34;&gt;2&lt;/span&gt;) &lt;span style=&#34;color:#666&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#40a070&#34;&gt;0.068&lt;/span&gt;&lt;span style=&#34;color:#666&#34;&gt;*&lt;/span&gt;(b&lt;span style=&#34;color:#666&#34;&gt;**&lt;/span&gt;&lt;span style=&#34;color:#40a070&#34;&gt;2&lt;/span&gt;))


&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;if&lt;/span&gt; __name__ &lt;span style=&#34;color:#666&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;:
  mypath &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;.&amp;#39;&lt;/span&gt;
  onlyfiles &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; [f &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;for&lt;/span&gt; f &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;in&lt;/span&gt; listdir(mypath) &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;if&lt;/span&gt; isfile(join(mypath, f)) &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;and&lt;/span&gt; f[&lt;span style=&#34;color:#666&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#40a070&#34;&gt;3&lt;/span&gt;:] &lt;span style=&#34;color:#666&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;jpg&amp;#39;&lt;/span&gt;]

  files &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; []

  &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;for&lt;/span&gt; f &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#007020&#34;&gt;sorted&lt;/span&gt;(onlyfiles):
  	bright &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; brightness(f)
  	&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;if&lt;/span&gt; (bright &lt;span style=&#34;color:#666&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#40a070&#34;&gt;16&lt;/span&gt;):
  		&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;print&lt;/span&gt;(f, bright)
  		files&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;append(f)

  &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;Found:&amp;#34;&lt;/span&gt;)		
  &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;join(files))&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Lässt man das Skript nun laufen und schaut mal in die Prozessorauslastung (&lt;code&gt;htop&lt;/code&gt;), dann fällt auf, dass da irgendwie
nur Prozessorkern was zu tun bekommt. Python läuft halt standardmäßig nur in einem Thread. Aber auch da lässt sich doch
was machen. Also wieder Google angeschmissen und klar, Python hat ne Bibliothek dafür:
&lt;a href=&#34;https://docs.python.org/3/library/multiprocessing.html&#34;&gt;multiprocessing&lt;/a&gt;
und da gibt es dann auch ein entsprechendes
&lt;a href=&#34;https://urban-institute.medium.com/using-multiprocessing-to-make-python-code-faster-23ea5ef996ba&#34;&gt;Tutorial&lt;/a&gt; zu.
Letztendlich kurz zusammengefasst: Die Aufgabe muss so runtergebrochen werden, dass sie in mehrere Teilaufgaben
berechnet werden kann. Bei einer Liste an Dateien ist das natürlich relativ einfach, da bekommt jeder Prozessorkern
einfach einen Teil der Liste. Bei n Kernen jeder dann eben aufgerundet 1/n. Damit hat der letzte Kern dann zwar etwas
weniger zu tun, aber die Abweichung ist kleiner als n. Also bei 18.000 Fotos eher zu vernachlässigen. Der Rechner hat
8 Kerne, das macht also dann 18.000/8 = 2.250. Das geht hier sogar genau auf, somit hat jeder Kern 2.250 Bilder, die
er sich anschauen soll.&lt;/p&gt;

&lt;p&gt;Der erste Versuch, einfach eine Liste per Referenz an die working-Funktion zu übergeben, ist gescheitert. Die Liste
war hinterher einfach leer. Aber auch dafür gibt es natürlich wieder eine &lt;a href=&#34;https://stackoverflow.com/a/10415215&#34;&gt;Lösung&lt;/a&gt;
und weniger später gibt es nun den kompletten Code, der alle Kerne auslastet:&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;color:#60a0b0;font-style:italic&#34;&gt;#! /usr/bin/python3&lt;/span&gt;
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;math&lt;/span&gt;
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;sys&lt;/span&gt;
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;multiprocessing&lt;/span&gt;
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;os&lt;/span&gt;

&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;os&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; listdir
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;os.path&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; isfile, join
&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0e84b5;font-weight:bold&#34;&gt;PIL&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;import&lt;/span&gt; Image, ImageStat


&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06287e&#34;&gt;brightness&lt;/span&gt;(im_file):
    im &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; Image&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#007020&#34;&gt;open&lt;/span&gt;(im_file)
    stat &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; ImageStat&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;Stat(im)
    r,g,b &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; stat&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;rms
    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;return&lt;/span&gt; math&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;sqrt(&lt;span style=&#34;color:#40a070&#34;&gt;0.241&lt;/span&gt;&lt;span style=&#34;color:#666&#34;&gt;*&lt;/span&gt;(r&lt;span style=&#34;color:#666&#34;&gt;**&lt;/span&gt;&lt;span style=&#34;color:#40a070&#34;&gt;2&lt;/span&gt;) &lt;span style=&#34;color:#666&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#40a070&#34;&gt;0.691&lt;/span&gt;&lt;span style=&#34;color:#666&#34;&gt;*&lt;/span&gt;(g&lt;span style=&#34;color:#666&#34;&gt;**&lt;/span&gt;&lt;span style=&#34;color:#40a070&#34;&gt;2&lt;/span&gt;) &lt;span style=&#34;color:#666&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#40a070&#34;&gt;0.068&lt;/span&gt;&lt;span style=&#34;color:#666&#34;&gt;*&lt;/span&gt;(b&lt;span style=&#34;color:#666&#34;&gt;**&lt;/span&gt;&lt;span style=&#34;color:#40a070&#34;&gt;2&lt;/span&gt;))


&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06287e&#34;&gt;working&lt;/span&gt;(selected_files, files, start, end, i):
    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;if&lt;/span&gt; end &lt;span style=&#34;color:#666&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#007020&#34;&gt;len&lt;/span&gt;(files):
        end &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#007020&#34;&gt;len&lt;/span&gt;(files)

    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;for&lt;/span&gt; f &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;in&lt;/span&gt; files[start:end]:
        bright &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; brightness(f)
        &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;if&lt;/span&gt; (bright &lt;span style=&#34;color:#666&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#40a070&#34;&gt;16&lt;/span&gt;):
            &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;print&lt;/span&gt;(f, bright)
            selected_files&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;append(f)


&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;if&lt;/span&gt; __name__ &lt;span style=&#34;color:#666&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;:
    mypath &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;.&amp;#39;&lt;/span&gt;
    onlyfiles &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; [f &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;for&lt;/span&gt; f &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;in&lt;/span&gt; listdir(mypath) &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;if&lt;/span&gt; isfile(join(mypath, f)) &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;and&lt;/span&gt; f[&lt;span style=&#34;color:#666&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#40a070&#34;&gt;3&lt;/span&gt;:] &lt;span style=&#34;color:#666&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;jpg&amp;#39;&lt;/span&gt;]

    thefiles &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#007020&#34;&gt;sorted&lt;/span&gt;(onlyfiles)

    processes &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; []

    cores &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; os&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;cpu_count()
    part_work &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; math&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;ceil(&lt;span style=&#34;color:#007020&#34;&gt;len&lt;/span&gt;(thefiles)&lt;span style=&#34;color:#666&#34;&gt;/&lt;/span&gt;cores)
    start &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; part_work

    manager &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; multiprocessing&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;Manager()
    selected_files &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; manager&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#007020&#34;&gt;list&lt;/span&gt;()

    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#007020&#34;&gt;range&lt;/span&gt;(&lt;span style=&#34;color:#40a070&#34;&gt;0&lt;/span&gt;, cores):
        end &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; start &lt;span style=&#34;color:#666&#34;&gt;+&lt;/span&gt; part_work
        p &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; multiprocessing&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;Process(target&lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt;working, args&lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt;(selected_files, thefiles, start, end, i))
        processes&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;append(p)
        p&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;start()
        start &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; end

    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;for&lt;/span&gt; process &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;in&lt;/span&gt; processes:
    	&lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;print&lt;/span&gt;(process&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;join())

    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;Done with all threads. Found &amp;#34;&lt;/span&gt;)
    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#666&#34;&gt;.&lt;/span&gt;join(selected_files))
    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;print&lt;/span&gt;()&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Und nun final noch das, worauf wir alle warten. Insgesamt ist es doch etwas enttäuschend, aber die Bilder rauschen
leider alle sehr und es war auch ziemlich wolkig.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://dev.ebroda.de/images/2021/2021_blitz_16842.jpg&#34; alt=&#34;Blitz Blitz&#34; /&gt;
&lt;img src=&#34;https://dev.ebroda.de/images/2021/2021_blitz_65.jpg&#34; alt=&#34;Blitz Blitz&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Erstaunlich auch, wie kurz die Blitze tatsächlich sind. Hier drei Frames, die direkt nacheinander kommen. Bei 30 FPS
also je etwa &lt;sup&gt;1&lt;/sup&gt;&amp;frasl;&lt;sub&gt;30&lt;/sub&gt; Sekunde = 33 ms auseinander:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://dev.ebroda.de/images/2021/2021_blitz_478.jpg&#34; alt=&#34;Blitz Blitz&#34; /&gt;
&lt;img src=&#34;https://dev.ebroda.de/images/2021/2021_blitz_479.jpg&#34; alt=&#34;Blitz Blitz&#34; /&gt;
&lt;img src=&#34;https://dev.ebroda.de/images/2021/2021_blitz_480.jpg&#34; alt=&#34;Blitz Blitz&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Das Skript funktioniert natürlich auch mit Videomaterial, z.B. von YouTube. Schaut da einfach mal nach &amp;ldquo;Gewitter&amp;rdquo;, da
gibt es auch einiges, z.B. &lt;a href=&#34;https://www.youtube.com/watch?v=halAtUgOdYM&#34;&gt;das hier&lt;/a&gt;. Testweise habe ich da auch mal
das Skript drübergeschickt und es hat die interessanten Bilder aussortiert. So, nun wird&amp;rsquo;s Zeit die Tabs zu schließen.
Der Teil hier ist fertig :)&lt;/p&gt;

&lt;p&gt;PS: Ist das schon Machine Learning? :)&lt;/p&gt;
</content>
      
    </item>
    
    <item>
      <title>Karten für Cards Against Humanity parsen</title>
      <link>https://dev.ebroda.de/posts/2020_04_cards_against_humanity/</link>
      <pubDate>Thu, 23 Apr 2020 23:30:00 +0000</pubDate>
      <author>Eike Broda</author>
      <guid>https://dev.ebroda.de/posts/2020_04_cards_against_humanity/</guid>
      <description></description>
      
      <content>

&lt;p&gt;Um das Sommerfest-Team auch während der Corona-Krise zusammenzuhalten, kam die Idee auf, im Web einige Spiele
zu spielen. Eines der gewünschten Spiele war &lt;a href=&#34;https://www.cardsagainsthumanity.com/&#34;&gt;Cards Against Humanity&lt;/a&gt; (CaH).
Von dem Spiel sind diverse Varianten im Internet zu finden, da das Ursprungsspiel unter CC-BY-NC-SA 2.0 Lizenz
steht. Nach ein bisschen ausprobieren war mit &lt;a href=&#34;https://github.com/howardchung/PixelsAgainstHumanity&#34;&gt;Pixels Against Humanity&lt;/a&gt; auch eine gut funktionierende Variante gefunden. Das Deployen auf einem U7 war einfach und somit läuft nun eine eigene CaH-Instanz unter &lt;a href=&#34;https://cah.wpaf.de&#34;&gt;cah.wpaf.de&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Beim Ausprobieren mit Freunden hatten wir ein paar Probleme mit den englischen Karten, die den Spielspaß
deutlich reduziert haben, da wir doch einiges nicht verstanden haben. Also kam die Idee auf, ob es nicht auch
deutsche Karten gibt.&lt;/p&gt;

&lt;p&gt;Erstmal wurde geguckt, wie die Karten denn überhaupt definiert werden. Diese Variante verwendet dafür recht einfach
eine json-Datei, die beim Start des Servers eingelesen wird.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
 &#39;blackCards&#39;: [{&amp;quot;text&amp;quot;: &amp;quot;I got 99 problems, but __ ain&#39;t one&amp;quot;, &amp;quot;pick&amp;quot;: 1}, ...],
 &#39;whiteCards&#39;: [&amp;quot;Active Listening&amp;quot;, ...],
 &#39;Base&#39;: {
	&#39;name&#39;: &#39;Base Set&#39;,
	&#39;black&#39;: [0,...],
    &#39;white&#39;: [0,...]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Zuerst gibt es für jede Farbe eine Liste mit Karten. Dabei sind die schwarzen Karten jeweils ein Dict aus dem Text und der Anzahl an weißen Karten, die auf die schwarze Karte gespielt werden sollen. Bei den weißen Karten ist es einfacher, hier ist es einfach eine Liste. Anschließend wird ein Set aus diesen Karten generiert. Es kann mehrere Sets geben, die über den Key identifiziert werden (hier &lt;code&gt;Base&lt;/code&gt;). Sets verfügen über einen Namen und referenzieren dann die weißen und schwarzen Karten über ihre Position in den entsprechenden Listen &lt;code&gt;blackCards&lt;/code&gt; und &lt;code&gt;whiteCards&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also ging die Suche los und Nikolas hat recht fix eine &lt;a href=&#34;https://github.com/Prior99/cah-fullformat-german/&#34;&gt;deutsche Variante in LaTeX&lt;/a&gt; auf GitHub gefunden. Das Problem war nun: Wie bekommt man die Fragen aus den TeX-Sources raus? Da hilft ein Blick in die Sources (&lt;a href=&#34;https://github.com/Prior99/cah-fullformat-german/blob/master/categories/basis.tex&#34;&gt;categories/basic.tex&lt;/a&gt;):&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-latex&#34;&gt;[...]
\titlecard{Basis}%
%
\whitecard{Basis}{Aktives Zuhören.}%
[...]
\blackcard{1}{Basis}{Ich habe 99 Probleme, \newline aber \underl ist keines davon.}%
% engl. &amp;quot;I got 99 problems, but __ ain&#39;t one&amp;quot;
[...]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Das sieht doch schon mal sehr gut aus, da es strukturiert ist. Die weißen Karten sind jeweils durch den Befehl
&lt;code&gt;\whitecard{Kategorie}{Text}&lt;/code&gt; gegeben und bei den schwarzen Karten analog, wobei dort zusätzlich noch die
Anzahl an zu legenden Karten mit angegeben ist. Das passt schon ziemlich gut zu dem, was wir in unserer json brauchen und lässt sich auch recht einfach mit Python parsen. Fangen wir mal an und legen uns ein Objekt an, dass die json-Struktur von oben repräsentiert. Anschließend suchen wir alle *.tex-Dateien im Verzeichnis (categories/) und lesen alle Zeilen ein. Das komplette Script gibt es &lt;a href=&#34;https://gist.github.com/ebroda/0b3726d71c1818316cbc500eae78cda5&#34;&gt;hier im Gist&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;import json
import glob

cards = {&#39;blackCards&#39;: [], &#39;whiteCards&#39;: [], &#39;Base&#39;: {&#39;name&#39;: &#39;Base Set dt.&#39;, &#39;black&#39;: [], &#39;white&#39;: []}}

tex_files = glob.glob(&amp;quot;*.tex&amp;quot;)
for tex in tex_files:
  with open(tex, &#39;r&#39;) as f:
    lines = f.readlines()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Damit haben wir jetzt eine große Liste von Strings, welche die einzelne Zeilen der Datei enthält. Die müssen wir jetzt einzeln verarbeiten, daher schauen wir uns die individuellen Zeilen einmal an.&lt;/p&gt;

&lt;p&gt;Wie auch oben im Beispiel zu sehen, beginnen einige Zeilen mit einem Prozentzeichen. So werden in LaTeX Kommentare eingeleitet. Diese Zeilen können wir ignorieren und springen mittels &lt;code&gt;continue&lt;/code&gt; einfach in die nächste Zeile.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;for line in lines:
	if line[0:1] == &#39;%&#39;:
		continue
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Jetzt wird es etwas trickreich. Ich zerteile die Zeile jeweils an der Zeichenkette &lt;code&gt;}{&lt;/code&gt;. Ein kleines Beispiel für die schwarze Karte oben:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; &#39;\blackcard{1}{Basis}{Ich habe 99 Probleme, \newline aber \underl ist keines davon.}%&#39;.split(&#39;}{&#39;)
[&#39;\blackcard{1&#39;, &#39;Basis&#39;, &#39;Ich habe 99 Probleme, \newline aber \underl ist keines davon.}%&#39;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Aus dem einzelnen String werden nun drei Strings. Man sieht nun schon, dass im ersten Teil die Anzahl Karten ist und im letzten Teil die Frage. Ein bisschen schön müssen wir die Teile aber noch machen, denn das &lt;code&gt;\blackcard{1&lt;/code&gt; wird nicht als 1 interpretiert.&lt;/p&gt;

&lt;p&gt;Aber erstmal schauen wir, was für eine Karte wir den haben. Dazu schauen wir einfach, was eigentlich in den Zeichen 1-5 der Zeile steht. Hier kommt uns die Repräsentation von Strings in Python entgegen, da wir hier einfach einen List-Operator verwenden können: Der erste Paramter gibt den Start an und der zweite Parameter das Ende an, wobei dieser Werte exklusiv, also nicht mehr in der Liste ist.&lt;/p&gt;

&lt;p&gt;Ähnlich machen wir das auch für den Pick, hierbei nutzen wir aus, dass negative Werte entsprechend vom Ende gerechnet werden. Wir gucken uns also nur das letzte Zeichen bis zum Ende an, da wir kein Ende explizit angegeben haben. Im anderen Fall des Texts (&lt;code&gt;data&lt;/code&gt;) nutzen wir es andersherum und schneiden die letzten 3 Zeichen ab, was &lt;code&gt;}%\n&lt;/code&gt; ist. Wo kommt jetzt das &lt;code&gt;\n&lt;/code&gt; her? Das ist ein Zeilenumbruch, der noch in der Zeile ist, aber nicht explizit angezeigt wird.&lt;/p&gt;

&lt;p&gt;Mittels replace() machen wir die Fragen noch schön und ersetzen einige LaTeX Besonderheiten, z.B. wird der LaTeX-Unterstrich &lt;code&gt;\\underline&lt;/code&gt; ein einfacher Unterstrich &lt;code&gt;_&lt;/code&gt;. Anschließend fügen wir die Karten unserer Liste an schwarzen bzw. weißen Karten hinzu. Bei den schwarzen müssen wir hier das dict erstellen, dass zusätzlich auch die Anzahl an zu spielenden Karten enthält.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;split = line.split(&#39;}{&#39;)

if line[1:6] == &#39;black&#39;:
    pick = split[0][-1:]
    data = split[2][:-3].replace(&#39;\\newline&#39;, &#39;&amp;lt;br /&amp;gt;&#39;).replace(&#39;- &#39;, &#39;&#39;).replace(&#39;\\SymbReg&#39;, &#39;&#39;).replace(&#39;\%&#39;,&#39;%&#39;).replace(&#39;\\underl&#39;, &#39;_&#39;).replace(&#39;\&#39;\&#39;&#39;, &#39;&amp;quot;&#39;).replace(&#39;``&#39;, &#39;&amp;quot;&#39;)
    cards[&#39;blackCards&#39;].append({&amp;quot;text&amp;quot;: data, &amp;quot;pick&amp;quot;: pick})

if line[1:6] == &#39;white&#39;:
	data = split[1][:-3].replace(&#39;\\newline&#39;, &#39;&amp;lt;br /&amp;gt;&#39;).replace(&#39;- &#39;, &#39;&#39;).replace(&#39;\\SymbReg&#39;, &#39;&#39;).replace(&#39;\%&#39;,&#39;%&#39;).replace(&#39;\\underl&#39;, &#39;_&#39;).replace(&#39;\&#39;\&#39;&#39;, &#39;&amp;quot;&#39;).replace(&#39;``&#39;, &#39;&amp;quot;&#39;)
    cards[&#39;whiteCards&#39;].append(data)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Jetzt haben wir das meiste schon geschafft. Wir haben die TeX-Sources in unsere Struktur überführt, jetzt müssen wir noch ein Set anlegen. Dafür schreiben wir einfach alle die Nummern aller unserer Karten in die Liste. Indizes fangen in Python bei 0 an, daher haben wir im folgenden range(0, Anzahl an Karten). Auch hier ist der zweite Parameter exklusiv, d.h. es ist das Interval [0, Anzahl Karten). Die Syntax ist jeweils die Kurzform für eine Schleife, die jede Zahl zwischen 0 und Anzahl Karten - 1 in die Liste &lt;code&gt;cards[&#39;Base&#39;][&#39;black&#39;]&lt;/code&gt; bzw. &lt;code&gt;cards[&#39;Base&#39;][&#39;white&#39;]&lt;/code&gt; einfügt. Abschließend schreiben wir unser Set noch in eine &lt;code&gt;cards.json&lt;/code&gt;, die wir nun auf den Server kopieren können und nach einem Neustart der CaH-Instanz nutzen können.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;
cards[&#39;Base&#39;][&#39;black&#39;] = [i for i in range(0, len(cards[&#39;blackCards&#39;]))]
cards[&#39;Base&#39;][&#39;white&#39;] = [i for i in range(0, len(cards[&#39;whiteCards&#39;]))]

with open(&#39;cards.json&#39;, &#39;w&#39;) as f:
    json.dump(cards, f)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;mehr-kartensets&#34;&gt;Mehr Kartensets&lt;/h2&gt;

&lt;p&gt;Bei der Recherche nach den deutschen Karten war ich auf das Projekt &lt;a href=&#34;https://github.com/crhallberg/json-against-humanity/tree/master/src&#34;&gt;JSON Against Humanity&lt;/a&gt; gestoßen, welches eine Vielzahl an unterschiedlichen Kartensets anbietet. Ein kurzer Blick in die Source zeigte, dass es hier jeweils eine &lt;code&gt;black.md.txt&lt;/code&gt; und eine &lt;code&gt;white.md.txt&lt;/code&gt; gibt, welche die Karten enthalten.&lt;/p&gt;

&lt;p&gt;Das lässt sich auch recht leicht parsen, größtes Problem war die Erkennung, wie viele Karten der Spieler legen muss. Aber auch das lässt sich in Python elegant lösen:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;max(1, line.count(&#39;_&#39;))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Es wird einfach die Anzahl an Unterstrichen auf der schwarzen Karte gezählt. Da nicht alle Karten einen Unterstrich haben, wird das Ergebnis in die &lt;code&gt;max&lt;/code&gt;-Funktion gegeben, die den höheren Wert zurückgibt. Somit ist sichergestellt, dass &lt;code&gt;pick&lt;/code&gt; immer mindestens 1 ist.&lt;/p&gt;

&lt;p&gt;Gleichzeitig sollten nun auch mehrere Karten-Sets möglich sein, daher brauchte die Logik für die Referenzierung der Karten in den Sets noch ein kleines Update. Zu Beginn des Einlesevorgangs wird die aktuelle Anzahl an weißen und schwarzen Karten abgefragt. Diese Zahl ist, nach dem Hinzufügen der Karten dieses Sets, der Startwert für die Referenzierung über die Indizes. Diese laufen dann wieder bis zur Anzahl der Karten - 1. Elegant, oder?&lt;/p&gt;

&lt;p&gt;Eine kleine Veränderung zum Repo ist noch, dass die Dateien jetzt als Konvention jeweils &lt;code&gt;[name].black&lt;/code&gt; und &lt;code&gt;[name].white&lt;/code&gt; heißen müssen.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;def get_pack(name, index):
    start_black = len(cards[&#39;blackCards&#39;])
    start_white = len(cards[&#39;whiteCards&#39;])

    with open(name + &#39;.black&#39;, &#39;r&#39;) as f:
        for line in f.readlines():
            if len(line) &amp;gt; 0:
                cards[&#39;blackCards&#39;].append({&amp;quot;text&amp;quot;: line, &amp;quot;pick&amp;quot;: max(1, line.count(&#39;_&#39;))})

    with open(name + &#39;.white&#39;, &#39;r&#39;) as f:
        for line in f.readlines():
            if len(line) &amp;gt; 0:
                cards[&#39;whiteCards&#39;].append(line)

    cards[index] = {&#39;Name&#39;: name}
    cards[index][&#39;black&#39;] = [i for i in range(start_black, len(cards[&#39;blackCards&#39;]))]
    cards[index][&#39;white&#39;] = [i for i in range(start_white, len(cards[&#39;whiteCards&#39;]))]

get_pack(&#39;prtg&#39;, &#39;Downtime&#39;)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;auswahl-des-sets&#34;&gt;Auswahl des Sets&lt;/h2&gt;

&lt;p&gt;Die Original-Variante von CaH hatte keine Auswahl des Sets im Frontend. Im Backend waren die Sets schon vorgesehen, aber die Auswahl war nicht möglich. Also habe ich mit etwas fluchen ins React-Frontend noch die Auswahl des Sets integriert. Dabei kam dann der Wunsch auf, auch weiterhin die Original-Variante spielen zu können. Also kleine Anpassung am Script: Wir laden zuerst mal das englische Original und ergänzen dann unsere Karten. Statt also das &lt;code&gt;cards&lt;/code&gt;-Objekt zu initialsieren, lesen wir einfach die bestehende, originale &lt;code&gt;cards.json&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;with open(&#39;en.json&#39;, &#39;r&#39;) as en:
    cards = json.load(en)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Da wird jetzt schon einiges an Karten in den Liste haben, müssen wir unsere Referenzen für das Set entsprechend anpassen. Statt bei 0 zu starten, müssen wir hier, analog zu den zusätzlichen Sets, vor dem Hinzufügen der Karten zählen, wie viele Karten schon in der Liste sind und dann dort beginnen.&lt;/p&gt;

&lt;h2 id=&#34;eigene-kartensets&#34;&gt;Eigene Kartensets&lt;/h2&gt;

&lt;p&gt;Nach dieser Vorarbeit lassen sich nun einfach und elegant weitere Kartensets anlegen. Es müssen einfach nur zwei Textdateien mit den vorgesehen Begriffen gefüllt werden und diese anschließend geparst werden. Das führte dann auch zu der Generierung eines Sommerfest-Sets. Schnell waren ein paar schwarze Karten und viele weiße Karten definiert und als Crowdsourcing in einem Cryptpad werden noch weitere Begriff gesucht und bald ergänzt.&lt;/p&gt;

&lt;p&gt;Ein kleiner Auszug:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;schwarz:
Das Wohnheim ist so leer ohne _.
_ auf der Bühne - ein Traum!
Ohne _ ist kein Sommerfest.

weiß:
Sommerfest-Keller
Kabeltrommel
RageCage
Band
Partyraum
ballern
Hemelinger
Bierdusche
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;cards-against-muggles&#34;&gt;Cards Against Muggles&lt;/h2&gt;

&lt;p&gt;Eine Besonderheit ist die Variante &lt;a href=&#34;https://muggles.cards/products/cards-against-muggles-full-version&#34;&gt;Cards Against Muggles&lt;/a&gt;. Hier gibt es nur eine PDF mit den Karten, aber keine Textdateien. Mittels des Unix-Tools &lt;code&gt;pdftotext&lt;/code&gt; lässt sich die PDF aber einfach in eine Textdatei konvertieren und nach kleinen Korrekturen kann man dann zwei Textdateien (black/white) draus machen, die sich auch einfach parsen lassen. Da die Karten nicht free sind, müsst ihr etwas zaubern, um sie spielen zu können :-)&lt;/p&gt;
</content>
      
    </item>
    
    <item>
      <title>Automatische Benachrichtigungen aus dem Schichten-Tool</title>
      <link>https://dev.ebroda.de/posts/updates_im_schichten_tool/</link>
      <pubDate>Mon, 15 Apr 2019 10:00:00 +0000</pubDate>
      <author>Eike Broda</author>
      <guid>https://dev.ebroda.de/posts/updates_im_schichten_tool/</guid>
      <description></description>
      
      <content>

&lt;p&gt;Das Schichten-Tool vom Sommerfest hat ein paar kleine Updates bekommen, zudem wurden ein paar Fehler gefixt.&lt;/p&gt;

&lt;h2 id=&#34;neue-features&#34;&gt;Neue Features&lt;/h2&gt;

&lt;p&gt;Bei den Optionen für den Schichtplan gibt es nun zwei neue Optionen: &amp;ldquo;Keine Eintragungen vor &amp;hellip;&amp;rdquo; und
&amp;ldquo;Eintragungen bis dahin deaktivieren&amp;rdquo;. An sich sind die beiden Funktionen selbsterklärend, trotzdem noch
eine kurze Erläuterung, wieso und was überhaupt.&lt;/p&gt;

&lt;p&gt;Um allen aus dem Team gleiche Chancen auf gute Schichten zu geben, sollen Schichten mit etwas Vorlauf
angekündigt werden und die Eintragungen auch dann erst möglich sein. Da das mit dem Ankündigen immer nicht
so richtig gut geklappt hat, gibt es nun zwei zusätzliche Optionen im Schichten-Tool. Die erste Option
&amp;ldquo;keine Eintragungen vor&amp;rdquo; ermöglicht einen Unix-Zeitstempel&lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; anzugeben, ab wann sich Staffler (wieder)
für Schichten eintragen können. Basierend darauf wird auch ein Hinweis im Schichtplan mit diesem Datum &amp;amp;
Uhrzeit angezeigt.&lt;/p&gt;

&lt;p&gt;Die zweite Option &amp;ldquo;Eintragungen bis dahin deaktivieren&amp;rdquo; ermöglicht zusätzlich den Schichtplan für jegliche
Eintragungen durch Staffler zu deaktivieren. Bis zum gegebenen Datum (s.o.) sind keinerlei Eintragungen von
Schichten im Plan möglich. Diese lassen sich erst wieder ab dem zuvor definierten Zeitpunkt eintragen. Durch
diese zusätzliche Option soll die Eintragung in zwei Wellen ermöglicht werden, wie z.B. erste Schicht, dann
zweite Schicht. Vor der ersten Eintragung ist die Option aktiv und nach der zweiten dann nicht mehr.&lt;/p&gt;

&lt;p&gt;Die Optionen sollten auch ruhig direkt bei der Erstellung genutzt werden, damit Staffler, die im Schichten-Tool
vorbeischauen, direkt wissen, dass der Plan noch in Arbeit ist und Eintragungen erst später möglich sein werden.&lt;/p&gt;

&lt;p&gt;Durch das erste Feature sind nun auch automatische Benachrichtigungen mittels Webhook im Slack-Channel
#announcements möglich.
Hierbei erfolgen Benachrichtigungen zwei Tage, drei Stunden und zum Beginn der Eintragung
(bzw. zu beginnenden Eintragungen im Laufe der nächsten Stunde). Diese Zeiten sind erst einmal willkürlich
festgelegt und können einfach angepasst werden. Sie sind aber hoffentlich ausreichend, um den Schichtvergabeprozess
fairer und transparenter darzustellen.
Nachfolgend ein Beispiel, wie das ganze dann in Slack aussieht:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;heute, 09:00 Uhr&lt;/em&gt;&lt;br /&gt;
Vorankündigung: Ab Montag, 15.04.2019 12:00 könnt ihr euch in den Schichtplan &lt;a href=&#34;#&#34;&gt;VoFi 2019 - Plakatieren &amp;amp; Flyern&lt;/a&gt; eintragen.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;heute, 12:00 Uhr&lt;/em&gt;&lt;br /&gt;
Ihr könnt euch &lt;strong&gt;jetzt&lt;/strong&gt; in den Schichtplan &lt;a href=&#34;#&#34;&gt;VoFi 2019 - Plakatieren &amp;amp; Flyern&lt;/a&gt; eintragen. (Zugangsdaten: Benutzer &amp;hellip;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Das ganze ist über einen Cronjob gelöst, der jeweils zur vollen Stunde prüft, ob für einen Schichtplan benachrichtigt werden
muss. Daher empfiehlt es sich, immer Schichten zur vollen Stunde freizuschalten. Sofern Schichten aber erst später in der
Stunde freigeschaltet werden, wird die Zeit in Minuten bis zur Freischaltung angegeben. Eine zusätzliche Benachrichtigung
zum finalen Zeitpunkt erfolgt dann nicht mehr.&lt;/p&gt;

&lt;p&gt;Und wo ich gerade dabei war, habe ich das Layout des Neu/Bearbeiten-Dialogs auch noch fix etwas überarbeitet.&lt;/p&gt;

&lt;p&gt;Die Tage eines Schichtplans können nun sortiert werden. Dafür kann jedem Tag eine Sortierung zugewiesen werden.
Diese Sortierung ist einfach eine Nummer und es wird absteigend sortiert; der Tag mit der höchsten Nummer kommt
also als erstes. Standardmäßig ist die Sortierung für alle Tage 1, sodass diese dann nach dem Erstellungsdatum
sortiert werden. Die Sortierung kann nur nachträglich per Bearbeitung angepasst werden.&lt;/p&gt;

&lt;h2 id=&#34;fixes&#34;&gt;Fixes&lt;/h2&gt;

&lt;p&gt;Beim Abmelden gab es im ersten Versuch immer eine Fehlermeldung, dass keine Schichten mehr frei sind. Die Ursache
war ein Features, das im letzten Jahr neu implementiert wurde und jeweils nach dem ersten Absenden prüft, ob in der
angegebenen Schicht überhaupt noch Plätze frei sind. Somit soll sichergestellt werden, dass bei parallelen Eintragungen
nicht mehr als die &amp;ldquo;zulässige&amp;rdquo; Anzahl an Schichten eingetragen wird. Allerdings ist die Abmeldung ein Sonderfall,
für welche diese Begrenzung nicht gilt. Dieser Fall wurde allerdings nicht ausgeklammert, wodurch weiterhin immer
abgefragt wurde, ob in der Schicht noch was frei ist &amp;hellip; ist jetzt behoben.&lt;/p&gt;

&lt;p&gt;Es gab in einigen Fällen ein paar unschöne Darstellungen, wenn irgendwo noch Werte fehlten. Das ist durch einige
zusätzliche isset()-Checks ebenfalls behoben.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34;&gt;

&lt;hr /&gt;

&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;Das sind die Sekunden seit dem 01.01.1970, 00:00. Für Programmierer eine einfachere Weise, um mit Datum &amp;amp; Zeit zu rechnen. Für die Konvertierung eines Datums &amp;amp; Uhrzeit in Unixtime gibt es entsprechende Webseiten, z.B. &lt;a href=&#34;https://unixtime.de&#34;&gt;unixtime.de&lt;/a&gt;.
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:1&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
      
    </item>
    
    <item>
      <title>Benutzerstatiken mit Fathom</title>
      <link>https://dev.ebroda.de/posts/fathom/</link>
      <pubDate>Thu, 21 Mar 2019 11:30:26 +0000</pubDate>
      <author>Eike Broda</author>
      <guid>https://dev.ebroda.de/posts/fathom/</guid>
      <description></description>
      
      <content>&lt;p&gt;Vor einiger Zeit bin ich zufällig beim Surfen auf &lt;a href=&#34;https://usefathom.com/&#34;&gt;Fathom&lt;/a&gt; gestoßen.
Ein Tracking-Tool für Webseiten wie Google Analytics oder &lt;a href=&#34;https://matomo.org&#34;&gt;Matomo&lt;/a&gt;,
dass allerdings die Privatsphäre der Nutzer in den Mittelpunkt stellt und nur sehr einfache Statistiken bereitstellt,
die aber letztendlich vollkommen ausreichend sind.&lt;/p&gt;

&lt;p&gt;Der Vorteil gegenüber Matomo ist, dass die Statistiken deutlich schneller laden. Matomo ist leider auf
meinem Hoster ziemlich langsam, wenn es darum geht die Daten zu konsolidieren. Vielleicht hätte ich das noch
tweaken können, aber letztendlich habe ich eh eher selten in die Statistiken geguckt, daher habe ich Matomo
auf allen Seiten wieder entfernt. Auch um mir nebenbei die super-hässlichen Cookie-Hinweise zu sparen,
die inzwischen auf jeder Webseite aufploppen und aus technischer Sicht doch ziemlich fragwürdig sind.&lt;/p&gt;

&lt;p&gt;Jetzt aber Fathom. Ich fand&amp;rsquo;s cool, hatte aber keine Seite um es zu testen. Jetzt möchte ich aber wissen,
ob das hier irgendwer liest, daher habe ich es jetzt hier mal testweise eingebunden. Also wenn ihr nicht
getrackt werden wollt, aktiviert einfach Do Not Track in eurem Browser. Fathom respektiert das.&lt;/p&gt;

&lt;p&gt;Für die Transparenz sind die Statistiken jetzt auch &lt;a href=&#34;https://fathom.wpaf.de/&#34;&gt;öffentlich&lt;/a&gt; verfügbar&lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.
Schaut einfach mal rein; aus meiner Sicht reichen die angebotenen Statistiken vollkommen aus.&lt;/p&gt;

&lt;p&gt;Fathom kann einfach auf einem eigenen Server installiert werden. Somit bleiben zusätzlich alle Daten auch bei euch.
Also anders als z.B bei Google Analytics. &lt;del&gt;Ich werde später auch eine Anleitung für das &lt;a href=&#34;https://lab.uberspace.de&#34;&gt;Uberlab&lt;/a&gt;
verfassen,&lt;/del&gt;
Es gibt eine &lt;a href=&#34;https://lab.uberspace.de/guide_fathom_lite.html&#34;&gt;Anleitung&lt;/a&gt;
im &lt;a href=&#34;https://lab.uberspace.de&#34;&gt;Uberlab&lt;/a&gt; dazu, wie einfach man Fathom auf einem &lt;a href=&#34;https://uberspace.de&#34;&gt;Uberspace 7&lt;/a&gt; deployen kann&lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;. Funktioniert nun ziemlich nice mit den neuen Web Backends.&lt;/p&gt;

&lt;p&gt;Auf dem legacy Uberspace 6 funktioniert Fathom aufgrund eines zu alten Kernels mit der aktuellen Version nicht mehr.
Es stürzt da direkt beim Start mit einer Fehlermeldung ab. Da hilft dann nur der Wechsel auf die neue Uberspace-Version &amp;hellip;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[me@host bin]$ ./fathom -v
FATAL: kernel too old
Abgebrochen
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&#34;footnotes&#34;&gt;

&lt;hr /&gt;

&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;Die Statistiken sind momentan nicht mehr öffentlich, da in einem Projekt auch private URLs mitgetrackt werden, die nicht öffentlich zugänglich sein sollten. Security by Obscurity ist natürlich (k)ein Top-Konzept, aber hier reicht es aus.
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:1&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;Update April 2020: Ich habe es endlich geschafft den Guide zu schreiben. Seit Ende April ist der Guide nun Teil des Uberlabs.
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:2&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
      
    </item>
    
    <item>
      <title>T-Shirts bestellen via Slack</title>
      <link>https://dev.ebroda.de/posts/tshirts_per_slack_bestellen/</link>
      <pubDate>Wed, 20 Mar 2019 20:22:30 +0000</pubDate>
      <author>Eike Broda</author>
      <guid>https://dev.ebroda.de/posts/tshirts_per_slack_bestellen/</guid>
      <description></description>
      
      <content>

&lt;p&gt;Für die komplette Crew des &lt;a href=&#34;https://sommerfest-vorstrasse.de&#34;&gt;Sommerfests&lt;/a&gt; gibt es jedes Jahr ein entsprechendes T-Shirt, intern Staff-Shirt genannt.
Dieses Shirt ist jeweils in der Farbe des Jahres und mit einem Staff-Aufdruck. Dabei für das ganze Team den Überblick zu behalten, wer schon ein
T-Shirt bestellt hat und wessen T-Shirt noch fehlt ist gar nicht so einfach. In den letzten Jahren haben wir das üblicherweise mit einem
GoogleDocs-Formular gemacht, wo sich dann alle Leute aus dem Team eintragen mussten. Allerdings hatte auch das einige Probleme: So haben sich teils
Staffler nur mit ihrem Vorname eingetragen. Ist an sich okay, nur blöd, wenn es im Team noch jemanden mit demselben Vornamen gibt. Alleine über die
Größe kann man dann schwer rückschließen, wer noch fehlt. Auch war die Transparenz teils nicht gegeben: Welche Größe habe ich nochmal bestellt und
habe ich schon bestellt?&lt;/p&gt;

&lt;h2 id=&#34;bestellen-per-slack&#34;&gt;Bestellen per Slack?&lt;/h2&gt;

&lt;p&gt;Das soll in diesem Jahr anders laufen. In einem Sommerfest-Protokoll von einem Treffen, auf dem ich verhindert war, las ich dann, dass die Bestellung
per Slack ablaufen soll. Wir kommunizieren seit Beginn des diesjährigen Orga-Zyklusses über das &amp;ldquo;neuartige&amp;rdquo; Slack. Läuft schon gut, aber irgendwie las
sich das zu wenig technisch. Meine Vision war eigentlich, dass auch mit ins Orga-Tool einzubauen. Aber das müsste man dort erst implementieren und das
Tool ist ja auch noch nicht alpha und nicht richtig genutzt. Von daher ist Slack als Ort der Aktiven schon eine bessere Idee. Aber es muss doch was
besseres geben, als alle schreiben ihre Größe in einen Channel und jemand Armes darf das dann am Ende auswerten. Was dann leider auch immer wieder zu
Problemen führt, weil man dann doch mal eine Shirt vergisst oder irgendwo ein Girlie statt einem normalen Shirt bestellt.&lt;/p&gt;

&lt;p&gt;Aber wie kann man das technisch besser lösen?&lt;/p&gt;

&lt;p&gt;Die erste Idee waren Slack-Commands (Slash-Commands), so bieten Integrationen und auch Slack selbst Befehle wie z.B. /giphy [keyword] an.
Das Beispiel sucht dann auf
Giphy nach einem Gif und bietet dem Nutzer die Möglichkeit das zu posten. Sowas müsste sich doch auch für die Bestellung der Staff-Shirts basteln lassen.
Kann ja nicht so schwer sein. Die erste Idee wäre dann /staffshirt girlie m gewesen, also dass der Nutzer dann sein Bestellung als Parameter übergibt.
Der gute &lt;a href=&#34;https://nikolasdas.de&#34;&gt;Niko&lt;/a&gt; hat dann aber mal ein bisschen in den Slack-Docs gewühlt und kam mit &lt;a href=&#34;https://api.slack.com/dialogs&#34;&gt;Dialogs&lt;/a&gt; &amp;ldquo;um
die Ecke&amp;rdquo;. Das ist ein Slack-Feature, was es ermöglicht in Slack einen Eingabedialog zu öffnen, in dem die Nutzer was eingeben und dann abschicken können.
Das klingt nochmal deutlich besser.&lt;/p&gt;

&lt;p&gt;Der Plan war nun also: Durch einen Befehl den Prozess intiieren, einen Dialog anzeigen und den Nutzer seine Bestellung machen lassen.&lt;/p&gt;

&lt;h2 id=&#34;let-s-go-dialog-implementieren&#34;&gt;Let&amp;rsquo;s go. Dialog implementieren&lt;/h2&gt;

&lt;p&gt;Klingt erstmal einfach, aber irgendwie muss man doch recht viel machen, damit es dann funktioniert. Aber ein bisschen Step-by-step.
Los geht&amp;rsquo;s mit einer eigenen Slack-Anwendung. Die kann man sich einfach online &lt;a href=&#34;https://api.slack.com/apps&#34;&gt;klicken&lt;/a&gt;, wenn an in einem Workspace ist.
Das gute ist, dass die Apps dann auch erstmal nur für den Workspace sichtbar sind. Man muss sich also keine Gedanken darüber machen, dass irgendwer
in der Welt sich jetzt diese App installiert.&lt;/p&gt;

&lt;p&gt;Nun muss man die App im Workspace installieren, damit man das notwendige OAuth-Token bekommt um weitere Aktionen auszuführen. Dafür muss die App aber
mindestens eine Berechtigung anfordern. Ich habe da einfach mal &amp;ldquo;Send messages as Sommerfest-Tools (&lt;em&gt;chat:write:bot&lt;/em&gt;)&amp;rdquo; als Berechtigung ausgewählt,
da später die App dem Nutzer eine Bestätigung als Nachricht schicken soll. Danach kann man die App dann dem Workspace hinzufügen und bekommt hinterher
auch das OAuth-Token für die Authentifizierung. Das sieht dann so ähnlich aus: xoxp-12345678901-12345678901-123456789012-1234567890abcdef1234567890abcdef.&lt;/p&gt;

&lt;p&gt;Nun kanns losgehen. Ebenfalls im dem Slack-Webportal können für die App die Slash commands definiert werden. Es muss angegeben werden, was der Befehl ist.
Okay, easy /staffshirt. Dann eine Request URL, okay &amp;hellip; lassen wir erstmal offen und dann noch eine Beschreibung, die Slack in der Vorschau für den Befehl
zeigt und ein Hinweis zur Nutzung, auch für die Autovervollständigung. Speichern. Hmm, ne - Slack sagt, leer ist keine gültige Request URL.
Aber da ist ein kleines Info-Zeichen in dem Feld, das sagt was die URI ist.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We’ll send an HTTP POST request with information you might need to this URL when the command is run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay, also brauche ich irgendwo einen Service der einen POST-Request empfangen kann. Die Sourcen vom Orga-Tool hatte ich gerade nicht auf&amp;rsquo;m Rechner und
nur dafür irgendwo extra ein Flask zum Laufen zu bringen, war mir dann zu viel. Daher die einfachste Variante genommen, die mit jedem SharedHost funktioniert:
PHP. Da kümmert sich selbstständig der Apache darum, dass das läuft und ich kann mir super einfach den Payload holen:&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span style=&#34;color:#007020&#34;&gt;&amp;lt;?php&lt;/span&gt;
var_dump(&lt;span style=&#34;color:#bb60d5&#34;&gt;$_POST&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So kann ich mir das dann dumpen, okay - ist etwas blöd bei nem Slack-Request, weil ich seh ja gar nicht, was genau Slack mir da schickt. Ich bekomme den
Output ja nicht zu sehen &lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. Von daher vielleicht besser den Input nehmen und in eine Datei schreiben. Das ist ja auch easy in PHP. In diesem Fall speichere
ich einfach den kompletten POST-Request als json und häng&amp;rsquo; jeden Request einfach immer an die Datei post_requests.txt an.&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span style=&#34;color:#007020&#34;&gt;&amp;lt;?php&lt;/span&gt;
file_put_contents(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;post_requests.txt&amp;#39;&lt;/span&gt;, json_encode(&lt;span style=&#34;color:#bb60d5&#34;&gt;$_POST&lt;/span&gt;), FILE_APPEND);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Schaut man sich dann an, was Slack einem schickt, so sieht man, dass es die nachfolgenden Elemente sind.&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;token:UZ3stU0rMIMTAWxxXXXXxXxX
team_id:ABCDEFGH
team_domain:some-slack
channel_id:XXXXENGL
channel_name:directmessage
user_id:UDYKDXXXX
user_name:ebroda
command:/staffshirt
text:
response_url:https://hooks.slack.com/commands/XXXXXXRYT1/580296238656/G3sOdzYyiGp7efvlT7xXxxxX
trigger_id: 582673415332.84734882919.0d06e430f7749f98742444xxx4xxx978&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Für die bessere Lesbarkeit hier als Key-Value-Pairs dargestellt&lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;. Eigentlich kommt das ganze als application/x-www-form-urlencoded, aber PHP parst das automatisch,
daher kann man in der $_POST-Variable von PHP direkt mit den Keys auf die Werte zugreifen. Details zu den einzelnen Zeilen finden sich in der Erläuterung in der
&lt;a href=&#34;https://api.slack.com/slash-commands#2._preparing_your_app_to_receive_commands&#34;&gt;Slack-API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Entscheidend ist in dieser Antwort die trigger_id. Diese wird benötigt um dem Nutzer dann einen Dialog anzeigen zu können. Auf diese kann nun einfach über $_POST
zugegriffen werden.Mittels des nachfolgenden Schnipsels, bekommen wir also die Trigger-ID.&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span style=&#34;color:#007020&#34;&gt;&amp;lt;?php&lt;/span&gt;
&lt;span style=&#34;color:#bb60d5&#34;&gt;$trigger&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bb60d5&#34;&gt;$_POST&lt;/span&gt;[&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;trigger_id&amp;#39;&lt;/span&gt;];
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Um dem Nutzern nun unseren Dialog anzuzeigen, muss ein POST-Request auf den &lt;em&gt;dialog.open&lt;/em&gt;-Endpoint der Slack-API erfolgen. Dabei sind noch einige Stolpersteine zu beachten, wie Slack
den Request gerne hätte. Eine Möglichkeit ist den Request mit den Optionen als JSON-String im Request-Body zu schicken. Dafür muss die Nachricht dann aber
auch den entsprechenden Content-Type application/json aufweisen und im Header das Slack-Token von der Installation als Authorization-Header mit dem Prefix
Bearer enthalten&lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;. Die nachfolgende Funktion setzt diese Problematik um und basiert auf &lt;a href=&#34;https://stackoverflow.com/a/6609181&#34;&gt;dieser&lt;/a&gt; Stackoverflow-Antwort.&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;function send_to_slack($uri, $data) {
  $slack_token = &amp;#34;xoxp-1234567890...&amp;#34;

  $options = array(
      &amp;#39;http&amp;#39; =&amp;gt; array(
          &amp;#39;header&amp;#39;  =&amp;gt; &amp;#34;Content-type: application/json; charset=utf-8\r\nAuthorization: Bearer &amp;#34; . $slack_token,
          &amp;#39;method&amp;#39;  =&amp;gt; &amp;#39;POST&amp;#39;,
          &amp;#39;content&amp;#39; =&amp;gt; json_encode($data)
      )
  );
  $context  = stream_context_create($options);
  return file_get_contents($uri, false, $context);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Was macht die Funktion? Sie erwartet eine URL (den API-Endpoint) und die Daten (als array). Dann wird ein zusätzliches Array erstellt, dass alle Einstellungen
für den POST-Request setzt. In diesem Fall also zwei Header, den Content-Type und die Authorization; die Methode, hier POST sowie den Request-Body a.k.a. content
der Nachricht. Anschließend wird das ganze verschickt und das Ergebnis zurückgegeben. Gegenüber der Stackoverflow-Antwort habe ich den Fehlerfall mal weggelassen,
um es einfach zu halten. Wir gehen ja nicht davon aus, dass beim Request irgendwas schief läuft bzw. wir merkens spätestens, wenn nichts passiert.&lt;/p&gt;

&lt;p&gt;Während der Entwicklung lohnt es sich auf jeden Fall die Antwort der API einfach auszugeben um Fehlermeldungen von Slack in dem Channel zu erhalten, in dem man
den Befehl ausgeführt hat.&lt;/p&gt;

&lt;h2 id=&#34;dialog-designen&#34;&gt;Dialog designen&lt;/h2&gt;

&lt;p&gt;Wir haben die Trigger id, wir können einen POST-Request absetzen, jetzt müssen wir den Dialog definieren und dann Anzeigen lassen. Dafür defineren wir erstmal
allgemein den Dialog, also z.B. welchen Titel der Dialog hat und wie der Button zum Abschicken beschriftet ist. Die _callback&lt;em&gt;id&lt;/em&gt; bekommen wir nach dem Absenden
von Slack wieder zurück, um den Dialog zu identifizieren. Das _notify_on&lt;em&gt;cancel&lt;/em&gt;-Flag bewirkt, dass auch bei einem Klick auf Abbrechen später ein Request
abgeschickt wird, sodass der Nutzer z.B. per Nachricht informiert werden kann, dass er den Prozess abgebrochen hat und kein Shirt bestellt wurde. Die eigentlichen
Formularfelder werden in der Liste &lt;em&gt;elements&lt;/em&gt; definiert.&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;{
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;callback_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;staffshirt&amp;#34;&lt;/span&gt;,
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;Staff-Shirt bestellen&amp;#34;&lt;/span&gt;,
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;submit_label&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;Bestellen&amp;#34;&lt;/span&gt;,
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;notify_on_cancel&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;true&lt;/span&gt;,
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;elements&amp;#34;&lt;/span&gt;: []
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Dafür kennt Slack 3 verschiedene
Feldtypen&lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;, die verwendet werden können: Text, (längeres) Textfeld und eine Auswahlliste. Für die Bestellung des T-Shirts soll der Nutzer die Größe des T-Shirts aus
einer Liste von vorgegebenen Größen auswählen können. Dadurch gibt es zum einen nur Bestellungen in Größen, die es auch wirklich gibt und zum anderen macht das später
die Auswertung einfacher.&lt;/p&gt;

&lt;p&gt;Die Definition eines Auswahlfelds für die Größen S, M, L und XL sieht dann so aus. Dabei ist jeweils Label das, was angezeigt wird. Der Type gibt an, dass es sich um
eine Auswahlliste handelt und das name-Attribut ist später für die Auswertung, wenn der Nutzer das Formular abschickt.
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;{
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;label&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;Größe&amp;#34;&lt;/span&gt;,
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;select&amp;#34;&lt;/span&gt;,
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;shirt_size&amp;#34;&lt;/span&gt;,
  &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;options&amp;#34;&lt;/span&gt;: [
    {
      &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;label&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;S&amp;#34;&lt;/span&gt;,
      &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;s&amp;#34;&lt;/span&gt;
    },
    {
      &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;label&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;M&amp;#34;&lt;/span&gt;,
      &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;m&amp;#34;&lt;/span&gt;
    },
    {
      &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;label&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;L&amp;#34;&lt;/span&gt;,
      &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;l&amp;#34;&lt;/span&gt;
    },
    {
      &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;label&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;XL&amp;#34;&lt;/span&gt;,
      &lt;span style=&#34;color:#062873;font-weight:bold&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#34;xl&amp;#34;&lt;/span&gt;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;

&lt;p&gt;Haben wir so das Formular definiert, muss das alles zusammen in JSON gepackt werden und abgeschickt werden.
Das kann dann wie nachfolgend aussehen. Die Funktion zum Senden wurde gekürzt.&lt;/p&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span style=&#34;color:#007020&#34;&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span style=&#34;color:#bb60d5&#34;&gt;$trigger&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bb60d5&#34;&gt;$_POST&lt;/span&gt;[&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;trigger_id&amp;#39;&lt;/span&gt;];

    &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#06287e&#34;&gt;send_to_slack&lt;/span&gt;(&lt;span style=&#34;color:#bb60d5&#34;&gt;$uri&lt;/span&gt;, &lt;span style=&#34;color:#bb60d5&#34;&gt;$data&lt;/span&gt;) {
      &lt;span style=&#34;color:#60a0b0;font-style:italic&#34;&gt;// ... (s.o.)
&lt;/span&gt;&lt;span style=&#34;color:#60a0b0;font-style:italic&#34;&gt;&lt;/span&gt;    }

    &lt;span style=&#34;color:#bb60d5&#34;&gt;$data&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(
        &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;trigger_id&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#bb60d5&#34;&gt;$trigger&lt;/span&gt;,
        &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;dialog&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(
            &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;callback_id&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;staffshirt&amp;#39;&lt;/span&gt;,
            &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;title&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;Staff-Shirt bestellen&amp;#39;&lt;/span&gt;,
            &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;submit_label&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;Bestellen&amp;#39;&lt;/span&gt;,
            &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;notify_on_cancel&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;true&lt;/span&gt;,
            &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;elements&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(
                &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(
                    &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;label&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;Größe&amp;#39;&lt;/span&gt;,
                    &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;type&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;select&amp;#39;&lt;/span&gt;,
                    &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;shirt_size&amp;#39;&lt;/span&gt;,
                    &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(
                        &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;label&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;S&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;value&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;s&amp;#39;&lt;/span&gt;),
                        &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;label&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;M&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;value&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;m&amp;#39;&lt;/span&gt;),
                        &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;label&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;L&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;value&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;l&amp;#39;&lt;/span&gt;),
                        &lt;span style=&#34;color:#007020;font-weight:bold&#34;&gt;array&lt;/span&gt;(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;label&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;XL&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;value&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#666&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;xl&amp;#39;&lt;/span&gt;)
                    )
                )
            )
        )
    );

    send_to_slack(&lt;span style=&#34;color:#4070a0&#34;&gt;&amp;#39;https://slack.com/api/dialog.open&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#bb60d5&#34;&gt;$data&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Damit Slack die Dialoge auch anzeigt, muss das in der App aktiviert werden. Dafür muss wieder im Webinterface von der Slack-Anwendung
die Nutzung von &lt;strong&gt;Interactive Components&lt;/strong&gt; aktiviert werden. Dabei muss eine Request URL angegeben werden. An diese URL postet Slack
jedes Mal, wenn ein Dialog oder ein anderes interaktives Element (Buttons, Actions, Message Menues) von einem Benutzer angeklickt bzw.
abgeschickt wird. Um den Endpoint für die Actions geht es in einem zweiten Teil.&lt;/p&gt;

&lt;h2 id=&#34;ergebnis&#34;&gt;Ergebnis&lt;/h2&gt;

&lt;p&gt;Das ganze nun auf einen Server deployed, die URLs in der Slack-Konfiguration angepasst und ab dafür. Und tatsächlich auf eine Eingabe
von /staffshirt öffnet sich ein Dialog:&lt;/p&gt;

&lt;figure&gt;
    &lt;img src=&#34;https://dev.ebroda.de/images/2019/2019_03_slack_dialog.png&#34;/&gt; &lt;figcaption&gt;
            &lt;h4&gt;Der Dialog zur Auswahl der T-Shirtgröße&lt;/h4&gt;
        &lt;/figcaption&gt;
&lt;/figure&gt;


&lt;figure&gt;
    &lt;img src=&#34;https://dev.ebroda.de/images/2019/2019_03_slack_dialog_auswahl.png&#34;/&gt; &lt;figcaption&gt;
            &lt;h4&gt;Auswahl einer T-Shirtgröße&lt;/h4&gt;
        &lt;/figcaption&gt;
&lt;/figure&gt;


&lt;h2 id=&#34;ausblick&#34;&gt;Ausblick&lt;/h2&gt;

&lt;p&gt;Der erste Dialog ist da, damit kann der Teil schon als abgeschlossen angenommen werden. Der T-Shirt-Typ, die Auswahl eines Extra-Shirts
sowie die Möglichkeit noch einen Kommentar anzugeben lässt sich nun leicht noch ergänzen. Im nächsten Teil wird es dann um das Speichern
der erhaltenen Werte gehen und in einem dritten Teil um die Generierung einer Liste der Bestellungen, der Kummulierung der einzelnen
Größen sowie eine Liste aller, die noch nicht bestellt haben.&lt;/p&gt;

&lt;p&gt;Die Implementierung ist schon fertig, ich hatte nur noch keine Zeit, dazu auch einen entsprechenden Post zu schreiben.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34;&gt;

&lt;hr /&gt;

&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;Tatsächlich zeigt Slack einem sogar doch die Rückgabe des POST-Requests als &amp;ldquo;nur für dich sichtbare&amp;rdquo;-Nachricht an.
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:1&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;Und ein paar der Variablen sind verändert/obfuscated.
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:2&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;Detailliert beschrieben werden die Anforderungen in der Slack-API-Dokumentation. Dieser spezielle Punkt &lt;a href=&#34;https://api.slack.com/web#json-encoded_bodies&#34;&gt;hier&lt;/a&gt;.
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:3&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;Details zu den einzelnen Typen finden sich in der &lt;a href=&#34;https://api.slack.com/dialogs#elements&#34;&gt;Slack-Doku&lt;/a&gt;.
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:4&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
      
    </item>
    
    <item>
      <title>Vision: Das Orgatool - Alles Wichtige für die Sommerfest-Orga an einem Ort</title>
      <link>https://dev.ebroda.de/posts/ideensammlung_orgatool/</link>
      <pubDate>Wed, 20 Mar 2019 20:20:30 +0000</pubDate>
      <author>Eike Broda</author>
      <guid>https://dev.ebroda.de/posts/ideensammlung_orgatool/</guid>
      <description></description>
      
      <content>&lt;p&gt;Im Februar 2017 habe ich angefangen ein Orga-Tool zu entwickeln. Eine Problematik war zum einen eine mangelhafte Mitgliederverwaltung. Bisher wurden
die Mitglieder in einer Excel-Liste in einer Dropbox verwaltet. Diese Liste wurde aber eher selten gepflegt und war auch nicht hinreichend zugänglich,
geschweige denn aktuell, so z.B. Mitglieder umgezogen sind. Eine Vision war daher einen zentralen Ort in einer Webanwendung zu schaffen, in dem alle
Mitglieder verwaltet werden und diese gleichzeitig die Möglichkeit haben ihre Daten selbst zu verwalten. Auch die Zahlung des Mitgliedsbeitrags wurde
dort nur kurz in der Excel-Tabelle vermerkt.&lt;/p&gt;

&lt;p&gt;Das nächste Problem war, dass wiedermal diverse Leute ihr Passwort für das Bandvoting vergessen hatten oder sich erst gar nicht registriert hatten.
Daher war die Vision auch, dass die Leute mit ihrer Anmeldung im Orga-Tool auch direkt einen Sommerfest-Account bekommen, mit dem sie sich in allen
Tools anmelden können. Geplant ist dafür irgendwann mal ein LDAP-Server bereitzustellen, gegen den die Anwendungen dann validieren können.&lt;/p&gt;

&lt;p&gt;Ein dritter Aspekt war die Mitglieder einfacher zu erreichen. Bisher wurde die Rundmail-Funktion im Forum genutzt, um allen Mitgliedern dort eine
E-Mail zu schicken. Das war aber nicht optimal, weil dazu die Sendenden immer Zugriff auf die Adminstration haben mussten. Als Vision war auch eine
Art &amp;ldquo;Weekly Digest&amp;rdquo; angedacht, welcher einmal wöchentlich über die letzten Aktivitäten informiert und eine Übersicht der nächsten Termine gibt.
Dieser sollte dann aber auch automatisch generiert werden. Wozu haben wir schließlich Computer?&lt;/p&gt;

&lt;p&gt;Das Sommerfest ist zudem in verschiedenen Ressorts organisiert. Um einen Überblick zu bekommen, wer was macht, sollte das Orga-Tool auch diese
Ressortstruktur abbilden und sichtbar machen, welche Mitglieder in den einzelnen Ressorts sind.&lt;/p&gt;

&lt;p&gt;Leider ist das Orga-Tool bis heute nicht fertig, die Priorität für das Tool war leide eher niedrig. Die Visionen sind aber weiterhin erhalten.
Ein paar Aspekte sind davon umgesetzt, so gibt es die Mitglieder, die Ressorts und zusätzlich auch Termine. Die Termine werden sogar mittels CalDav
ausgeliefert, sodass man sie abonnieren kann und einfach alle Sommerfest-Termine in seinen Kalender bekommt&lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. Zusätzlich gibt es auch die
Möglichkeit für jeden Termin ein Protokoll in Markdown zu tippen und dort zu hinterlegen. Soweit so gut, leider fehlen immer noch diverse
Funktionalitäten und das Tool ist bis heute eher &amp;lsquo;ne alpha als &amp;lsquo;ne beta. Cool an der Sache ist auch die Integration in Slack, wobei Integration
momentan noch recht hoch gegriffen ist. Tatsächlich postet das Tool &amp;ldquo;nur&amp;rdquo; bei jedem neuen Termin diesen auch automatisch in Slack. Ist nicht mehr
als &amp;lsquo;n Webhook, aber trotzdem cool&amp;hellip;&lt;/p&gt;

&lt;p&gt;Um es mal niederzuschreiben nachfolgend noch die weiteren Visionen für das Orga-Tool:&lt;/p&gt;

&lt;p&gt;Die Mitgliederverwaltung endlich vollständig, mit allen Funktionalitäten, die ich mir oben so ausgedacht hatte. Auch z.B. die Möglichkeit einfach
online mittels Klick seine Kündigung einzureichen und somit seinen Account einfach zum Jahresende auslaufen zu lassen. Es sollte so easy sein,
dass auch die Mitglieder selbst somit die Liste der Aktiven pflegen. Wer raus ist, ist dann raus.
Gleichzeitg soll der &lt;strong&gt;Beitritt&lt;/strong&gt; auch möglichst einfach sein. Im besten Fall können wir das auch alles in einem großen Formular erledigen, an
dessen Ende man dann ein PDF generiert bekommt, das ausdruckt und unterschreibt und dann schon Mitglied ist. Der Mitgliedsbeitrag soll sich
dann einfach per PayPal bezahlen lassen und wenn man die Transaktion abgeschlossen hat, wird der Betrag automatisch auch als bezahlt verbucht.&lt;/p&gt;

&lt;p&gt;Was zum nächsten großen Punkt führt: Die &lt;strong&gt;Finanzverwaltung&lt;/strong&gt;. Auch das ist momentan ein großes Excel-Sheet mit ein klein bisschen Logik.
Besonders schön finde ich das aber nicht. Aus mehreren Gründen. Oder Visionen, wie es eigentlich laufen sollte. Dazu hole ich mal ein bisschen aus.
Jedes Ressort sollte einen gewissen finanziellen Rahmen haben (Budget) in dem es ohne weitere Zustimmung seine Einkäufe tätigen kann, zumindest für
Artikel kleiner einem bestimmten Wert von n €. Diese Budget gibt es momentan nicht in dieser Excel-Tabelle. Dafür gibt es noch eine Extra-Tabelle,
zumindest endlich in diesem Jahr mal. Aber wie cool wäre es, wenn jedes Ressort direkt sehen würde, wie viel Budget es noch hat? Und wie viel Geld
es schon ausgegeben hat? Auch insgesamt für die Transparenz im Team wäre es gut.&lt;/p&gt;

&lt;p&gt;Das erfordert dann aber auch, dass alle Buchungen im Tool verbucht werden. Momentan passiert das in der Excel-Tabelle, somit ist der Aufwand etwas
höher, wenn man mehrere Rechnungen hat. Aber hier soll die Webapp einen Vorteil ausspielen, indem sie den Finanzer entlastet und zwar dadurch, dass
die Staffler ihre Rechnungen direkt selbst eintragen und hochladen können. Der Finanzer prüft anschließend nur noch die Rechnung und checkt die
Eintragung nochmal gegen. Würde ein paar Sachen auch vereinfachen. Genauso das Auffinden von Rechnungen und der Rechnungsdokumente. Die haben nun
recht kryptische Dateinamen nach dem Muster [#ressort Ressort]/[#ressort]_[#lfdNummer].pdf bekommen.
Der Dateiname steht mit in der Excel-Tabelle. Somit kann man über die Buchung dann die Rechnung finden, aber das ist auch eher umständlich, sich
dann noch durch Ordner zu klicken und die richtige Datei zu suchen anstatt einfach direkt auf einen Button &amp;ldquo;Rechnung&amp;rdquo; zu klicken und das PDF
angezeigt zu bekommen.&lt;/p&gt;

&lt;p&gt;Zusätzlich ist momentan die Abrechnung von &lt;strong&gt;Auslagen&lt;/strong&gt; ein größeres Thema. Einige Leute im Team&lt;sup class=&#34;footnote-ref&#34; id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34;&gt;2&lt;/a&gt;&lt;/sup&gt; sammeln so einiges an Auslagen an, die dann
irgendwann abgerechnet werden müssen. Dabei kann aber auch schon mal was unter den Tisch fallen, dann wird mal ne Rechnung vergessen. Auch das
sollte das Orga-Tool abnehmen, indem es auch einen Bezahlt-Status kennt, also wer die Rechnung beglichen hat und automatisch alles aufaddiert,
was der Nutzer momentan an Auslagen hat. Auch &lt;strong&gt;Vorschüsse&lt;/strong&gt; für Ausgaben, wenn es also vorab Geld gibt, können dort auch einfach verbucht werden
und alles wird transparenter und nachvollziehbarer. Wenn man dann noch einen Änderungslog mit integriert, dann macht das Sachen noch einfacher.
Insgesamt soll das auch mehr &lt;strong&gt;Statistiken&lt;/strong&gt; bringen. Wie oben schon angesprochen, die Transparenz der einzelnen Ressorts - das soll sich später
auch allgemein für die Finanzen finden. Zum Beispiel auch die Entwicklung der Ausgaben über mehrere Jahre &amp;hellip; solche Sachen. Zudem soll für jede
Buchung auch abgefragt werden, ob es sich um eine einmalige Ausgabe (z.B. Anschaffung) handelt oder eine jährliche Ausgabe. So wird auch eine
erste grobe Budgetschätzung einfach mit einem simplen Query möglich.&lt;/p&gt;

&lt;p&gt;Beim Thema Anschaffungen kommen wir dann auch zu einer anderen Sache, die momentan in keiner vernünftigen Variante existiert:
ein &lt;strong&gt;Inventarmanagement&lt;/strong&gt;. Das Sommerfest besitzt einiges an Material, aber das ist nicht inventarisiert. Wir haben keinen vernünftigen Überblick,
was wir eigentlich alles haben. Vor allem bei größeren/kostenintensiveren Gegenständen wäre das schon interessant, hier auch eine Inventarisierung
vorzunehmen. Weiterführend dann auch für den Verleih von Artikeln.&lt;/p&gt;

&lt;p&gt;Nächster großer Punkt ist eine &lt;strong&gt;ToDo-Liste&lt;/strong&gt; (Issue-Tracking). Es gibt eine Vielzahl an Aufgaben, die sich jedes Jahr wiederholen und leider
passiert es doch immer wieder, dass einige Sachen vergessen werden und dann in letzter Sekunde noch erledigt werden. Hierzu soll ein Issue-Tracking
Abhilfe schaffen, in dem alle Aktivitäten aus allen Ressorts verwaltet werden, samt Deadlines und Verantwortlichen. Sodass ein Blick in die Liste
einen Aufschluss gibt, was momentan ansteht. Auch hier wieder ein &amp;ldquo;einmalig/jährlich&amp;rdquo;-Flag, um direkt einen Überblick über alle notwendigen
Aufgaben zu bekommen. Das ganze sollte dann auch als Kanban-Board dargestellt werden.&lt;/p&gt;

&lt;p&gt;Das ganze System soll auch unterschiedliche &lt;strong&gt;Berechtigungen&lt;/strong&gt; abbilden. Dafür ist vor allem später das LDAP gedacht, wobei natürlich im Orga-Tool
dann diese Berechtigungen auch umgesetzt werden müssen. Dabei ist auch angedacht allen Mitgliedern eines Ressorts direkt Zugriff auf das
E-Mail-Postfach des Ressorts zu geben, also effektiv der Login durch das Orga-Tool. Und auch andere Passwörter könnten so im Tool hinterlegt werden,
wobei noch zu klären ist, wie man das zumindest noch etwas absichern kann.&lt;/p&gt;

&lt;p&gt;Um die alte &lt;strong&gt;Rundmail&lt;/strong&gt;-Funktionalität abzulösen soll es auch die Möglichkeit geben, eine Mail an alle Mitglieder des Ressorts zu schicken. Das
ließe sich auch kombinieren mit &lt;strong&gt;Ressort-News&lt;/strong&gt;, also der Möglicheit Artikel zu verfassen um z.B. über aktuelle Aktivitäten im Ressort zu
informieren. Dieser Punkt ist allerdings durch die Kommunikation in Slack etwas unwichtiger geworden. Lediglich für die Einladung zur
Mitgliederversammlung wäre eine Rundmail-Funktionalität wünschenswert.&lt;/p&gt;

&lt;p&gt;Zudem soll das Orga-Tool auch ein &lt;strong&gt;Archiv&lt;/strong&gt; der Kommunikation sein, da wir die kostenlose Variante von Slack nutzen und somit nur die letzten 10.000
Nachrichten in Slack einsehen können.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[Update 1]&lt;/strong&gt;&lt;br /&gt;
Ich hab bisher die Vision nie so detailliert aufgeschrieben, sondern immer eher in meinem Kopf mit mir rumgetragen. Daher ist das sicherlich weder
vollständig noch abschließend. Und hier nun die erste Ergänzung:&lt;/p&gt;

&lt;p&gt;Es soll auch eine &lt;strong&gt;Kommentarfunktion&lt;/strong&gt; geben, sodass man z.B. ein Event kommentieren kann. Einen Überblick über alle Aktivitäten auf der Plattform
gibt ein &lt;strong&gt;Activity-Feed&lt;/strong&gt;, der alle Kommentare und ähnliches auflistet. Zusätzlich ist denkbar, auch pro Nutzer eine Sammlung von Aktivitäten zu
seinen Inhalten anzubieten (so &amp;lsquo;ne Art Abonnieren-Funktion) für eigene Beiträge, wie man es von facebook kennt.&lt;br /&gt;
&lt;strong&gt;[/Update 1]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eine eher im Spaß entstandende Idee ist die des &lt;strong&gt;Bullshit-Bingo&lt;/strong&gt;. Hierbei sollten von den Nutzern Begriffe in eine Sammlung ergänzt werden, aus
denen dann ein individuelles Bingofeld für jeden Nutzer generiert wird, was dieser dann interaktiv während des Treffens am Handy ausfüllen kann.&lt;/p&gt;

&lt;p&gt;An Ideen mangelt es wahrlich nicht, leider aber an Zeit. Daher ist bisher erst ein sehr kleiner Teil dieser ziemlich umfangreichen Vision
umgesetzt. Aber die Hoffnung stirbt zuletzt :)&lt;/p&gt;
&lt;div class=&#34;footnotes&#34;&gt;

&lt;hr /&gt;

&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;Gut, bei Android ist das mit Boardmitteln aus irgendwelchen Gründen immer noch nicht möglich, aber mit ner kleinen Extra-App funktionierts &amp;hellip;
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:1&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;Hüst, so Leute wie ich &amp;hellip; :D
 &lt;a class=&#34;footnote-return&#34; href=&#34;#fnref:2&#34;&gt;&lt;sup&gt;zurück&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
      
    </item>
    
    <item>
      <title>Moin!</title>
      <link>https://dev.ebroda.de/posts/moin/</link>
      <pubDate>Wed, 20 Mar 2019 19:55:26 +0000</pubDate>
      <author>Eike Broda</author>
      <guid>https://dev.ebroda.de/posts/moin/</guid>
      <description></description>
      
      <content>&lt;p&gt;Ich bin Eike, Informatiker. Ich bastel&amp;rsquo; immer mal wieder kleine Sachen, die teils schon recht nerdig sind, und will der Welt das nun mitteilen.
Daher dieser Blog, der sehr spärlich gepflegt werden wird.&lt;/p&gt;

&lt;p&gt;Vermutlich wird es zumeist auch ums &lt;a href=&#34;https://sommerfest-vorstrasse.de&#34;&gt;Sommerfest&lt;/a&gt; gehen,
da bin ich nämlich Ressortleiter für #digitales und somit für die komplette digitale Infrastruktur für die Organisation und Bremens größte
Studentenparty verantwortlich. Digitale Infrastruktur klingt super fancy, tatsächlich ist es auch mehr als man so denkt. Für die Organisation
steht inzwischen ein großes Portfolio an unterschiedlichen Anwendungen zur Verfügung: ein internes Diskussionsforum, ein Slack-Workspace,
die Wenseite, Git-Repositories, E-Mails, das Bandbewerbungsportal tunefish, ein Tool zur Verwaltung der Schichten und noch ein paar kleine
Anwendungen machen dort die Organisation einfacher.&lt;/p&gt;

&lt;p&gt;Präferierte Programmiersprachen sind Python für Webgeschichten und Java für &amp;ldquo;andere&amp;rdquo; Sachen, aber je nach Projekt kann das durchaus auch mal
abweichen. So basiert die Sommerfest-Webseite auf Jekyll aus dem Ruby-Bereich und einige ältere Anwendungen laufen noch auf PHP-Basis.&lt;/p&gt;

&lt;p&gt;Dieser Blog basiert übrigens auf &lt;a href=&#34;https://gohugo.io&#34;&gt;Hugo&lt;/a&gt;, momentan mit einem super-simplen &lt;a href=&#34;https://github.com/ribice/kiss&#34;&gt;Kiss&lt;/a&gt;-Theme.
Ich wollte was, wo ich die Artikel in Markdown schreiben kann und was super-schnell ist. Da bot sich das dann an, auch wenn ich vorher
nie was damit gemacht habe. Es ist noch einfacher und schneller als Jekyll. Nice.&lt;/p&gt;
</content>
      
    </item>
    
  </channel>
</rss>