Files
transcribator/src/telegram.zig
Mikhail Kilin 17eb61b259
All checks were successful
ci/woodpecker/push/deploy Pipeline was successful
Add /speak command for multi-voice TTS in group chats
- /speak <voice> enables TTS with selected voice (irina, denis, dmitri)
- /speak stop disables TTS in the chat
- Private chats keep always-on TTS behavior
- Add PIPER_VOICES env var for voice-to-URL mapping
- Add chat type field to distinguish private/group chats

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 00:56:30 +03:00

173 lines
5.2 KiB
Zig

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);
}
};