Interaktive Umfrage-App für Google Chat mit Node.js erstellen

1. Einführung

Mit Google Chat-Apps können Sie Ihre Dienste und Ressourcen direkt in Google Chat einbinden. So können Nutzer Informationen abrufen und schnell Maßnahmen ergreifen, ohne die Unterhaltung verlassen zu müssen.

In diesem Codelab erfahren Sie, wie Sie eine Umfrage-App mit Node.js und Cloud Functions erstellen und bereitstellen.

Eine App, die eine Umfrage postet, in der die Mitglieder des Bereichs gefragt werden, ob Apps toll sind, und die Stimmen mit einer interaktiven Kartenmitteilung sammelt.

Lerninhalte

  • Cloud Shell verwenden
  • In Cloud Functions bereitstellen
  • Nutzereingaben mit Slash-Befehlen und Dialogfeldern abrufen
  • Leitfaden zum Erstellen interaktiver Karten

2. Einrichtung und Anforderungen

Erstellen Sie ein Google Cloud-Projekt und aktivieren Sie dann die APIs und Dienste, die von der Chat-App verwendet werden.

Vorbereitung

Für die Entwicklung einer Google Chat-App ist ein Google Workspace-Konto mit Zugriff auf Google Chat erforderlich. Wenn Sie noch kein Google Workspace-Konto haben, erstellen Sie eines und melden Sie sich an, bevor Sie mit diesem Codelab fortfahren.

Einrichtung der Umgebung im eigenen Tempo

  1. Öffnen Sie die Google Cloud Console und erstellen Sie ein Projekt.

    Menü „Projekt auswählen“Die neue Schaltfläche „Projekt“Die Projekt-ID

    Merken Sie sich die Projekt-ID, einen eindeutigen Namen für alle Google Cloud-Projekte. Der Name oben ist bereits vergeben und kann daher nicht verwendet werden. Sie wird in diesem Codelab später als PROJECT_ID bezeichnet.
  1. Als Nächstes müssen Sie in der Cloud Console die Abrechnung aktivieren, um Google Cloud-Ressourcen verwenden zu können.

Die Durchführung dieses Codelabs sollte keine oder nur geringe Kosten verursachen. Folgen Sie unbedingt den Anweisungen im Abschnitt „Bereinigen“ am Ende des Codelabs, in dem beschrieben wird, wie Sie Ressourcen herunterfahren, damit nach dieser Anleitung keine Abrechnung erfolgt. Neuen Nutzern von Google Cloud steht das Programm Kostenlose Testversion mit einem Guthaben von 300$ zur Verfügung.

Google Cloud Shell

Während Sie Google Cloud von Ihrem Laptop aus per Fernzugriff nutzen können, wird in diesem Codelab Google Cloud Shell verwendet, eine Befehlszeilenumgebung, die in Google Cloud ausgeführt wird.

Cloud Shell aktivieren

  1. Klicken Sie in der Cloud Console auf Cloud Shell aktivieren Cloud Shell-Symbol.

    Das Cloud Shell-Symbol in der Menüleiste

     Wenn Sie Cloud Shell zum ersten Mal öffnen, wird eine informative Willkommensnachricht angezeigt. Wenn die Begrüßungsnachricht angezeigt wird, klicken Sie auf Weiter. Die Willkommensnachricht wird nicht noch einmal angezeigt. Hier ist die Begrüßungsnachricht:

    Cloud Shell-Willkommensnachricht

    Die Bereitstellung und Verbindung mit Cloud Shell sollte nur wenige Augenblicke dauern. Nachdem Sie eine Verbindung hergestellt haben, wird das Cloud Shell-Terminal angezeigt:

    Das Cloud Shell-Terminal

    Auf dieser virtuellen Maschine sind alle Entwicklertools installiert, die Sie benötigen. Sie bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und läuft in Google Cloud, was die Netzwerkleistung und Authentifizierung erheblich verbessert. Sie können alle Aufgaben in diesem Codelab mit einem Browser oder Ihrem Chromebook erledigen.Wenn Sie mit Cloud Shell verbunden sind, sollten Sie sehen, dass Sie bereits authentifiziert sind und das Projekt bereits auf Ihre Projekt-ID festgelegt ist.
  2. Führen Sie in Cloud Shell den folgenden Befehl aus, um zu prüfen, ob Sie authentifiziert sind:
    gcloud auth list
    
    Wenn Sie aufgefordert werden, Cloud Shell für einen GCP-API-Aufruf zu autorisieren, klicken Sie auf Autorisieren.

    Befehlsausgabe
    Credentialed Accounts
    ACTIVE  ACCOUNT
    *       <my_account>@<my_domain.com>
    
    Wenn Ihr Konto nicht standardmäßig ausgewählt ist, führen Sie Folgendes aus:
    $ gcloud config set account <ACCOUNT>
    
  1. Prüfen Sie, ob Sie das richtige Projekt ausgewählt haben. Führen Sie diesen Befehl in Cloud Shell aus:
    gcloud config list project
    
    Befehlsausgabe
    [core]
    project = <PROJECT_ID>
    
    Wenn das richtige Projekt nicht zurückgegeben wird, können Sie es mit diesem Befehl festlegen:
    gcloud config set project <PROJECT_ID>
    
    Befehlsausgabe
    Updated property [core/project].
    

In diesem Codelab verwenden Sie Befehlszeilenoperationen und bearbeiten Dateien. Für die Dateibearbeitung können Sie den integrierten Code-Editor von Cloud Shell, den Cloud Shell-Editor, verwenden. Klicken Sie dazu auf der rechten Seite der Cloud Shell-Symbolleiste auf Editor öffnen. Beliebte Editoren wie Vim und Emacs sind ebenfalls in Cloud Shell verfügbar.

3. Cloud Functions, Cloud Build und Google Chat APIs aktivieren

Aktivieren Sie in Cloud Shell die folgenden APIs und Dienste:

gcloud services enable \
  cloudfunctions \
  cloudbuild.googleapis.com \
  chat.googleapis.com

Dieser Vorgang kann einige Momente dauern.

Nach Abschluss des Vorgangs wird eine Erfolgsmeldung wie die folgende angezeigt:

Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.

4. Erste Chat-App erstellen

Projekt initialisieren

Zuerst erstellen und stellen Sie eine einfache „Hello World“-App bereit. Chat-Apps sind Webservices, die auf HTTPS-Anfragen reagieren und mit einer JSON-Nutzlast antworten. Für diese App verwenden Sie Node.js und Cloud Functions.

Erstellen Sie in Cloud Shell ein neues Verzeichnis mit dem Namen poll-app und wechseln Sie dorthin:

mkdir ~/poll-app
cd ~/poll-app

Alle verbleibenden Aufgaben für das Codelab und die Dateien, die Sie erstellen, befinden sich in diesem Verzeichnis.

Initialisieren Sie das Node.js-Projekt:

npm init

NPM stellt mehrere Fragen zur Projektkonfiguration, z. B. zu Name und Version. Drücken Sie für jede Frage ENTER, um die Standardwerte zu übernehmen. Der Standardeinstiegspunkt ist eine Datei mit dem Namen index.js, die wir als Nächstes erstellen.

Backend der Chat-App erstellen

Jetzt können Sie mit der Erstellung der App beginnen. Erstellen Sie eine Datei mit dem Namen index.js und folgendem Inhalt:

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  if (event.type === 'MESSAGE') {
    reply = {
        text: `Hello ${event.user.displayName}`
    };
  }
  res.json(reply)
}

Die App kann noch nicht viel, aber das ist in Ordnung. Sie fügen später weitere Funktionen hinzu.

Anwendung bereitstellen

Um die „Hello World“-App bereitzustellen, stellen Sie die Cloud-Funktion bereit, konfigurieren Sie die Chat-App in der Google Cloud Console und senden Sie eine Testnachricht an die App, um die Bereitstellung zu überprüfen.

Cloud Functions-Funktion bereitstellen

Geben Sie den folgenden Befehl ein, um die Cloud-Funktion der „Hello World“-App bereitzustellen:

gcloud functions deploy app --trigger-http --security-level=secure-always --allow-unauthenticated --runtime nodejs14

Wenn der Vorgang abgeschlossen ist, sollte die Ausgabe in etwa so aussehen:

