Filter Query dan bot.on()

Filter query merupakan argument pertama dari bot.on() yang berbentuk string.

Pengenalan

Sebagian besar framework bot—atau bahkan semuanya?—hanya menyediakan pemfilteran update yang sederhana, misalnya cuma disediakan on("message") dan sejenisnya. Pemfilteran untuk jenis pesan lainnya diserahkan kepada developer bot masing-masing untuk ditangani sendiri, yang mana sering kali mengarah ke penggunaan statemen if yang tidak ada habisnya di dalam kode mereka.

Sebaliknya, grammY dilengkapi dengan bahasa query-nya sendiri yang dapat digunakan untuk memfilter pesan yang kamu inginkan.

grammY memiliki lebih dari 650 filter berbeda yang siap dipakai, dan tidak menutup kemungkinan lebih banyak lagi filter yang akan ditambahkan seiring berjalannya waktu. Setiap filter yang valid dapat dilengkapi menggunakan auto-complete di code editor. Dengan demikian, kamu cukup mengetik bot.on(""), lalu buka auto-complete, kemudian telusuri semua query yang tersedia dengan cara mengetik sesuatu.

Pencarian Filter Query

Type inference bot.on() akan memahami filter query yang sedang kamu pilih. Dari situ, ia akan mengerucutkan beberapa type context yang ada.

bot.on("message", (ctx) => {
  // Bisa jadi undefined kalau pesan yang diterima tidak ada teksnya.
  const text: string | undefined = ctx.msg.text;
});
bot.on("message:text", (ctx) => {
  // Text selalu tersedia karena handler ini dipanggil
  // ketika pesan yang diterima hanya berupa teks.
  const text: string = ctx.msg.text;
});

Filter query grammY diimplementasikan baik di runtime maupun di type level.

Contoh Query

Berikut ini beberapa contoh query:

Query Biasa

Filter sederhana untuk update dan sub-filter:

bot.on("message"); // dipanggil untuk jenis pesan apapun
bot.on("message:text"); // hanya menerima pesan teks
bot.on("message:photo"); // hanya menerima pesan foto

Filter untuk Entity

Sub-filter yang masuk satu tingkat lebih dalam:

bot.on("message:entities:url"); // untuk pesan yang mengandung url
bot.on("message:entities:code"); // untuk pesan yang berisi cuplikan kode
bot.on("edited_message:entities"); // untuk pesan diedit yang mengandung entity apapun bentuknya

Mengosongkan Value

Kamu dapat mengosongkan value tertentu di filter query. grammY kemudian akan mencari value yang sesuai dengan query kamu.

bot.on(":text"); // Seluruh pesan dan postingan channel yang mengandung teks
bot.on("message::url"); // Pesan yang mengandung URL baik di teks maupun di caption (foto, dan sebagainya)
bot.on("::email"); // Seluruh pesan dan postingan channel yang mengandung email di teks maupun di caption-nya

Mengosongkan value pertama akan mencocokkan pesan serta postingan channel. Perlu diingat bahwa ctx.msg memberi kamu akses ke pesan dan postingan channel manapun yang cocok dengan query yang diberikan.

Mengosongkan value kedua akan mencocokkan entity, baik di pesan maupun di caption. Kamu bisa menghilangkan bagian pertama dan kedua secara bersamaan.

Shortcut

Query engine grammY memiliki shortcut yang dapat mengelompokkan query-query yang saling berkaitan.

msg

Shortcut msg—bukan msg micin, loh ya 😬—mengelompokkan pesan dan postingan channel. Dengan menggunakan msgsama halnya dengan menyimak aktivitas message dan channel_post.

bot.on("msg"); // Pesan atau postingan channel apapun bentuknya
bot.on("msg:text"); // Sama dengan menggunakan `:text`

edit

Shortcut edit mengelompokkan pesan dan postingan channel yang diedit.

bot.on("edit"); // Semua pesan dan postingan yang diedit
bot.on("edit:text"); // Pesan teks yang diedit
bot.on("edit::url"); // Pesan atau caption yang diedit dan mengandung URL di teksnya
bot.on("edit:location"); // Lokasi terkini (live location) yang diperbarui

:media

Shortcut :media mengelompokkan pesan foto dan video.

bot.on("message:media"); // pesan foto atau video
bot.on("edited_channel_post:media"); // postingan channel yang diedit dan berupa media
bot.on(":media"); // pesan atau postingan channel berupa media

:file

Shortcut :file menggabungkan semua pesan yang mengandung file. Dengan begitu, kamu bisa yakin kalau await ctx.getFile() pasti mengembalikan sebuah object file.

bot.on(":file"); // Pesan atau postingan channel yang mengandung file
bot.on("edit:file"); // Pesan atau postingan channel yang sudah diedit dan mengandung file

Syntactic Sugar

Catatan penerjemah: syntactic sugar merupakan sebuah sintaks yang dibuat sedemikian rupa supaya lebih mudah dibaca dan digunakan oleh programmer.

Terdapat dua potongan query khusus yang bisa membuat proses pemfilteran menjadi semakin mudah. Kamu bisa mendeteksi bot di query dengan menggunakan potongan query :is_bot. Sedangkan syntactic sugar :me dapat digunakan untuk merujuk ke bot kamu di query, yang mana akan membandingkan id user bot kamu.

// Pesan service mengenai sebuah bot yang bergabung ke chat
bot.on("message:new_chat_members:is_bot");
// Pesan service mengenai bot kamu telah dikeluarkan dari chat
bot.on("message:left_chat_member:me");

Perhatikan bahwa meskipun syntactic sugar ini bisa digunakan untuk bekerja dengan pesan service, ia sebaiknya tidak digunakan untuk mendeteksi apakah seseorang benar-benar bergabung atau meninggalkan chat. Pesan service pada dasarnya adalah pesan untuk menginformasikan pengguna di dalam chat tersebut. Adakalanya dalam beberapa kasus, pesan itu tidak akan selalu terlihat. Misalnya, di grup besar atau supergroup tidak akan ada pesan service mengenai pengguna yang telah bergabung atau meninggalkan chat. Akibatnya, bot kamu bisa jadi tidak akan mendeteksinya. Oleh karena itu, kamu harus menyimak update chat member.

Mengombinasikan Beberapa Query

Kamu bisa mengombinasikan filter query menggunakan operator AND maupun OR.

Mengombinasikan Menggunakan OR

Kalau ingin memasang beberapa bagian middleware dibalik penggabungan OR dari dua buah query, kamu bisa memasang keduanya ke bot.on() di dalam sebuah array.

// Dijalankan jika pembaruan berupa pesan OR pesan yang diedit
bot.on(["message", "edited_message"] /* , ... */);
// Dijalankan jika berupa hashtag OR email OR terdapat entity mention di dalam teks atau caption tersebut
bot.on(["::hashtag", "::email", "::mention"] /* , ... */);

Middleware tersebut akan dijalankan ketika salah satu dari query yang disediakan terdapat kecocokan. Urutan kueri tidak menjadi masalah.

Mengombinasikan Menggunakan AND

Kalau ingin memasang beberapa bagian middleware dibalik penggabungan AND dari dua buah query, kamu bisa merangkai keduanya ke bot.on().

// Mencocokkan URL yang di-forward
bot.on("::url").on(":forward_date" /* , ... */);
// Mencocokkan foto yang mengandung hashtag di caption-nya
bot.on(":photo").on("::hashtag" /* , ... */);

Middleware tersebut akan dijalankan ketika seluruh query yang disediakan terdapat kecocokan. Urutan kueri tidak menjadi masalah.

Menyusun Query yang Kompleks

Secara teknis kamu bisa mengombinasikan filter query ke rangkaian yang lebih kompleks selama mereka termasuk CNFopen in new window, meski sepertinya ini tidak terlalu bermanfaat juga.

bot
  // Mencocokkan semua potingan channel atau pesan terusan ...
  .on(["channel_post", ":forward_date"])
  // ... yang berupa teks ...
  .on(":text")
  // ... dan berisi sekurang-kurangnya satu url, hashtag, atau cashtag.
  .on(["::url", "::hashtag", "::cashtag"] /* , ... */);

Type inference ctx akan memindai seluruh rangkaian tersebut dan memeriksa setiap elemen dari ketiga panggilan .on. Contohnya, dari potongan kode di atas, type inference dapat mendeteksi kalau ctx.msg.text adalah property yang dibutuhkan.

Tips Berguna

Berikut ini fitur-fitur filter query yang kurang begitu terkenal tetapi bisa sangat membantu. Beberapa diantaranya merupakan fitur tingkat lanjut, silahkan baca materi berikutnya.

Update Member Chat

Kamu bisa menggunakan filter query berikut untuk menerima status update mengenai bot-mu.

bot.on("my_chat_member"); // start, stop, join, atau leave

Filter tadi akan terpicu saat bot dimulai atau distop di chat pribadi. Kalau di grup, ia akan terpicu saat bot ditambahkan atau dikeluarkan. Kamu bisa memeriksa ctx.myChatMember untuk mencari tahu apa yang sebenarnya terjadi.

Hati-hati! Filter ini tidak sama dengan

bot.on("chat_member");

yang digunakan untuk mendeteksi perubahan status member chat lainnya, misalnya ketika seseorang bergabung, dipromosikan, dan sebagainya.

Ingat! Update chat_member perlu diaktifkan secara eksplisit dengan cara menentukan allowed_updates saat memulai bot kamu.

Mengombinasikan Query dengan Method Lain

Kamu bisa mengombinasikan beberapa filter query dengan method-method lain di class Composer (API Referenceopen in new window), misalnya command atau filter. Dengan begitu, kamu bisa membuat pola penanganan pesan menjadi lebih fleksibel.

bot.on(":forward_date").command("help"); // Command /help yang di-forward

// Tangani command yang berasal dari private chat saja.
const pm = bot.filter((ctx) => ctx.chat?.type === "private");
pm.command("start");
pm.command("help");

Memfilter Berdasarkan Jenis Pengirim Pesan

Terdapat lima jenis penulis pesan di Telegram, yaitu

  1. Penulis postingan channel;
  2. Forward otomatis dari channel ke grup diskusi terkait;
  3. Akun pengguna biasa, termasuk bot (misal, pesan yang “normal”);
  4. Admin yang mengirim atas nama grup (admin anonimopen in new window);
  5. Pengguna yang mengirim pesan sebagai salah satu dari channel mereka.

Kamu dapat mengombinasikan filter query dengan mekanisme penanganan update lainnya untuk mengetahui jenis penulis pesan tersebut.

// Postingan channel yang dikirim oleh `ctx.senderChat`
bot.on("channel_post");

// Forward otomatis dari channel `ctx.senderChat`:
bot.on("message:is_automatic_forward");
// Pesan biasa yang dikirim oleh `ctx.from`
bot.on("message").filter((ctx) => ctx.senderChat === undefined);
// Admin anonim di `ctx.chat`
bot.on("message").filter((ctx) => ctx.senderChat?.id === ctx.chat.id);
// Pengguna yang mengirim pesan atas nama channel mereka `ctx.senderChat`
bot.on("message").filter((ctx) =>
  ctx.senderChat !== undefined && ctx.senderChat.id !== ctx.chat.id
);

Memfilter Berdasarkan Property User

Kamu perlu melakukan request tambahan jika ingin memfilter berdasarkan kriteria user, misalnya await ctx.getAuthor() untuk memperoleh informasi penulis pesan tersebut. Filter query tidak akan secara otomatis melakukan request API lanjutan. Meski demikian, pemfilteran semacam itu masih cukup mudah dilakukan:

bot.on("message").filter(
  async (ctx) => {
    const user = await ctx.getAuthor();
    return user.status === "creator" || user.status === "administrator";
  },
  (ctx) => {
    // Menangani pesan dari creator dan para admin.
  },
);

Memanfaatkan Kembali Logika Filter Query

Secara internal, bot.on bergantung kepada sebuah function bernama matchFilter. Function ini menerima sebuah filter query lalu meng-compile-nya menjadi predicate function. Predicate tersebut lalu diteruskan ke bot.filter untuk memfilter update.

Kamu bisa mengimpor matchFilter secara langsung jika ingin menggunakannya di logika pemrogramanmu. Misalnya, kamu jadi bisa mengabaikan update yang cocok dengan query tertentu:

