Membangun aplikasi polling interaktif untuk Google Chat dengan Node.js

1. Pengantar

Aplikasi Google Chat menghadirkan layanan dan referensi Anda langsung ke Google Chat, sehingga pengguna dapat memperoleh informasi dan mengambil tindakan cepat tanpa meninggalkan percakapan.

Dalam codelab ini, Anda akan mempelajari cara membangun dan men-deploy aplikasi polling menggunakan Node.js dan Cloud Functions.

Aplikasi memposting polling yang menanyakan kepada anggota ruang apakah aplikasi itu luar biasa dan mengumpulkan suara dengan pesan kartu interaktif.

Yang akan Anda pelajari

  • Menggunakan Cloud Shell
  • Men-deploy ke Cloud Functions
  • Mendapatkan input pengguna dengan dialog dan perintah garis miring
  • Membuat kartu interaktif

2. Penyiapan dan persyaratan

Buat project Google Cloud, lalu aktifkan API dan layanan yang akan digunakan aplikasi Chat

Prasyarat

Pengembangan aplikasi Google Chat memerlukan akun Google Workspace dengan akses ke Google Chat. Jika Anda belum memiliki akun Google Workspace, buat akun dan login sebelum melanjutkan dengan codelab ini.

Penyiapan lingkungan mandiri

  1. Buka Konsol Google Cloud dan buat project.

    Menu pilih projectTombol Project baruProject ID

    Ingat project ID-nya, yakni nama unik di semua project Google Cloud (maaf, nama di atas sudah digunakan dan tidak akan berfungsi untuk Anda). Project ID tersebut selanjutnya akan dirujuk di codelab ini sebagai PROJECT_ID.
  1. Selanjutnya, untuk menggunakan resource Google Cloud, aktifkan penagihan di Konsol Cloud

Menjalankan operasi dalam codelab ini seharusnya tidak memerlukan banyak biaya, bahkan mungkin tidak sama sekali. Pastikan untuk mengikuti semua petunjuk di bagian "Pembersihan" di bagian akhir codelab yang memberi tahu Anda cara mematikan resource agar Anda tidak dikenai biaya setelah mengikuti tutorial ini. Pengguna baru Google Cloud memenuhi syarat untuk mengikuti program Uji Coba Gratis senilai $300 USD.

Google Cloud Shell

Meskipun Google Cloud dapat dioperasikan secara jarak jauh dari laptop Anda, dalam codelab ini kita akan menggunakan Google Cloud Shell, yakni lingkungan command line yang berjalan di Google Cloud.

Mengaktifkan Cloud Shell

  1. Dari Cloud Console, klik Activate Cloud Shell Ikon Cloud Shell.

    Ikon Cloud Shell pada panel menu

    Saat pertama kali membuka Cloud Shell, Anda akan melihat pesan selamat datang yang bersifat deskriptif. Jika Anda melihat pesan selamat datang, klik Continue. Pesan selamat datang tidak akan muncul lagi. Berikut pesan selamat datangnya:

    Pesan selamat datang Cloud Shell

    Hanya perlu waktu beberapa saat untuk melakukan penyediaan dan terhubung ke Cloud Shell. Setelah terhubung, Anda akan melihat Terminal Cloud Shell:

    Terminal Cloud Shell

    Virtual machine ini dimuat dengan semua alat pengembangan yang Anda perlukan. Layanan ini menawarkan direktori beranda tetap sebesar 5 GB dan beroperasi di Google Cloud, sehingga sangat meningkatkan performa dan autentikasi jaringan. Semua pekerjaan Anda dalam codelab ini dapat dilakukan dengan browser atau Chromebook Anda. Setelah terhubung ke Cloud Shell, Anda akan melihat bahwa Anda telah diautentikasi dan project sudah disetel ke project ID Anda.
  2. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa Anda telah diautentikasi:
    gcloud auth list
    
    Jika Anda diminta untuk mengotorisasi Cloud Shell agar dapat melakukan panggilan API GCP, klik Authorize.

    Output perintah
    Credentialed Accounts
    ACTIVE  ACCOUNT
    *       <my_account>@<my_domain.com>
    
    Jika akun Anda tidak dipilih secara default, jalankan:
    $ gcloud config set account <ACCOUNT>
    
  1. Konfirmasikan bahwa Anda telah memilih project yang benar. Di Cloud Shell, jalankan:
    gcloud config list project
    
    Output perintah
    [core]
    project = <PROJECT_ID>
    
    Jika project yang benar tidak ditampilkan, Anda dapat menyetelnya dengan perintah ini:
    gcloud config set project <PROJECT_ID>
    
    Output perintah
    Updated property [core/project].
    

Saat menyelesaikan codelab ini, Anda akan menggunakan operasi command line dan mengedit file. Untuk mengedit file, Anda dapat menggunakan editor kode bawaan Cloud Shell, Cloud Shell Editor, dengan mengklik Open Editor di sebelah kanan toolbar Cloud Shell. Editor populer seperti Vim dan Emacs juga tersedia di Cloud Shell.

3. Mengaktifkan Cloud Functions, Cloud Build, dan Google Chat API

Dari Cloud Shell, aktifkan API dan layanan ini:

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

Operasi ini mungkin memerlukan waktu beberapa saat sampai selesai.

Setelah selesai, akan muncul pesan sukses yang mirip dengan yang berikut ini:

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

4. Membuat aplikasi Chat awal

Menginisialisasi project

Untuk memulai, Anda akan membuat dan men-deploy aplikasi "Halo Dunia" sederhana. Aplikasi chat adalah layanan web yang merespons permintaan https dan merespons dengan payload JSON. Untuk aplikasi ini, Anda akan menggunakan Node.js dan Cloud Functions.

Di Cloud Shell, buat direktori baru bernama poll-app lalu bukalah:

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

Semua sisa pekerjaan untuk codelab dan file yang akan Anda buat akan berada di direktori ini.

Inisialisasi project Node.js:

npm init

NPM akan mengajukan beberapa pertanyaan tentang konfigurasi project, seperti nama dan versi. Untuk setiap pertanyaan, tekan ENTER untuk menyetujui nilai default. Titik entri default adalah file bernama index.js, yang akan kita buat berikutnya.

Membuat backend aplikasi Chat

Saatnya mulai membuat aplikasi. Buat file bernama index.js dengan konten berikut:

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

Aplikasi ini belum bisa berbuat banyak, tapi tidak apa-apa. Anda akan menambahkan lebih banyak fungsi nanti.

Men-deploy aplikasi

Untuk men-deploy aplikasi "Halo Dunia", Anda akan men-deploy Cloud Function, mengonfigurasi aplikasi Chat di Konsol Google Cloud, dan mengirim pesan pengujian ke aplikasi untuk memverifikasi deployment.

Men-deploy Cloud Function

Untuk men-deploy Cloud Function aplikasi "Halo Dunia", masukkan perintah berikut:

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

Setelah selesai, outputnya akan terlihat seperti ini:

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'

Catat URL dari fungsi yang di-deploy di properti httpsTrigger.url. Anda akan menggunakan ini di langkah berikutnya.

Mengonfigurasi aplikasi

Untuk mengonfigurasi aplikasi, buka halaman Chat configuration di Cloud Console

  1. Hapus centang pada Build this Chat app as a Workspace add-on, lalu klik DISABLE untuk mengonfirmasi.
  2. Di App name, masukkan "PollCodelab".
  3. Di Avatar URL, masukkan https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png.
  4. Di Description, masukkan "Poll app for codelab".
  5. Di bagian Functionality, pilih Receive 1:1 messages dan Join spaces and group conversations.
  6. Di bagian Connection settings, pilih HTTP endpoint URL dan tempelkan URL untuk Cloud Function (properti httpsTrigger.url dari bagian terakhir).
  7. Di bagian Permissions, pilih Specific people and groups in your domain dan masukkan alamat email Anda.
  8. Klik Save.

Aplikasi sekarang siap mengirim pesan.

Menguji aplikasi

Sebelum melanjutkan, periksa apakah aplikasi berfungsi atau tidak dengan menambahkannya ke ruang di Google Chat.

  1. Buka Google Chat.
  2. Di sebelah Chat, klik + > Cari aplikasi.
  3. Masukkan "PollCodelab" dalam penelusuran.
  4. Klik Chat.
  5. Untuk mengirim pesan ke aplikasi, ketik "Halo" lalu tekan enter.

Aplikasi akan memberikan respons dengan pesan halo singkat.

Setelah kerangka dasar terpasang, saatnya mengubahnya menjadi sesuatu yang lebih berguna.

5. Membangun fitur polling

Ringkasan singkat tentang cara kerja aplikasi

Aplikasi ini terdiri dari dua bagian utama:

  1. Perintah garis miring yang menampilkan dialog untuk mengonfigurasi polling.
  2. Kartu interaktif untuk memberi suara dan melihat hasil.

Aplikasi ini juga memerlukan beberapa status untuk menyimpan konfigurasi dan hasil polling. Hal ini dapat dilakukan dengan Firestore atau database apa pun, atau status dapat disimpan dalam pesan aplikasi itu sendiri. Karena aplikasi ini ditujukan untuk polling cepat informal dari suatu tim, penyimpanan status dalam pesan aplikasi cocok untuk kasus penggunaan ini.

Model data untuk aplikasi ini (dinyatakan dalam TypeScript) adalah:

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

Selain topik atau pertanyaan dan daftar pilihan, status akan mencantumkan id dan nama penulis serta jumlah suara yang tercatat. Untuk mencegah pengguna memberi suara beberapa kali, suara akan disimpan sebagai peta ID pengguna atas indeks pilihan mereka.

Tentu saja ada banyak pendekatan lain, tetapi ini adalah titik awal yang baik untuk menjalankan polling cepat di sebuah ruang.

Mengimplementasikan perintah konfigurasi polling

Untuk mengizinkan pengguna memulai dan mengonfigurasi polling, siapkan perintah garis miring yang akan membuka sebuah dialog. Ini adalah proses multi-langkah:

  1. Daftarkan perintah garis miring yang memulai polling.
  2. Buat dialog yang menyiapkan polling.
  3. Biarkan aplikasi mengenali dan menangani perintah garis miring.
  4. Buat kartu interaktif yang memfasilitasi pemungutan suara dalam polling.
  5. Implementasikan kode yang memungkinkan aplikasi menjalankan polling.
  6. Deploy ulang fungsi cloud.

Mendaftarkan perintah garis miring

Untuk mendaftarkan perintah garis miring, kembali ke halaman Konfigurasi chat di konsol (APIs & Services > Dashboard > Hangouts Chat API > Configuration).

  1. Pada Slash commands, klik Add a new slash command.
  2. Pada Name, masukkan "/poll"
  3. Pada Command id, masukkan "1"
  4. Pada Description, masukkan "Start a poll".
  5. Pilih Opens a dialog.
  6. Klik Done.
  7. Klik Save.

Aplikasi sekarang mengenali perintah /poll, dan akan membuka dialog. Selanjutnya, mari kita konfigurasikan dialognya.

Membuat jenis konfigurasi sebagai dialog

Perintah garis miring akan membuka dialog untuk mengonfigurasi topik polling dan kemungkinan pilihan. Buat file baru bernama config-form.js dengan konten berikut:

/** 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;

Kode ini menghasilkan jenis dialog, sehingga pengguna dapat menyiapkan polling. Kode ini juga mengekspor konstanta untuk jumlah maksimum pilihan yang bisa dimiliki sebuah pertanyaan. Anda sebaiknya mengisolasi pembuatan markup UI ke dalam fungsi stateless dengan segala status diteruskan sebagai parameter. Hal ini akan memfasilitasi penggunaan kembali, dan nantinya kartu ini akan dirender dalam konteks yang berbeda.

Implementasi ini juga akan menguraikan kartu menjadi unit atau komponen yang lebih kecil. Meskipun tidak diperlukan, teknik ini merupakan praktik terbaik karena cenderung lebih mudah dibaca dan dipelihara saat membangun antarmuka yang kompleks.

Untuk melihat contoh JSON lengkap yang dibangunnya, lihat di alat Pembuat Kartu.

Menangani perintah garis miring

Perintah garis miring akan muncul sebagai peristiwa MESSAGE saat dikirim ke aplikasi. Update index.js untuk memeriksa keberadaan perintah garis miring melalui peristiwa MESSAGE dan untuk merespons dengan dialog. Ganti index.js dengan yang berikut:

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.',
        },
      },
    },
  }
}

Aplikasi sekarang akan menampilkan dialog ketika perintah /poll dipanggil. Uji interaksi dengan men-deploy ulang Cloud Function dari Cloud Shell.

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

Setelah Cloud Function di-deploy, kirim pesan ke aplikasi dengan perintah /poll untuk menguji dialog dan perintah garis miring. Dialog akan mengirimkan peristiwa CARD_CLICKED dengan tindakan kustom start_poll. Peristiwa ini akan ditangani di titik entri yang diupdate tempat metode startPoll dipanggil. Untuk saat ini, metode startPoll dinonaktifkan untuk menutup dialog. Di bagian selanjutnya, Anda akan mengimplementasikan fungsi pemungutan suara dan menghubungkan semua bagian menjadi satu.

Mengimplementasikan kartu pemberian suara

Untuk mengimplementasikan bagian pemungutan suara dari aplikasi, mulailah dengan menentukan kartu interaktif yang menyediakan antarmuka bagi pengguna untuk memberikan suara.

Mengimplementasikan antarmuka pemberian suara

Buat file bernama vote-card.js dengan konten berikut:

/**
 * 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;

Implementasinya mirip dengan pendekatan yang diambil dengan dialog, meskipun markup untuk kartu interaktif sedikit berbeda dari dialog. Seperti sebelumnya, Anda dapat melihat contoh JSON yang dibuat di alat Pembuat Kartu.

Mengimplementasikan tindakan pemberian suara

Kartu pemungutan suara menyertakan tombol untuk setiap pilihan. Indeks pilihan ini, bersama dengan status polling berseri, dipasang ke tombol tersebut. Aplikasi akan menerima CARD_CLICKED dengan tindakan vote bersama dengan data apa pun yang dipasang ke tombol sebagai parameter.

Update index.js dengan:

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],
  }
}

Metode recordVote akan mengurai status tersimpan dan mengupdatenya dengan suara pengguna, lalu merender ulang kartu. Hasil polling akan diserialisasi dan disimpan dengan kartu setiap kali diperbarui.

Menghubungkan semua bagian

Aplikasi ini hampir selesai. Setelah mengimplementasikan perintah garis miring bersama dengan pemungutan suara, satu-satunya hal yang perlu dilakukan adalah menyelesaikan metode startPoll.

Tapi, ada syaratnya.

Saat konfigurasi polling dikirimkan, aplikasi perlu melakukan dua tindakan:

  1. Menutup dialog.
  2. Memposting pesan baru ke ruang dengan kartu suara.

Sayangnya, balasan langsung ke permintaan HTTP hanya dapat dilakukan sekali, dan itu harus yang pertama. Untuk memposting kartu suara, aplikasi harus menggunakan Chat API untuk membuat pesan baru secara asinkron.

Menambahkan library klien

Jalankan perintah berikut untuk mengupdate dependensi aplikasi guna menyertakan klien Google API untuk Node.js.

npm install --save googleapis

Memulai polling

Update index.js ke versi final di bawah ini:

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],
  }
}

Deploy ulang fungsi:

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

Anda sekarang seharusnya dapat menjalankan aplikasi sepenuhnya. Coba jalankan perintah /poll untuk memberikan pertanyaan dan beberapa pilihan. Setelah dikirim, kartu polling akan muncul.

Berikan suara Anda dan lihat apa yang terjadi.

Melakukan polling sendirian tentu saja tidak begitu berguna, jadi undanglah beberapa teman atau rekan kerja untuk mencobanya.

6. Selamat

Selamat! Anda telah berhasil membangun dan men-deploy aplikasi Google Chat menggunakan Cloud Functions. Meskipun codelab ini membahas banyak konsep inti untuk membangun aplikasi, masih banyak lagi hal yang bisa dipelajari. Baca referensi di bawah dan jangan lupa untuk menghapus project Anda agar terhindar dari biaya tambahan.

Aktivitas tambahan

Jika Anda ingin menjelajahi platform Chat dan aplikasi ini secara lebih mendalam, berikut beberapa hal yang dapat Anda coba sendiri:

  • Apa yang terjadi jika Anda menggunakan fitur sebutan @ untuk aplikasi tersebut? Coba update aplikasi untuk meningkatkan kualitas perilakunya.
  • Menserialisasikan status polling di kartu berfungsi baik untuk ruang kecil, tetapi ada batasannya. Cobalah beralih ke opsi yang lebih baik.
  • Bagaimana jika penulis ingin mengedit polling, atau berhenti mengambil suara baru? Bagaimana Anda akan mengimplementasikan fitur-fitur tersebut?
  • Endpoint aplikasi belum diamankan. Coba tambahkan verifikasi untuk memastikan permintaan berasal dari Google Chat.

Ini hanyalah beberapa cara berbeda untuk meningkatkan kualitas aplikasi. Silakan coba dengan cara Anda sendiri.

Pembersihan

Agar tidak menimbulkan biaya pada akun Google Cloud Platform Anda untuk resource yang digunakan dalam tutorial ini:

  • Di Cloud Console, buka halaman Manage resource. Di pojok kiri atas, klik Menu ikon menu > IAM & Admin > Manage Resources.
  1. Dalam daftar project, pilih project Anda lalu klik Delete.
  2. Pada dialog, ketik project ID, lalu klik Shut down untuk menghapus project.

Pelajari lebih lanjut

Untuk mengetahui informasi selengkapnya tentang cara mengembangkan aplikasi Chat, lihat:

Untuk mengetahui informasi selengkapnya tentang cara melakukan pengembangan di Konsol Google Cloud, lihat: