Tutoriel Slack – Commandes Slash

Ce tutoriel décrit l'utilisation de Cloud Functions pour mettre en œuvre une commande Slash Slack qui effectue une recherche dans l'API Google Knowledge Graph.

Objectifs

  • Créer une commande Slash dans Slack
  • Rédiger et déployer une fonction cloud HTTP
  • Effectuer une recherche dans l'API Google Knowledge Graph à l'aide de la commande Slash

Coûts

Ce tutoriel fait appel à des composants facturables de Cloud Platform, en particulier :

  • Google Cloud Functions

Utilisez le simulateur de coût pour générer une estimation des coûts en fonction de votre utilisation prévue.

Avant de commencer

  1. Connectez-vous à votre compte Google.

    Si vous n'en possédez pas déjà un, vous devez en créer un.

  2. Dans Google Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Google Cloud.

    Accéder à la page de sélection du projet

  3. Assurez-vous que la facturation est activée pour votre projet Cloud. Découvrez comment vérifier que la facturation est activée pour votre projet.

  4. Activer les API Cloud Functions, Cloud Build, and Google Knowledge Graph Search.

    Activer les API

  5. Mettez à jour les composants gcloud :
    gcloud components update
  6. Préparez votre environnement de développement.

Visualiser le flux de données

Le flux de données dans l'application du tutoriel sur la commande Slack Slash comporte plusieurs étapes :

  1. L'utilisateur exécute la commande Slash /kg <search_query> dans une chaîne Slack.
  2. Slack envoie la commande payload au point de terminaison déclencheur de la fonction cloud.
  3. La fonction Cloud envoie une requête avec la requête de recherche de l'utilisateur à l'API Knowledge Graph.
  4. L'API Knowledge Graph répond avec les résultats correspondants.
  5. La fonction Cloud met en forme la réponse dans un message Slack.
  6. La fonction cloud renvoie le message à Slack.
  7. L'utilisateur visualise la réponse mise en forme dans la chaîne Slack.

Observez le schéma ci-dessous pour visualiser les étapes :

Créer la clé API Knowledge Graph

Sur la page Identifiants de Google Cloud Console, cliquez sur le bouton Créer des identifiants, puis sélectionnez Clé API. Notez cette clé, car vous l'utiliserez pour accéder à l'API Knowledge Graph dans la section suivante.

Préparer la fonction

  1. Clonez le dépôt de l'exemple d'application sur votre ordinateur local :

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

    C#

    git clone https://github.com/GoogleCloudPlatform/dotnet-docs-samples.git

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

  2. Accédez au répertoire qui contient l'exemple de code de Cloud Functions :

    Node.js

    cd nodejs-docs-samples/functions/slack/

    Python

    cd python-docs-samples/functions/slack/

    Go

    cd golang-samples/functions/slack/

    Java

    cd java-docs-samples/functions/slack/

    C#

    cd dotnet-docs-samples/functions/slack/

Déployer la fonction

Pour déployer la fonction qui est exécutée lorsque vous (ou Slack) effectuez une requête HTTP POST sur le point de terminaison de la fonction, exécutez la commande suivante dans le répertoire contenant l'exemple de code (ou, dans le cas de Java, le fichier pom.xml) :

Remplacez YOUR_SLACK_SIGNING_SECRET par le secret de signature fourni par Slack sur la page Informations générales de la configuration de votre application, et YOUR_KG_API_KEY par la clé API Knowledge Graph créée précédemment.

Node.js

gcloud functions deploy kgSearch \
--runtime nodejs10 \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated
Vous pouvez attribuer les valeurs suivantes à l'option --runtime, afin de spécifier votre version préférée de Node.js :
  • nodejs10
  • nodejs12

Python

gcloud functions deploy kg_search \
--runtime python37 \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated
Vous pouvez attribuer les valeurs suivantes à l'option --runtime, afin de spécifier votre version préférée de Python :
  • python37
  • python38

Go

gcloud functions deploy KGSearch \
--runtime go111 \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated
Vous pouvez attribuer les valeurs suivantes à l'option --runtime, afin de spécifier votre version préférée de Go :
  • go111
  • go113

Java

gcloud functions deploy java-slack-function \
--entry-point functions.SlackSlashCommand \
--runtime java11 \
--memory 512MB \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated

C#

