Démarrage rapide en Go

Un programme Go qui rafraîchit un token, liste vos écrans et place une playlist sur un écran. Les blocs de cette page forment un seul fichier.

1 Obtenez un token

Ouvrez le CMS dans Paramètres, Applications connectées et cliquez sur Générer un token. Un membre sans droits d’administrateur ne voit pas cet onglet.

Choisissez ensuite l’organisation, qui détermine les écrans que le programme voit, et cliquez sur Autoriser. La page suivante affiche l’identifiant client et le token de rafraîchissement, une seule fois. Copiez les deux.

Dans votre terminal, définissez l’identifiant client et le chemin du fichier de token, puis écrivez le token de rafraîchissement dans ce fichier. read -rs attend sans rien afficher : collez le token de rafraîchissement et appuyez sur Entrée, il reste ainsi hors de l’historique du shell :

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"

Le programme nécessite Go 1.18 ou plus récent. Il n’utilise que la bibliothèque standard : il n’y a donc rien à installer. Placez les blocs ci-dessous dans l’ordre dans main.go et lancez go run main.go.

2 Rafraîchissez le token

Un token d’accès est valable 60 minutes. Chaque rafraîchissement renvoie aussi un nouveau token de rafraîchissement : une valeur dans l’environnement est donc périmée après une exécution.

Le programme écrit le nouveau token dans un fichier temporaire du même répertoire, applique chmod 600 et le renomme par-dessus le fichier de GIBEON_REFRESH_TOKEN_FILE. Il n’utilise le token d’accès qu’ensuite. Un processus qui s’arrête pendant cette utilisation conserve ainsi un token de rafraîchissement valide.

Ce bloc contient aussi les imports et les fonctions utilitaires. Pour tout statut hors 2xx, send arrête le programme avec le code de sortie 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 Listez vos écrans

Cet appel renvoie tous les écrans de votre tenant. Le programme affiche l’id, le name et le status de chaque écran, et jamais un 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 Assignez une playlist et publiez

Cette étape ne s’exécute que si GIBEON_PLAYER_ID et GIBEON_PLAYLIST_ID sont définis. L’id de l’écran figure dans la liste de l’étape précédente. L’id de la playlist figure dans la barre d’adresse du CMS quand vous ouvrez une playlist : /playlists/PLAYLIST_ID/content. Exportez les deux et relancez le programme.

Un écran avec des zones joue celles-ci ; sinon sa séquence ; sinon son planning ; sinon sa propre playlist. Le corps de la requête vide la séquence et le planning : la playlist joue donc, sauf si l’écran a des zones.

Le PATCH seul ne change rien à l’écran : tant que vous ne publiez pas, il continue d’afficher l’ancienne affectation. La publication construit un nouveau snapshot et prévient l’écran immédiatement.

La dernière fonction, main, appelle les trois dans l’ordre.

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)
}

Et ensuite