Für die komplette Crew des Sommerfests 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?
Bestellen per Slack?
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 “neuartige” 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.
Aber wie kann man das technisch besser lösen?
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 Niko hat dann aber mal ein bisschen in den Slack-Docs gewühlt und kam mit Dialogs “um die Ecke”. 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.
Der Plan war nun also: Durch einen Befehl den Prozess intiieren, einen Dialog anzeigen und den Nutzer seine Bestellung machen lassen.
Let’s go. Dialog implementieren
Klingt erstmal einfach, aber irgendwie muss man doch recht viel machen, damit es dann funktioniert. Aber ein bisschen Step-by-step. Los geht’s mit einer eigenen Slack-Anwendung. Die kann man sich einfach online klicken, 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.
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 “Send messages as Sommerfest-Tools (chat:write:bot)” 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.
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 … 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.
We’ll send an HTTP POST request with information you might need to this URL when the command is run.
Okay, also brauche ich irgendwo einen Service der einen POST-Request empfangen kann. Die Sourcen vom Orga-Tool hatte ich gerade nicht auf’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:
<?php
var_dump($_POST);
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 1. 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’ jeden Request einfach immer an die Datei post_requests.txt an.
<?php
file_put_contents('post_requests.txt', json_encode($_POST), FILE_APPEND);
Schaut man sich dann an, was Slack einem schickt, so sieht man, dass es die nachfolgenden Elemente sind.
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.0d06e430f7749f98742444xxx4xxx978Für die bessere Lesbarkeit hier als Key-Value-Pairs dargestellt2. 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 Slack-API.
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.
<?php
$trigger = $_POST['trigger_id'];
Um dem Nutzern nun unseren Dialog anzuzeigen, muss ein POST-Request auf den dialog.open-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 enthalten3. Die nachfolgende Funktion setzt diese Problematik um und basiert auf dieser Stackoverflow-Antwort.
function send_to_slack($uri, $data) {
$slack_token = "xoxp-1234567890..."
$options = array(
'http' => array(
'header' => "Content-type: application/json; charset=utf-8\r\nAuthorization: Bearer " . $slack_token,
'method' => 'POST',
'content' => json_encode($data)
)
);
$context = stream_context_create($options);
return file_get_contents($uri, false, $context);
}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.
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.
Dialog designen
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 _callbackid bekommen wir nach dem Absenden von Slack wieder zurück, um den Dialog zu identifizieren. Das _notify_oncancel-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 elements definiert.
{
"callback_id": "staffshirt",
"title": "Staff-Shirt bestellen",
"submit_label": "Bestellen",
"notify_on_cancel": true,
"elements": []
}Dafür kennt Slack 3 verschiedene Feldtypen4, 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.
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.
{
"label": "Größe",
"type": "select",
"name": "shirt_size",
"options": [
{
"label": "S",
"value": "s"
},
{
"label": "M",
"value": "m"
},
{
"label": "L",
"value": "l"
},
{
"label": "XL",
"value": "xl"
}
]
}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.
<?php
$trigger = $_POST['trigger_id'];
function send_to_slack($uri, $data) {
// ... (s.o.)
}
$data = array(
'trigger_id' => $trigger,
'dialog' => array(
'callback_id' => 'staffshirt',
'title' => 'Staff-Shirt bestellen',
'submit_label' => 'Bestellen',
'notify_on_cancel' => true,
'elements' => array(
array(
'label' => 'Größe',
'type' => 'select',
'name' => 'shirt_size',
'options' => array(
array('label' => 'S', 'value' => 's'),
array('label' => 'M', 'value' => 'm'),
array('label' => 'L', 'value' => 'l'),
array('label' => 'XL', 'value' => 'xl')
)
)
)
)
);
send_to_slack('https://slack.com/api/dialog.open', $data);
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 Interactive Components 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.
Ergebnis
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:
Der Dialog zur Auswahl der T-Shirtgröße
Auswahl einer T-Shirtgröße
Ausblick
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.
Die Implementierung ist schon fertig, ich hatte nur noch keine Zeit, dazu auch einen entsprechenden Post zu schreiben.
- Tatsächlich zeigt Slack einem sogar doch die Rückgabe des POST-Requests als “nur für dich sichtbare”-Nachricht an. zurück
- Und ein paar der Variablen sind verändert/obfuscated. zurück
- Detailliert beschrieben werden die Anforderungen in der Slack-API-Dokumentation. Dieser spezielle Punkt hier. zurück
- Details zu den einzelnen Typen finden sich in der Slack-Doku. zurück