Slack-Anleitung – Slash-Befehle (2. Generation)


In dieser Anleitung wird gezeigt, wie mit Cloud Functions ein Slack-Slash-Befehl implementiert wird, der die Google Knowledge Graph API durchsucht.

Ziele

  • Slash-Befehl in Slack erstellen
  • Cloud Functions-HTTP-Funktion schreiben und bereitstellen
  • In Google Knowledge Graph API mit dem Slash-Befehl suchen

Kosten

In diesem Dokument verwenden Sie die folgenden kostenpflichtigen Komponenten von Google Cloud:

  • Cloud Functions
  • Cloud Build
  • Artifact Registry
  • Cloud Logging

Weitere Informationen finden Sie unter Cloud Functions-Preise.

Mit dem Preisrechner können Sie eine Kostenschätzung für Ihre voraussichtliche Nutzung vornehmen. Neuen Google Cloud-Nutzern steht möglicherweise eine kostenlose Testversion zur Verfügung.

Vorbereitung

  1. Melden Sie sich bei Ihrem Google Cloud-Konto an. Wenn Sie mit Google Cloud noch nicht vertraut sind, erstellen Sie ein Konto, um die Leistungsfähigkeit unserer Produkte in der Praxis sehen und bewerten zu können. Neukunden erhalten außerdem ein Guthaben von 300 $, um Arbeitslasten auszuführen, zu testen und bereitzustellen.
  2. Wählen Sie in der Google Cloud Console auf der Seite der Projektauswahl ein Google Cloud-Projekt aus oder erstellen Sie eines.

    Zur Projektauswahl

  3. Die Abrechnung für das Google Cloud-Projekt muss aktiviert sein.

  4. Cloud Functions, Cloud Build, Artifact Registry, and Logging APIs aktivieren.

    Aktivieren Sie die APIs

  5. Installieren Sie die Google Cloud CLI.
  6. Führen Sie folgenden Befehl aus, um die gcloud CLI zu initialisieren:

    gcloud init
  7. Wählen Sie in der Google Cloud Console auf der Seite der Projektauswahl ein Google Cloud-Projekt aus oder erstellen Sie eines.

    Zur Projektauswahl

  8. Die Abrechnung für das Google Cloud-Projekt muss aktiviert sein.

  9. Cloud Functions, Cloud Build, Artifact Registry, and Logging APIs aktivieren.

    Aktivieren Sie die APIs

  10. Installieren Sie die Google Cloud CLI.
  11. Führen Sie folgenden Befehl aus, um die gcloud CLI zu initialisieren:

    gcloud init
  12. Wenn Sie die gcloud CLI bereits installiert haben, aktualisieren Sie sie mit dem folgenden Befehl:

    gcloud components update
  13. Bereiten Sie die Entwicklungsumgebung vor.

Datenfluss visualisieren

Der Datenfluss der Anwendung in der Anleitung für Slack-Slash-Befehle umfasst mehrere Schritte:

  1. Der Nutzer führt den Slash-Befehl /kg <search_query> in einem Slack-Kanal aus.
  2. Slack sendet die Befehlsnutzlast an den Trigger-Endpunkt der Cloud Functions-Funktion.
  3. Die Cloud Functions-Funktion sendet eine Anfrage mit der Suchanfrage des Nutzers an die Knowledge Graph API.
  4. Die Knowledge Graph API antwortet mit übereinstimmenden Ergebnissen.
  5. Die Cloud Functions-Funktion formatiert die Antwort in eine Slack-Nachricht.
  6. Die Cloud Functions-Funktion sendet die Nachricht an Slack zurück.
  7. Der Nutzer sieht die formatierte Antwort im Slack-Kanal.

Eine grafische Darstellung des Ablaufs:

Anmeldedaten abrufen

Zum Bereitstellen Ihrer Funktion benötigen Sie einen API-Schlüssel, der von der Google Cloud Console bereitgestellt wird, sowie ein Slack-Signatur-Secret.

Knowledge Graph API-Schlüssel abrufen

Klicken Sie auf der Seite Google Cloud Console-Anmeldedaten auf Anmeldedaten erstellen und wählen Sie die Option API-Schlüssel aus. Merken Sie sich diesen Schlüssel, da Sie ihn in Ihren deploy-Befehl aufnehmen müssen. Mit diesem Schlüssel kann Ihre Funktion auf die Knowledge Graph API zugreifen.

Slack-Signatur-Secret abrufen

Sie benötigen außerdem das Slack-Signatur-Secret, um Ihre Funktion bereitzustellen. Erstellen Sie zum Abrufen des Slack-Signatur-Secrets die Slack-Anwendung, die Ihren Slack-Slash-Befehl hostet. Diese Anwendung muss mit einem Slack-Team verknüpft sein, für das Sie Berechtigungen zum Installieren von Integrationen haben.

  1. Rufen Sie die Seite Meine Apps in Slack auf und klicken Sie auf Create New App (Neue App erstellen).

  2. Wählen Sie From Scratch aus.

  3. Geben Sie einen Namen für die Anwendung an und wählen Sie einen Slack-Arbeitsbereich aus, in dem Sie Berechtigungen zum Installieren von Integrationen haben.

  4. Klicken Sie auf Create App (App erstellen).

    Die App wird erstellt und die Anzeige ändert sich auf die Seite Allgemeine Informationen.

  5. Kopieren Sie auf der Seite Allgemeine Informationen Ihr Slack-Signatur-Secret und speichern Sie es.

  6. Speichern Sie die Änderungen.

Als Nächstes müssen Sie den Quellcode abrufen und die Funktion bereitstellen. Nachdem Sie die Funktion bereitgestellt haben, konfigurieren Sie Ihre Slack-Anwendung für die Integration in die bereitgestellte Funktion, wie unter Anwendung konfigurieren beschrieben.

Funktion vorbereiten

  1. Klonen Sie das Repository der Beispiel-App auf Ihren lokalen Computer:

    Node.js

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

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

    Python

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

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

    Einfach loslegen (Go)

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

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

    Java

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

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

    C#

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

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

    Ruby

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

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

    PHP

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

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

  2. Wechseln Sie in das Verzeichnis, das den Cloud Functions-Beispielcode enthält:

    Node.js

    cd nodejs-docs-samples/functions/slack/

    Python

    cd python-docs-samples/functions/slack/

    Einfach loslegen (Go)

    cd golang-samples/functions/functionsv2/slack/

    Java

    cd java-docs-samples/functions/slack/

    C#

    cd dotnet-docs-samples/functions/slack/SlackKnowledgeGraphSearch/

    Ruby

    cd ruby-docs-samples/functions/slack/

    PHP

    cd php-docs-samples/functions/slack_slash_command/

Funktion bereitstellen

Führen Sie den folgenden Befehl in dem Verzeichnis aus, das den Beispielcode (oder die Datei pom.xml für Java) enthält. Dadurch stellen Sie diejenige Funktion bereit, die ausgeführt wird, wenn Sie (oder Slack) eine HTTP POST-Anfrage an den Endpunkt der Funktion stellen.

Ersetzen Sie YOUR_SLACK_SIGNING_SECRET durch das Signatur-Secret, das Slack auf der Seite Allgemeine Informationen Ihrer Anwendungskonfiguration bereitstellt, und YOUR_KG_API_KEY durch den soeben erstellten Knowledge Graph API-Schlüssel.

Node.js

gcloud functions deploy nodejs-slack-function \
--gen2 \
--runtime=nodejs20 \
--region=REGION \
--source=. \
--entry-point=kgSearch \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated

Verwenden Sie das Flag --runtime, um die Laufzeit-ID einer unterstützten Node.js-Version anzugeben und die Funktion auszuführen.

Python

gcloud functions deploy python-slack-function \
--gen2 \
--runtime=python312 \
--region=REGION \
--source=. \
--entry-point=kg_search \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated

Verwenden Sie das Flag --runtime, um die Laufzeit-ID einer unterstützten Python-Version anzugeben und die Funktion auszuführen.

Einfach loslegen (Go)

gcloud functions deploy go-slack-function \
--gen2 \
--runtime=go121 \
--region=REGION \
--source=. \
--entry-point=KGSearch \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated

Verwenden Sie das Flag --runtime, um die Laufzeit-ID einer unterstützten Go-Version anzugeben und die Funktion auszuführen.

Java

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

Verwenden Sie das Flag --runtime, um die Laufzeit-ID einer unterstützten Java-Version anzugeben und die Funktion auszuführen.

C#

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

Verwenden Sie das Flag --runtime, um die Laufzeit-ID einer unterstützten .NET-Version anzugeben, um Ihre Funktion auszuführen.

Ruby

gcloud functions deploy ruby-slack-function \
--gen2 \
--runtime=ruby32 \
--region=REGION \
--source=. \
--entry-point=kg_search \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated

Verwenden Sie das Flag --runtime, um die Laufzeit-ID einer unterstützten Ruby-Version anzugeben, um die Funktion auszuführen.

PHP

gcloud functions deploy php-slack-function \
--gen2 \
--runtime=php82 \
--region=REGION \
--source=. \
--entry-point=receiveRequest \
--trigger-http \
--set-env-vars "SLACK_SECRET=YOUR_SLACK_SIGNING_SECRET,KG_API_KEY=YOUR_KG_API_KEY" \
--allow-unauthenticated

Verwenden Sie das Flag --runtime, um die Laufzeit-ID einer unterstützten PHP-Version anzugeben, um die Funktion auszuführen.

Anwendung konfigurieren

Nachdem die Funktion bereitgestellt wurde, müssen Sie einen Slack-Slash-Befehl erstellen, der die Abfrage bei jeder Auslösung des Befehls an Ihre Cloud Functions-Funktion sendet:

  1. Kehren Sie zur Slack-Anwendung zurück, die Sie oben erstellt haben.

  2. Wählen Sie die Anwendung aus, rufen Sie Slash-Befehle auf und klicken Sie auf die Schaltfläche Neuen Befehl erstellen.

  3. Geben Sie /kg als Namen des Befehls ein.

  4. Geben Sie im Feld Anfrage-URL die URL Ihrer Funktion ein.

    Sie können entweder die URL aus der Befehlsausgabe deploy kopieren oder zur Seite Cloud Functions – Übersicht in der Google Cloud Console wechseln und auf die Funktion klicken, um die Seite Funktionsdetails zu öffnen und die URL von dort zu kopieren.

  5. Geben Sie eine kurze Beschreibung ein und klicken Sie auf Speichern.

  6. Rufen Sie Allgemeine Informationen auf.

  7. Klicken Sie auf In Arbeitsbereich installieren und folgen Sie der Anleitung auf dem Bildschirm, um die Anwendung für Ihren Arbeitsbereich zu aktivieren.

    Ihr Slack-Slash-Befehl sollte bald online gehen.

Den Code verstehen

Abhängigkeiten importieren

Die Anwendung muss mehrere Abhängigkeiten importieren, um mit den Google Cloud Platform-Diensten zu kommunizieren:

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
)

Einfach loslegen (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;
}

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

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

PHP

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Psr7\Response;

Webhook erhalten

Die folgende Funktion wird ausgeführt, wenn Sie (oder Slack) eine HTTP POST-Anfrage an den Endpunkt der Funktion stellen:

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)

Einfach loslegen (Go)


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

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

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

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"`
}

func init() {
	functions.HTTP("KGSearch", kgSearch)
	setup(context.Background())
}

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

	bodyBytes, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Printf("failed to read body: %v", err)
		http.Error(w, "could not read request body", http.StatusBadRequest)
		return
	}
	if r.Method != "POST" {
		http.Error(w, "Only POST requests are accepted", http.StatusMethodNotAllowed)
		return
	}
	formData, err := url.ParseQuery(string(bodyBytes))
	if err != nil {
		log.Printf("Error: Failed to Parse Form: %v", err)
		http.Error(w, "Couldn't parse form", http.StatusBadRequest)
		return
	}

	result, err := verifyWebHook(r, bodyBytes, slackSecret)
	if err != nil || !result {
		log.Printf("verifyWebhook failed: %v", err)
		http.Error(w, "Failed to verify request signature", http.StatusBadRequest)
		return
	}

	if len(formData.Get("text")) == 0 {
		log.Printf("no search text found: %v", formData)
		http.Error(w, "search text was empty", http.StatusBadRequest)
		return
	}
	kgSearchResponse, err := makeSearchRequest(formData.Get("text"))
	if err != nil {
		log.Printf("makeSearchRequest failed: %v", err)
		http.Error(w, "makeSearchRequest failed", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	if err = json.NewEncoder(w).Encode(kgSearchResponse); err != nil {
		http.Error(w, fmt.Sprintf("failed to marshal results: %v", err), 500)
		return
	}
}

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

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

PHP

/**
 * Receive a Slash Command request from Slack.
 */
function receiveRequest(ServerRequestInterface $request): ResponseInterface
{
    // Validate request
    if ($request->getMethod() !== 'POST') {
        // [] = empty headers
        return new Response(405);
    }

    // Parse incoming URL-encoded requests from Slack
    // (Slack requests use the "application/x-www-form-urlencoded" format)
    $bodyStr = $request->getBody();
    parse_str($bodyStr, $bodyParams);

    if (!isset($bodyParams['text'])) {
        // [] = empty headers
        return new Response(400);
    }

    if (!isValidSlackWebhook($request, $bodyStr)) {
        // [] = empty headers
        return new Response(403);
    }

    $query = $bodyParams['text'];

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

    // Format response to Slack
    // See https://api.slack.com/docs/message-formatting
    $formatted_message = formatSlackMessage($kgResponse, $query);

    return new Response(
        200,
        ['Content-Type' => 'application/json'],
        $formatted_message
    );
}

Die folgende Funktion authentifiziert die eingehende Anfrage, da die durch Slack bereitgestellte Kopfzeile X-Slack-Signature geprüft wird:

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.")

Einfach loslegen (Go)


// verifyWebHook verifies the request signature.
// See https://api.slack.com/docs/verifying-requests-from-slack.
func verifyWebHook(r *http.Request, body []byte, 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, fmt.Errorf("checkTimestamp(%v): %v %v", t, ageOk, age)
	}

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

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

// checkTimestamp allows 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();
    }
}

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

