Initial commit: Telegram voice/video transcription bot in Zig

Long-polling bot that accepts voice messages and video notes,
sends them to Whisper STT API, and replies with transcription text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-18 15:32:04 +03:00
commit 819b28a672
9 changed files with 550 additions and 0 deletions

114
src/telegram.zig Normal file
View File

@@ -0,0 +1,114 @@
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 },
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,
};
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) !std.json.Parsed(GetUpdatesResponse) {
const url = try std.fmt.allocPrint(self.allocator, "{s}/getUpdates?offset={d}&timeout={d}&allowed_updates=[\"message\"]", .{ self.api_base, offset, timeout });
defer self.allocator.free(url);
const body = try http.httpGet(self.allocator, url);
defer self.allocator.free(body);
return std.json.parseFromSlice(GetUpdatesResponse, self.allocator, body, .{ .ignore_unknown_fields = true });
}
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);
}
};