ptgb
A complete Telegram Bot API client for Dart

New Bot API Concepts

Part of the Examples. A guided tour of checklists, rich messages, suggested posts, ephemeral messages, guest queries, and join-request guard bots.

Source: example/20_new_bot_api_concepts.dart

// ignore_for_file: file_names
// (numbered intentionally for reading/run order -- see README.md)
// ============================================================================
// 20 — NEWER BOT API CONCEPTS: checklists, rich messages, suggested posts,
//      ephemeral messages, guest queries, and guard-bot join requests
// ============================================================================
//
// Bot API versions 9.1 through 10.2 introduced a handful of features that
// don't fit naturally into the earlier numbered examples because they're
// genuinely different concepts, not just new parameters on familiar
// methods. This file is a guided tour of each, in isolation, so you can
// see the shape of the request/response for each one. A few of them only
// make sense in contexts this file can't fully simulate on its own (a
// connected business account, a guest-mode-enabled bot, a chat that's
// designated this bot as its "guard bot") — those sections say so and show
// the calls you'd make once that context exists.
//
// HOW TO RUN:
//   dart run example/20_new_bot_api_concepts.dart   (with a `.env` file)
// ============================================================================
import 'dart:developer';

import 'package:ptgb/ptgb.dart';

Future<void> main() async {
  final bot = Bot();

  await for (final update in bot.poll()) {
    // ------------------------------------------------------------------
    // CHECKLISTS (Bot API 9.1) — sendChecklist / editMessageChecklist
    // ------------------------------------------------------------------
    // Checklists are a business-account-only message type: a titled list
    // of tasks the recipient can check off. Sending one requires a
    // `businessConnectionId`, which your bot receives via a
    // `business_connection` update once a user connects their Telegram
    // Business account to it (Settings > Business > Chatbots, on their
    // end). This snippet shows the call shape; wire it up to a real
    // `update.businessConnection?['id']` once you have one connected.
    if (update.text == '/checklist-demo') {
      const businessConnectionId = 'REPLACE_WITH_A_REAL_BUSINESS_CONNECTION_ID';
      final checklist = InputChecklist(
        'Trip packing list',
        const [
          InputChecklistTask(1, 'Passport'),
          InputChecklistTask(2, 'Charger'),
          InputChecklistTask(3, 'Sunscreen'),
        ],
        othersCanMarkTasksAsDone: true,
      );
      log('Would call: bot.sendChecklist($businessConnectionId, chatId, '
          'checklist "${checklist.title}")');
      // await bot.sendChecklist(businessConnectionId, update.chatId!, checklist);
      //
      // To edit it afterwards (e.g. after the message_id comes back):
      //   final updated = InputChecklist('Trip packing list', [
      //     const InputChecklistTask(1, 'Passport'),
      //     const InputChecklistTask(2, 'Charger'),
      //     const InputChecklistTask(3, 'Sunscreen'),
      //     const InputChecklistTask(4, 'Book'), // task added
      //   ]);
      //   await bot.editMessageChecklist(businessConnectionId, chatId, messageId, updated);
      continue;
    }

    // ------------------------------------------------------------------
    // RICH MESSAGES (Bot API 10.1/10.2) — sendRichMessage /
    // sendRichMessageDraft
    // ------------------------------------------------------------------
    // Rich messages are structured content — headings, lists, tables,
    // block quotes, embedded media — built from `InputRichBlock*` JSON
    // objects rather than a single plain-text caption. ptgb passes
    // `richMessage` through as raw `Json` (see `sendRichMessage`'s doc
    // comment for why), matching Telegram's `InputRichMessage` shape:
    // a `blocks` array of block objects. This is a minimal two-block
    // example — a heading followed by a paragraph.
    if (update.text == '/rich-demo') {
      final chatId = update.chatId!;
      final richMessage = {
        'blocks': [
          {
            'type': 'section_heading',
            'text': {'type': 'plain', 'text': 'Weekly update'},
          },
          {
            'type': 'paragraph',
            'text': {
              'type': 'plain',
              'text': 'Everything shipped on time this week.',
            },
          },
        ],
      };
      await bot.sendRichMessage(chatId, richMessage);
      // `sendRichMessageDraft` sends the same shape but marks it as a
      // still-being-generated preview — handy for streaming an AI
      // response block by block, the rich-content equivalent of
      // `sendMessageDraft` for plain text (see example/11 for that).
      continue;
    }

    // ------------------------------------------------------------------
    // SUGGESTED POSTS (Bot API 9.2) — approveSuggestedPost /
    // declineSuggestedPost
    // ------------------------------------------------------------------
    // In a channel's "direct messages" chat, users can suggest a post for
    // the channel; it arrives to the bot as an ordinary message with
    // `suggested_post_info` set. The bot (with the right admin rights)
    // approves or declines it — it isn't published until approved.
    final suggestedPostInfo =
        update.anyMessage?['suggested_post_info'] as Json?;
    if (suggestedPostInfo != null) {
      final chatId = update.chatId!;
      final messageId = update.messageId!;
      log('Suggested post received: $suggestedPostInfo');
      // Approve immediately:
      //   await bot.approveSuggestedPost(chatId, messageId);
      // Approve for a specific future time instead:
      //   await bot.approveSuggestedPost(chatId, messageId, sendDate: someUnixTime);
      // Or decline with a reason shown to the poster:
      //   await bot.declineSuggestedPost(chatId, messageId, comment: 'Not on-topic for this channel');
      await bot.approveSuggestedPost(chatId, messageId);
      continue;
    }

    // ------------------------------------------------------------------
    // EPHEMERAL MESSAGES (Bot API 10.2) — editEphemeralMessage* /
    // deleteEphemeralMessage
    // ------------------------------------------------------------------
    // An ephemeral message is visible only to the bot and one specific
    // user in a group, even though it was "sent to the chat" — useful for
    // per-user prompts in a shared space without spamming everyone else.
    // You create one by passing `receiverUserId` (and, if replying to a
    // callback query, `callbackQueryId`) to a handful of send* methods —
    // see `sendLivePhoto`'s doc comment for the pattern, which applies
    // the same way to `sendMessage` and friends. Once sent, Telegram gives
    // you an `ephemeral_message_id` (0 for the ordinary `message_id`,
    // since ephemeral messages don't have one) to edit or delete it with.
    if (update.text == '/ephemeral-demo') {
      final chatId = update.chatId!;
      final userId = update.userId!;
      // NOTE: sendMessage doesn't currently expose receiverUserId/
      // callbackQueryId as typed parameters — reach for the low-level
      // `bot.call` escape hatch to set them until a typed overload lands:
      final sent = await bot.call('sendMessage', {
        'chat_id': chatId,
        'text': 'Only you can see this message.',
        'receiver_user_id': userId,
      }) as Json;
      final ephemeralMessageId = sent['ephemeral_message_id'] as int;
      await Future<void>.delayed(const Duration(seconds: 3));
      await bot.editEphemeralMessageText(
        chatId,
        userId,
        ephemeralMessageId,
        'Still only you — but edited 3 seconds later.',
      );
      // await bot.deleteEphemeralMessage(chatId, userId, ephemeralMessageId);
      continue;
    }

    // ------------------------------------------------------------------
    // GUEST QUERIES (Bot API 10.0) — Update.guestMessage / answerGuestQuery
    // ------------------------------------------------------------------
    // Guest Mode lets a bot receive messages and reply in chats it isn't
    // a member of, if the chat summons it as a guest (and the bot has
    // `supports_guest_queries` enabled via @BotFather). Such messages
    // arrive as `update.guestMessage` instead of `update.message`; use
    // its `guest_query_id` (exposed as the `update.guestQueryId`
    // shortcut) to send exactly one reply back via `answerGuestQuery`.
    if (update.guestMessage != null) {
      final guestQueryId = update.guestQueryId!;
      log('Guest message: ${update.text}');
      await bot.answerGuestQuery(guestQueryId, {
        'type': 'article',
        'id': '1',
        'title': 'Hello from a guest bot',
        'input_message_content': {'message_text': 'Thanks for summoning me!'},
      });
      continue;
    }

    // ------------------------------------------------------------------
    // GUARD-BOT JOIN REQUESTS (Bot API 10.1) — answerChatJoinRequestQuery /
    // sendChatJoinRequestWebApp
    // ------------------------------------------------------------------
    // Compare this to example/13, which covers the *ordinary*
    // approve/decline-immediately flow via `approveChatJoinRequest`. If a
    // chat instead designates this bot as its "guard bot" (see
    // `ChatFullInfo.guard_bot`), join requests arrive with an extra
    // `query_id` field (exposed as `update.chatJoinRequestQueryId`) and
    // the bot has a 10-second window to resolve them differently — either
    // directly, or by first showing the requester a Mini App (e.g. to run
    // a captcha) before deciding.
    final chatJoinRequestQueryId = update.chatJoinRequestQueryId;
    if (chatJoinRequestQueryId != null) {
      // Resolve directly:
      //   await bot.answerChatJoinRequestQuery(chatJoinRequestQueryId, 'approve');
      //   await bot.answerChatJoinRequestQuery(chatJoinRequestQueryId, 'decline');
      //   await bot.answerChatJoinRequestQuery(chatJoinRequestQueryId, 'queue'); // let a human decide
      //
      // ...or show a Mini App first and decide based on what it reports back:
      //   await bot.sendChatJoinRequestWebApp(chatJoinRequestQueryId, {
      //     'url': 'https://example.com/verify',
      //   });
      log('Guard-bot join request query: $chatJoinRequestQueryId');
      await bot.answerChatJoinRequestQuery(chatJoinRequestQueryId, 'queue');
      continue;
    }

    if (update.text == '/start') {
      final chatId = update.chatId;
      if (chatId != null) {
        await bot.sendMessage(
          chatId,
          'Try /checklist-demo, /rich-demo, or /ephemeral-demo to see the '
          'newer Bot API concepts covered in this example.',
        );
      }
    }
  }
}

Run it from the package root:

dart run example/20_new_bot_api_concepts.dart