gcloud functions deploy csharp-slack-function \
--entry-point SlackKnowledgeGraphSearch.Function \
--runtime dotnet3 \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated

Configurer l'application

Une fois la fonction déployée, vous devez créer une commande Slack Slash qui envoie la requête à votre fonction cloud à chaque déclenchement de la commande :

  1. Créez une application Slack pour héberger votre commande Slack Slash. Associez-la à une équipe Slack dans laquelle vous disposez des autorisations pour installer des intégrations.

  2. Accédez aux commandes Slash et cliquez sur le bouton Create new command (Créer une commande).

  3. Saisissez /kg comme nom de la commande.

  4. Entrez l'URL de la commande :

    Node.js

    https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/kgSearch

    Python

    https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/kg_search

    Go

    https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/KGSearch

    Java

    https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/java-slack-function

    C#

    https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/csharp-slack-function

    YOUR_REGION est la région dans laquelle votre fonction Cloud est déployée et YOUR_PROJECT_ID est votre ID de projet Cloud.

    Les deux valeurs sont visibles dans votre terminal lorsque le déploiement de votre fonction est terminé.

  5. Cliquez sur Enregistrer.

  6. Allez à Informations de base.

  7. Cliquez sur Install your app to your workspace (Installer votre application dans votre espace de travail) et suivez les instructions à l'écran pour activer l'application pour votre espace de travail.

    Votre commande Slash Slack devrait être bientôt disponible.

Comprendre le code

Importer des dépendances

L'application doit importer plusieurs dépendances afin de pouvoir communiquer avec les services Google Cloud Platform :

Node.js

const {google} = require('googleapis');
const {verifyRequestSignature} = require('@slack/events-api');

// Get a reference to the Knowledge Graph Search component
const kgsearch = google.kgsearch('v1');

Python

import os

from flask import jsonify
import googleapiclient.discovery
from slack.signature import SignatureVerifier

kgsearch = googleapiclient.discovery.build(
    'kgsearch',
    'v1',
    developerKey=os.environ['KG_API_KEY'],
    cache_discovery=False)

Go


package slack

import (
	"context"
	"log"
	"os"

	"google.golang.org/api/kgsearch/v1"
	"google.golang.org/api/option"
)

var (
	entitiesService *kgsearch.EntitiesService
	kgKey           string
	slackSecret     string
)

func setup(ctx context.Context) {
	kgKey = os.Getenv("KG_API_KEY")
	slackSecret = os.Getenv("SLACK_SECRET")

	if entitiesService == nil {
		kgService, err := kgsearch.NewService(ctx, option.WithAPIKey(kgKey))
		if err != nil {
			log.Fatalf("kgsearch.NewClient: %v", err)
		}
		entitiesService = kgsearch.NewEntitiesService(kgService)
	}
}

Java

private static final Logger logger = Logger.getLogger(SlackSlashCommand.class.getName());
private static final String API_KEY = getenv("KG_API_KEY");
private static final String SLACK_SECRET = getenv("SLACK_SECRET");
private static final Gson gson = new Gson();

private final String apiKey;
private final Kgsearch kgClient;
private final SlackSignature.Verifier verifier;

public SlackSlashCommand() throws IOException, GeneralSecurityException {
  this(new SlackSignature.Verifier(new SlackSignature.Generator(SLACK_SECRET)));
}

SlackSlashCommand(SlackSignature.Verifier verifier) throws IOException, GeneralSecurityException {
  this(verifier, API_KEY);
}

SlackSlashCommand(SlackSignature.Verifier verifier, String apiKey)
    throws IOException, GeneralSecurityException {
  this.verifier = verifier;
  this.apiKey = apiKey;
  this.kgClient = new Kgsearch.Builder(
      GoogleNetHttpTransport.newTrustedTransport(), new JacksonFactory(), null).build();
}

// Avoid ungraceful deployment failures due to unset environment variables.
// If you get this warning you should redeploy with the variable set.
private static String getenv(String name) {
  String value = System.getenv(name);
  if (value == null) {
    logger.warning("Environment variable " + name + " was not set");
    value = "MISSING";
  }
  return value;
}

C#

using Google.Apis.Kgsearch.v1;
using Google.Apis.Services;
using Google.Cloud.Functions.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;

// Specify a startup class to use for dependency injection.
// This can also be specified on the function type.
[assembly: FunctionsStartup(typeof(SlackKnowledgeGraphSearch.Startup))]

namespace SlackKnowledgeGraphSearch
{
    public class Startup : FunctionsStartup
    {
        private static readonly TimeSpan SlackTimestampTolerance = TimeSpan.FromMinutes(5);

        public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services)
        {
            // These can come from an environment variable or a configuration file.
            string kgApiKey = context.Configuration["KG_API_KEY"];
            string slackSigningSecret = context.Configuration["SLACK_SECRET"];

            services.AddSingleton(new KgsearchService(new BaseClientService.Initializer
            {
                ApiKey = kgApiKey
            }));
            services.AddSingleton(new SlackRequestVerifier(slackSigningSecret, SlackTimestampTolerance));
            base.ConfigureServices(context, services);
        }
    }
}

Recevoir le webhook

La fonction suivante est exécutée lorsque vous (ou Slack) effectuez une requête HTTP POST sur le point de terminaison de la fonction :

Node.js

/**
 * Receive a Slash Command request from Slack.
 *
 * Trigger this function by creating a Slack slash command with this URL:
 * https://[YOUR_REGION]-[YOUR_PROJECT_ID].cloudfunctions.net/kgSearch
 *
 * @param {object} req Cloud Function request object.
 * @param {object} req.body The request payload.
 * @param {string} req.rawBody Raw request payload used to validate Slack's message signature.
 * @param {string} req.body.text The user's search query.
 * @param {object} res Cloud Function response object.
 */
exports.kgSearch = async (req, res) => {
  try {
    if (req.method !== 'POST') {
      const error = new Error('Only POST requests are accepted');
      error.code = 405;
      throw error;
    }

    // Verify that this request came from Slack
    verifyWebhook(req);

    // Make the request to the Knowledge Graph Search API
    const response = await makeSearchRequest(req.body.text);

    // Send the formatted message back to Slack
    res.json(response);

    return Promise.resolve();
  } catch (err) {
    console.error(err);
    res.status(err.code || 500).send(err);
    return Promise.reject(err);
  }
};

Python

def kg_search(request):
    if request.method != 'POST':
        return 'Only POST requests are accepted', 405

    verify_signature(request)
    kg_search_response = make_search_request(request.form['text'])
    return jsonify(kg_search_response)

Go


// Package slack is a Cloud Function which recieves a query from
// a Slack command and responds with the KG API result.
package slack

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"
)

type oldTimeStampError struct {
	s string
}

func (e *oldTimeStampError) Error() string {
	return e.s
}

const (
	version                     = "v0"
	slackRequestTimestampHeader = "X-Slack-Request-Timestamp"
	slackSignatureHeader        = "X-Slack-Signature"
)

type attachment struct {
	Color     string `json:"color"`
	Title     string `json:"title"`
	TitleLink string `json:"title_link"`
	Text      string `json:"text"`
	ImageURL  string `json:"image_url"`
}

// Message is the a Slack message event.
// see https://api.slack.com/docs/message-formatting
type Message struct {
	ResponseType string       `json:"response_type"`
	Text         string       `json:"text"`
	Attachments  []attachment `json:"attachments"`
}

// KGSearch uses the Knowledge Graph API to search for a query provided
// by a Slack command.
func KGSearch(w http.ResponseWriter, r *http.Request) {
	setup(r.Context())

	bodyBytes, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Fatalf("Couldn't read request body: %v", err)
	}
	r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

	if r.Method != "POST" {
		http.Error(w, "Only POST requests are accepted", 405)
	}
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Couldn't parse form", 400)
		log.Fatalf("ParseForm: %v", err)
	}

	// Reset r.Body as ParseForm depletes it by reading the io.ReadCloser.
	r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
	result, err := verifyWebHook(r, slackSecret)
	if err != nil {
		log.Fatalf("verifyWebhook: %v", err)
	}
	if !result {
		log.Fatalf("signatures did not match.")
	}

	if len(r.Form["text"]) == 0 {
		log.Fatalf("empty text in form")
	}
	kgSearchResponse, err := makeSearchRequest(r.Form["text"][0])
	if err != nil {
		log.Fatalf("makeSearchRequest: %v", err)
	}
	w.Header().Set("Content-Type", "application/json")
	if err = json.NewEncoder(w).Encode(kgSearchResponse); err != nil {
		log.Fatalf("json.Marshal: %v", err)
	}
}

Java

/**
 * Receive a Slash Command request from Slack.
 *
 * @param request Cloud Function request object.
 * @param response Cloud Function response object.
 * @throws IOException if Knowledge Graph request fails
 */
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {

  // Validate request
  if (!"POST".equals(request.getMethod())) {
    response.setStatusCode(HttpURLConnection.HTTP_BAD_METHOD);
    return;
  }

  // reader can only be read once per request, so we preserve its contents
  String bodyString = request.getReader().lines().collect(Collectors.joining());

  // Slack sends requests as URL-encoded strings
  //   Java 11 doesn't have a standard library
  //   function for this, so do it manually
  Map<String, String> body = new HashMap<>();
  for (String keyValuePair : bodyString.split("&")) {
    String[] keyAndValue = keyValuePair.split("=");
    if (keyAndValue.length == 2) {
      String key = keyAndValue[0];
      String value = keyAndValue[1];

      body.put(key, value);
    }
  }

  if (body == null || !body.containsKey("text")) {
    response.setStatusCode(HttpURLConnection.HTTP_BAD_REQUEST);
    return;
  }

  if (!isValidSlackWebhook(request, bodyString)) {
    response.setStatusCode(HttpURLConnection.HTTP_UNAUTHORIZED);
    return;
  }

  String query = body.get("text");

  // Call knowledge graph API
  JsonObject kgResponse = searchKnowledgeGraph(query);

  // Format response to Slack
  // See https://api.slack.com/docs/message-formatting
  BufferedWriter writer = response.getWriter();

  writer.write(formatSlackMessage(kgResponse, query));

  response.setContentType("application/json");
}

C#

private readonly ILogger _logger;
private readonly KgsearchService _kgService;
private readonly SlackRequestVerifier _verifier;

public Function(ILogger<Function> logger, KgsearchService kgService, SlackRequestVerifier verifier) =>
    (_logger, _kgService, _verifier) = (logger, kgService, verifier);

public async Task HandleAsync(HttpContext context)
{
    var request = context.Request;
    var response = context.Response;

    // Validate request
    if (request.Method != "POST")
    {
        _logger.LogWarning("Unexpected request method '{method}'", request.Method);
        response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
        return;
    }

    if (!request.HasFormContentType)
    {
        _logger.LogWarning("Unexpected content type '{contentType}'", request.ContentType);
        response.StatusCode = (int) HttpStatusCode.BadRequest;
        return;
    }

    // We need to read the request body twice: once to validate the signature,
    // and once to read the form content. We copy it into a memory stream,
    // so that we can rewind it after reading.
    var bodyCopy = new MemoryStream();
    await request.Body.CopyToAsync(bodyCopy);
    request.Body = bodyCopy;
    bodyCopy.Position = 0;

    if (!_verifier.VerifyRequest(request, bodyCopy.ToArray()))
    {
        _logger.LogWarning("Slack request verification failed");
        response.StatusCode = (int) HttpStatusCode.Unauthorized;
        return;
    }

    var form = await request.ReadFormAsync();
    if (!form.TryGetValue("text", out var query))
    {
        _logger.LogWarning("Slack request form did not contain a text element");
        response.StatusCode = (int) HttpStatusCode.BadRequest;
        return;
    }

    var kgResponse = await SearchKnowledgeGraphAsync(query);
    string formattedResponse = FormatSlackMessage(kgResponse, query);
    response.ContentType = "application/json";
    await response.WriteAsync(formattedResponse);
}

La fonction suivante authentifie la requête entrante en vérifiant l'en-tête X-Slack-Signature fourni par Slack :

Node.js

/**
 * Verify that the webhook request came from Slack.
 *
 * @param {object} req Cloud Function request object.
 * @param {string} req.headers Headers Slack SDK uses to authenticate request.
 * @param {string} req.rawBody Raw body of webhook request to check signature against.
 */
const verifyWebhook = req => {
  const signature = {
    signingSecret: process.env.SLACK_SECRET,
    requestSignature: req.headers['x-slack-signature'],
    requestTimestamp: req.headers['x-slack-request-timestamp'],
    body: req.rawBody,
  };

  if (!verifyRequestSignature(signature)) {
    const error = new Error('Invalid credentials');
    error.code = 401;
    throw error;
  }
};

Python

