Go-Schnellstart

Ein Go-Programm, das ein Token erneuert, deine Bildschirme auflistet und eine Playlist auf einen Bildschirm legt. Die Blöcke auf dieser Seite ergeben zusammen eine Datei.

1 Hol dir ein Token

Öffne das CMS unter Einstellungen, Verbundene Apps und klick auf Token generieren. Ein Mitglied ohne Administratorrechte sieht diesen Tab nicht.

Wähl danach die Organisation, die festlegt, welche Bildschirme das Programm sieht, und klick auf Zulassen. Die nächste Seite zeigt die Client-ID und das Refresh-Token, nur einmal. Kopiere beides.

Setz im Terminal die Client-ID und den Pfad der Token-Datei, und schreib das Refresh-Token in diese Datei. read -rs wartet, ohne etwas anzuzeigen: Füge das Refresh-Token ein und drück Enter, so bleibt es aus der Shell-History:

export GIBEON_CLIENT_ID=CLIENT_ID
export GIBEON_REFRESH_TOKEN_FILE=./gibeon-refresh-token
touch "$GIBEON_REFRESH_TOKEN_FILE"
chmod 600 "$GIBEON_REFRESH_TOKEN_FILE"
read -rs GIBEON_REFRESH_TOKEN && printf '%s' "$GIBEON_REFRESH_TOKEN" > "$GIBEON_REFRESH_TOKEN_FILE"

Das Programm braucht Go 1.18 oder neuer. Es nutzt nur die Standardbibliothek, du musst also nichts installieren. Leg die Blöcke unten der Reihe nach in main.go ab und starte go run main.go.

2 Erneuere das Token

Ein Access-Token gilt 60 Minuten. Jede Erneuerung liefert auch ein neues Refresh-Token, ein Wert in der Umgebung ist also nach einem Lauf veraltet.

Das Programm schreibt das neue Token in eine temporäre Datei im selben Verzeichnis, setzt chmod 600 und benennt sie über die Datei in GIBEON_REFRESH_TOKEN_FILE um. Erst danach nutzt es das Access-Token. Ein Prozess, der dabei abbricht, behält so ein gültiges Refresh-Token.

Dieser Block enthält auch die Imports und die Hilfsfunktionen. Bei jedem Status außerhalb von 2xx beendet send das Programm mit Exit-Code 1.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"
)

var client = &http.Client{Timeout: 30 * time.Second}

type player struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Status string `json:"status"`
}

// exitOn stops the program with a non-zero exit code.
func exitOn(err error) {
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

// send runs the request and stops the program on any non-2xx status.
func send(req *http.Request) []byte {
	response, err := client.Do(req)
	exitOn(err)
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	exitOn(err)
	if response.StatusCode < 200 || response.StatusCode > 299 {
		exitOn(fmt.Errorf("%s %s: %s", req.Method, req.URL, response.Status))
	}
	return body
}

func refresh() string {
	tokenFile := os.Getenv("GIBEON_REFRESH_TOKEN_FILE")
	refreshToken, err := os.ReadFile(tokenFile)
	exitOn(err)

	form := url.Values{
		"grant_type":    {"refresh_token"},
		"client_id":     {os.Getenv("GIBEON_CLIENT_ID")},
		"refresh_token": {strings.TrimSpace(string(refreshToken))},
	}
	req, err := http.NewRequest("POST", "https://id.gibeon.io/auth/v1/oauth/token", strings.NewReader(form.Encode()))
	exitOn(err)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	var tokens struct {
		AccessToken  string `json:"access_token"`
		RefreshToken string `json:"refresh_token"`
	}
	exitOn(json.Unmarshal(send(req), &tokens))
	if tokens.RefreshToken == "" {
		exitOn(fmt.Errorf("the response holds no refresh token"))
	}

	// Store the new refresh token before anything uses the access token.
	tmp, err := os.CreateTemp(filepath.Dir(tokenFile), ".gibeon-refresh-token-*")
	exitOn(err)
	_, err = tmp.WriteString(tokens.RefreshToken)
	exitOn(err)
	exitOn(tmp.Close())
	exitOn(os.Chmod(tmp.Name(), 0o600))
	exitOn(os.Rename(tmp.Name(), tokenFile))

	return tokens.AccessToken
}

3 Liste deine Bildschirme auf

Das liefert alle Bildschirme in deinem Tenant. Das Programm gibt von jedem Bildschirm id, name und status aus, und nie ein Token.

func listScreens(accessToken string) {
	req, err := http.NewRequest("GET", "https://www.gibeon.io/v1/players", nil)
	exitOn(err)
	req.Header.Set("Authorization", "Bearer "+accessToken)

	var result struct {
		Data []player `json:"data"`
	}
	exitOn(json.Unmarshal(send(req), &result))

	for _, p := range result.Data {
		fmt.Println(p.ID, p.Name, p.Status)
	}
}

4 Weise eine Playlist zu und veröffentliche

Dieser Schritt läuft nur, wenn GIBEON_PLAYER_ID und GIBEON_PLAYLIST_ID gesetzt sind. Die ID des Bildschirms steht in der Liste aus dem vorigen Schritt. Die ID der Playlist steht in der Adressleiste des CMS, wenn du eine Playlist öffnest: /playlists/PLAYLIST_ID/content. Exportiere beide und starte das Programm noch einmal.

Ein Bildschirm mit Zonen spielt diese; sonst seine Sequenz; sonst seine Planung; sonst seine eigene Playlist. Der Body leert die Sequenz und die Planung, also spielt die Playlist, außer der Bildschirm hat Zonen.

Der PATCH allein ändert nichts auf dem Bildschirm: Bis du veröffentlichst, zeigt er weiter die alte Zuweisung. Das Veröffentlichen baut einen neuen Snapshot und benachrichtigt den Bildschirm sofort.

Die letzte Funktion, main, ruft die drei der Reihe nach auf.

func assignAndPublish(accessToken string) {
	playerID := os.Getenv("GIBEON_PLAYER_ID")
	playlistID := os.Getenv("GIBEON_PLAYLIST_ID")
	if playerID == "" || playlistID == "" {
		return
	}

	body, err := json.Marshal(map[string]any{
		"playlist_id": playlistID,
		"sequence_id": nil,
		"planning_id": nil,
	})
	exitOn(err)
	req, err := http.NewRequest("PATCH", fmt.Sprintf("https://www.gibeon.io/v1/players/%s", playerID), bytes.NewReader(body))
	exitOn(err)
	req.Header.Set("Authorization", "Bearer "+accessToken)
	req.Header.Set("Content-Type", "application/json")

	var result struct {
		Data player `json:"data"`
	}
	exitOn(json.Unmarshal(send(req), &result))

	req, err = http.NewRequest("POST", fmt.Sprintf("https://www.gibeon.io/v1/players/%s/publish", playerID), nil)
	exitOn(err)
	req.Header.Set("Authorization", "Bearer "+accessToken)
	send(req)

	fmt.Println(result.Data.ID, result.Data.Name, result.Data.Status)
}

func main() {
	accessToken := refresh()
	listScreens(accessToken)
	assignAndPublish(accessToken)
}

Wie geht’s weiter