availableMemoryMb: 256
buildId: 993b2ca9-2719-40af-86e4-42c8e4563a4b
buildName: projects/595241540133/locations/us-central1/builds/993b2ca9-2719-40af-86e4-42c8e4563a4b
entryPoint: app
httpsTrigger:
  securityLevel: SECURE_ALWAYS
  url: https://us-central1-poll-app-codelab.cloudfunctions.net/app
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/poll-app-codelab/locations/us-central1/functions/app
runtime: nodejs14
serviceAccountEmail: poll-app-codelab@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-66a01777-67f0-46d7-a941-079c24414822/94057943-2b7c-4b4c-9a21-bb3acffc84c6.zip
status: ACTIVE
timeout: 60s
updateTime: '2021-09-17T19:30:33.694Z'
versionId: '1'

Notieren Sie sich die URL der bereitgestellten Funktion im Attribut httpsTrigger.url. Sie benötigen sie im nächsten Schritt.

Anwendung konfigurieren

Rufen Sie in der Cloud Console die Seite Chatkonfiguration auf, um die App zu konfigurieren.

  1. Deaktivieren Sie Diese Chat-App als Workspace-Add‑on erstellen und klicken Sie zur Bestätigung auf DEAKTIVIEREN.
  2. Geben Sie unter App-Name „PollCodelab“ ein.
  3. Geben Sie unter Avatar URL (Avatar-URL) https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png ein.
  4. Geben Sie unter Beschreibung „Poll app for codelab“ ein.
  5. Wählen Sie unter Funktionsweise die Optionen 1:1-Nachrichten empfangen und Gruppenbereichen und Gruppenunterhaltungen beitreten aus.
  6. Wählen Sie unter Verbindungseinstellungen die Option HTTP-Endpunkt-URL aus und fügen Sie die URL für die Cloud-Funktion ein (die httpsTrigger.url-Property aus dem letzten Abschnitt).
  7. Wählen Sie unter Berechtigungen die Option Bestimmte Personen und Gruppen in Ihrer Domain aus und geben Sie Ihre E-Mail-Adresse ein.
  8. Klicken Sie auf Speichern.

Die App ist jetzt bereit für Nachrichten.

Anwendung testen

Bevor Sie fortfahren, sollten Sie prüfen, ob die App funktioniert, indem Sie sie einem Bereich in Google Chat hinzufügen.

  1. Zu Google Chat
  2. Klicken Sie neben „Chat“ auf + > Apps suchen.
  3. Geben Sie „PollCodelab“ in die Suche ein.
  4. Klicken Sie auf Chat.
  5. Wenn Sie der App eine Nachricht senden möchten, geben Sie „Hallo“ ein und drücken Sie die Eingabetaste.

Die App sollte mit einer kurzen Begrüßung antworten.

Jetzt, da ein grundlegendes Gerüst vorhanden ist, ist es an der Zeit, es in etwas Nützlicheres zu verwandeln.

5. Umfragefunktionen erstellen

Kurze Übersicht über die Funktionsweise der App

Die App besteht aus zwei Hauptteilen:

  1. Ein Slash-Befehl, mit dem ein Dialogfeld zum Konfigurieren der Umfrage angezeigt wird.
  2. Eine interaktive Karte zum Abstimmen und Ansehen der Ergebnisse.

Die App benötigt außerdem einen Zustand, in dem die Umfragekonfiguration und die Ergebnisse gespeichert werden. Dies kann mit Firestore oder einer beliebigen Datenbank erfolgen. Der Status kann auch in den App-Nachrichten selbst gespeichert werden. Da diese App für schnelle, informelle Umfragen in einem Team gedacht ist, eignet sich die Speicherung des Status in den App-Nachrichten hervorragend für diesen Anwendungsfall.

Das Datenmodell für die App (in TypeScript ausgedrückt) sieht so aus:

interface Poll {
  /* Question/topic of poll */
  topic: string;
  /** User that submitted the poll */
  author: {
    /** Unique resource name of user */
    name: string;
    /** Display name */
    displayName: string;
  };
  /** Available choices to present to users */
  choices: string[];
  /** Map of user ids to the index of their selected choice */
  votes: { [key: string]: number };
}

Neben dem Thema oder der Frage und einer Liste mit Auswahlmöglichkeiten enthält der Status die ID und den Namen des Autors sowie die aufgezeichneten Stimmen. Damit Nutzer nicht mehrmals abstimmen können, werden die Stimmen als Zuordnung von Nutzer-IDs zum Index der ausgewählten Option gespeichert.

Es gibt natürlich viele verschiedene Ansätze, aber dies ist ein guter Ausgangspunkt für die Durchführung von Schnellumfragen in einem Projektbereich.

Befehl zur Konfiguration von Umfragen implementieren

Damit Nutzer Umfragen starten und konfigurieren können, müssen Sie einen Slash-Befehl einrichten, der ein Dialogfeld öffnet. Hierzu sind mehrere Schritte erforderlich:

  1. Registrieren Sie den Slash-Befehl, mit dem eine Umfrage gestartet wird.
  2. Dialogfeld zum Einrichten einer Umfrage erstellen
  3. Die App soll den Slash-Befehl erkennen und verarbeiten.
  4. Erstellen Sie interaktive Karten, die die Teilnahme an der Umfrage erleichtern.
  5. Implementieren Sie den Code, mit dem die App Umfragen ausführen kann.
  6. Stellen Sie die Cloud Functions-Funktion noch einmal bereit.

Slash-Befehl registrieren

Wenn Sie einen Slash-Befehl registrieren möchten, kehren Sie in der Console zur Seite Chat-Konfiguration zurück (APIs & Dienste > Dashboard > Hangouts Chat API > Konfiguration).

  1. Klicken Sie unter Slash-Befehle auf Neuen Slash-Befehl hinzufügen.
  2. Geben Sie unter Name „/poll“ ein.
  3. Geben Sie unter Befehls-ID den Wert „1“ ein.
  4. Geben Sie unter Beschreibung „Eine Umfrage starten“ ein.
  5. Wählen Sie Öffnet ein Dialogfeld aus.
  6. Klicken Sie auf Fertig.
  7. Klicken Sie auf Speichern.

Die App erkennt jetzt den Befehl /poll und öffnet ein Dialogfeld. Als Nächstes konfigurieren wir den Dialog.

Konfigurationsformular als Dialogfeld erstellen

Mit dem Slash-Befehl wird ein Dialogfeld geöffnet, in dem Sie das Umfragethema und die möglichen Antworten konfigurieren können. Erstellen Sie eine neue Datei mit dem Namen config-form.js und folgendem Inhalt:

/** Upper bounds on number of choices to present. */
const MAX_NUM_OF_OPTIONS = 5;

/**
 * Build widget with instructions on how to use form.
 *
 * @returns {object} card widget
 */
function helpText() {
  return {
    textParagraph: {
      text: 'Enter the poll topic and up to 5 choices in the poll. Blank options will be omitted.',
    },
  };
}

/**
 * Build the text input for a choice.
 *
 * @param {number} index - Index to identify the choice
 * @param {string|undefined} value - Initial value to render (optional)
 * @returns {object} card widget
 */
function optionInput(index, value) {
  return {
    textInput: {
      label: `Option ${index + 1}`,
      type: 'SINGLE_LINE',
      name: `option${index}`,
      value: value || '',
    },
  };
}

/**
 * Build the text input for the poll topic.
 *
 * @param {string|undefined} topic - Initial value to render (optional)
 * @returns {object} card widget
 */
function topicInput(topic) {
  return {
    textInput: {
      label: 'Topic',
      type: 'MULTIPLE_LINE',
      name: 'topic',
      value: topic || '',
    },
  };
}

/**
 * Build the buttons/actions for the form.
 *
 * @returns {object} card widget
 */
function buttons() {
  return {
    buttonList: {
      buttons: [
        {
          text: 'Submit',
          onClick: {
            action: {
              function: 'start_poll',
            },
          },
        },
      ],
    },
  };
}

/**
 * Build the configuration form.
 *
 * @param {object} options - Initial state to render with form
 * @param {string|undefined} options.topic - Topic of poll (optional)
 * @param {string[]|undefined} options.choices - Text of choices to display to users (optional)
 * @returns {object} card
 */
function buildConfigurationForm(options) {
  const widgets = [];
  widgets.push(helpText());
  widgets.push(topicInput(options.topic));
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
    const choice = options?.choices?.[i];
    widgets.push(optionInput(i, choice));
  }
  widgets.push(buttons());

  // Assemble the card
  return {
    sections: [
      {
        widgets,
      },
    ],
  };
}

exports.MAX_NUM_OF_OPTIONS = MAX_NUM_OF_OPTIONS;
exports.buildConfigurationForm = buildConfigurationForm;

Mit diesem Code wird das Dialogfeld generiert, in dem der Nutzer die Umfrage einrichten kann. Außerdem wird eine Konstante für die maximale Anzahl von Antwortmöglichkeiten exportiert, die eine Frage haben kann. Es empfiehlt sich, das Erstellen des UI-Markups in zustandslose Funktionen zu isolieren, wobei der Zustand als Parameter übergeben wird. Sie erleichtert die Wiederverwendung und wird später in verschiedenen Kontexten gerendert.

