const std = @import("std"); const http = @import("http.zig"); const Allocator = std.mem.Allocator; pub const Voice = struct { file_id: []const u8, duration: ?i64 = null, }; pub const VideoNote = struct { file_id: []const u8, duration: ?i64 = null, }; pub const Message = struct { message_id: i64, chat: struct { id: i64, @"type": []const u8 = "private", }, text: ?[]const u8 = null, voice: ?Voice = null, video_note: ?VideoNote = null, }; pub const Update = struct { update_id: i64, message: ?Message = null, }; pub const GetUpdatesResponse = struct { ok: bool, result: []Update = &.{}, }; pub const GetFileResponse = struct { ok: bool, result: ?struct { file_path: ?[]const u8 = null, } = null, }; pub const SendMessageBody = struct { chat_id: i64, text: []const u8, reply_to_message_id: ?i64 = null, }; /// Parsed JSON result that also owns the raw JSON body. /// Must call deinit() to free both. pub fn OwnedParsed(comptime T: type) type { return struct { parsed: std.json.Parsed(T), raw_body: []u8, allocator: Allocator, pub fn deinit(self: *@This()) void { self.parsed.deinit(); self.allocator.free(self.raw_body); } }; } pub const TelegramBot = struct { allocator: Allocator, token: []const u8, api_base: []const u8, pub fn init(allocator: Allocator, token: []const u8) !TelegramBot { const api_base = try std.fmt.allocPrint(allocator, "https://api.telegram.org/bot{s}", .{token}); return .{ .allocator = allocator, .token = token, .api_base = api_base, }; } pub fn deinit(self: *TelegramBot) void { self.allocator.free(self.api_base); } pub fn getUpdates(self: *TelegramBot, offset: i64, timeout: u32) !OwnedParsed(GetUpdatesResponse) { const url = try std.fmt.allocPrint(self.allocator, "{s}/getUpdates?offset={d}&timeout={d}", .{ self.api_base, offset, timeout }); defer self.allocator.free(url); const body = try http.httpGet(self.allocator, url); const parsed = std.json.parseFromSlice(GetUpdatesResponse, self.allocator, body, .{ .ignore_unknown_fields = true }) catch { self.allocator.free(body); return error.HttpRequestFailed; }; return .{ .parsed = parsed, .raw_body = body, .allocator = self.allocator, }; } pub fn getFilePath(self: *TelegramBot, file_id: []const u8) ![]u8 { const url = try std.fmt.allocPrint(self.allocator, "{s}/getFile?file_id={s}", .{ self.api_base, file_id }); defer self.allocator.free(url); const body = try http.httpGet(self.allocator, url); defer self.allocator.free(body); const parsed = try std.json.parseFromSlice(GetFileResponse, self.allocator, body, .{ .ignore_unknown_fields = true }); defer parsed.deinit(); if (parsed.value.result) |result| { if (result.file_path) |fp| { return self.allocator.dupe(u8, fp); } } return error.HttpRequestFailed; } pub fn downloadFile(self: *TelegramBot, file_path: []const u8, dest: []const u8) !void { const url = try std.fmt.allocPrint(self.allocator, "https://api.telegram.org/file/bot{s}/{s}", .{ self.token, file_path }); defer self.allocator.free(url); try http.downloadToFile(self.allocator, url, dest); } pub fn sendMessage(self: *TelegramBot, chat_id: i64, text: []const u8, reply_to: ?i64) !void { const url = try std.fmt.allocPrint(self.allocator, "{s}/sendMessage", .{self.api_base}); defer self.allocator.free(url); const msg = SendMessageBody{ .chat_id = chat_id, .text = text, .reply_to_message_id = reply_to, }; const json_body = std.json.Stringify.valueAlloc(self.allocator, msg, .{}) catch return; defer self.allocator.free(json_body); const resp = http.httpPostJson(self.allocator, url, json_body) catch return; self.allocator.free(resp); } pub fn sendVoice(self: *TelegramBot, chat_id: i64, ogg_path: []const u8, reply_to: ?i64) !void { const url = try std.fmt.allocPrint(self.allocator, "{s}/sendVoice", .{self.api_base}); defer self.allocator.free(url); const chat_id_str = try std.fmt.allocPrint(self.allocator, "{d}", .{chat_id}); defer self.allocator.free(chat_id_str); var fields_buf: [2][2][]const u8 = undefined; var field_count: usize = 1; fields_buf[0] = .{ "chat_id", chat_id_str }; var reply_str: ?[]u8 = null; defer if (reply_str) |s| self.allocator.free(s); if (reply_to) |r| { reply_str = try std.fmt.allocPrint(self.allocator, "{d}", .{r}); fields_buf[1] = .{ "reply_to_message_id", reply_str.? }; field_count = 2; } const resp = try http.httpPostMultipart( self.allocator, url, "voice", ogg_path, "voice.ogg", fields_buf[0..field_count], ); self.allocator.free(resp); } };