PHP

/**
 * Verify that the webhook request came from Slack.
 */
function isValidSlackWebhook(ServerRequestInterface $request): bool
{
    $SLACK_SECRET = getenv('SLACK_SECRET');

    // Check for headers
    $timestamp = $request->getHeaderLine('X-Slack-Request-Timestamp');
    $signature = $request->getHeaderLine('X-Slack-Signature');
    if (!$timestamp || !$signature) {
        return false;
    }

    // Compute signature
    $plaintext = sprintf('v0:%s:%s', $timestamp, $request->getBody());
    $hash = sprintf('v0=%s', hash_hmac('sha256', $plaintext, $SLACK_SECRET));

    return $hash === $signature;
}

Knowledge Graph API abfragen

Die folgende Funktion sendet eine Anfrage mit der Suchanfrage des Nutzers an die Knowledge Graph API:

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)

Einfach loslegen (Go)

func makeSearchRequest(query string) (*Message, error) {
	res, err := entitiesService.Search().Query(query).Limit(1).Do()
	if err != nil {
		return nil, 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();
}

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

PHP

/**
 * Send the user's search query to the Knowledge Graph API.
 */
function searchKnowledgeGraph(string $query): Google_Service_Kgsearch_SearchResponse
{
    $API_KEY = getenv('KG_API_KEY');

    $apiClient = new Google\Client();
    $apiClient->setDeveloperKey($API_KEY);

    $service = new Google_Service_Kgsearch($apiClient);

    $params = ['query' => $query];

    $kgResults = $service->entities->search($params);

    return $kgResults;
}

Slack-Nachricht formatieren

Mit der folgenden Funktion wird das Knowledge Graph-Ergebnis schließlich in eine umfangreich formatierte Slack-Nachricht verwandelt, die dem Nutzer angezeigt wird:

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

Einfach loslegen (Go)


package slack

import (
	"encoding/json"
	"fmt"

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

type ItemList struct {
	Items []ItemListElement `json:"itemListElement"`
}
type ItemListElement struct {
	Result EntitySearchResult `json:"result"`
}
type EntitySearchResult struct {
	Name         string       `json:"name"`
	Description  string       `json:"description"`
	DetailedDesc DetailedDesc `json:"detailedDescription"`
	URL          string       `json:"url"`
	Image        Image
}
type DetailedDesc struct {
	ArticleBody string
	URL         string
}
type Image struct {
	ContentURL string
}

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
	}

	// The KnowledgeGraph API returns an empty interface. To make this more
	// useful, we convert it back to json, and unmarshal into specific types.
	jsonstring, err := response.MarshalJSON()
	if err != nil {
		return nil, err
	}
	r := &ItemList{}
	if err := json.Unmarshal(jsonstring, r); err != nil {
		return nil, fmt.Errorf("failed to unmarshal json: %w", err)
	}
	result := r.Items[0].Result

	attach := Attachment{Color: "#3367d6"}
	attach.Title = result.Name
	attach.TitleLink = result.DetailedDesc.URL
	attach.Text = result.DetailedDesc.ArticleBody
	attach.ImageURL = result.Image.ContentURL

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

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

PHP

/**
 * Format the Knowledge Graph API response into a richly formatted Slack message.
 */
function formatSlackMessage(Google_Service_Kgsearch_SearchResponse $kgResponse, string $query): string
{
    $responseJson = [
        'response_type' => 'in_channel',
        'text' => 'Query: ' . $query
    ];

    $entityList = $kgResponse['itemListElement'];

    // Extract the first entity from the result list, if any
    if (empty($entityList)) {
        $attachmentJson = ['text' => 'No results match your query...'];
        $responseJson['attachments'] = $attachmentJson;

        return json_encode($responseJson);
    }

    $entity = $entityList[0]['result'];

    // Construct Knowledge Graph response attachment
    $title = $entity['name'];
    if (isset($entity['description'])) {
        $title = $title . ' ' . $entity['description'];
    }
    $attachmentJson = ['title' => $title];

    if (isset($entity['detailedDescription'])) {
        $detailedDescJson = $entity['detailedDescription'];
        $attachmentJson = array_merge([
            'title_link' => $detailedDescJson[ 'url'],
            'text' => $detailedDescJson['articleBody'],
        ], $attachmentJson);
    }

    if (isset($entity['image'])) {
        $imageJson = $entity['image'];
        $attachmentJson['image_url'] = $imageJson['contentUrl'];
    }

    $responseJson['attachments'] = array($attachmentJson);

    return json_encode($responseJson);
}

Zeitlimits in der Slack API

Die Slack API erwartet, dass Ihre Funktion innerhalb von 3 Sekunden nach dem Erhalt einer Webhook-Anfrage reagiert.

Die Ausführung der Befehle in dieser Anleitung dauert in der Regel weniger als 3 Sekunden. Bei Befehlen, deren Ausführung länger dauert, empfiehlt es sich, eine Funktion zu konfigurieren, die Anfragen per Push an ein Pub/Sub-Thema sendet, das als Aufgabenwarteschlange fungiert, und zwar, einschließlich der response_url der Anfragen.

Anschließend können Sie eine zweite Funktion erstellen, die von Pub/Sub ausgelöst wird, diese Aufgaben verarbeitet und die Ergebnisse an die response_url von Slack zurücksendet.

Slash-Befehl verwenden

  1. Geben Sie diesen Befehl in Ihren Slack-Kanal ein, wenn die Bereitstellung der Funktion abgeschlossen ist:

    /kg giraffe

    Sie sollten den Knowledge Graph-Eintrag für "Giraffe" sehen.

  2. Prüfen Sie die Logs, um die Ausgabe der Funktionsausführung zu sehen:

    gcloud functions logs read --limit 100
    

Bereinigen

Damit Ihrem Google Cloud-Konto die in dieser Anleitung verwendeten Ressourcen nicht in Rechnung gestellt werden, löschen Sie entweder das Projekt, das die Ressourcen enthält, oder Sie behalten das Projekt und löschen die einzelnen Ressourcen.

Projekt löschen

Am einfachsten vermeiden Sie weitere Kosten, wenn Sie das zum Ausführen der Anleitung erstellte Projekt löschen.

So löschen Sie das Projekt:

  1. Wechseln Sie in der Google Cloud Console zur Seite Ressourcen verwalten.

    Zur Seite „Ressourcen verwalten“

  2. Wählen Sie in der Projektliste das Projekt aus, das Sie löschen möchten, und klicken Sie dann auf Löschen.
  3. Geben Sie im Dialogfeld die Projekt-ID ein und klicken Sie auf Shut down (Beenden), um das Projekt zu löschen.

Cloud Functions-Funktion löschen

Führen Sie den folgenden Befehl aus, um die in dieser Anleitung bereitgestellte Cloud Functions-Funktion zu löschen:

Node.js

gcloud functions delete nodejs-slack-function --gen2 --region REGION 

Python

gcloud functions delete python-slack-function --gen2 --region REGION 

Einfach loslegen (Go)

gcloud functions delete go-slack-function --gen2 --region REGION 

Java

gcloud functions delete java-slack-function --gen2 --region REGION 

C#

gcloud functions delete csharp-slack-function --gen2 --region REGION 

Ruby

gcloud functions delete ruby-slack-function --gen2 --region REGION 

PHP

gcloud functions delete php-slack-function --gen2 --region REGION 

Sie können Cloud Functions-Funktionen auch über die Google Cloud Console löschen.