Bei dieser Implementierung wird die Karte auch in kleinere Einheiten oder Komponenten zerlegt. Diese Technik ist zwar nicht erforderlich, aber eine Best Practice, da sie bei der Entwicklung komplexer Benutzeroberflächen in der Regel besser lesbar und wartungsfreundlicher ist.

Ein Beispiel für das vollständige JSON, das erstellt wird, finden Sie im Card Builder-Tool.

Slash-Befehl verarbeiten

Slash-Befehle werden als MESSAGE-Ereignisse angezeigt, wenn sie an die App gesendet werden. Aktualisieren Sie index.js, um über ein MESSAGE-Ereignis nach dem Vorhandensein eines Slash-Befehls zu suchen und mit einem Dialogfeld zu antworten. Ersetzen Sie index.js durch Folgendes:

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function startPoll(event) {
  // Not fully implemented yet -- just close the dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  }
}

In der App wird jetzt ein Dialogfeld angezeigt, wenn der Befehl /poll aufgerufen wird. Testen Sie die Interaktion, indem Sie die Cloud Functions-Funktion über Cloud Shell neu bereitstellen.

gcloud functions deploy app --trigger-http --security-level=secure-always

Nachdem die Cloud Functions-Funktion bereitgestellt wurde, senden Sie der App eine Nachricht mit dem Befehl /poll, um den Slash-Befehl und den Dialog zu testen. Im Dialogfeld wird ein CARD_CLICKED-Ereignis mit der benutzerdefinierten Aktion start_poll gesendet. Das Ereignis wird im aktualisierten Einstiegspunkt verarbeitet, wo die Methode startPoll aufgerufen wird. Derzeit wird die startPoll-Methode nur verwendet, um das Dialogfeld zu schließen. Im nächsten Abschnitt implementieren Sie die Abstimmungsfunktion und verbinden alle Teile.

Wahlkarte implementieren

Um die Abstimmungsfunktion der App zu implementieren, definieren Sie zuerst die interaktive Karte, über die Nutzer abstimmen können.

Abstimmungsoberfläche implementieren

Erstellen Sie eine Datei mit dem Namen vote-card.js und dem folgendem Inhalt:

/**
 * Creates a small progress bar to show percent of votes for an option. Since
 * width is limited, the percentage is scaled to 20 steps (5% increments).
 *
 * @param {number} voteCount - Number of votes for this option
 * @param {number} totalVotes - Total votes cast in the poll
 * @returns {string} Text snippet with bar and vote totals
 */
function progressBarText(voteCount, totalVotes) {
  if (voteCount === 0 || totalVotes === 0) {
    return '';
  }

  // For progress bar, calculate share of votes and scale it
  const percentage = (voteCount * 100) / totalVotes;
  const progress = Math.round((percentage / 100) * 20);
  return '▀'.repeat(progress);
}

/**
 * Builds a line in the card for a single choice, including
 * the current totals and voting action.
 *
 * @param {number} index - Index to identify the choice
 * @param {string|undefined} value - Text of the choice
 * @param {number} voteCount - Current number of votes cast for this item
 * @param {number} totalVotes - Total votes cast in poll
 * @param {string} state - Serialized state to send in events
 * @returns {object} card widget
 */
function choice(index, text, voteCount, totalVotes, state) {
  const progressBar = progressBarText(voteCount, totalVotes);
  return {
    keyValue: {
      bottomLabel: `${progressBar} ${voteCount}`,
      content: text,
      button: {
        textButton: {
          text: 'vote',
          onClick: {
            action: {
              actionMethodName: 'vote',
              parameters: [
                {
                  key: 'state',
                  value: state,
                },
                {
                  key: 'index',
                  value: index.toString(10),
                },
              ],
            },
          },
        },
      },
    },
  };
}

/**
 * Builds the card header including the question and author details.
 *
 * @param {string} topic - Topic of the poll
 * @param {string} author - Display name of user that created the poll
 * @returns {object} card widget
 */
function header(topic, author) {
  return {
    title: topic,
    subtitle: `Posted by ${author}`,
    imageUrl:
      'https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png',
    imageStyle: 'AVATAR',
  };
}

/**
 * Builds the configuration form.
 *
 * @param {object} poll - Current state of poll
 * @param {object} poll.author - User that submitted the poll
 * @param {string} poll.topic - Topic of poll
 * @param {string[]} poll.choices - Text of choices to display to users
 * @param {object} poll.votes - Map of cast votes keyed by user ids
 * @returns {object} card
 */
function buildVoteCard(poll) {
  const widgets = [];
  const state = JSON.stringify(poll);
  const totalVotes = Object.keys(poll.votes).length;

  for (let i = 0; i < poll.choices.length; ++i) {
    // Count votes for this choice
    const votes = Object.values(poll.votes).reduce((sum, vote) => {
      if (vote === i) {
        return sum + 1;
      }
      return sum;
    }, 0);
    widgets.push(choice(i, poll.choices[i], votes, totalVotes, state));
  }

  return {
    header: header(poll.topic, poll.author.displayName),
    sections: [
      {
        widgets,
      },
    ],
  };
}

exports.buildVoteCard = buildVoteCard;

Die Implementierung ähnelt dem Ansatz beim Dialogfeld, die Markierung für interaktive Karten unterscheidet sich jedoch geringfügig von der für Dialogfelder. Wie bisher können Sie eine Vorschau des generierten JSON-Codes im Card Builder-Tool aufrufen.

„Vote“-Aktion implementieren

Die Abstimmungskarte enthält für jede Antwortmöglichkeit eine Schaltfläche. Der Index dieser Auswahl wird zusammen mit dem serialisierten Status der Umfrage an die Schaltfläche angehängt. Die App empfängt eine CARD_CLICKED mit der Aktion vote sowie alle Daten, die als Parameter an die Schaltfläche angehängt sind.

