// КругСвоих Bot SDK for Dart / Flutter // https://krugsvoih.ru/bots.html // // Add to pubspec.yaml: // dependencies: // http: ^1.2.0 // // Quick start: // // final bot = KSBot('YOUR_TOKEN'); // // bot.onCommand('start', (msg) async { // await bot.sendMessage(msg.chatId, 'Привет! 👋'); // }); // // await bot.runPolling(); library ksbot; import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; const _baseUrl = 'https://v.krugsvoih.ru/bot'; // ── Types ────────────────────────────────────────────────────────────────────── class BotError implements Exception { final String description; const BotError(this.description); @override String toString() => 'BotError: $description'; } class BotInfo { final String id, username, displayName; final String? avatarUrl; final bool isBot; BotInfo.fromJson(Map j) : id = j['id'] as String, username = j['username'] as String, displayName = j['displayName'] as String, avatarUrl = j['avatarUrl'] as String?, isBot = (j['isBot'] as bool?) ?? false; } class Message { final String id, chatId, senderId, senderUsername, content; final DateTime sentAt; final bool isEdited; Message.fromJson(Map j) : id = j['id'] as String, chatId = j['chatId'] as String, senderId = j['senderId'] as String, senderUsername = j['senderUsername'] as String, content = j['content'] as String, sentAt = DateTime.parse(j['sentAt'] as String), isEdited = (j['isEdited'] as bool?) ?? false; } class CallbackQuery { final String id, fromUserId, fromUsername, chatId, messageId, data; CallbackQuery.fromJson(Map j) : id = j['id'] as String, fromUserId = j['fromUserId'] as String, fromUsername = j['fromUsername'] as String, chatId = j['chatId'] as String, messageId = j['messageId'] as String, data = j['data'] as String; } class Update { final int updateId; final String type; final Message? message; final CallbackQuery? callbackQuery; Update.fromJson(Map j) : updateId = j['updateId'] as int, type = j['type'] as String, message = j['message'] != null ? Message.fromJson(j['message'] as Map) : null, callbackQuery = j['callbackQuery'] != null ? CallbackQuery.fromJson(j['callbackQuery'] as Map) : null; } class ChatInfo { final String id, name, type; ChatInfo.fromJson(Map j) : id = j['id'] as String, name = j['name'] as String, type = j['type'] as String; } class ChatMember { final String userId, username, displayName, role; final String? avatarUrl; final bool isBot; ChatMember.fromJson(Map j) : userId = j['userId'] as String, username = j['username'] as String, displayName = j['displayName'] as String, avatarUrl = j['avatarUrl'] as String?, role = j['role'] as String, isBot = (j['isBot'] as bool?) ?? false; } class WebhookInfo { final String? url; final int pendingUpdateCount; WebhookInfo.fromJson(Map j) : url = j['url'] as String?, pendingUpdateCount = (j['pendingUpdateCount'] as int?) ?? 0; } class BotCommand { final String command, description; const BotCommand(this.command, this.description); Map toJson() => {'command': command, 'description': description}; } /// Helper: builds an inline keyboard button. Map kbButton(String text, {String? callbackData, String? url}) { final btn = {'text': text}; if (callbackData != null) btn['callbackData'] = callbackData; if (url != null) btn['url'] = url; return btn; } /// Helper: builds an inline keyboard markup. /// /// Example: /// ```dart /// final markup = inlineKeyboard([ /// [kbButton('✅ Да', callbackData: 'yes'), kbButton('❌ Нет', callbackData: 'no')], /// [kbButton('🌐 Сайт', url: 'https://krugsvoih.ru')], /// ]); /// ``` Map inlineKeyboard(List>> rows) => {'inlineKeyboard': rows}; // ── Client ───────────────────────────────────────────────────────────────────── class KSBot { final String token; late final String _base; int _offset = 0; bool _running = false; final _messageHandlers = Function(Message)>[]; final _commandHandlers = Function(Message)>{}; final _callbackHandlers = Function(CallbackQuery)>[]; KSBot(this.token) { _base = '$_baseUrl/$token'; } // ── Low-level request ────────────────────────────────────────────────────── Future _request(String method, [Map? data]) async { final url = Uri.parse('$_base/$method'); late http.Response resp; if (data == null) { resp = await http.get(url); } else { resp = await http.post(url, headers: {'Content-Type': 'application/json'}, body: jsonEncode(data)); } final body = jsonDecode(resp.body) as Map; if (!(body['ok'] as bool)) { throw BotError(body['description'] as String? ?? 'Unknown error'); } return body['result']; } // ── Bot info ─────────────────────────────────────────────────────────────── Future getMe() async => BotInfo.fromJson((await _request('getMe')) as Map); // ── Sending messages ─────────────────────────────────────────────────────── Future sendMessage( String chatId, String text, { String? replyToMessageId, Map? replyMarkup, }) async { final payload = {'chatId': chatId, 'text': text}; if (replyToMessageId != null) payload['replyToMessageId'] = replyToMessageId; if (replyMarkup != null) payload['replyMarkup'] = replyMarkup; return Message.fromJson((await _request('sendMessage', payload)) as Map); } Future sendPhoto( String chatId, String photoUrl, { String? caption, Map? replyMarkup, }) async { final payload = {'chatId': chatId, 'photoUrl': photoUrl}; if (caption != null) payload['caption'] = caption; if (replyMarkup != null) payload['replyMarkup'] = replyMarkup; return Message.fromJson((await _request('sendPhoto', payload)) as Map); } Future sendDocument( String chatId, String documentUrl, String fileName, { String? caption, Map? replyMarkup, }) async { final payload = { 'chatId': chatId, 'documentUrl': documentUrl, 'fileName': fileName, }; if (caption != null) payload['caption'] = caption; if (replyMarkup != null) payload['replyMarkup'] = replyMarkup; return Message.fromJson((await _request('sendDocument', payload)) as Map); } /// Upload and send a photo file directly (no external hosting needed). Future uploadPhoto( String chatId, File photo, { String? caption, }) async { final url = Uri.parse('$_base/sendPhoto/upload'); final req = http.MultipartRequest('POST', url); req.fields['chatId'] = chatId; if (caption != null) req.fields['caption'] = caption; req.files.add(await http.MultipartFile.fromPath('photo', photo.path)); final streamed = await req.send(); final resp = await http.Response.fromStream(streamed); final body = jsonDecode(resp.body) as Map; if (!(body['ok'] as bool)) throw BotError(body['description'] as String? ?? 'Unknown error'); return Message.fromJson(body['result'] as Map); } /// Upload and send any file directly (no external hosting needed). Future uploadDocument( String chatId, File document, { String? caption, }) async { final url = Uri.parse('$_base/sendDocument/upload'); final req = http.MultipartRequest('POST', url); req.fields['chatId'] = chatId; if (caption != null) req.fields['caption'] = caption; req.files.add(await http.MultipartFile.fromPath('document', document.path)); final streamed = await req.send(); final resp = await http.Response.fromStream(streamed); final body = jsonDecode(resp.body) as Map; if (!(body['ok'] as bool)) throw BotError(body['description'] as String? ?? 'Unknown error'); return Message.fromJson(body['result'] as Map); } Future forwardMessage( String fromChatId, String messageId, String toChatId) async => Message.fromJson((await _request('forwardMessage', { 'fromChatId': fromChatId, 'messageId': messageId, 'toChatId': toChatId, })) as Map); // ── Editing and deleting ─────────────────────────────────────────────────── Future editMessageText( String chatId, String messageId, String sentAt, String text) async => Message.fromJson((await _request('editMessageText', { 'chatId': chatId, 'messageId': messageId, 'sentAt': sentAt, 'text': text, })) as Map); Future deleteMessage( String chatId, String messageId, String sentAt) async => _request('deleteMessage', {'chatId': chatId, 'messageId': messageId, 'sentAt': sentAt}); // ── Chat management ──────────────────────────────────────────────────────── Future getChat(String chatId) async => ChatInfo.fromJson((await _request('getChat', {'chatId': chatId})) as Map); Future> getChatMembers(String chatId) async { final list = (await _request('getChatMembers', {'chatId': chatId})) as List; return list.map((e) => ChatMember.fromJson(e as Map)).toList(); } Future getChatMember(String chatId, String userId) async => ChatMember.fromJson((await _request('getChatMember', { 'chatId': chatId, 'userId': userId, })) as Map); Future banChatMember(String chatId, String userId) => _request('banChatMember', {'chatId': chatId, 'userId': userId}); Future unbanChatMember(String chatId, String userId) => _request('unbanChatMember', {'chatId': chatId, 'userId': userId}); Future leaveChat(String chatId) => _request('leaveChat', {'chatId': chatId}); // ── Pinning messages ─────────────────────────────────────────────────────── Future pinMessage(String chatId, String messageId, String sentAt) => _request('pinChatMessage', { 'chatId': chatId, 'messageId': messageId, 'sentAt': sentAt, }); Future unpinMessage(String chatId) => _request('unpinChatMessage', {'chatId': chatId}); // ── Updates ──────────────────────────────────────────────────────────────── Future> getUpdates({ int offset = 0, int limit = 100, int timeout = 0, }) async { final list = (await _request('getUpdates', { 'offset': offset, 'limit': limit, 'timeout': timeout, })) as List; return list.map((e) => Update.fromJson(e as Map)).toList(); } // ── Webhooks ─────────────────────────────────────────────────────────────── Future setWebhook(String url) => _request('setWebhook', {'url': url}); Future deleteWebhook() => _request('deleteWebhook'); Future getWebhookInfo() async => WebhookInfo.fromJson((await _request('getWebhookInfo')) as Map); // ── Commands ─────────────────────────────────────────────────────────────── Future setMyCommands(List commands) => _request('setMyCommands', {'commands': commands.map((c) => c.toJson()).toList()}); Future> getMyCommands() async { final list = (await _request('getMyCommands')) as List; return list.map((e) { final m = e as Map; return BotCommand(m['command'] as String, m['description'] as String); }).toList(); } // ── Callback queries ─────────────────────────────────────────────────────── Future answerCallbackQuery( String callbackQueryId, { String? text, bool showAlert = false, }) { final payload = { 'callbackQueryId': callbackQueryId, 'showAlert': showAlert, }; if (text != null) payload['text'] = text; return _request('answerCallbackQuery', payload); } // ── Handler registration ─────────────────────────────────────────────────── void onMessage(FutureOr Function(Message) handler) { _messageHandlers.add((msg) async => handler(msg)); } void onCommand(String command, FutureOr Function(Message) handler) { _commandHandlers[command.replaceFirst(RegExp(r'^/'), '')] = (msg) async => handler(msg); } void onCallback(FutureOr Function(CallbackQuery) handler) { _callbackHandlers.add((cb) async => handler(cb)); } // ── Dispatch ─────────────────────────────────────────────────────────────── Future _dispatch(Update update) async { if (update.message != null) { final msg = update.message!; final text = msg.content; if (text.startsWith('/')) { final parts = text.trim().split(RegExp(r'\s+')); final cmd = parts[0].replaceFirst('/', '').split('@')[0].toLowerCase(); final handler = _commandHandlers[cmd]; if (handler != null) { await handler(msg); return; } } for (final h in _messageHandlers) { await h(msg); } } if (update.callbackQuery != null) { for (final h in _callbackHandlers) { await h(update.callbackQuery!); } } } // ── Polling loop ─────────────────────────────────────────────────────────── Future runPolling({int pollTimeout = 30}) async { final me = await getMe(); print('Bot @${me.username} started. Press Ctrl+C to stop.'); _running = true; while (_running) { try { final updates = await getUpdates(offset: _offset, timeout: pollTimeout); for (final u in updates) { try { await _dispatch(u); } catch (e) { print('[handler error] $e'); } _offset = u.updateId + 1; } } on BotError catch (e) { print('[bot error] $e'); await Future.delayed(const Duration(seconds: 5)); } catch (e) { if (!_running) break; print('[error] $e'); await Future.delayed(const Duration(seconds: 5)); } } } void stop() => _running = false; }