Percakapan (conversations
)
Jika kamu mencari cara untuk membuat obrolan yang saling berkesinambungan, plugin ini merupakan pilihan yang tepat.
Sebagai contoh, kamu ingin bot menanyakan tiga pertanyaan ke user:
- menu apa yang ingin dipesan,
- berapa jumlahnya, dan
- di mana alamat pengirimannya.
Berikut kira-kira percakapan yang dapat dibuat:
Bot : "Halo, User_42069! Mau pesan apa hari ini?"
User_42069 : "Nasi goreng"
Bot : "Baik. Berapa item yang ingin dipesan?"
User_42069 : "3"
Bot : "Oke. Mau dikirim ke mana pesanannya?"
User_42069 : "Perumahan Komodo Blok A-1, Manggarai Barat"
Bot : "Pesanan akan segera dikirim ke alamat tujuan!"
Seperti yang kita lihat, bot akan menunggu jawaban dari user untuk setiap pertanyaan yang diajukan. Kemampuan itulah yang ditawarkan oleh plugin ini.
Mulai Cepat
Plugin percakapan membawa konsep baru yang tidak akan kamu temukan di belahan dunia mana pun. Sebelum melangkah ke sana, silahkan bermain-main dengan contoh mulai cepat berikut:
import { Bot, type Context } from "grammy";
import {
type Conversation,
type ConversationFlavor,
conversations,
createConversation,
} from "@grammyjs/conversations";
const bot = new Bot<ConversationFlavor<Context>>(""); // <-- taruh token bot di antara "" (https://t.me/BotFather)
bot.use(conversations());
/** Buat percakapannya */
async function hello(conversation: Conversation, ctx: Context) {
await ctx.reply("Halo! Siapa nama kamu?");
const { message } = await conversation.waitFor("message:text");
await ctx.reply(`Selamat datang di chat, ${message.text}!`);
}
bot.use(createConversation(hello));
bot.command("enter", async (ctx) => {
// Masuk ke function "hello" yang telah kita buat di atas.
await ctx.conversation.enter("hello");
});
bot.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { Bot } = require("grammy");
const { conversations, createConversation } = require(
"@grammyjs/conversations",
);
const bot = new Bot(""); // <-- taruh token bot di antara "" (https://t.me/BotFather)
bot.use(conversations());
/** Buat percakapannya */
async function hello(conversation, ctx) {
await ctx.reply("Halo! Siapa nama kamu?");
const { message } = await conversation.waitFor("message:text");
await ctx.reply(`Selamat datang di chat, ${message.text}!`);
}
bot.use(createConversation(hello));
bot.command("enter", async (ctx) => {
// Masuk ke function "hello" yang telah kita buat di atas.
await ctx.conversation.enter("hello");
});
bot.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Bot, type Context } from "https://deno.land/x/grammy@v1.35.0/mod.ts";
import {
type Conversation,
type ConversationFlavor,
conversations,
createConversation,
} from "https://deno.land/x/grammy_conversations@v2.0.1/mod.ts";
const bot = new Bot<ConversationFlavor<Context>>(""); // <-- taruh token bot di antara "" (https://t.me/BotFather)
bot.use(conversations());
/** Buat percakapannya */
async function hello(conversation: Conversation, ctx: Context) {
await ctx.reply("Halo! Siapa nama kamu?");
const { message } = await conversation.waitFor("message:text");
await ctx.reply(`Selamat datang di chat, ${message.text}!`);
}
bot.use(createConversation(hello));
bot.command("enter", async (ctx) => {
// Masuk ke function "hello" yang telah kita buat di atas.
await ctx.conversation.enter("hello");
});
bot.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Ketika percakapan hello
dijalankan, berikut yang akan terjadi secara berurutan:
- Bot mengirim pesan
Halo! Siapa nama kamu
.? - Bot menunggu balasan pesan teks dari user.
- Bot mengirim pesan
Selamat datang di chat
., (nama user)! - Percakapan berakhir.
Sekarang, mari kita lanjut ke bagian menariknya.
Cara Kerja Plugin Percakapan
Berikut bagaimana suatu pesan ditangani menggunakan cara tradisional:
bot.on("message", async (ctx) => {
// tangani satu pesan
});
2
3
Di penangan pesan biasa, kamu hanya bisa memiliki satu context object.
Coba bandingkan dengan percakapan:
async function hello(conversation: Conversation, ctx0: Context) {
const ctx1 = await conversation.wait();
const ctx2 = await conversation.wait();
// menangani tiga pesan
}
2
3
4
5
Di percakapan, kamu bisa memiliki tiga context object!
Layaknya penangan biasa, plugin percakapan hanya menerima satu context object yang berasal dari sistem middleware. Tiba-tiba, sekarang jadi tersedia tiga context object. Kok bisa?
Rahasianya adalah function percakapan tidak dieksekusi selayaknya function pada umumnya (meski sebenarnya kita bisa saja memprogramnya seperti itu).
Plugin Percakapan Ibarat Mesin Pengulang
Function percakapan tidak dieksekusi selayaknya function pada umumnya.
Ketika memasuki sebuah percakapan, ia hanya dieksekusi hingga pemanggilan wait()
pertama. Function tersebut kemudian akan diinterupsi dan tidak akan dieksekusi lebih lanjut. Plugin akan mengingat bahwa wait()
telah tercapai dan menyimpan semua informasi terkait.
Kemudian, ketika update selanjutnya tiba, percakapan akan dieksekusi lagi dari awal. Bedanya, kali ini, tidak ada pemanggilan API yang dilakukan, yang mana membuat kode kamu berjalan sangat cepat dan tidak memiliki dampak apapun. Aksi tersebut dinamakan replay atau ulang. Setelah tiba di pemanggilan wait()
yang telah tercapai di pemrosesan sebelumnya, pengeksekusian function dilanjutkan secara normal.
async function hello( // |
conversation: Conversation, // |
ctx0: Context, // |
) { // |
await ctx0.reply("Halo!"); // |
const ctx1 = await conversation.wait(); // A
await ctx1.reply("Halo lagi!"); //
const ctx2 = await conversation.wait(); //
await ctx2.reply("Selamat tinggal!"); //
} //
2
3
4
5
6
7
8
9
10
async function hello( // .
conversation: Conversation, // .
ctx0: Context, // .
) { // .
await ctx0.reply("Halo!"); // .
const ctx1 = await conversation.wait(); // A
await ctx1.reply("Halo lagi!"); // |
const ctx2 = await conversation.wait(); // B
await ctx2.reply("Selamat tinggal!"); //
} //
2
3
4
5
6
7
8
9
10
async function hello( // .
conversation: Conversation, // .
ctx0: Context, // .
) { // .
await ctx0.reply("Halo!"); // .
const ctx1 = await conversation.wait(); // A
await ctx1.reply("Halo lagi!"); // .
const ctx2 = await conversation.wait(); // B
await ctx2.reply("Selamat tinggal!"); // |
} // —
2
3
4
5
6
7
8
9
10
- Ketika memasuki sebuah percakapan, function akan dieksekusi hingga
A
. - Ketika update selanjutnya tiba, function akan diulang hingga
A
, lalu dieksekusi secara normal dariA
hinggaB
. - Ketika update terakhir tiba, function akan diulang hingga
B
, lalu dieksekusi secara normal sampai akhir.
Dari ilustrasi di atas, kita tahu bahwa setiap baris kode yang ditulis akan dieksekusi beberapa kali—sekali secara normal, dan beberapa kali selama pengulangan. Oleh karena itu, baik ketika dieksekusi pertama kali, maupun ketika dieksekusi berkali-kali, kode yang ditulis harus dipastikan memiliki perilaku yang sama.
Jika kamu melakukan pemanggilan API melalui ctx
—termasuk ctx
, plugin akan menanganinya secara otomatis. Sebaliknya, yang perlu mendapat perhatikan khusus adalah komunikasi database kamu.
Berikut yang perlu diperhatikan:
Pedoman Penggunaan
Setelah memahami cara kerja plugin percakapan, kita akan menentukan satu aturan utama untuk kode yang berada di dalam function percakapan. Aturan ini wajib dipatuhi agar kode dapat berjalan dengan baik.
ATURAN UTAMA
Setiap kode yang memiliki perilaku berbeda di setiap pengulangan wajib dibungkus dengan conversation
.
Cara penerapannya seperti ini:
// SALAH
const response = await aksesDatabase();
// BENAR
const response = await conversation.external(() => aksesDatabase());
2
3
4
Dengan membungkus sebagian kode menggunakan conversation
, kamu telah memberi tahu plugin bahwa kode tersebut harus diabaikan selama proses pengulangan. Nilai kembalian kode tersebut akan disimpan oleh plugin, lalu digunakan kembali di pengulangan selanjutnya. Hasilnya, berdasarkan contoh di atas, akses ke database hanya akan dilakukan sekali selama proses pengulangan berlangsung.
GUNAKAN conversation
untuk …
- membaca atau menulis file, database/session, jaringan, atau status global (global state),
- memanggil
Math
atau.random() Date
,.now() - melakukan pemanggilan API menggunakan
bot
atau instance.api Api
independen lainnya.
JANGAN GUNAKAN conversation
untuk …
- memanggil
ctx
atau context action lainnya,.reply - memanggil
ctx
atau method API Bot lain menggunakan.api .send Message ctx
..api
Selain itu, plugin percakapan juga menyediakan beberapa method pembantu untuk conversation
. Ia tidak hanya mempermudah penggunaan Math
dan Date
, tetapi juga mempermudah debugging dengan cara menyembunyikan log selama proses pengulangan.
// await conversation.external(() => Math.random());
const rnd = await conversation.random();
// await conversation.external(() => Date.now());
const now = await conversation.now();
// await conversation.external(() => console.log("abc"));
await conversation.log("abc");
2
3
4
5
6
Pertanyaannya, kok bisa conversation
dan conversation
memulihkan nilai kembalian tersebut ketika proses pengulangan berlangsung? Pasti ia sebelumnya telah mengingat dan menyimpan nilai tersebut, bukan?
Tepat sekali!
Percakapan Menyimpan Nilai Terkait
Percakapan menyimpan dua jenis data di database. Secara bawaan, ia menggunakan database ringan berbasis Map
yang disimpan di memory. Tetapi, kamu bisa dengan mudah menggunakan database permanen jika mengehendakinya.
Berikut beberapa hal yang perlu kamu ketahui:
- Plugin percakapan menyimpan semua update.
- Plugin percakapan menyimpan semua nilai kembalian
conversation
dan hasil pemanggilan API yang dilakukan..external
Segelintir update di dalam percakapan memang tidak akan menyebabkan masalah yang serius—perlu diingat, satu pemanggilan get
menggunakan long polling bisa mencapai 100 update.
Namun, jika kamu tidak pernah keluar dari suatu percakapan, lambat laun data-data tersebut akan terus menumpuk yang mengakibatkan penurunan performa bot secara signifikan. Oleh karena itu, hindari pengulangan yang tidak berujung (infinite loops).
Context Object Percakapan
Ketika suatu percakapan dieksekusi, ia menggunakan update tersimpan untuk membuat context object dari dasar. Context object tersebut berbeda dengan context object yang digunakan di middleware. Jika menggunakan TypeScript, kamu akan memiliki dua varian context object:
- Context object luar merupakan context object yang digunakan di middleware. Ia menyediakan akses ke
ctx
. Untuk TypeScript, kamu perlu menyertakan.conversation .enter Conversation
. Context object luar juga bisa memiliki property tambahan untuk setiap plugin yang diinstal melaluiFlavor bot
..use - Context object dalam—atau biasa disebut sebagai context object percakapan—merupakan context object yang dihasilkan oleh plugin percakapan. Ia tidak menyediakan akses ke
ctx
, dan secara bawaan, ia juga tidak menyediakan akses ke plugin mana pun. Jika kamu ingin context object dalam memiliki property tersuai, silahkan gulir ke bawah..conversation .enter
Selain itu, kedua context type luar dan dalam juga perlu disertakan ke percakapan. Kode TypeScript kamu seharusnya kurang lebih seperti ini:
import { Bot, type Context } from "grammy";
import {
type Conversation,
type ConversationFlavor,
} from "@grammyjs/conversations";
// Context object luar (mencakup semua plugin middleware)
type MyContext = ConversationFlavor<Context>;
// Context object dalam (mencakup semua plugin percakapan)
type MyConversationContext = Context;
// Gunakan context type luar untuk bot.
const bot = new Bot<MyContext>("");
// Gunakan kedua type luar dan dalam untuk percakapan.
type MyConversation = Conversation<MyContext, MyConversationContext>;
// Buat percakapannya.
async function example(
conversation: MyConversation,
ctx0: MyConversationContext,
) {
// Semua context object di dalam percakapan
// memiliki type `MyConversationContext`.
const ctx1 = await conversation.wait();
// Context object luar dapat diakses
// melalui `conversation.external` dan
// telah dikerucutkan menjadi type `MyContext`.
const session = await conversation.external((ctx) => ctx.session);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Bot, type Context } from "https://deno.land/x/grammy@v1.35.0/mod.ts";
import {
type Conversation,
type ConversationFlavor,
} from "https://deno.land/x/grammy_conversations@v2.0.1/mod.ts";
// Context object luar (mencakup semua plugin middleware)
type MyContext = ConversationFlavor<Context>;
// Context object dalam (mencakup semua plugin percakapan)
type MyConversationContext = Context;
// Gunakan context type luar untuk bot.
const bot = new Bot<MyContext>("");
// Gunakan kedua type luar dan dalam untuk percakapan.
type MyConversation = Conversation<MyContext, MyConversationContext>;
// Buat percakapannya.
async function example(
conversation: MyConversation,
ctx0: MyConversationContext,
) {
// Semua context object di dalam percakapan
// memiliki type `MyConversationContext`.
const ctx1 = await conversation.wait();
// Context object luar dapat diakses
// melalui `conversation.external` dan
// telah dikerucutkan menjadi type `MyContext`.
const session = await conversation.external((ctx) => ctx.session);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Kode di atas tidak mencontohkan adanya plugin yang terinstal di percakapan. Namun, ketika kamu menginstalnya,
My
tidak akan lagi berupa typeConversation Context Context
dasar.
Dengan demikian, setiap percakapan bisa memiliki variasi context type yang berbeda-beda sesuai dengan keinginan.
Selamat! Jika kamu dapat memahami semua materi di atas dengan lancar, bagian tersulit dari panduan ini telah berhasil kamu lewati. Selanjunya, kita akan membahas fitur-fitur yang ditawarkan oleh plugin ini.
Memasuki Percakapan
Kamu bisa memasuki suatu percakapan melalui penangan biasa.
Secara bawaan, nama suatu percakapan akan identik dengan nama function-nya. Kamu bisa mengganti nama tersebut ketika menginstalnya ke bot.
Percakapan juga bisa menerima beberapa argument. Tetapi ingat, argument tersebut akan disimpan dalam bentuk string JSON. Artinya, kamu perlu memastikan ia dapat diproses oleh JSON
.
Selain itu, kamu juga bisa memasuki sebuah percakapan dari percakapan lain dengan cara memanggil function JavaScript biasa. Function tersebut nantinya dapat mengakses nilai kembalian percakapan yang dipanggil tersebut. Akan tetapi, akses yang sama tidak bisa didapatkan jika kamu memasuki percakapan dari dalam middleware.
/**
* Nilai kembalian function JavaScript berikut
* hanya bisa diakses ketika percakapan ini
* dipanggil dari percakapan lainnya.
*/
async function jokesBapakBapak(conversation: Conversation, ctx: Context) {
await ctx.reply("Kota apa yang warganya bapak-bapak semua?");
return "Purwo-daddy";
}
/**
* Function berikut menerima dua argument: `answer` dan `config`.
* Semua argument wajib berupa tipe yang bisa diubah ke JSON.
*/
async function percakapan(
conversation: Conversation,
ctx: Context,
answer: string,
config: { text: string },
) {
const jawaban = await jokesBapakBapak(conversation, ctx);
if (answer === jawaban) {
await ctx.reply(jawaban);
await ctx.reply(config.text);
}
}
/**
* Ubah nama function `jokesBapakBapak` menjadi `tebak-receh`.
*/
bot.use(createConversation(jokesBapakBapak, "tebak-receh"));
bot.use(createConversation(percakapan));
/**
* Command berikut hanya akan memberi tebakan
* tanpa memberi tahu jawabannya.
*/
bot.command("tebak", async (ctx) => {
await ctx.conversation.enter("tebak-receh");
});
/**
* Command berikut akan memberi tebakan
* sekaligus memberi tahu jawabannya.
*/
bot.command("tebak_jawab", async (ctx) => {
/**
* Untuk menyerderhanakan contoh kode,
* kita menginput kedua argument secara statis,
* yaitu `Purwo-daddy` dan `{ text: "Xixixi..." }`.
*
* Untuk kasus tebak-tebakan ini
* mungkin akan jauh lebih menarik
* jika argument tersebut dibuat dinamis.
* Misalnya, argument pertama ("Purwo-daddy")
* dapat diganti dengan jawaban user.
*
* Selamat bereksperimen!
*/
await ctx.conversation.enter("percakapan", "Purwo-daddy", {
text: "Xixixi...",
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* Nilai kembalian function JavaScript berikut
* hanya bisa diakses ketika percakapan ini
* dipanggil dari percakapan lainnya.
*/
async function jokesBapakBapak(conversation, ctx) {
await ctx.reply("Kota apa yang warganya bapak-bapak semua?");
return "Purwo-daddy";
}
/**
* Function berikut menerima dua argument: `answer` dan `config`.
* Semua argument wajib berupa tipe yang bisa diubah ke JSON.
*/
async function percakapan(conversation, ctx, answer, config) {
const jawaban = await jokesBapakBapak(conversation, ctx);
if (answer === jawaban) {
await ctx.reply(jawaban);
await ctx.reply(config.text);
}
}
/**
* Ubah nama function `jokesBapakBapak` menjadi `tebak-receh`.
*/
bot.use(createConversation(jokesBapakBapak, "tebak-receh"));
bot.use(createConversation(percakapan));
/**
* Command berikut hanya akan memberi tebakan
* tanpa memberi tahu jawabannya.
*/
bot.command("tebak", async (ctx) => {
await ctx.conversation.enter("tebak-receh");
});
/**
* Command berikut akan memberi tebakan
* sekaligus memberi tahu jawabannya.
*/
bot.command("tebak_jawab", async (ctx) => {
/**
* Untuk menyerderhanakan contoh kode,
* kita menginput kedua argument secara statis,
* yaitu `Purwo-daddy` dan `{ text: "Xixixi..." }`.
*
* Untuk kasus tebak-tebakan ini
* mungkin akan jauh lebih menarik
* jika argument tersebut dibuat dinamis.
* Misalnya, argument pertama ("Purwo-daddy")
* dapat diganti dengan jawaban user.
*
* Selamat bereksperimen!
*/
await ctx.conversation.enter("percakapan", "Purwo-daddy", {
text: "Xixixi...",
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Type Safety untuk Argument
Pastikan parameter percakapan kamu menggunakan type yang sesuai, dan argument yang diteruskan ke pemanggilan enter
cocok dengan type tersebut. Plugin percakapan tidak dapat melakukan pengecekan type di luar conversation
dan ctx
.
Perlu diperhatikan bahwa urutan middleware akan berpengaruh. Suatu percakapan hanya bisa dimasuki jika ia diinstal sebelum penangan melakukan pemanggilan enter
.
Menunggu Update
Tujuan pemanggilan wait
yang paling dasar adalah menunggu update selanjutnya tiba.
const ctx = await conversation.wait();
Ia mengembalikan sebuah context object. Semua pemanggilan wait
memiliki konsep dasar ini.
Memilah Pemanggilan wait
Jika kamu ingin menunggu jenis update tertentu, kamu bisa menerapkan pemilahan ke pemanggilan wait
.
// Pilah layaknya filter query di `bot.on`
const message = await conversation.waitFor("message");
// Pilah pesan teks layaknya `bot.hears`.
const hears = await conversation.waitForHears(/regex/);
// Pilah command layaknya `bot.command`.
const start = await conversation.waitForCommand("start");
// Dan sebagainya...
2
3
4
5
6
7
Silahkan lihat referensi API berikut untuk mengetahui semua metode yang tersedia untuk memilah pemanggilan wait
.
Pemanggilan wait
terpilah memastikan update yang diterima sesuai dengan filter yang diterapkan. Jika bot menerima sebuah update yang tidak sesuai, update tersebut akan diabaikan begitu saja. Untuk mengatasinya, kamu bisa menginstal sebuah callback function agar function tersebut dipanggil ketika update yang diterima tidak sesuai.
const message = await conversation.waitFor(":photo", {
otherwise: (ctx) =>
ctx.reply("Maaf, saya hanya bisa menerima pesan berupa foto."),
});
2
3
4
Semua pemanggilan wait
terpilah bisa saling dirangkai untuk memilah beberapa hal sekaligus.
// Pilah foto yang mengandung keterangan "Indonesia"
let photoWithCaption = await conversation.waitFor(":photo")
.andForHears("Indonesia");
// Tangani setiap pemilahan menggunakan function `otherwise`
// yang berbeda:
photoWithCaption = await conversation
.waitFor(":photo", {
otherwise: (ctx) => ctx.reply("Mohon kirimkan saya sebuah foto!"),
})
.andForHears("Indonesia", {
otherwise: (ctx) =>
ctx.reply('Keterangan foto selain "Indonesia" tidak diperbolehkan.'),
});
2
3
4
5
6
7
8
9
10
11
12
13
Jika kamu menerapkan otherwise
ke salah satu pemanggilan wait
saja, ia hanya akan dipanggil untuk filter tersebut.
Memeriksa Context Object
Mengurai context object merupakan hal yang cukup umum untuk dilakukan. Dengan melakukan penguraian, kamu bisa melakukan pengecekan secara mendalam untuk setiap data yang diterima.
const { message } = await conversation.waitFor("message");
if (message.photo) {
// Tangani pesan foto
}
2
3
4
Sebagai tambahan, percakapan juga merupakan tempat yang ideal untuk melakukan pengecekan menggunakan has
Keluar dari Percakapan
Cara paling mudah untuk keluar dari suatu percakapan adalah dengan melakukan return
. Selain itu, percakapan juga bisa dihentikan dengan melempar sebuah error.
Jika cara di atas masih belum cukup, kamu bisa secara paksa menghentikan suatu percakapan menggunakan halt
:
async function convo(conversation: Conversation, ctx: Context) {
// Semua percabangan berikut mencoba keluar dari percakapan:
if (ctx.message?.text === "return") {
return;
} else if (ctx.message?.text === "error") {
throw new Error("ERROR!");
} else {
await conversation.halt(); // tidak akan pernah mengembalikan nilai (return)
}
}
2
3
4
5
6
7
8
9
10
Kamu juga bisa keluar dari suatu percakapan dari dalam middleware:
bot.use(conversations());
bot.command("keluar", async (ctx) => {
await ctx.conversation.exit("convo");
});
2
3
4
Cara-cara di atas bisa dilakukan bahkan sebelum percakapan yang ditarget diinstal ke sistem middleware. Dengan kata lain, hanya dengan menginstal plugin percakapan itu sendiri, kamu bisa melakukan hal-hal di atas.
Percakapan Hanyalah Sebuah JavaScript
Setelah efek samping teratasi, percakapan hanyalah sebuah function JavaScript biasa. Meski alur kerjanya terlihat aneh, biasanya ketika mengembangkan sebuah bot, kita akan dengan mudah mengabaikannya. Semua syntax JavaScript biasa dapat ia proses dengan baik.
Semua hal yang dibahas di bagian selanjutnya cukup lazim jika kamu terbiasa menggunakan percakapan. Namun, jika masih awam, beberapa hal berikut akan terdengar asing.
Variable, Percabangan, dan Perulangan
Kamu bisa menggunakan variable biasa untuk menyimpan suatu nilai/status di antara setiap update. Percabangan menggunakan if
atau switch
juga bisa dilakukan. Hal yang sama juga berlaku untuk perulangan for
dan while
.
await ctx.reply(
"Kirim semua nomor favoritmu! Pisah setiap nomor dengan tanda koma!",
);
const { message } = await conversation.waitFor("message:text");
const numbers = message.text.split(",");
let jumlah = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
jumlah += n;
}
}
await ctx.reply("Jumlah nomor-nomor tersebut adalah " + jumlah);
2
3
4
5
6
7
8
9
10
11
12
13
Lihat? Ia hanyalah sebuah JavaScript, bukan?
Function dan Rekursif
Kamu bisa membagi suatu percakapan menjadi beberapa function. Mereka dapat memanggil satu sama lain atau bahkan melakukan rekursif (memanggil dirinya sendiri). Malahan, plugin percakapan tidak tahu kalau kamu telah menggunakan sebuah function.
Berikut kode yang sama seperti di atas, tetapi di-refactor menjadi beberapa function:
/** Percakapan untuk menghitung jumlah semua angka */
async function sumConvo(conversation: Conversation, ctx: Context) {
await ctx.reply(
"Kirim semua nomor favoritmu! Pisah setiap nomor dengan tanda koma!",
);
const { message } = await conversation.waitFor("message:text");
const numbers = message.text.split(",");
await ctx.reply("Jumlah nomor-nomor tersebut adalah " + sumStrings(numbers));
}
/** Konversi semua string menjadi angka, lalu hitung jumlahnya */
function sumStrings(numbers: string[]): number {
let jumlah = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
jumlah += n;
}
}
return jumlah;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Percakapan untuk menghitung jumlah semua angka */
async function sumConvo(conversation, ctx) {
await ctx.reply(
"Kirim semua nomor favoritmu! Pisah setiap nomor dengan tanda koma!",
);
const { message } = await conversation.waitFor("message:text");
const numbers = message.text.split(",");
await ctx.reply("Jumlah nomor-nomor tersebut adalah " + sumStrings(numbers));
}
/** Konversi semua string menjadi angka, lalu hitung jumlahnya */
function sumStrings(numbers) {
let jumlah = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
jumlah += n;
}
}
return jumlah;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Sekali lagi, ia hanyalah sebuah JavaScript.
Module dan Class
JavaScript memiliki higher-order function, class, serta metode-metode lain untuk mengubah struktur kode menjadi beberapa module. Umumnya, mereka semua bisa diubah menjadi percakapan.
Sekali lagi, berikut kode yang sama seperti di atas, tetapi di-refactor menjadi sebuah module sederhana:
/**
* Module untuk menjumlahkan semua angka yang diberikan
* oleh user.
*
* Penangan percakapan harus disematkan agar module
* dapat dijalankan.
*/
function sumModule(conversation: Conversation) {
/** Konversi semua string menjadi angka, lalu hitung jumlahnya */
function sumStrings(numbers) {
let jumlah = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
jumlah += n;
}
}
return jumlah;
}
/** Minta user untuk mengirim semua nomor favoritnya */
async function askForNumbers(ctx: Context) {
await ctx.reply(
"Kirim semua nomor favoritmu! Pisah setiap nomor dengan tanda koma!",
);
}
/** Tunggu user mengirim nomor-nomornya, lalu balas dengan jumlah semua nomor tersebut */
async function sumUserNumbers() {
const ctx = await conversation.waitFor(":text");
const jumlah = sumStrings(ctx.msg.text);
await ctx.reply("Jumlah nomor-nomor tersebut adalah " + jumlah);
}
return { askForNumbers, sumUserNumbers };
}
/** Percakapan untuk menjumlahkan semua nomor */
async function sumConvo(conversation: Conversation, ctx: Context) {
const mod = sumModule(conversation);
await mod.askForNumbers(ctx);
await mod.sumUserNumbers();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Module untuk menjumlahkan semua angka yang diberikan
* oleh user.
*
* Penangan percakapan harus disematkan agar module
* dapat dijalankan.
*/
function sumModule(conversation: Conversation) {
/** Konversi semua string menjadi angka, lalu hitung jumlahnya */
function sumStrings(numbers) {
let jumlah = 0;
for (const str of numbers) {
const n = parseInt(str.trim(), 10);
if (!isNaN(n)) {
jumlah += n;
}
}
return jumlah;
}
/** Minta user untuk mengirim semua nomor favoritnya */
async function askForNumbers(ctx: Context) {
await ctx.reply("Kirim semua nomor favoritmu! Pisah setiap nomor dengan tanda koma!");
}
/** Tunggu user mengirim nomor-nomornya, lalu balas dengan jumlah semua nomor tersebut */
async function sumUserNumbers() {
const ctx = await conversation.waitFor(":text");
const sum = sumStrings(ctx.msg.text);
await ctx.reply("Jumlah nomor-nomor tersebut adalah: " + sum);
}
return { askForNumbers, sumUserNumbers };
}
/** Percakapan untuk menjumlahkan semua nomor */
async function sumConvo(conversation: Conversation, ctx: Context) {
const mod = sumModule(conversation);
await mod.askForNumbers(ctx);
await mod.sumUserNumbers();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Meski terlihat berlebihan untuk tugas sesederhana menjumlahkan nomor, namun kamu bisa menangkap secara garis besar konsep yang kami maksud.
Yup, kamu benar, ia hanyalah sebuah JavaScript.
Menyimpan Percakapan
Secara bawaan, semua data yang disimpan oleh plugin percakapan disimpan di dalam memory. Artinya, ketika memory tersebut dimatikan, semua proses akan keluar dari percakapan, sehingga mau tidak mau harus dimulai ulang.
Jika ingin menyimpan data-data tersebut ketika server dimulai ulang, kamu harus mengintegrasikan plugin percakapan ke sebuah database. Kami telah membuat berbagai jenis storage adapter untuk mempermudah pengintegrasian tersebut. Mereka semua menggunakan adapter yang sama yang digunakan oleh plugin session.
Katakanlah kamu hendak menyimpan data terkait ke sebuah file bernama data
ke dalam direktori di sebuah diska. Berarti, kamu memerlukan File
.
import { FileAdapter } from "@grammyjs/storage-file";
bot.use(conversations({
storage: new FileAdapter({ dirName: "data-percakapan" }),
}));
2
3
4
5
import { FileAdapter } from "https://deno.land/x/grammy_storages@v2.4.2/file/src/mod.ts";
bot.use(conversations({
storage: new FileAdapter({ dirName: "data-percakapan" }),
}));
2
3
4
5
Selesai!
Semua jenis storage adapter bisa digunakan asalkan ia mampu menyimpan data berupa Versioned
dari Conversation
. Kedua type tersebut dapat di-import dari plugin percakapan secara langsung. Dengan kata lain, jika kamu ingin menempatkan storage tersebut ke sebuah variable, kamu bisa melakukannya menggunakan type berikut:
const storage = new FileAdapter<VersionedState<ConversationData>>({
dirName: "data-percakapan",
});
2
3
Secara umum, type yang sama juga bisa diterapkan ke storage adapter lainnya.
Membuat Versi Data
Jika status percapakan disimpan di sebuah database, lalu di kemudian hari kamu mengubah kode sumber bot, dapat dipastikan akan terjadi ketidakcocokan antara data yang tersimpan dengan function percakapan yang baru. Akibatnya, data tersebut menjadi korup sehingga pengulangan tidak dapat berjalan sebagaimana mestinya.
Kamu bisa mengatasi permasalahan tersebut dengan cara menyematkan versi kode. Setiap kali percakapan diubah, versi kode tersebut akan ditambahkan. Dengan begitu, ketika plugin percakapan mendeteksi ketidakcocokan versi, ia secara otomatis akan memigrasi semua data terkait.
bot.use(conversations({
storage: {
type: "key",
version: 42, // bisa berupa angka atau string
adapter: storageAdapter,
},
}));
2
3
4
5
6
7
Jika versi tidak ditentukan, secara bawaan ia akan bernilai 0
.
Lupa Mengganti Versinya? Jangan Khawatir!
Plugin percakapan dilengkapi dengan proteksi untuk menangani skenario-skenario penyebab data terkorupsi. Jika terdeteksi, sebuah error akan dilempar dari dalam percakapan terkait, sehingga percakapan tersebut mengalami crash.
Selama error tersebut tidak ditangkap dan diredam, percakapan dengan sendirinya akan menghapus data yang tidak sesuai dan memulai ulang dengan benar.
Ingat, proteksi ini tidak mencakup semua skenario. Oleh karena itu, di kesempatan selanjutnya, kamu harus memastikan nomor versi diperbarui dengan benar.
Data yang Tidak Dapat Di-serialize
Catatan Terjemahan
Kami tidak menemukan terjemahan yang tepat untuk serialize. Oleh karena itu, istilah tersebut ditulis seperti apa adanya.
Istilah serialize sendiri adalah proses mengubah struktur suatu data menjadi format yang dapat disimpan. Dalam konteks ini, data akan diubah menjadi format JSON.
Seperti yang telah kita ketahui, semua data yang dikembalikan dari conversation
akan disimpan. Oleh karena itu, data-data tersebut harus berupa tipe yang bisa di-serialize.
const largeNumber = await conversation.external({
// Memanggil sebuah API yang mengembalikan sebuah BigInt (tidak bisa diubah menjadi JSON).
task: () => 1000n ** 1000n,
// Sebelum disimpan, konversi bigint menjadi string.
beforeStore: (n) => String(n),
// Sebelum digunakan, kembalikan string menjadi bigint.
afterLoad: (str) => BigInt(str),
});
2
3
4
5
6
7
8
Jika ingin melempar sebuah error dari task
, kamu bisa menyematkan function serialize tambahan untuk object error. Coba lihat External
di referensi API.
Kunci Penyimpanan
Catatan Terjemahan
Istilah kunci yang digunakan di sini bukan dalam artian mengunci data menggunakan kata sandi atau semacamnya, melainkan merujuk ke terjemahan key untuk storage keys, sebuah tanda identifikasi untuk setiap data di suatu penyimpanan.
Secara bawaan, data percakapan disimpan menggunakan setiap chat sebagai kunci penyimpanannya. Perilaku tersebut identik dengan cara kerja plugin session.
Karenanya, suatu percakapan tidak dapat menangani update dari berbagai chat. Jika tidak menghendaki perilaku tersebut, kamu bisa membuat function kunci penyimpananmu sendiri. Untuk session, kami tidak merekomendasikan untuk menggunakan opsi tersebut di serverless karena berpotensi menyebabkan tumpang tindih (race conditions).
Selain itu, sama seperti session, kamu bisa menyimpan data percakapan menggunakan awalan tertentu menggunakan opsi prefix
. Ia akan berguna jika kamu hendak menggunakan storage adapter yang sama untuk data session dan data percakapan. Dengan menggunakan awalan, data tidak akan saling berbenturan karena nama yang identik.
Berikut caranya:
bot.use(conversations({
storage: {
type: "key",
adapter: storageAdapter,
getStorageKey: (ctx) => ctx.from?.id.toString(),
prefix: "convo-",
},
}));
2
3
4
5
6
7
8
Jika user dengan ID 424242
memasuki sebuah percakapan, kunci penyimpanannya akan menjadi convo
.
Silahkan lihat referensi API Conversation
untuk memahami lebih detail mengenai penyimpanan data menggunakan plugin percakapan. Detail yang dijelaskan di antaranya termasuk cara menyimpan data menggunakan type:
sehingga function kunci penyimpanan tidak lagi diperlukan.
Menggunakan Plugin di Dalam Percakapan
Sebelumnya, kita telah membahas mengenai context object yang digunakan oleh percakapan berbeda dengan context object yang digunakan oleh middleware. Artinya, meski suatu plugin telah diinstal ke bot, namun ia tidak akan terinstal untuk percakapan.
Untungnya, semua plugin grammY selain session kompatibel dengan percakapan. Berikut contoh cara menginstal plugin hidrasi ke percakapan:
// Instal plugin percakapan untuk lingkup luar saja.
type MyContext = ConversationFlavor<Context>;
// Instal plugin hidrasi untuk lingkup dalam saja.
type MyConversationContext = HydrateFlavor<Context>;
bot.use(conversations());
// Sertakan context object luar dan dalam.
type MyConversation = Conversation<MyContext, MyConversationContext>;
async function convo(conversation: MyConversation, ctx: MyConversationContext) {
// Plugin hidrasi terinstal untuk paramater `ctx` di dalam sini.
const other = await conversation.wait();
// Plugin hidrasi juga terinstal untuk variable `other` di dalam sini.
}
bot.use(createConversation(convo, { plugins: [hydrate()] }));
bot.command("enter", async (ctx) => {
// Plugin hidrasi TIDAK terinstal untuk `ctx` di dalam sini.
await ctx.conversation.enter("convo");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bot.use(conversations());
async function convo(conversation, ctx) {
// Plugin hidrasi terinstal untuk paramater `ctx` di dalam sini.
const other = await conversation.wait();
// Plugin hidrasi juga terinstal untuk variable `other` di dalam sini.
}
bot.use(createConversation(convo, { plugins: [hydrate()] }));
bot.command("enter", async (ctx) => {
// Plugin hidrasi TIDAK terinstal untuk `ctx` di dalam sini.
await ctx.conversation.enter("convo");
});
2
3
4
5
6
7
8
9
10
11
12
13
Di middleware biasa, plugin akan menggunakan context object yang tersedia untuk menjalankan kode terkait. Kemudian, ia akan memanggil next
untuk menunggu middleware yang ada di hilir selesai, lalu dilanjut dengan menjalankan kode yang tersisa.
Tetapi, hal tersebut tidak berlaku untuk percakapan karena ia bukanlah sebuah middleware. Artinya, plugin juga tidak dapat berinteraksi dengan percakapan selayaknya middleware.
Context object yang dihasilkan oleh percakapan akan diteruskan ke plugin untuk diproses secara normal. Dari sudut pandang plugin, satu-satunya plugin yang tersedia hanyalah dirinya, dan penangan di hilir dianggap tidak ada. Setelah semua plugin terselesaikan, context object tersebut akan tersedia kembali untuk percakapan.
Dampaknya, semua tugas pembersihan yang dilakukan oleh plugin dilakukan sebelum function percakapan dijalankan. Semua plugin selain session dapat bekerja dengan baik dengan alur kerja di atas. Jika kamu hendak menggunakan session, silahkan gulir ke bawah.
Plugin Bawaan
Jika kamu memiliki banyak percakapan yang menggunakan plugin yang sama, kamu bisa menerapkan plugin bawaan. Dengan begitu, kamu tidak perlu lagi memasang hydrate
ke create
:
// TypeScript memerlukan dua jenis context type.
// Oleh karena itu, pastikan untuk menginstalnya.
bot.use(conversations<MyContext, MyConversationContext>({
plugins: [hydrate()],
}));
// Hidrasi akan terinstal untuk percakapan berikut.
bot.use(createConversation(convo));
2
3
4
5
6
7
bot.use(conversations({
plugins: [hydrate()],
}));
// Hidrasi akan terinstal untuk percakapan berikut.
bot.use(createConversation(convo));
2
3
4
5
Pastikan varian context semua plugin bawaan terinstal ke semua context type percakapan.
Menggunakan Plugin Transformer di Dalam Percakapan
Jika kamu hendak menginstal suatu plugin ke bot
, ia tidak akan bisa dipasang ke array plugins
secara langsung. Alih-alih, kamu harus memasangnya ke instance Api
untuk setiap context object. Langkah tersebut dapat dilakukan dengan mudah dari dalam plugin middleware biasa:
bot.use(createConversation(convo, {
plugins: [async (ctx, next) => {
ctx.api.config.use(transformer);
await next();
}],
}));
2
3
4
5
6
Ganti transformer
dengan plugin yang ingin diinstal. Kamu bisa menginstal beberapa transformer di pemanggilan ctx
yang sama.
Mengakses Session di Dalam Percakapan
Plugin session tidak bisa diinstal ke dalam percakapan layaknya plugin lain karena ia memiliki perilaku yang berbeda. Kamu tidak bisa memasangnya ke array plugins
karena alur kerjanya akan menjadi seperti ini:
- Membaca data,
- Memanggil
next
(yang mana langsung selesai), - Menulis kembali data yang sama,
- Menyerahkan context ke percakapan lain.
Perhatikan bagaimana session di atas disimpan (nomor 3) bahkan sebelum kamu mengubahnya (nomor 2). Akibatnya, semua perubahan yang terjadi di data session akan hilang.
Untuk mengatasinya, kamu bisa menggunakan conversation
untuk mengakses context object luar.
// Baca data session yang ada di dalam percakapan.
const session = await conversation.external((ctx) => ctx.session);
// Ubah data session-nya.
session.count += 1;
// Simpan data session.
await conversation.external((ctx) => {
ctx.session = session;
});
2
3
4
5
6
7
8
9
10
Di sisi lain, karena plugin session mengakses database, ia dapat menimbulkan efek samping yang tidak diinginkan. Oleh karena itu, berdasarkan aturan utama, kita wajib membungkus session dengan conversation
ketika hendak mengaksesnya.
Menu Percakapan
Kamu bisa membuat sebuah menu menggunakan plugin menu di luar percakapan serta memasangnya ke array plugins
seperti plugin pada umumnya.
Akan tetapi, kamu tidak dapat menunggu update dari dalam menu karena penangan tombol menu tidak memiliki akses ke percakapan terkait.
Idealnya, ketika suatu tombol ditekan, ia mampu untuk menunggu update dan bernavigasi di antara menu ketika user menekan tombol terkait. Aksi tersebut dapat dicapai dengan cara membuat menu percakapan menggunakan conversation
.
let surel = "";
const menuSurel = conversation.menu()
.text("Lihat alamat surel", (ctx) => ctx.reply(surel || "kosong"))
.text(
() => surel ? "Ganti alamat surel" : "Tambah alamat surel",
async (ctx) => {
await ctx.reply("Apa alamat surel Anda?");
const response = await conversation.waitFor(":text");
surel = response.msg.text;
await ctx.reply(`Alamat surel Anda adalah ${surel}!`);
ctx.menu.update();
},
)
.row()
.url("Tentang", "https://grammy.dev");
const daftarMenu = conversation.menu()
.submenu("Ke menu surel", menuSurel, async (ctx) => {
await ctx.reply("Menuju ke menu…");
});
await ctx.reply("Berikut menu yang tersedia:", {
reply_markup: daftarMenu,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
conversation
menghasilkan sebuah menu yang terdiri atas beberapa tombol, persis seperti yang dilakukan oleh plugin menu. Bahkan, jika kamu membaca Conversation
di referensi API, ia sangat mirip dengan Menu
dari plugin menu.
Menu percakapan akan tetap aktif selama percakapan terkait juga aktif. Oleh karena itu, kami menyarankan untuk memanggil ctx
sebelum keluar dari percakapan.
Jika kamu tidak ingin keluar dari percakapan terkait, kamu bisa dengan mudah meletakkan potongan kode berikut di akhir function percakapan. Akan tetapi, perlu diingat kembali bahwa membiarkan percakapan tetap aktif selamanya dapat menimbulkan dampak yang buruk.
// Tunggu selamanya.
await conversation.waitUntil(() => false, {
otherwise: (ctx) => ctx.reply("Mohon gunakan menu di atas!"),
});
2
3
4
Perlu diketahui juga bahwa menu percakapan tidak akan mengintervensi menu lain yang berada di luar. Dengan kata lain, menu yang berada di dalam percakapan tidak akan menangani update yang ditujukan untuk menu yang berada di luar, dan begitu pula sebaliknya.
Interoperabilitas Plugin Menu
Sebuah menu yang didefinisikan di luar percakapan (menu luar) dapat digunakan di dalam percakapan. Caranya adalah dengan mendefinisikan sebuah menu percakapan di dalam function percakapan terkait. Selama percakapan tersebut aktif, menu percakapan akan mengambil alih menu luar. Kendali akan diambil kembali oleh menu luar ketika percakapan tersebut selesai.
Pastikan kedua menu diberi string identifikasi yang sama:
// Di luar percakapan (plugin menu):
const menu = new Menu("menu-saya");
// Di dalam percakapan (plugin percakapan):
const menu = conversation.menu("menu-saya");
2
3
4
Agar dapat bekerja dengan baik, kamu harus memastikan kedua menu memiliki struktur yang identik. Jika strukturnya tidak sama, saat tombol ditekan, menu tersebut akan dianggap telah kedaluwarsa, sehingga penangan tombol terkait tidak akan dipanggil.
Struktur menu ditentukan berdasarkan dua hal
- Bentuk menu (jumlah baris ataupun jumlah tombol di setiap baris); dan
- Label tombol.
Umumnya, praktik terbaik yang disarankan adalah secepatnya mengubah struktur menu percakapan setelah memasuki percakapan. Dengan begitu, menu dapat teridentifikasi oleh percakapan, sehingga membuatnya dapat segera diaktifkan.
Jika suatu menu masih menyisakan suatu percakapan (karena tidak ditutup), menu luar dapat mengambil alih kembali kendali. Sekali lagi, asalkan struktur menunya identik.
Contoh penerapan interoperabilitas ini dapat kamu temukan di repositori kumpulan contoh bot.
Formulir Percakapan
Percakapan sering kali digunakan untuk membuat formulir dalam bentuk tampilan chat.
Semua pemanggilan wait
mengembalikan context object. Akan tetapi, ketika menunggu sebuah pesan teks, mungkin kamu hanya ingin mengetahui teks pesannya saja, alih-alih isi context object-nya.
Kamu bisa menggunakan formulir percakapan untuk melakukan validasi update dengan data yang telah diekstrak dari context object. Berikut contoh isian formulir dalam bentuk chat:
await ctx.reply("Silahkan kirim foto yang ingin dikecilkan!");
const foto = await conversation.form.photo();
await ctx.reply("Berapa ukuran lebar foto yang diinginkan?");
const lebar = await conversation.form.int();
await ctx.reply("Berapa ukuran tinggi foto yang diinginkan?");
const tinggi = await conversation.form.int();
await ctx.reply(`Mengubah ukuran foto menjadi ${lebar}x${tinggi} ...`);
const hasil = await ubahUkuranFoto(foto, lebar, tinggi);
await ctx.replyWithPhoto(hasil);
2
3
4
5
6
7
8
9
Silahkan kunjungi referensi API Conversation
untuk melihat macam-macam isian lain yang tersedia.
Semua isian formulir menerima function otherwise
yang akan dijalankan ketika update yang diperoleh tidak cocok. Selain itu, ia juga menerima function action
yang akan dijalankan ketika isian formulir telah diisi dengan benar.
// Tunggu huruf vokal.
const op = await conversation.form.select(["A", "I", "U", "E", "O"], {
action: (ctx) => ctx.deleteMessage(),
otherwise: (ctx) => ctx.reply("Hanya menerima A, I, U, E, atau O!"),
});
2
3
4
5
Formulir percakapan bahkan menyediakan cara untuk membuat isian formulir tersuai menggunakan conversation
.
Batas Waktu Tunggu
Kamu bisa menentukan batas waktu untuk setiap update yang ditunggu.
// Tunggu selama satu jam sebelum keluar dari percakapan.
const satuJamDalamSatuanMilidetik = 60 * 60 * 1000;
await conversation.wait({ maxMilliseconds: satuJamDalamSatuanMilidetik });
2
3
conversation
akan dipanggil ketika pemanggilan wait
telah tercapai.
conversation
akan dipanggil lagi saat update selanjutnya tiba. Jika update yang diterima melebihi kurun waktu max
, percakapan akan dihentikan, dan update tersebut akan dikembalikan ke sistem middleware. Middleware hilir kemudian akan dipanggil.
Proses di atas akan membuat percakapan seolah-olah tidak aktif.
Yang perlu diperhatikan adalah kode tidak akan dijalankan tepat setelah waktu yang telah ditentukan terlampaui. Melainkan, ia hanya akan dijalankan tepat saat update selanjutnya tiba.
Kamu bisa menentukan nilai batas waktu bawaan untuk semua pemanggilan wait
di dalam percakapan.
// Selalu tunggu selama satu jam.
const satuJamDalamSatuanMilidetik = 60 * 60 * 1000;
bot.use(createConversation(convo, {
maxMillisecondsToWait: satuJamDalamSatuanMilidetik,
}));
2
3
4
5
Nilai bawaan dapat ditimpa dengan cara menetapkan nilai yang diinginkan ke pemanggilan wait
secara langsung.
Aktivitas Masuk dan Keluar
Jika kamu ingin function tertentu dipanggil ketika bot memasuki suatu percakapan, kamu bisa menambahkan callback function ke opsi on
. Demikian pula untuk aktivitas keluar, kamu juga bisa menerapkan hal yang sama ke opsi on
.
bot.use(conversations({
onEnter(id, ctx) {
// Masuk ke percakapan `id`.
},
onExit(id, ctx) {
// Keluar dari percakapan `id`.
},
}));
2
3
4
5
6
7
8
Masing-masing callback menerima dua jenis nilai. Nilai pertama (id
) adalah string identifikasi untuk percakapan yang sedang mengalami aktivitas masuk atau keluar. Nilai kedua (ctx
) adalah context object dari middleware yang ada di sekitar percakapan tersebut.
Perlu dicatat, callback hanya akan dipanggil ketika aktivitas masuk atau keluar dilakukan melalui ctx
. Selain itu, callback on
akan dipanggil ketika percakapan menghentikan dirinya sendiri menggunakan conversation
maupun saat batas waktu tunggu telah tercapai.
Pemanggilan wait
Secara Bersamaan
Kita bisa menggunakan floating promises untuk menunggu beberapa promise secara bersamaan—pada contoh kali ini, kita menggunakan Promise
. Ketika update-baru diterima, hanya pemanggilan wait
pertama yang memiliki kecocokan yang akan terselesaikan.
Misalnya, berdasarkan contoh di bawah, jika user mengirim pesan teks, yang akan terselesaikan terlebih dahulu adalah conversation
, sementara conversation
akan tetap menunggu sampai ada foto yang dikirim.
await ctx.reply("Kirimkan saya sebuah foto beserta keterangannya!");
const [textContext, photoContext] = await Promise.all([
conversation.waitFor(":text"),
conversation.waitFor(":photo"),
]);
await ctx.replyWithPhoto(photoContext.msg.photo.at(-1).file_id, {
caption: textContext.msg.text,
});
2
3
4
5
6
7
8
Dari contoh di atas, tidak menjadi masalah ketika user mengirimkan foto atau teks terlebih dahulu. Kedua promise akan terselesaikan sesuai dengan urutan pengiriman dua pesan yang ditunggu oleh kode tersebut. Promise
juga bekerja sebagaimana mestinya, ia akan selesai saat semua promise yang diberikan terselesaikan.
Cara yang sama juga bisa digunakan untuk hal-hal lainnya. Berikut contoh cara menginstal penyimak perintah keluar secara global di dalam suatu percakapan:
conversation.waitForCommand("keluar") // tidak menggunakan `await`!
.then(() => conversation.halt());
2
Begitu percakapan berakhir, semua pemanggilan wait
yang tertunda akan dibatalkan. Sebagai contoh, begitu percakapan berikut dimasuki, ia akan selesai begitu saja tanpa menunggu update selanjutnya tiba.
async function convo(conversation: Conversation, ctx: Context) {
const _promise = conversation.wait() // tidak menggunakan `await`!
.then(() => ctx.reply("Pesan ini tidak akan pernah dikirim!"));
// Percakapan selesai begitu saja.
}
2
3
4
5
6
async function convo(conversation, ctx) {
const _promise = conversation.wait() // tidak menggunakan `await`!
.then(() => ctx.reply("Pesan ini tidak akan pernah dikirim!"));
// Percakapan selesai begitu saja.
}
2
3
4
5
6
Secara internal, ketika beberapa pemanggilan wait
dicapai dalam waktu yang bersamaan, plugin percakapan akan memantau semua pemanggilan wait
tersebut. Begitu update selanjutnya tiba, ia akan mengulang function percakapan sekali untuk setiap pemanggilan wait
yang ditemui hingga salah satu diantaranya menerima update tersebut. Jika di antara pemanggilan wait
tertunda tersebut tidak ada satu pun yang menerima update, maka update tersebut akan dibuang.
Kembali ke Titik Cek
Seperti yang telah kita ketahui, plugin percakapan memantau eksekusi function percakapan.
Dengan begitu, kita dapat membuat titik cek di sepanjang proses tersebut. Titik cek berisi informasi mengenai seberapa jauh function percakapan terkait telah dijalankan. Nantinya, informasi tersebut akan digunakan untuk kembali ke titik cek yang telah ditentukan.
Aksi apapun yang sudah telanjur dilakukan tentunya tidak dapat dianulir. Artinya, memutar balik ke titik cek tidak akan menganulir pesan yang sudah telanjur terkirim.
const checkpoint = conversation.checkpoint();
if (ctx.hasCommand("reset")) {
await conversation.rewind(checkpoint);
}
2
3
4
5
Titik cek akan sangat berguna untuk “mengulang kembali”. Namun, layaknya label
di JavaScript break
dan continue
, melompat di antara kode seperti itu dapat membuat kode tersebut sulit dibaca. Oleh karena itu, gunakan fitur ini seperlunya saja.
Layaknya pemanggilan wait
, memutar balik percakapan akan membatalkan proses eksekusi terkait, kemudian ia akan mengulang function hingga ke titik di mana titik cek tersebut dibuat. Memutar balik percakapan tidak secara harfiah mengeksekusi function secara terbalik, meski seakan-akan ia terlihat seperti itu.
Percakapan Paralel
Percakapan yang berlangsung di chat yang berbeda diproses secara terpisah dan selalu dijalankan secara paralel.
Sebaliknya, secara bawaan, setiap chat hanya boleh memiliki satu percakapan aktif. Jika kamu mencoba memasuki sebuah percakapan disaat percakapan lain sedang aktif, pemanggilan enter
yang dilakukan akan melempar sebuah galat.
Kamu bisa mengubah perilaku tersebut dengan cara menandai suatu percakapan sebagai paralel.
bot.use(createConversation(convo, { parallel: true }));
Aksi di atas akan mengubah dua hal.
Pertama, kamu sekarang bisa memasuki percakapan tersebut meski terdapat percakapan lain yang masih aktif. Misalkan kamu memiliki percakapan captcha
dan settings
, kamu bisa memiliki lima percakapan captcha
dan dua belas percakapan settings
aktif di chat yang sama.
Kedua, ketika percakapan terkait tidak menerima update, update tersebut tidak akan dibuang. Alih-alih, kendali akan diserahkan kembali ke sistem middleware terkait,
Semua percakapan yang terinstal memiliki kesempatan untuk menangani update yang tiba hingga salah satu dari mereka menerimanya. Akan tetapi, hanya satu percakapan saja yang bisa menangani update tersebut.
Ketika beberapa percakapan yang berbeda aktif secara bersamaan, urutan middleware akan menentukan percakapan mana yang akan menangani update tersebut terlebih dahulu. Sedangkan, ketika satu percakapan aktif beberapa kali, percakapan yang paling awal (yang dimasuki terlebih dahulu) akan menangani update tersebut terlebih dahulu.
Berikut ilustrasi contohnya:
async function captcha(conversation: Conversation, ctx: Context) {
const user = ctx.from!.id;
await ctx.reply("Selamat datang di grup! Apa framework bot di dunia?");
const answer = await conversation.waitFor(":text").andFrom(user);
if (answer.msg.text === "grammY") {
await ctx.reply("Tepat sekali!");
} else {
await ctx.banAuthor();
}
}
async function settings(conversation: Conversation, ctx: Context) {
const user = ctx.from!.id;
const main = conversation.checkpoint();
const options = ["Pengaturan Chat", "Tentang", "Privasi"];
await ctx.reply("Selamat datang di pengaturan!", {
reply_markup: Keyboard.from(options
.map((btn) => [Keyboard.text(btn)])),
});
const option = await conversation.waitFor(":text")
.andFrom(user)
.and((ctx) => options.includes(ctx.msg.text), {
otherwise: (ctx) =>
ctx.reply("Mohon gunakan tombol yang telah disediakan!"),
});
await openSettingsMenu(option, main);
}
bot.use(createConversation(captcha));
bot.use(createConversation(settings));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
async function captcha(conversation, ctx) {
const user = ctx.from.id;
await ctx.reply("Selamat datang di grup! Apa framework bot di dunia?");
const answer = await conversation.waitFor(":text").andFrom(user);
if (answer.msg.text === "grammY") {
await ctx.reply("Tepat sekali!");
} else {
await ctx.banAuthor();
}
}
async function settings(conversation, ctx) {
const user = ctx.from.id;
const main = conversation.checkpoint();
const options = ["Pengaturan Chat", "Tentang", "Privasi"];
await ctx.reply("Selamat datang di pengaturan!", {
reply_markup: Keyboard.from(options
.map((btn) => [Keyboard.text(btn)])),
});
const option = await conversation.waitFor(":text")
.andFrom(user)
.and((ctx) => options.includes(ctx.msg.text), {
otherwise: (ctx) =>
ctx.reply("Mohon gunakan tombol yang telah disediakan!"),
});
await openSettingsMenu(option, main);
}
bot.use(createConversation(captcha));
bot.use(createConversation(settings));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Kode di atas ditujukan untuk chat grup. Ia menyediakan dua buah percakapan: captcha
dan settings
. Percakapan captcha
digunakan untuk memastikan hanya developer terbaik yang join chat tersebut—promosi grammY tanpa malu, hahaha. Percakapan settings
digunakan untuk mengimplementasikan menu pengaturan di chat grup.
Perlu diperhatikan, semua pemanggilan wait
akan melakukan pemilahan berdasarkan user id.
Mari kita asumsikan beberapa hal berikut telah dilakukan:
- Kamu memanggil
ctx
untuk memasuki percakapan.conversation .enter("captcha") captcha
saat menanganiupdate
dari user dengan idctx
..from .id = == 42 - Kamu memanggil
ctx
untuk memasuki percakapan.conversation .enter("settings") settings
saat menanganiupdate
dari user dengan idctx
..from .id = == 3 - Kamu memanggil
ctx
untuk memasuki percakapan.conversation .enter("captcha") captcha
saat menanganiupdate
dari user dengan idctx
..from .id = == 43
Artinya, tiga percakapan di atas telah aktif di chat grup tersebut—captcha
aktif dua kali dan settings
aktif sekali.
Perlu diketahui,
ctx
menyediakan berbagai cara untuk keluar dari percakapan, bahkan ketika percakapan paralel diaktifkan..conversation
Selanjutnya, hal-hal berikut akan terjadi secara berurutan:
- User dengan id
3
mengirim sebuah pesan yang mengandung teksTentang
. - Sebuah update berupa pesan teks tiba.
- Instance percakapan
captcha
pertama diulang. - Pemanggilan
wait
menerima update tersebut. Tetapi, karena adanya filterFor(": text") and
, maka update tersebut akan ditolak.From(42) - Instance percakapan
captcha
kedua diulang. - Pemanggilan
wait
menerima update tersebut. Tetapi, karena adanya filterFor(": text") and
, maka update tersebut akan ditolak.From(43) - Semua instance
captcha
menolak update tersebut, maka kendali diserahkan kembali ke sistem middleware. - Instance percakapan
settings
diulang. - Pemanggilan
wait
telah terselesaikan danoption
akan berisi context object yang mengandung update pesan teks tersebut. - Function
open
dipanggil. Ia kemudian akan mengirim teksSettings Menu Tentang
ke user dan memutar balik percakapan kembali kemain
, yang menyebabkan menu tersebut dimulai ulang.
Coba perhatikan, meski dua percakapan di atas menunggu user 42
dan 43
untuk menyelesaikan captcha-nya, bot dengan benar membalas user 3
yang telah memulai menu pengaturan
. Artinya, pemanggilan wait
terpilah mampu menentukan update mana yang relevan untuk percakapan yang sedang berlangsung. Update yang tertolak akan diambil oleh percakapan lainnya.
Meski contoh di atas menggunakan chat grup untuk mengilustrasikan kemampuan percakapan dalam menangani beberapa user secara paralel, namun sebenarnya percakapan paralel dapat digunakan untuk semua jenis chat. Dengan kata lain, ia juga bisa digunakan untuk menunggu hal-hal lain di sebuah chat yang hanya memiliki satu user saja.
Selain itu, percakapan paralel juga dapat dikombinasikan dengan batas waktu tunggu untuk meminimalkan jumlah percakapan aktif.
Memeriksa Percakapan Aktif
Kamu bisa memeriksa percakapan mana yang sedang aktif dari dalam middleware dengan cara berikut:
bot.command("stats", (ctx) => {
const convo = ctx.conversation.active("convo");
console.log(convo); // 0 atau 1
const isActive = convo > 0;
console.log(isActive); // false atau true
});
2
3
4
5
6
Ketika id percakapan disematkan ke ctx
, ia akan mengembalikan nilai 1
jika percakapan tersebut sedang aktif, untuk sebaliknya ia akan mengembalikan nilai 0
.
Jika percakapan paralel diaktifkan, ia akan mengembalikan jumlah percakapan terkait yang sedang aktif.
Memanggil ctx
tanpa disertai argument akan mengembalikan sebuah object berisi daftar percakapan yang sedang aktif. Id percakapan digunakan sebagai key, sedangkan untuk value-nya berisi jumlah percakapan aktif untuk id tersebut.
Misalnya, jika percakapan captcha
aktif dua kali dan percakapan settings
aktif sekali, maka ctx
akan menghasilkan nilai berikut:
bot.command("stats", (ctx) => {
const stats = ctx.conversation.active();
console.log(stats); // { captcha: 2, settings: 1 }
});
2
3
4
Migrasi dari Versi 1.x ke 2.x
Percakapan 2.0 ditulis ulang sepenuhnya dari awal.
Meski konsep-konsep dasar API-nya masih tetap sama, namun, di balik layar, implementasi kedua versi tersebut benar-benar berbeda.
Singkatnya, penyesuaikan kode untuk proses migrasi dari versi 1.x ke 2.x sangatlah minim, hanya saja kamu perlu menghapus semua data yang tersimpan agar semua percakapan dapat dimulai ulang dari awal.
Migrasi Data dari Versi 1.x ke 2.x
Sayangnya, ketika melakukan pemutakhiran dari versi 1.x ke 2.x, tidak ada cara untuk mempertahankan status percakapan yang sedang berlangsung.
Oleh karena itu, data-data tersebut harus dihapus terlebih dahulu dari session. Kami menyarankan untuk mengikuti panduan migrasi session.
Mempertahankan data percakapan menggunakan versi 2.x dapat dilakukan dengan cara berikut.
Perubahan Type dari Versi 1.x ke 2.x
Di versi 1.x, context type di dalam percakapan identik dengan context type yang digunakan di middleware.
Di versi 2.x, kamu harus mendeklarasikan dua context type, yaitu context type luar dan context type dalam. Kedua type tersebut seharusnya tidak pernah sama. Jika ternyata mereka tetap sama, maka ada yang salah dengan kode kamu. Alasannya adalah karena context type luar harus terinstal Conversation
, sedangkan context type dalam seharusnya tidak terinstal varian type tersebut.
Selain itu, sekarang kamu bisa menginstal beberapa plugin secara terpisah untuk setiap percakapan.
Perubahan Cara Mengakses Session dari Versi 1.x ke 2.x
conversation
tidak bisa lagi digunakan. Sebagai gantinya, gunakan conversation
.
// Membaca data session.
const session = await conversation.session;
const session = await conversation.external((ctx) => ctx.session);
// Menulis data session.
conversation.session = newSession;
await conversation.external((ctx) => {
ctx.session = newSession;
});
2
3
4
5
6
7
8
9
ctx
bisa diakses di versi 1.x, akan tetapi cara tersebut tidaklah benar. Oleh karena itu,.session ctx
tidak lagi tersedia di versi 2.x..session
Perubahan Kompatibilitas Plugin dari Versi 1.x ke 2.x
Percakapan 1.x kurang kompatibel dengan plugin manapun. Beberapa diantaranya dapat teratasi dengan menggunakan conversation
.
Opsi tersebut telah dihilangkan di versi 2.x. Sebagai gantinya, kamu bisa menambahkan beberapa plugin dengan cara menyematkannya ke array plugins
, seperti yang telah dijelaskan di sini.
Untuk session, ia membutuhkan penanganan khusus. Sedangkan untuk menu, ia telah mengalami peningkatan kompatibilitas semenjak hadirnya menu percakapan.
Perubahan Percakapan Paralel dari Versi 1.x ke 2.x
Percakapan paralel tidak jauh berbeda di antara kedua versi.
Namun, di masa lalu, fitur ini menimbulkan berbagai permasalahan ketika digunakan secara tidak sengaja. Di versi 2.x, kamu perlu secara eksplisit menyematkan { parallel:
jika ingin menggunakan fitur ini, seperti yang telah di jelaskan di bagian ini.
Satu-satunya perubahan yang signifikan adalah update tidak lagi diteruskan ke sistem middleware secara bawaan. Proses tersebut hanya akan dilakukan ketika suatu percakapan ditandai sebagai paralel.
Perlu dicatat, semua method wait
dan kolom isian formulir menyediakan sebuah opsi next
untuk menimpa perilaku bawaan. Opsi tersebut merupakan hasil perubahan nama opsi drop
di versi 1.x, sehingga makna kedua opsi juga bertolak belakang.
Perubahan Formulir dari Versi 1.x ke 2.x
Fitur formulir di versi 1.x benar-benar berantakan. Contohnya, conversation
mengembalikan isi pesan teks bahkan untuk pesan edited
yang telah usang. Kejanggalan-kejanggalan tersebut telah diperbaiki di versi 2.x.
Memperbaiki kekutu atau bug secara teknis tidak dihitung sebagai perubahan yang signifikan. Meski demikian, ia termasuk perubahan perilaku yang cukup mencolok.