Membuat aplikasi polling interaktif untuk Google Chat dengan Node.js

1. Pengantar

Aplikasi Google Chat menghadirkan layanan dan referensi Anda langsung ke Google Chat, sehingga memungkinkan pengguna mendapatkan informasi dan melakukan tindakan cepat tanpa keluar dari percakapan.

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

Yang akan Anda pelajari

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

2. Penyiapan dan Persyaratan

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

Prasyarat

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

Penyiapan lingkungan mandiri

  1. Buka Google Cloud Console, lalu buat project.

    Menu pilih projectTombol Project baruProject ID

    Ingatlah project ID, nama unik di semua project Google Cloud (maaf, nama di atas telah 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 Cloud Console.

Menjalankan operasi dalam codelab ini seharusnya tidak memerlukan banyak biaya, bahkan mungkin tidak sama sekali. Pastikan untuk mengikuti petunjuk yang ada di bagian "Membersihkan" di akhir codelab yang memberi tahu Anda cara menonaktifkan resource sehingga tidak menimbulkan penagihan di luar 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 dari jarak jauh dari laptop Anda, dalam codelab ini kami akan menggunakan Google Cloud Shell, lingkungan command line yang berjalan di Google Cloud.

Mengaktifkan Cloud Shell

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

    Ikon Cloud Shell di panel menu

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

    Pesan selamat datang Cloud Shell

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

    Terminal Cloud Shell

    Mesin virtual ini berisi 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 dalam codelab ini dapat dilakukan dengan browser atau Chromebook.Setelah terhubung ke Cloud Shell, Anda akan melihat bahwa Anda sudah diautentikasi dan project sudah ditetapkan ke project ID Anda.
  2. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa Anda telah diautentikasi:
    gcloud auth list
    
    Jika Anda diminta untuk mengizinkan Cloud Shell guna melakukan panggilan API GCP, klik Otorisasi.

    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. Pastikan Anda telah memilih project yang tepat. 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 sisi kanan toolbar Cloud Shell. Editor populer seperti Vim dan Emacs juga tersedia di Cloud Shell.

3 Aktifkan Cloud Functions, Cloud Build, dan Google Chat API

Dari Cloud Shell, aktifkan API dan layanan berikut:

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

Operasi ini mungkin memerlukan waktu beberapa saat untuk selesai.

Setelah selesai, pesan sukses yang serupa dengan pesan ini akan muncul:

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 "Hello world" 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 dan buka direktori tersebut:

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

Semua tugas yang tersisa untuk codelab dan file yang akan Anda buat akan berada di direktori ini.

Lakukan inisialisasi project Node.js:

npm init

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

Membuat backend aplikasi Chat

Saatnya untuk 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 belum melakukan banyak hal, tetapi tidak masalah. Anda akan menambahkan fungsi lainnya nanti.

Terapkan aplikasi

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

Men-deploy Cloud Function

Untuk men-deploy Cloud Function aplikasi "Hello world", masukkan perintah berikut:

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

Jika sudah selesai, output 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'

Perhatikan URL fungsi yang di-deploy di properti httpsTrigger.url. Anda akan menggunakannya pada langkah berikutnya.

Mengonfigurasi aplikasi

Untuk mengonfigurasi aplikasi, bukaKonfigurasi Chat pada Cloud Console (API & Layanan >Dasbor >API Hangouts Chat >Konfigurasi ).

  1. Di Nama aplikasi, masukkan "PollCodelab".
  2. Di URL Avatar, masukkan https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png.
  3. Di Deskripsi, masukkan "Aplikasi polling untuk codelab".
  4. pada Fungsi, pilih Aplikasi dapat menerima pesan langsung dan Aplikasi berfungsi di ruang dengan beberapa pengguna.
  5. Di bagian Setelan koneksi, pilih URL Aplikasi dan tempel URL untuk Cloud Function (properti httpsTrigger.url dari bagian terakhir).
  6. Pada Izin, pilih Orang dan grup tertentu di domain, lalu masukkan alamat email Anda.
  7. Klik simpan.

Kini aplikasi sudah siap mengirim pesan.

Menguji aplikasi

Sebelum melanjutkan, pastikan aplikasi berfungsi dengan menambahkannya ke ruang di Google Chat.

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

Aplikasi akan merespons dengan pesan halo singkat.

Sekarang, setelah kerangka dasar sudah siap, saatnya mengubahnya menjadi lebih berguna!

5. Membuat 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 juga memerlukan beberapa status untuk menyimpan konfigurasi dan hasil polling. Hal ini dapat dilakukan dengan Firestore atau database apa pun, atau status dapat disimpan di pesan aplikasi itu sendiri. Karena aplikasi ini ditujukan untuk polling singkat tim secara cepat, penyimpanan status dalam pesan aplikasi sangat cocok untuk kasus penggunaan ini.

Model data untuk aplikasi (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, negara bagian ini menyertakan nama dan ID penulis serta rekaman suara. Untuk mencegah pengguna memberikan suara beberapa kali, suara disimpan sebagai peta ID pengguna ke indeks pilihan mereka.

Tentu saja, ada banyak pendekatan yang berbeda, namun hal ini memberikan titik awal yang baik untuk menjalankan polling cepat dalam ruang.

Menerapkan perintah konfigurasi polling

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

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

Mendaftarkan perintah garis miring

Untuk mendaftarkan perintah garis miring, kembali ke halaman Konfigurasi Chat di konsol (API & Layanan > Dasbor > Hangouts Chat API > Konfigurasi).

  1. Pada bagian Perintah garis miring, klik Tambahkan perintah garis miring.
  2. Di Nama, masukkan "/pol"
  3. Di Command id, masukkan "1"
  4. Di Deskripsi, masukkan "Mulai polling".
  5. Pilih Opens a dialog.
  6. Klik Done.
  7. Klik Save.

Aplikasi kini mengenali perintah /poll, dan membuka dialog. Selanjutnya, mari kita konfigurasi dialog.

Membuat formulir konfigurasi sebagai dialog

Perintah garis miring 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 formulir dialog yang memungkinkan pengguna menyiapkan polling. Contoh ini juga mengekspor konstanta untuk jumlah maksimum pilihan yang dapat dimiliki pertanyaan. Ini adalah praktik yang baik untuk mengisolasi mem-build markup UI menjadi fungsi stateless dengan status apa pun yang diteruskan sebagai parameter. Ini mempermudah penggunaan kembali, dan selanjutnya kartu ini akan dirender dalam konteks yang berbeda.

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

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

Menangani perintah garis miring

Perintah garis miring muncul sebagai peristiwa MESSAGE saat dikirim ke aplikasi. Mengupdate index.js untuk memeriksa keberadaan perintah garis miring melalui peristiwa MESSAGE dan 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 kini akan menampilkan dialog dengan saat perintah /poll dipanggil. Uji interaksi dengan menekan kembali 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 mengirim peristiwa CARD_CLICKED dengan tindakan kustom start_poll. Peristiwa ditangani di titik entri yang diperbarui tempatnya memanggil metode startPoll. Untuk saat ini, metode startPoll diganti untuk hanya menutup dialog. Di bagian berikutnya, Anda akan menerapkan fungsi pemberian suara dan menghubungkan semua bagian secara bersamaan.

Terapkan kartu pemungutan suara

Untuk menerapkan bagian pemberian suara pada aplikasi, mulailah dengan menentukan kartu interaktif yang menyediakan antarmuka bagi orang untuk memberikan suara.

Menerapkan antarmuka voting

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 serupa dengan pendekatan yang digunakan pada 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 voting

Kartu voting menyertakan tombol untuk setiap pilihan. Indeks pilihan tersebut, beserta status serial polling, dilampirkan ke tombol tersebut. Aplikasi menerima CARD_CLICKED dengan tindakan vote beserta data yang dilampirkan 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 mengurai status tersimpan dan memperbaruinya dengan suara pengguna, lalu merender ulang kartu. Hasil polling diserialkan dan disimpan dengan kartu setiap kali diperbarui.

Hubungkan bagian-bagian

Aplikasi ini hampir selesai. Dengan perintah garis miring yang diimplementasikan beserta pemungutan suara, satu-satunya hal yang harus dilakukan adalah menyelesaikan metode startPoll.

Tapi, ada yang mau Anda bayar.

Saat konfigurasi polling dikirim, aplikasi perlu melakukan dua tindakan:

  1. Tutup dialog.
  2. Posting pesan baru ke ruang berisi kartu voting.

Sayangnya, balasan langsung ke permintaan HTTP hanya dapat melakukan satu, dan harus menjadi 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 agar menyertakan klien Google API untuk Node.js.

npm install --save googleapis

Memulai polling

Update index.js ke versi final di bawah:

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

Sekarang Anda sudah dapat sepenuhnya melatih aplikasi. Coba panggil perintah /poll untuk memberikan pertanyaan dan beberapa pilihan. Setelah dikirim, kartu polling akan muncul.

Berikan suara Anda dan lihat apa yang terjadi.

Tentu saja polling sendiri tidak terlalu berguna, jadi undang beberapa teman atau rekan kerja untuk mencobanya.

6. Selamat

Selamat! Anda telah berhasil mem-build dan men-deploy aplikasi Google Chat menggunakan Cloud Functions. Meskipun codelab membahas banyak konsep inti untuk mem-build aplikasi, ada banyak hal lainnya yang dapat dijelajahi. Lihat referensi di bawah ini dan jangan lupa membersihkan project Anda untuk menghindari biaya tambahan.

Aktivitas tambahan

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

  • Apa yang terjadi saat Anda @ menyebut aplikasi? Coba update aplikasi untuk memperbaiki perilaku.
  • Melakukan serialisasi status polling di kartu tidak masalah untuk ruang kecil, tetapi memiliki batasan. Coba beralih ke opsi yang lebih baik.
  • Bagaimana jika penulis ingin mengedit polling, atau berhenti mengambil suara baru? Bagaimana cara Anda menerapkan fitur tersebut?
  • Endpoint aplikasi belum diamankan. Coba tambahkan beberapa verifikasi untuk memastikan permintaan berasal dari Google Chat.

Ini hanyalah beberapa cara berbeda untuk meningkatkan aplikasi. Bersenang-senang dan gunakan imajinasi Anda!

Pembersihan

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

  • Di Cloud Console, buka halaman Mengelola resource. Klik Di pojok kiri atas, klik Menuikon menu > IAM & Admin > Manage Resources.
  1. Dalam daftar project, pilih project Anda lalu klik Hapus.
  2. Pada dialog, ketik project ID, lalu klik Matikan untuk menghapus project.

Pelajari lebih lanjut

Untuk mengetahui informasi selengkapnya tentang mengembangkan aplikasi Chat, lihat:

Untuk informasi selengkapnya tentang pengembangan di Google Cloud Console, lihat: