ptgb
A complete Telegram Bot API client for Dart

God Mode Bot

Part of the Examples. Everything above, combined into one polished, menu-driven bot.

Source: example/11_god_mode_bot.dart

// ignore_for_file: file_names
// (numbered intentionally for reading/run order -- see README.md)

// ============================================================================
// 11 — GOD MODE: a full-featured showcase bot
// ============================================================================
//
// This is the "kitchen sink" example: a single bot that ties together
// everything shown in the earlier numbered examples, plus a few extras,
// behind a friendly /menu. Use it as a reference / copy-paste starting
// point for a real project — it's organized into clearly separated
// sections so you can lift out only the parts you need.
//
// FEATURES DEMONSTRATED:
//   • Command routing + registered bot commands
//   • Inline & reply keyboards, including a live-editing counter
//   • Sending photos, albums, locations, and stickers
//   • Polls, quizzes, dice, and message reactions
//   • Inline mode (@yourbot ...)
//   • Telegram Stars payments
//   • Chat administration (mute/pin) + forum topics
//   • Web App `initData` verification
//   • A typing indicator while "thinking"
//   • Graceful shutdown on Ctrl+C
//
// HOW TO RUN:
//   dart run example/11_god_mode_bot.dart   (with a `.env` file, see example 02)
// ============================================================================

import 'dart:async';
import 'dart:developer';
import 'dart:io';

import 'package:ptgb/ptgb.dart';

// In-memory counter state, keyed by "chatId:messageId" (see example 06).
final Map<String, int> _counters = {};

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

  // --- One-time setup ------------------------------------------------------
  await bot.setMyCommands([
    {'command': 'menu', 'description': 'Show everything this bot can do'},
    {'command': 'counter', 'description': 'A live-editing +/- counter'},
    {'command': 'photo', 'description': 'Send a photo + an album'},
    {'command': 'location', 'description': 'Send a venue on the map'},
    {'command': 'poll', 'description': 'Send a quiz poll'},
    {'command': 'dice', 'description': 'Roll an animated die'},
    {'command': 'buy', 'description': 'Buy something with Telegram Stars'},
  ]);
  await bot.setMyDescription(
    description: 'A ptgb demo bot showcasing keyboards, media, polls, payments, and more.',
  );

  log('God-mode bot running. Press Ctrl+C to stop.');

  // Graceful shutdown: release the HTTP client's resources instead of
  // leaving connections open when the process is killed.
  ProcessSignal.sigint.watch().listen((_) {
    log('Shutting down...');
    bot.dispose();
    exit(0);
  });

  await for (final update in bot.poll()) {
    // Route each update to its handler. Order matters: check the more
    // specific payload types (callback query, inline query, ...) before
    // falling back to plain text commands.
    try {
      if (update.callbackQuery != null) {
        await _handleCallback(bot, update);
      } else if (update.inlineQuery != null) {
        await _handleInlineQuery(bot, update);
      } else if (update.preCheckoutQuery != null) {
        await bot.answerPreCheckoutQuery(update.preCheckoutQuery!['id'] as String, true);
      } else if (update.message?['successful_payment'] != null) {
        await bot.sendMessage(update.chatId!, '✅ Payment received, thank you!');
      } else if (update.text != null) {
        await _handleCommand(bot, update);
      }
    } on TelegramApiException catch (e) {
      // Never let one failed API call crash the whole bot — log and continue.
      log('Telegram API error: $e');
    }
  }
}

// ---------------------------------------------------------------------------
// Command handling
// ---------------------------------------------------------------------------