// Abaikan semua pesan atau postingan channel yang berupa teks.
bot.drop(matchFilter(":text"));

Dengan analogi yang sama, kamu bisa menggunakan type filter query yang digunakan di internal grammY:

Memanfaatkan Kembali Type Filter Query

Secara internal, matchFilter menggunakan type predicatesopen in new window TypeScript untuk mengerucutkan type ctx. Ia mengambil sebuah type C extends Context dan Q extends FilterQuery yang kemudian menghasilkan ctx is Filter<C, Q>. Dengan kata lain, type Filter adalah hasil yang kamu terima untuk ctx di middleware.

Kamu bisa meng-import Filter secara langsung jika ingin menggunakannya di logika pemrogramanmu. Sebagai contoh, kamu bisa mendefinisikan sebuah function handler untuk menangani object context tertentu yang sudah difilter oleh filter query:

function handler(ctx: Filter<Context, ":text">) {
  // Tangani object context yang sudah dikerucutkan
}

bot.on(":text", handler);

Lihat referensi API untuk matchFilteropen in new window, Filteropen in new window, dan FilterQueryopen in new window.

Bahasa Query

Bagian ini ditujukan untuk kamu yang ingin memahami filter query grammY secara lebih dalam. Informasi berikut tidak berisi pengetahuan yang diperlukan untuk membuat sebuah bot.

Struktur Query

Setiap query terdiri atas maksimal tiga komponen. Kami memisahkan antara query L1, L2, dan L3, seperti "message", "message:entities", dan "message:entities:url", secara berurutan tergantung dari seberapa banyak komponen yang dimiliki query.

Komponen-komponen query dipisahkan oleh titik dua (:). Dari bagian awal hingga titik dua pertama atau string terakhir query tersebut, kami menyebutnya dengan komponen L1 query. Dari titik dua pertama hingga titik dua kedua atau string terakhir query tersebut, kami menyebutnya dengan komponen L2 query. Dari titik dua kedua hingga string terakhir query tersebut, kami menyebutnya dengan komponen L3 query.

Contoh:

Filter QueryKomponen L1Komponen L2Komponen L3
"message""message"undefinedundefined
"message:entities""message""entities"undefined
"message:entities:mention""message""entities""mention"

Validasi Query

Meski type system bisa menangkap semua filter query yang tidak valid di compile time, namun grammY tetap memeriksa semua filter query di runtime selama proses penyusunan. Setiap filter query akan dicocokkan dengan struktur validasi untuk diperiksa apakah query tersebut memang valid. Dengan begitu, ia akan langsung gagal saat itu juga—alih-alih gagal di runtime—ketika hasilnya tidak valid, karena pernah terjadi sebelumnya, ketika bug di TypeScript menyebabkan masalah serius terhadap type inference system lanjutan yang menjadi penyokong filter query. Jika suatu saat bug tersebut muncul, kita bisa mencegah masalah serupa terjadi lagi. Selain itu, kamu juga akan diberikan pesan error yang lebih bermanfaat.

Performa

grammY mampu memeriksa setiap filter query dalam waktu konstan (amortized) per update, tidak terikat oleh struktur query ataupun update yang masuk.

Validasi filter query hanya dilakukan sekali, ketika bot diinisialisasi dan bot.on() dipanggil.

Ketika dimulai, grammY menurunkan function predicate dari filter query dengan cara memecahnya menjadi beberapa komponen query. Setiap komponen akan di-map ke sebuah function yang mengerjakan satu pemeriksaan in, atau dua pemeriksaan jika komponen tersebut diabaikan dan dua value perlu dilakukan pemeriksaan. Function-function ini kemudian disatukan untuk membentuk sebuah predicate yang akan memeriksa sebanyak mungkin value yang relevan untuk query, tanpa melakukan proses perulangan terhadap key object Update.

Sistem ini menggunakan lebih sedikit operasi dibandingkan dengan beberapa library lainnya, dimana dibutuhkan beberapa pengecekan array ketika melakukan routing update. Dengan demikian, sistem filter query grammY jauh lebih unggul.