def verify_signature(request):
    request.get_data()  # Decodes received requests into request.data

    verifier = SignatureVerifier(os.environ['SLACK_SECRET'])

    if not verifier.is_valid_request(request.data, request.headers):
        raise ValueError('Invalid request/credentials.')

Go


// verifyWebHook verifies the request signature.
// See https://api.slack.com/docs/verifying-requests-from-slack.
func verifyWebHook(r *http.Request, slackSigningSecret string) (bool, error) {
	timeStamp := r.Header.Get(slackRequestTimestampHeader)
	slackSignature := r.Header.Get(slackSignatureHeader)

	t, err := strconv.ParseInt(timeStamp, 10, 64)
	if err != nil {
		return false, fmt.Errorf("strconv.ParseInt(%s): %v", timeStamp, err)
	}

	if ageOk, age := checkTimestamp(t); !ageOk {
		return false, &oldTimeStampError{fmt.Sprintf("checkTimestamp(%v): %v %v", t, ageOk, age)}
		// return false, fmt.Errorf("checkTimestamp(%v): %v %v", t, ageOk, age)
	}

	if timeStamp == "" || slackSignature == "" {
		return false, fmt.Errorf("either timeStamp or signature headers were blank")
	}

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return false, fmt.Errorf("ioutil.ReadAll(%v): %v", r.Body, err)
	}

	// Reset the body so other calls won't fail.
	r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

	baseString := fmt.Sprintf("%s:%s:%s", version, timeStamp, body)

	signature := getSignature([]byte(baseString), []byte(slackSigningSecret))

	trimmed := strings.TrimPrefix(slackSignature, fmt.Sprintf("%s=", version))
	signatureInHeader, err := hex.DecodeString(trimmed)

	if err != nil {
		return false, fmt.Errorf("hex.DecodeString(%v): %v", trimmed, err)
	}

	return hmac.Equal(signature, signatureInHeader), nil
}

func getSignature(base []byte, secret []byte) []byte {
	h := hmac.New(sha256.New, secret)
	h.Write(base)

	return h.Sum(nil)
}

// Arbitrarily trusting requests time stamped less than 5 minutes ago.
func checkTimestamp(timeStamp int64) (bool, time.Duration) {
	t := time.Since(time.Unix(timeStamp, 0))

	return t.Minutes() <= 5, t
}

Java

/**
 * Verify that the webhook request came from Slack.
 *
 * @param request Cloud Function request object in {@link HttpRequest} format.
 * @param requestBody Raw body of webhook request to check signature against.
 * @return true if the provided request came from Slack, false otherwise
 */
boolean isValidSlackWebhook(HttpRequest request, String requestBody) {
  // Check for headers
  Optional<String> maybeTimestamp = request.getFirstHeader("X-Slack-Request-Timestamp");
  Optional<String> maybeSignature = request.getFirstHeader("X-Slack-Signature");
  if (!maybeTimestamp.isPresent() || !maybeSignature.isPresent()) {
    return false;
  }

  Long nowInMs = ZonedDateTime.now().toInstant().toEpochMilli();

  return verifier.isValid(maybeTimestamp.get(), requestBody, maybeSignature.get(), nowInMs);
}

C#

using Microsoft.AspNetCore.Http;
using System;
using System.Security.Cryptography;
using System.Text;

namespace SlackKnowledgeGraphSearch
{
    public class SlackRequestVerifier
    {
        private const string Version = "v0";
        private const string SignaturePrefix = Version + "=";
        private static readonly byte[] VersionBytes = Encoding.UTF8.GetBytes(Version);
        private static readonly byte[] ColonBytes = Encoding.UTF8.GetBytes(":");

        private readonly byte[] _signingKeyBytes;
        private readonly TimeSpan _timestampTolerance;

        public SlackRequestVerifier(string signingKey, TimeSpan timestampTolerance) =>
            (_signingKeyBytes, _timestampTolerance) =
            (Encoding.UTF8.GetBytes(signingKey), timestampTolerance);

        /// <summary>
        /// Verifies a request signature with the given content, which would
        /// normally have been read from the request beforehand.
        /// </summary>
        public bool VerifyRequest(HttpRequest request, byte[] content)
        {
            if (!request.Headers.TryGetValue("X-Slack-Request-Timestamp", out var timestampHeader) ||
                !request.Headers.TryGetValue("X-Slack-Signature", out var signatureHeader))
            {
                return false;
            }

            // Validate that the request isn't too old.
            if (!int.TryParse(timestampHeader, out var unixSeconds))
            {
                return false;
            }
            var timestamp = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
            if (timestamp + _timestampTolerance < DateTimeOffset.UtcNow)
            {
                return false;
            }

            // Hash the version:timestamp:content
            using var hmac = new HMACSHA256(_signingKeyBytes);
            AddToHash(VersionBytes);
            AddToHash(ColonBytes);
            AddToHash(Encoding.UTF8.GetBytes(timestampHeader));
            AddToHash(ColonBytes);
            byte[] hash = hmac.ComputeHash(content);
            void AddToHash(byte[] bytes) => hmac.TransformBlock(bytes, 0, bytes.Length, null, 0);

            // Compare the resulting signature with the signature in the request.
            return CreateSignature(hash) == (string) signatureHeader;
        }

        private const string Hex = "0123456789abcdef";
        private static string CreateSignature(byte[] hash)
        {
            var builder = new StringBuilder(SignaturePrefix, SignaturePrefix.Length + hash.Length * 2);
            foreach (var b in hash)
            {
                builder.Append(Hex[b >> 4]);
                builder.Append(Hex[b & 0xf]);
            }
            return builder.ToString();
        }
    }
}

Interroger l'API Knowledge Graph

La fonction cloud envoie une demande avec la requête de recherche de l'utilisateur à l'API Knowledge Graph.

Node.js

/**
 * Send the user's search query to the Knowledge Graph API.
 *
 * @param {string} query The user's search query.
 */
const makeSearchRequest = query => {
  return new Promise((resolve, reject) => {
    kgsearch.entities.search(
      {
        auth: process.env.KG_API_KEY,
        query: query,
        limit: 1,
      },
      (err, response) => {
        console.log(err);
        if (err) {
          reject(err);
          return;
        }

        // Return a formatted message
        resolve(formatSlackMessage(query, response));
      }
    );
  });
};

Python

def make_search_request(query):
    req = kgsearch.entities().search(query=query, limit=1)
    res = req.execute()
    return format_slack_message(query, res)

Go

func makeSearchRequest(query string) (*Message, error) {
	res, err := entitiesService.Search().Query(query).Limit(1).Do()
	if err != nil {
		return nil, fmt.Errorf("Do: %v", err)
	}
	return formatSlackMessage(query, res)
}

Java

/**
 * Send the user's search query to the Knowledge Graph API.
 *
 * @param query The user's search query.
 * @return The Knowledge graph API results as a {@link JsonObject}.
 * @throws IOException if Knowledge Graph request fails
 */
JsonObject searchKnowledgeGraph(String query) throws IOException {
  Kgsearch.Entities.Search kgRequest = kgClient.entities().search();
  kgRequest.setQuery(query);
  kgRequest.setKey(apiKey);

  return gson.fromJson(kgRequest.execute().toString(), JsonObject.class);
}

C#

private async Task<SearchResponse> SearchKnowledgeGraphAsync(string query)
{
    _logger.LogInformation("Performing Knowledge Graph search for '{query}'", query);
    var request = _kgService.Entities.Search();
    request.Limit = 1;
    request.Query = query;
    return await request.ExecuteAsync();
}

Mettre en forme le message Slack

Enfin, la fonction suivante met en forme le résultat Knowledge Graph en message Slack enrichi qui sera visible par l'utilisateur :

Node.js

/**
 * Format the Knowledge Graph API response into a richly formatted Slack message.
 *
 * @param {string} query The user's search query.
 * @param {object} response The response from the Knowledge Graph API.
 * @returns {object} The formatted message.
 */
const formatSlackMessage = (query, response) => {
  let entity;

  // Extract the first entity from the result list, if any
  if (
    response &&
    response.data &&
    response.data.itemListElement &&
    response.data.itemListElement.length > 0
  ) {
    entity = response.data.itemListElement[0].result;
  }

  // Prepare a rich Slack message
  // See https://api.slack.com/docs/message-formatting
  const slackMessage = {
    response_type: 'in_channel',
    text: `Query: ${query}`,
    attachments: [],
  };

  if (entity) {
    const attachment = {
      color: '#3367d6',
    };
    if (entity.name) {
      attachment.title = entity.name;
      if (entity.description) {
        attachment.title = `${attachment.title}: ${entity.description}`;
      }
    }
    if (entity.detailedDescription) {
      if (entity.detailedDescription.url) {
        attachment.title_link = entity.detailedDescription.url;
      }
      if (entity.detailedDescription.articleBody) {
        attachment.text = entity.detailedDescription.articleBody;
      }
    }
    if (entity.image && entity.image.contentUrl) {
      attachment.image_url = entity.image.contentUrl;
    }
    slackMessage.attachments.push(attachment);
  } else {
    slackMessage.attachments.push({
      text: 'No results match your query...',
    });
  }

  return slackMessage;
};