Future<void> _handleCommand(Bot bot, Update update) async {
  final chatId = update.chatId!;
  final command = update.text!.split(' ').first;

  switch (command) {
    case '/menu':
    case '/start':
      await bot.sendMessage(
        chatId,
        '*God Mode Demo Bot* 🤖\n\nTry any of the commands below:',
        parseMode: ParseMode.markdown,
        replyMarkup: InlineKeyboardMarkup([
          [InlineKeyboardButton.callback('🔢 Counter', 'menu:counter')],
          [InlineKeyboardButton.callback('📷 Photos', 'menu:photo')],
          [InlineKeyboardButton.callback('📍 Location', 'menu:location')],
          [InlineKeyboardButton.callback('❓ Quiz', 'menu:poll')],
          [InlineKeyboardButton.callback('🎲 Dice', 'menu:dice')],
          [InlineKeyboardButton.callback('⭐ Buy something', 'menu:buy')],
        ]),
      );

    case '/counter':
      final sent = await bot.sendMessage(chatId, 'Count: 0', replyMarkup: _counterKeyboard());
      _counters['$chatId:${sent['message_id']}'] = 0;

    case '/photo':
      // Show a "sending photo..." indicator while we prepare the album.
      await bot.sendChatAction(chatId, ChatAction.uploadPhoto);
      await bot.sendPhoto(chatId, InputFile.url('https://picsum.photos/800/600'), caption: 'A single photo.');
      await bot.sendMediaGroup(chatId, [
        InputMediaPhoto(InputFile.url('https://picsum.photos/seed/a/600')),
        InputMediaPhoto(InputFile.url('https://picsum.photos/seed/b/600'), caption: 'Album item 2'),
      ]);

    case '/location':
      await bot.sendVenue(
        chatId,
        41.0082,
        28.9784,
        'Hagia Sophia',
        'Sultan Ahmet, Istanbul, Turkey',
      );

    case '/poll':
      await bot.sendPoll(
        chatId,
        'Which of these is a Telegram Bot API method?',
        ['sendMessage', 'sendEmail', 'sendSMS'],
        type: PollType.quiz,
        correctOptionId: 0,
      );

    case '/dice':
      await bot.sendDice(chatId, emoji: DiceEmoji.dart);

    case '/buy':
      await bot.sendInvoice(
        chatId,
        'Supporter Badge',
        'A small way to support this demo bot.',
        'supporter_badge_v1',
        'XTR',
        [
          {'label': 'Supporter Badge', 'amount': 25},
        ],
      );

    default:
      await bot.sendMessage(chatId, 'Unknown command — try /menu to see what I can do.');
  }
}

// ---------------------------------------------------------------------------
// Callback query handling (inline keyboard button taps)
// ---------------------------------------------------------------------------

InlineKeyboardMarkup _counterKeyboard() => InlineKeyboardMarkup.single([
      InlineKeyboardButton.callback('➖', 'counter:dec'),
      InlineKeyboardButton.callback('🔄', 'counter:reset'),
      InlineKeyboardButton.callback('➕', 'counter:inc'),
    ]);

Future<void> _handleCallback(Bot bot, Update update) async {
  final callback = update.callbackQuery!;
  final data = update.callbackData ?? '';
  final chatId = update.chatId!;
  final messageId = (callback['message'] as Map)['message_id'] as int;

  // The main /menu screen re-dispatches to the same logic as the slash
  // commands, so tapping a button behaves identically to typing the command.
  if (data.startsWith('menu:')) {
    await bot.answerCallbackQuery(callback['id'] as String);
    // Build a minimal synthetic "message" update carrying the button's
    // target command, so `_handleCommand` can handle it uniformly.
    final syntheticMessage = <String, dynamic>{
      ...callback['message'] as Map<String, dynamic>,
      'text': '/${data.substring(5)}',
    };
    await _handleCommand(bot, Update({'update_id': update.updateId, 'message': syntheticMessage}));
    return;
  }

  if (data.startsWith('counter:')) {
    final key = '$chatId:$messageId';
    final current = _counters[key] ?? 0;
    final next = switch (data) {
      'counter:inc' => current + 1,
      'counter:dec' => current - 1,
      _ => 0,
    };
    _counters[key] = next;
    await bot.answerCallbackQuery(callback['id'] as String);
    await bot.editMessageText(
      'Count: $next',
      chatId: chatId,
      messageId: messageId,
      replyMarkup: _counterKeyboard(),
    );
  }
}

// ---------------------------------------------------------------------------
// Inline mode handling (@yourbot ...)
// ---------------------------------------------------------------------------

Future<void> _handleInlineQuery(Bot bot, Update update) async {
  final query = update.inlineQuery!;
  final searchText = (query['query'] as String? ?? '').trim();

  await bot.answerInlineQuery(query['id'] as String, [
    {
      'type': 'article',
      'id': '1',
      'title': 'Send: "$searchText"',
      'input_message_content': {'message_text': searchText.isEmpty ? 'Hello from god mode!' : searchText},
    },
  ]);
}

Run it from the package root:

dart run example/11_god_mode_bot.dart