Instructivo de Slack: comandos de barra (1ª gen.)


En este instructivo, se muestra cómo usar Cloud Run Functions para implementar un comando de barra de Slack que realiza búsquedas en la API de Gráfico de conocimiento de Google.

Objetivos

  • Crear un comando de barra en Slack
  • Escribir e implementar una Cloud Run Function HTTP
  • Buscar en la API de Gráfico de conocimiento de Google con el comando de barra

Costos

En este documento, usarás los siguientes componentes facturables de Google Cloud:

  • Cloud Run functions

Para generar una estimación de costos en función del uso previsto, usa la calculadora de precios.

Es posible que los usuarios de Google Cloud nuevos cumplan con los requisitos para acceder a una prueba gratuita.

Antes de comenzar

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. Enable the Cloud Functions, Cloud Build, and Google Knowledge Graph Search APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  5. Install the Google Cloud CLI.

  6. Si usas un proveedor de identidad externo (IdP), primero debes acceder a gcloud CLI con tu identidad federada.

  7. Para inicializar gcloud CLI, ejecuta el siguiente comando:

    gcloud init
  8. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  9. Verify that billing is enabled for your Google Cloud project.

  10. Enable the Cloud Functions, Cloud Build, and Google Knowledge Graph Search APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  11. Install the Google Cloud CLI.

  12. Si usas un proveedor de identidad externo (IdP), primero debes acceder a gcloud CLI con tu identidad federada.

  13. Para inicializar gcloud CLI, ejecuta el siguiente comando:

    gcloud init
  14. Si ya tienes instalado gcloud CLI, ejecuta el siguiente comando para actualizarla:

    gcloud components update
  15. Prepara tu entorno de desarrollo.
  16. Visualiza el flujo de datos

    El flujo de datos en la aplicación del instructivo del comando de barra de Slack incluye varios pasos, como se muestra a continuación:

    1. El usuario ejecuta el comando de barra /kg <search_query> en un canal de Slack.
    2. Slack envía la carga útil del comando al extremo activador de la función.
    3. La función envía una solicitud con la búsqueda del usuario a la API de Gráfico de conocimiento.
    4. La API de Gráfico de conocimiento responde con todos los resultados coincidentes.
    5. La función da formato a la respuesta y la transforma en un mensaje de Slack.
    6. La función envía el mensaje de vuelta a Slack.
    7. El usuario ve la respuesta con formato en el canal de Slack.

    El siguiente gráfico puede ayudarte a visualizar los pasos:

    Crea una clave de API de Gráfico de conocimiento

    En la página Credenciales de la consola deGoogle Cloud , haz clic en el botón Crear credenciales y selecciona Clave de API. Recuerda esta clave, puesto que la usarás para acceder a la API de Gráfico de conocimiento en la siguiente sección.

    Prepara la función

    1. Clona el repositorio de la app de muestra en tu máquina local:

      Node.js

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

      De manera opcional, puedes descargar la muestra como un archivo zip y extraerla.

      Python

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

      De manera opcional, puedes descargar la muestra como un archivo zip y extraerla.

      Go

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

      De manera opcional, puedes descargar la muestra como un archivo ZIP y extraerla.

      Java

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

      De manera opcional, puedes descargar la muestra como un archivo ZIP y extraerla.

      Ruby

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

      De manera opcional, puedes descargar la muestra como un archivo zip y extraerla.

    2. Ve al directorio que contiene el código de muestra de funciones de Cloud Run:

      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/

      Ruby

      cd ruby-docs-samples/functions/slack/

    Implementa la función

    Para implementar la función que se activa cuando tú o Slack hacen una solicitud HTTP POST al extremo de la función, ejecuta el siguiente comando en el directorio que contiene el código de muestra (o el archivo pom.xml en el caso de Java):

    Reemplaza YOUR_SLACK_SIGNING_SECRET por el secreto de firma que proporciona Slack en la página Información básica de la configuración de tu app y YOUR_KG_API_KEY por la clave de la API de Gráfico de conocimiento que creaste antes.

    Node.js

    gcloud functions deploy kgSearch \
    --runtime nodejs20 \
    --trigger-http \
    --set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
    --allow-unauthenticated

    Usa la marca --runtime para especificar el ID del entorno de ejecución de una versión compatible de Node.js para ejecutar la función.

    Python

    gcloud functions deploy kg_search \
    --runtime python312 \
    --trigger-http \
    --set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
    --allow-unauthenticated

    Usa la marca --runtime para especificar el ID de entorno de ejecución de una versión de Python compatible para ejecutar la función.

    Go

    gcloud functions deploy KGSearch \
    --runtime go121 \
    --trigger-http \
    --set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
    --allow-unauthenticated

    Usa la marca --runtime para especificar el ID de entorno de ejecución de una versión de Go compatible para ejecutar tu función.

    Java

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

    Usa la marca --runtime para especificar el ID del entorno de ejecución de una versión de Java compatible para ejecutar tu función.

    Ruby

    gcloud functions deploy kg_search --runtime ruby33 \
    -
    -trigger-http \
    -
    -set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
    -
    -allow-unauthenticated

    Usa la marca --runtime para especificar el ID de entorno de ejecución de una versión de Ruby compatible para ejecutar tu función.

    Configura la aplicación

    Después de implementar la función, debes crear un comando de barra en Slack que envíe la consulta a tu función cada vez que se active el comando.

    1. Crea una app de Slack para alojar el comando de barra de Slack. Asóciala con un equipo de Slack en el que tengas permisos para instalar integraciones.

    2. Ve a Comandos de barra y haz clic en el botón Crear comando nuevo.

    3. Ingresa /kg como el nombre del comando.

    4. Ingresa la URL del comando, como en el ejemplo:

      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

      Ruby

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

      donde YOUR_REGION es la región en la cual se implementa tu función y YOUR_PROJECT_ID es el ID del proyecto de Cloud.

      Ambos valores pueden verse en la terminal cuando la función termina de implementarse.

    5. Haz clic en Guardar.

    6. Ve a Información básica.

    7. Haz clic en Instala la app en tu lugar de trabajo y sigue las instrucciones en pantalla para habilitar la aplicación en tu lugar de trabajo.

      Tu comando de barra de Slack debería estar en línea en breve.

    Comprende el código

    Importa dependencias

    La aplicación debe importar varias dependencias con el fin de comunicarse con los servicios de Google Cloud Platform:

    Node.js

    const functions = require('@google-cloud/functions-framework');
    const google = require('@googleapis/kgsearch');
    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 functions_framework
    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.NewService: %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 GsonFactory(), 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;
    }

    Ruby

    require "functions_framework"
    require "slack-ruby-client"
    require "google/apis/kgsearch_v1"
    
    # This block is executed during cold start, before the function begins
    # handling requests. This is the recommended way to create shared resources
    # and objects.
    FunctionsFramework.on_startup do
      # Create a global handler object, configured with the environment-provided
      # API key and signing secret.
      kg_search = KGSearch.new kg_api_key:     ENV["KG_API_KEY"],
                               signing_secret: ENV["SLACK_SECRET"]
      set_global :kg_search, kg_search
    end
    
    # The KGSearch class implements the logic of validating and responding
    # to requests. More methods of this class are shown below.
    class KGSearch
      def initialize kg_api_key:, signing_secret:
        # Create the global client for the Knowledge Graph Search Service,
        # configuring it with your API key.
        @client = Google::Apis::KgsearchV1::KgsearchService.new
        @client.key = kg_api_key
    
        # Save signing secret for use by the signature validation method.
        @signing_secret = signing_secret
      end

    Recibe el webhook

    La siguiente función se ejecuta cuando tú (o Slack) haces una solicitud HTTP POST al extremo de la función:

    Node.js

    /**
     * Receive a Slash Command request from Slack.
     *
     * Trigger this function by creating a Slack slash command with the HTTP Trigger URL.
     * You can find the HTTP URL in the Cloud Console or using `gcloud functions describe`
     *
     * @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.
     */
    functions.http('kgSearch', async (req, res) => {
      try {
        if (req.method !== 'POST') {
          const error = new Error('Only POST requests are accepted');
          error.code = 405;
          throw error;
        }
    
        if (!req.body.text) {
          const error = new Error('No text found in body.');
          error.code = 400;
          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

    @functions_framework.http
    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"
    	"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 := io.ReadAll(r.Body)
    	if err != nil {
    		log.Fatalf("Couldn't read request body: %v", err)
    	}
    	r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
    
    	if r.Method != "POST" {
    		http.Error(w, "Only POST requests are accepted", http.StatusMethodNotAllowed)
    	}
    	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 = io.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");
    }

    Ruby

    # Handler for the function endpoint.
    FunctionsFramework.http "kg_search" do |request|
      # Return early if the request is not a POST.
      unless request.post?
        return [405, {}, ["Only POST requests are accepted."]]
      end
    
      # Access the global Knowledge Graph Search client
      kg_search = global :kg_search
    
      # Verify the request signature and return early if it failed.
      unless kg_search.signature_valid? request
        return [401, {}, ["Signature validation failed."]]
      end
    
      # Query the Knowledge Graph and format a Slack message with the response.
      # This method returns a nested hash, which the Functions Framework will
      # convert to JSON automatically.
      kg_search.make_search_request request.params["text"]
    end

    Con la siguiente función, se verifica el encabezado X-Slack-Signature que proporcionó Slack para autenticar la solicitud:

    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,
      };
    
      // This method throws an exception if an incoming request is invalid.
      verifyRequestSignature(signature);
    };

    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): %w", 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 := io.ReadAll(r.Body)
    	if err != nil {
    		return false, fmt.Errorf("io.ReadAll(%v): %w", r.Body, err)
    	}
    
    	// Reset the body so other calls won't fail.
    	r.Body = io.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): %w", 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);
    }

    Ruby

    # slack-ruby-client expects a Rails-style request object with a "headers"
    # method, but the Functions Framework provides only a Rack request.
    # To avoid bringing in Rails as a dependency, we'll create a simple class
    # that implements the "headers" method and delegates everything else back to
    # the Rack request object.
    require "delegate"
    class RequestWithHeaders < SimpleDelegator
      def headers
        env.each_with_object({}) do |(key, val), result|
          if /^HTTP_(\w+)$/ =~ key
            header = Regexp.last_match(1).split("_").map(&:capitalize).join("-")
            result[header] = val
          end
        end
      end
    end
    
    # This is a method of the KGSearch class.
    # It determines whether the given request's signature is valid.
    def signature_valid? request
      # Wrap the request with our class that provides the "headers" method.
      request = RequestWithHeaders.new request
    
      # Validate the request signature.
      slack_request = Slack::Events::Request.new request,
                                                 signing_secret: @signing_secret
      slack_request.valid?
    end

    Consulta a la API de Gráfico de conocimiento

    La siguiente función envía una solicitud con la búsqueda del usuario a la API de Gráfico de conocimiento.

    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: %w", 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);
    }

    Ruby

    # This is a method of the KGSearch class.
    # It makes an API call to the Knowledge Graph Search Service, and formats
    # a Slack message as a nested Hash object.
    def make_search_request query
      response = @client.search_entities query: query, limit: 1
      format_slack_message query, response
    end

    Formato de mensaje de Slack

    Finalmente, la siguiente función da formato al resultado de Gráfico de conocimiento y lo transforma en un mensaje de Slack con formato enriquecido que se le mostrará al usuario.

    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": f"Query: {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);
    }

    Ruby

    # This is a method of the KGSearch class.
    # It takes a raw SearchResponse from the Knowledge Graph Search Service,
    # and formats a Slack message.
    def format_slack_message query, response
      result = response.item_list_element&.first&.fetch "result", nil
      attachment =
        if result
          name = result.fetch "name", nil
          description = result.fetch "description", nil
          details = result.fetch "detailedDescription", {}
          { "title"      => name && description ? "#{name}: #{description}" : name,
            "title_link" => details.fetch("url", nil),
            "text"       => details.fetch("articleBody", nil),
            "image_url"  => result.fetch("image", nil)&.fetch("contentUrl", nil) }
        else
          { "text" => "No results match your query." }
        end
      { "response_type" => "in_channel",
        "text"          => "Query: #{query}",
        "attachments"   => [attachment.compact] }
    end

    Tiempo de espera de la API de Slack

    La API de Slack espera que tu función responda dentro de 3 segundos después de recibir una solicitud de webhook.

    A los comandos que aparecen en este instructivo, por lo general, les toma menos de 3 segundos responder. En el caso de los comandos con tiempos de ejecución más largos, se recomienda configurar una función para enviar solicitudes (incluida la response_url) a un tema de Pub/Sub que actúe como una lista de tareas en cola.

    Después, puedes crear una segunda función activada por Pub/Sub que procese esas tareas y envíe resultados de vuelta a la response_url de Slack.

    Usa el comando de barra

    1. Escribe el comando en el canal de Slack:

      /kg giraffe
    2. Revisa los registros para asegurarte de que las ejecuciones se completaron:

      gcloud functions logs read --limit 100

    Limpia

    Para evitar que se apliquen cargos a tu cuenta de Google Cloud por los recursos usados en este instructivo, borra el proyecto que contiene los recursos o conserva el proyecto y borra los recursos individuales.

    Borra el proyecto

    La manera más fácil de eliminar la facturación es borrar el proyecto que creaste para el instructivo.

    Para borrar el proyecto, sigue estos pasos:

    1. In the Google Cloud console, go to the Manage resources page.

      Go to Manage resources

    2. In the project list, select the project that you want to delete, and then click Delete.
    3. In the dialog, type the project ID, and then click Shut down to delete the project.

    Borra la función

    Para borrar la función que implementaste en este instructivo, ejecuta el siguiente comando:

    Node.js

    gcloud functions delete kgSearch 

    Python

    gcloud functions delete kg_search 

    Go

    gcloud functions delete KGSearch 

    Java

    gcloud functions delete java-slack-function 

    Ruby

    gcloud functions delete kg_search 

    También puedes borrar funciones de Cloud Run en la consola deGoogle Cloud .