Python

def format_slack_message(query, response):
    entity = None
    if response and response.get('itemListElement') is not None and \
       len(response['itemListElement']) > 0:
        entity = response['itemListElement'][0]['result']

    message = {
        'response_type': 'in_channel',
        'text': 'Query: {}'.format(query),
        'attachments': []
    }

    attachment = {}
    if entity:
        name = entity.get('name', '')
        description = entity.get('description', '')
        detailed_desc = entity.get('detailedDescription', {})
        url = detailed_desc.get('url')
        article = detailed_desc.get('articleBody')
        image_url = entity.get('image', {}).get('contentUrl')

        attachment['color'] = '#3367d6'
        if name and description:
            attachment['title'] = '{}: {}'.format(entity["name"],
                                                  entity["description"])
        elif name:
            attachment['title'] = name
        if url:
            attachment['title_link'] = url
        if article:
            attachment['text'] = article
        if image_url:
            attachment['image_url'] = image_url
    else:
        attachment['text'] = 'No results match your query.'
    message['attachments'].append(attachment)

    return message

Go


package slack

import (
	"fmt"

	"google.golang.org/api/kgsearch/v1"
)

func formatSlackMessage(query string, response *kgsearch.SearchResponse) (*Message, error) {
	if response == nil {
		return nil, fmt.Errorf("empty response")
	}

	if response.ItemListElement == nil || len(response.ItemListElement) == 0 {
		message := &Message{
			ResponseType: "in_channel",
			Text:         fmt.Sprintf("Query: %s", query),
			Attachments: []attachment{
				{
					Color: "#d6334b",
					Text:  "No results match your query.",
				},
			},
		}
		return message, nil
	}

	entity, ok := response.ItemListElement[0].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("could not parse response entity")
	}
	result, ok := entity["result"].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("error formatting response result")
	}

	attach := attachment{Color: "#3367d6"}
	if name, ok := result["name"].(string); ok {
		if description, ok := result["description"].(string); ok {
			attach.Title = fmt.Sprintf("%s: %s", name, description)
		} else {
			attach.Title = name
		}
	}
	if detailedDesc, ok := result["detailedDescription"].(map[string]interface{}); ok {
		if url, ok := detailedDesc["url"].(string); ok {
			attach.TitleLink = url
		}
		if article, ok := detailedDesc["articleBody"].(string); ok {
			attach.Text = article
		}
	}
	if image, ok := result["image"].(map[string]interface{}); ok {
		if imageURL, ok := image["contentUrl"].(string); ok {
			attach.ImageURL = imageURL
		}
	}

	message := &Message{
		ResponseType: "in_channel",
		Text:         fmt.Sprintf("Query: %s", query),
		Attachments:  []attachment{attach},
	}
	return message, nil
}

Java

/**
 * Helper method to copy properties between {@link JsonObject}s
 */
void addPropertyIfPresent(
    JsonObject target, String targetName, JsonObject source, String sourceName) {
  if (source.has(sourceName)) {
    target.addProperty(targetName, source.get(sourceName).getAsString());
  }
}

/**
 * Format the Knowledge Graph API response into a richly formatted Slack message.
 *
 * @param kgResponse The response from the Knowledge Graph API as a {@link JsonObject}.
 * @param query The user's search query.
 * @return The formatted Slack message as a JSON string.
 */
String formatSlackMessage(JsonObject kgResponse, String query) {
  JsonObject attachmentJson = new JsonObject();

  JsonObject responseJson = new JsonObject();
  responseJson.addProperty("response_type", "in_channel");
  responseJson.addProperty("text", String.format("Query: %s", query));

  JsonArray entityList = kgResponse.getAsJsonArray("itemListElement");

  // Extract the first entity from the result list, if any
  if (entityList.size() == 0) {
    attachmentJson.addProperty("text", "No results match your query...");
    responseJson.add("attachments", attachmentJson);

    return gson.toJson(responseJson);
  }

  JsonObject entity = entityList.get(0).getAsJsonObject().getAsJsonObject("result");

  // Construct Knowledge Graph response attachment
  String title = entity.get("name").getAsString();
  if (entity.has("description")) {
    title = String.format("%s: %s", title, entity.get("description").getAsString());
  }
  attachmentJson.addProperty("title", title);

  if (entity.has("detailedDescription")) {
    JsonObject detailedDescJson = entity.getAsJsonObject("detailedDescription");
    addPropertyIfPresent(attachmentJson, "title_link", detailedDescJson, "url");
    addPropertyIfPresent(attachmentJson, "text", detailedDescJson, "articleBody");
  }

  if (entity.has("image")) {
    JsonObject imageJson = entity.getAsJsonObject("image");
    addPropertyIfPresent(attachmentJson, "image_url", imageJson, "contentUrl");
  }

  JsonArray attachmentList = new JsonArray();
  attachmentList.add(attachmentJson);

  responseJson.add("attachments", attachmentList);

  return gson.toJson(responseJson);
}

C#

private string FormatSlackMessage(SearchResponse kgResponse, string query)
{
    JObject attachment = new JObject();
    JObject response = new JObject();

    response["response_type"] = "in_channel";
    response["text"] = $"Query: {query}";

    var element = kgResponse.ItemListElement?.FirstOrDefault() as JObject;
    if (element is object && element.TryGetValue("result", out var entityToken) &&
        entityToken is JObject entity)
    {
        string title = (string) entity["name"];
        if (entity.TryGetValue("description", out var description))
        {
            title = $"{title}: {description}";
        }
        attachment["title"] = title;
        if (entity.TryGetValue("detailedDescription", out var detailedDescriptionToken) &&
            detailedDescriptionToken is JObject detailedDescription)
        {
            AddPropertyIfPresent(detailedDescription, "url", "title_link");
            AddPropertyIfPresent(detailedDescription, "articleBody", "text");
        }
        if (entity.TryGetValue("image", out var imageToken) &&
            imageToken is JObject image)
        {
            AddPropertyIfPresent(image, "contentUrl", "image_url");
        }
    }
    else
    {
        attachment["text"] = "No results match your query...";
    }
    response["attachments"] = new JArray { attachment };
    return response.ToString();

    void AddPropertyIfPresent(JObject parent, string sourceProperty, string targetProperty)
    {
        if (parent.TryGetValue(sourceProperty, out var propertyValue))
        {
            attachment[targetProperty] = propertyValue;
        }
    }
}

Délais avant expiration de l'API Slack

L'API Slack s'attend à ce que votre fonction réponde dans un délai de trois secondes suivant la réception d'une requête webhook.

Les commandes utilisées dans ce tutoriel prennent généralement moins de trois secondes pour répondre. Pour les commandes à exécution plus longue, nous recommandons de configurer une fonction pour les requêtes push (y compris leur response_url) envoyées à un sujet Pub/Sub qui agit en tant que file d'attente de tâches.

Ensuite, vous pouvez créer une deuxième fonction déclenchée par Pub/Sub qui traite ces tâches et renvoie les résultats à l'adresse response_url de Slack.

Utiliser la commande Slash

  1. Tapez la commande dans votre chaîne Slack :

    /kg giraffe
  2. Consultez les journaux pour vous assurer que les exécutions sont terminées :

    gcloud functions logs read --limit 100
    

Nettoyer

Afin d'éviter que des frais ne soient facturés sur votre compte Google Cloud Platform pour les ressources utilisées dans ce tutoriel, procédez comme suit :

Supprimer le projet

Le moyen le plus simple d'empêcher la facturation est de supprimer le projet que vous avez créé pour ce tutoriel.

Pour supprimer le projet :

  1. Dans Cloud Console, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer .
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Supprimer une fonction Cloud Functions

Pour supprimer la fonction cloud que vous avez déployée dans ce tutoriel, exécutez la commande suivante :

Node.js

gcloud functions delete kgSearch 

Python

gcloud functions delete kg_search 

Go

gcloud functions delete KGSearch 

Java

gcloud functions delete java-slack-function 

C#

gcloud functions delete csharp-slack-function 

Vous pouvez également supprimer des fonctions Cloud Functions à partir de la console Google Cloud.