Aktualisiere index.js mit:

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');
const { buildVoteCard } = require('./vote-card');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    } else if (event.action?.actionMethodName === 'vote') {
        reply = recordVote(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function startPoll(event) {
  // Not fully implemented yet -- just close the dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  }
}

/**
 * Handle the custom vote action. Updates the state to record
 * the user's vote then rerenders the card.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function recordVote(event) {
  const parameters = event.common?.parameters;

  const choice = parseInt(parameters['index']);
  const userId = event.user.name;
  const state = JSON.parse(parameters['state']);

  // Add or update the user's selected option
  state.votes[userId] = choice;

  const card = buildVoteCard(state);
  return {
    thread: event.message.thread,
    actionResponse: {
      type: 'UPDATE_MESSAGE',
    },
    cards: [card],
  }
}

Die Methode recordVote parst den gespeicherten Status, aktualisiert ihn mit der Abstimmung des Nutzers und rendert die Karte dann neu. Die Umfrageergebnisse werden serialisiert und mit der Karte gespeichert, wenn sie aktualisiert wird.

Die Teile verbinden

Die App ist fast fertig. Nachdem wir den Slash-Befehl zusammen mit der Abstimmungsfunktion implementiert haben, müssen wir nur noch die startPoll-Methode fertigstellen.

Die Sache hat aber einen Haken.

Wenn die Umfragekonfiguration eingereicht wird, muss die App zwei Aktionen ausführen:

  1. Schließen Sie das Dialogfeld.
  2. Posten Sie eine neue Nachricht im Gruppenbereich mit der Abstimmungskarte.

Leider kann die direkte Antwort auf die HTTP-Anfrage nur eine der beiden Aktionen ausführen und muss die erste sein. Damit die Karte mit der Abstimmung gepostet werden kann, muss die App die Chat API verwenden, um asynchron eine neue Nachricht zu erstellen.

Clientbibliothek hinzufügen

Führen Sie den folgenden Befehl aus, um die Abhängigkeiten der App so zu aktualisieren, dass der Google API-Client für Node.js enthalten ist.

npm install --save googleapis

Umfrage starten

Aktualisieren Sie index.js auf die unten stehende endgültige Version:

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');
const { buildVoteCard } = require('./vote-card');
const {google} = require('googleapis');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    } else if (event.action?.actionMethodName === 'vote') {
        reply = recordVote(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
async function startPoll(event) {
  // Get the form values
  const formValues = event.common?.formInputs;
  const topic = formValues?.['topic']?.stringInputs.value[0]?.trim();
  const choices = [];
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
    const choice = formValues?.[`option${i}`]?.stringInputs.value[0]?.trim();
    if (choice) {
      choices.push(choice);
    }
  }

  if (!topic || choices.length === 0) {
    // Incomplete form submitted, rerender
    const dialog = buildConfigurationForm({
      topic,
      choices,
    });
    return {
      actionResponse: {
        type: 'DIALOG',
        dialogAction: {
          dialog: {
            body: dialog,
          },
        },
      },
    };
  }

  // Valid configuration, build the voting card to display
  // in the space
  const pollCard = buildVoteCard({
    topic: topic,
    author: event.user,
    choices: choices,
    votes: {},
  });
  const message = {
    cards: [pollCard],
  };
  const request = {
    parent: event.space.name,
    requestBody: message,
  };
  // Use default credentials (service account)
  const credentials = new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/chat.bot'],
  });
  const chatApi = google.chat({
    version: 'v1',
    auth: credentials,
  });
  await chatApi.spaces.messages.create(request);

  // Close dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  };
}

/**
 * Handle the custom vote action. Updates the state to record
 * the user's vote then rerenders the card.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function recordVote(event) {
  const parameters = event.common?.parameters;

  const choice = parseInt(parameters['index']);
  const userId = event.user.name;
  const state = JSON.parse(parameters['state']);

  // Add or update the user's selected option
  state.votes[userId] = choice;

  const card = buildVoteCard(state);
  return {
    thread: event.message.thread,
    actionResponse: {
      type: 'UPDATE_MESSAGE',
    },
    cards: [card],
  }
}

Stellen Sie die Funktion noch einmal bereit:

gcloud functions deploy app --trigger-http --security-level=secure-always

Sie sollten die App jetzt vollständig nutzen können. Versuchen Sie, den Befehl /poll aufzurufen, indem Sie eine Frage und einige Auswahlmöglichkeiten angeben. Nach dem Einreichen wird die Umfragekarte angezeigt.

Gib deine Stimme ab und sieh dir an, was passiert.

Natürlich ist es nicht besonders nützlich, sich selbst zu befragen. Laden Sie also Freunde oder Kollegen ein, es auszuprobieren.

6. Glückwunsch

Glückwunsch! Sie haben erfolgreich eine Google Chat-App mit Cloud Functions erstellt und bereitgestellt. In diesem Codelab wurden viele der grundlegenden Konzepte für die Entwicklung einer App behandelt. Es gibt aber noch viel mehr zu entdecken. Sehen Sie sich die Ressourcen unten an und denken Sie daran, Ihr Projekt zu bereinigen, um zusätzliche Gebühren zu vermeiden.

Zusätzliche Aktivitäten

Wenn Sie die Chat-Plattform und diese App genauer kennenlernen möchten, können Sie Folgendes ausprobieren:

  • Was passiert, wenn Sie die App mit @ erwähnen? Aktualisieren Sie die App, um das Verhalten zu verbessern.
  • Die Serialisierung des Umfragezustands auf der Karte ist für kleine Bereiche in Ordnung, hat aber Grenzen. Versuchen Sie, zu einer besseren Option zu wechseln.
  • Was kann der Autor tun, wenn er die Umfrage bearbeiten oder keine neuen Stimmen mehr annehmen möchte? Wie würden Sie diese Funktionen implementieren?
  • Der App-Endpunkt ist noch nicht gesichert. Fügen Sie eine Bestätigung hinzu, um sicherzustellen, dass die Anfragen von Google Chat stammen.

Das sind nur einige der vielen Möglichkeiten, die App zu verbessern. Viel Spaß und lass deiner Fantasie freien Lauf!

Bereinigen

So vermeiden Sie, dass Ihrem Google Cloud Platform-Konto die in dieser Anleitung verwendeten Ressourcen berechnet werden:

  • Wechseln Sie in der Cloud Console zur Seite Ressourcen verwalten. Klicken Sie oben links auf das Dreistrich-MenüMenüsymbol > „IAM & Verwaltung“ > Ressourcen verwalten.
  1. Wählen Sie in der Projektliste Ihr Projekt aus und klicken Sie auf Löschen.
  2. Geben Sie im Dialogfeld die Projekt-ID ein und klicken Sie auf Beenden, um das Projekt zu löschen.

Weitere Informationen

Weitere Informationen zum Entwickeln von Chat-Apps finden Sie hier:

Weitere Informationen zur Entwicklung in der Google Cloud Console finden Sie unter: