// КругСвоих Bot SDK for C# // https://krugsvoih.ru/bots.html // // Requires .NET 6+ (uses HttpClient and System.Text.Json). // No NuGet packages needed. // // Quick start: // // var bot = new KsBot("YOUR_TOKEN"); // // bot.OnCommand("start", async (msg) => // await bot.SendMessageAsync(msg.ChatId, "Привет! 👋")); // // await bot.RunPollingAsync(CancellationToken.None); using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; namespace KrugsvoihBot; // ── Types ────────────────────────────────────────────────────────────────────── public class BotError : Exception { public string Description { get; } public BotError(string description) : base(description) => Description = description; } public record BotInfo( string Id, string Username, string DisplayName, string? AvatarUrl, bool IsBot ); public record Message( string Id, string ChatId, string SenderId, string SenderUsername, string Content, DateTime SentAt, bool IsEdited ); public record CallbackQuery( string Id, string FromUserId, string FromUsername, string ChatId, string MessageId, string Data ); public record Update( long UpdateId, string Type, Message? Message, CallbackQuery? CallbackQuery ); public record ChatInfo(string Id, string Name, string Type); public record ChatMember( string UserId, string Username, string DisplayName, string? AvatarUrl, string Role, bool IsBot ); public record WebhookInfo(string? Url, int PendingUpdateCount); public record BotCommand(string Command, string Description); public record InlineKeyboardButton( string Text, string? CallbackData = null, string? Url = null ); public record InlineKeyboardMarkup(IList> InlineKeyboard); public record SendMessageOptions( string? ReplyToMessageId = null, InlineKeyboardMarkup? ReplyMarkup = null ); public record SendMediaOptions( string? Caption = null, InlineKeyboardMarkup? ReplyMarkup = null ); // ── Client ───────────────────────────────────────────────────────────────────── public class KsBot { private const string BaseUrl = "https://v.krugsvoih.ru/bot"; private readonly string _base; private readonly HttpClient _http; private long _offset; private readonly List> _messageHandlers = new(); private readonly Dictionary> _commandHandlers = new(); private readonly List> _callbackHandlers = new(); private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; public KsBot(string token) { _base = $"{BaseUrl}/{token}"; _http = new HttpClient { Timeout = TimeSpan.FromSeconds(40) }; } // ── Low-level request ────────────────────────────────────────────────────── private async Task RequestAsync(string method, object? payload = null, CancellationToken ct = default) { var url = $"{_base}/{method}"; HttpResponseMessage resp; if (payload is null) { resp = await _http.GetAsync(url, ct); } else { var body = new StringContent( JsonSerializer.Serialize(payload, JsonOpts), Encoding.UTF8, "application/json"); resp = await _http.PostAsync(url, body, ct); } var raw = await resp.Content.ReadAsStringAsync(ct); using var doc = JsonDocument.Parse(raw); var root = doc.RootElement; if (!root.GetProperty("ok").GetBoolean()) { var desc = root.TryGetProperty("description", out var d) ? d.GetString()! : "Unknown error"; throw new BotError(desc); } return JsonSerializer.Deserialize( root.GetProperty("result").GetRawText(), JsonOpts)!; } // ── Bot info ─────────────────────────────────────────────────────────────── public Task GetMeAsync(CancellationToken ct = default) => RequestAsync("getMe", null, ct); // ── Sending messages ─────────────────────────────────────────────────────── public Task SendMessageAsync(string chatId, string text, SendMessageOptions? opts = null, CancellationToken ct = default) { var payload = new Dictionary { ["chatId"] = chatId, ["text"] = text }; if (opts?.ReplyToMessageId is not null) payload["replyToMessageId"] = opts.ReplyToMessageId; if (opts?.ReplyMarkup is not null) payload["replyMarkup"] = opts.ReplyMarkup; return RequestAsync("sendMessage", payload, ct); } public Task SendPhotoAsync(string chatId, string photoUrl, SendMediaOptions? opts = null, CancellationToken ct = default) { var payload = new Dictionary { ["chatId"] = chatId, ["photoUrl"] = photoUrl }; if (opts?.Caption is not null) payload["caption"] = opts.Caption; if (opts?.ReplyMarkup is not null) payload["replyMarkup"] = opts.ReplyMarkup; return RequestAsync("sendPhoto", payload, ct); } public Task SendDocumentAsync(string chatId, string documentUrl, string fileName, SendMediaOptions? opts = null, CancellationToken ct = default) { var payload = new Dictionary { ["chatId"] = chatId, ["documentUrl"] = documentUrl, ["fileName"] = fileName, }; if (opts?.Caption is not null) payload["caption"] = opts.Caption; if (opts?.ReplyMarkup is not null) payload["replyMarkup"] = opts.ReplyMarkup; return RequestAsync("sendDocument", payload, ct); } public Task ForwardMessageAsync(string fromChatId, string messageId, string toChatId, CancellationToken ct = default) => RequestAsync("forwardMessage", new { fromChatId, messageId, toChatId }, ct); // ── Editing and deleting ─────────────────────────────────────────────────── public Task EditMessageTextAsync(string chatId, string messageId, string sentAt, string text, CancellationToken ct = default) => RequestAsync("editMessageText", new { chatId, messageId, sentAt, text }, ct); public async Task DeleteMessageAsync(string chatId, string messageId, string sentAt, CancellationToken ct = default) => await RequestAsync("deleteMessage", new { chatId, messageId, sentAt }, ct); // ── Chat management ──────────────────────────────────────────────────────── public Task GetChatAsync(string chatId, CancellationToken ct = default) => RequestAsync("getChat", new { chatId }, ct); public Task> GetChatMembersAsync(string chatId, CancellationToken ct = default) => RequestAsync>("getChatMembers", new { chatId }, ct); public Task GetChatMemberAsync(string chatId, string userId, CancellationToken ct = default) => RequestAsync("getChatMember", new { chatId, userId }, ct); public async Task BanChatMemberAsync(string chatId, string userId, CancellationToken ct = default) => await RequestAsync("banChatMember", new { chatId, userId }, ct); public async Task UnbanChatMemberAsync(string chatId, string userId, CancellationToken ct = default) => await RequestAsync("unbanChatMember", new { chatId, userId }, ct); public async Task LeaveChatAsync(string chatId, CancellationToken ct = default) => await RequestAsync("leaveChat", new { chatId }, ct); // ── Pinning messages ─────────────────────────────────────────────────────── public async Task PinMessageAsync(string chatId, string messageId, string sentAt, CancellationToken ct = default) => await RequestAsync("pinChatMessage", new { chatId, messageId, sentAt }, ct); public async Task UnpinMessageAsync(string chatId, CancellationToken ct = default) => await RequestAsync("unpinChatMessage", new { chatId }, ct); // ── Updates ──────────────────────────────────────────────────────────────── public Task> GetUpdatesAsync(long offset = 0, int limit = 100, int timeout = 0, CancellationToken ct = default) => RequestAsync>("getUpdates", new { offset, limit, timeout }, ct); // ── Webhooks ─────────────────────────────────────────────────────────────── public async Task SetWebhookAsync(string url, CancellationToken ct = default) => await RequestAsync("setWebhook", new { url }, ct); public async Task DeleteWebhookAsync(CancellationToken ct = default) => await RequestAsync("deleteWebhook", null, ct); public Task GetWebhookInfoAsync(CancellationToken ct = default) => RequestAsync("getWebhookInfo", null, ct); // ── Commands ─────────────────────────────────────────────────────────────── public async Task SetMyCommandsAsync(IEnumerable commands, CancellationToken ct = default) => await RequestAsync("setMyCommands", new { commands }, ct); public Task> GetMyCommandsAsync(CancellationToken ct = default) => RequestAsync>("getMyCommands", null, ct); // ── Callback queries ─────────────────────────────────────────────────────── public async Task AnswerCallbackQueryAsync(string callbackQueryId, string? text = null, bool showAlert = false, CancellationToken ct = default) => await RequestAsync("answerCallbackQuery", new { callbackQueryId, text, showAlert }, ct); // ── Handler registration ─────────────────────────────────────────────────── public KsBot OnMessage(Func handler) { _messageHandlers.Add(handler); return this; } public KsBot OnMessage(Action handler) => OnMessage(msg => { handler(msg); return Task.CompletedTask; }); public KsBot OnCommand(string command, Func handler) { _commandHandlers[command.TrimStart('/')] = handler; return this; } public KsBot OnCommand(string command, Action handler) => OnCommand(command, msg => { handler(msg); return Task.CompletedTask; }); public KsBot OnCallback(Func handler) { _callbackHandlers.Add(handler); return this; } public KsBot OnCallback(Action handler) => OnCallback(cb => { handler(cb); return Task.CompletedTask; }); // ── Dispatch ─────────────────────────────────────────────────────────────── private async Task DispatchAsync(Update update) { if (update.Message is { } msg) { var text = msg.Content ?? ""; if (text.StartsWith("/")) { var cmd = text.Split(' ')[0].TrimStart('/').Split('@')[0].ToLower(); if (_commandHandlers.TryGetValue(cmd, out var h)) { await h(msg); return; } } foreach (var h in _messageHandlers) await h(msg); } if (update.CallbackQuery is { } cb) foreach (var h in _callbackHandlers) await h(cb); } // ── Polling loop ─────────────────────────────────────────────────────────── public async Task RunPollingAsync(CancellationToken ct, int pollTimeout = 30) { var me = await GetMeAsync(ct); Console.WriteLine($"Bot @{me.Username} started. Press Ctrl+C to stop."); while (!ct.IsCancellationRequested) { try { var updates = await GetUpdatesAsync(_offset, 100, pollTimeout, ct); foreach (var u in updates) { try { await DispatchAsync(u); } catch (Exception ex) { Console.Error.WriteLine($"[handler error] {ex.Message}"); } _offset = u.UpdateId + 1; } } catch (OperationCanceledException) { break; } catch (BotError ex) { Console.Error.WriteLine($"[bot error] {ex.Description}"); await Task.Delay(5000, ct); } catch (Exception ex) { Console.Error.WriteLine($"[error] {ex.Message}"); await Task.Delay(5000, ct); } } } // ── Keyboard helpers ─────────────────────────────────────────────────────── public static InlineKeyboardMarkup InlineKeyboard(params IList[] rows) => new(rows); public static InlineKeyboardButton CallbackButton(string text, string callbackData) => new(text, CallbackData: callbackData); public static InlineKeyboardButton UrlButton(string text, string url) => new(text, Url: url); }