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

104
src/http.zig Normal file
View File

@@ -0,0 +1,104 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
pub fn httpGet(allocator: Allocator, url: []const u8) ![]u8 {
var client: std.http.Client = .{ .allocator = allocator };
defer client.deinit();
var aw: std.Io.Writer.Allocating = .init(allocator);
defer aw.deinit();
const result = client.fetch(.{
.location = .{ .url = url },
.response_writer = &aw.writer,
}) catch return error.HttpRequestFailed;
if (result.status != .ok) return error.HttpRequestFailed;
return aw.toOwnedSlice() catch return error.OutOfMemory;
}
pub fn httpPostJson(allocator: Allocator, url: []const u8, body: []const u8) ![]u8 {
var client: std.http.Client = .{ .allocator = allocator };
defer client.deinit();
var aw: std.Io.Writer.Allocating = .init(allocator);
defer aw.deinit();
const result = client.fetch(.{
.location = .{ .url = url },
.method = .POST,
.payload = body,
.headers = .{ .content_type = .{ .override = "application/json" } },
.response_writer = &aw.writer,
}) catch return error.HttpRequestFailed;
if (result.status != .ok) return error.HttpRequestFailed;
return aw.toOwnedSlice() catch return error.OutOfMemory;
}
pub fn httpPostMultipart(
allocator: Allocator,
url: []const u8,
file_field: []const u8,
file_path: []const u8,
file_name: []const u8,
fields: []const [2][]const u8,
) ![]u8 {
const boundary = "----ZigMultipartBoundary9876543210";
// Build multipart body
var body_writer: std.Io.Writer.Allocating = .init(allocator);
defer body_writer.deinit();
// Add form fields
for (fields) |field| {
body_writer.writer.print("--{s}\r\nContent-Disposition: form-data; name=\"{s}\"\r\n\r\n{s}\r\n", .{ boundary, field[0], field[1] }) catch return error.OutOfMemory;
}
// Add file field header
body_writer.writer.print("--{s}\r\nContent-Disposition: form-data; name=\"{s}\"; filename=\"{s}\"\r\nContent-Type: application/octet-stream\r\n\r\n", .{ boundary, file_field, file_name }) catch return error.OutOfMemory;
// Read and append file content
const file = std.fs.openFileAbsolute(file_path, .{}) catch return error.HttpRequestFailed;
defer file.close();
const file_content = file.readToEndAlloc(allocator, 100 * 1024 * 1024) catch return error.HttpRequestFailed;
defer allocator.free(file_content);
body_writer.writer.writeAll(file_content) catch return error.OutOfMemory;
body_writer.writer.print("\r\n--{s}--\r\n", .{boundary}) catch return error.OutOfMemory;
const body = body_writer.written();
// Send request
var client: std.http.Client = .{ .allocator = allocator };
defer client.deinit();
var response_aw: std.Io.Writer.Allocating = .init(allocator);
defer response_aw.deinit();
const content_type = std.fmt.allocPrint(allocator, "multipart/form-data; boundary={s}", .{boundary}) catch return error.OutOfMemory;
defer allocator.free(content_type);
const result = client.fetch(.{
.location = .{ .url = url },
.method = .POST,
.payload = body,
.headers = .{ .content_type = .{ .override = content_type } },
.response_writer = &response_aw.writer,
}) catch return error.HttpRequestFailed;
if (result.status != .ok) return error.HttpRequestFailed;
return response_aw.toOwnedSlice() catch return error.OutOfMemory;
}
pub fn downloadToFile(allocator: Allocator, url: []const u8, dest_path: []const u8) !void {
const data = try httpGet(allocator, url);
defer allocator.free(data);
const file = std.fs.createFileAbsolute(dest_path, .{}) catch return error.HttpRequestFailed;
defer file.close();
file.writeAll(data) catch return error.HttpRequestFailed;
}