const std = @import("std"); const types = @import("types.zig"); const utils = @import("utils.zig"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Bot = @This(); const Config = @import("Config.zig"); const DB = @import("DB.zig"); const HttpClient = std.http.Client; const HttpMethod = std.http.Method; const Parsed = std.json.Parsed; const Uri = std.Uri; allocator: Allocator, http_client: HttpClient, config: Config, db: *DB, base_uri: Uri = Uri.parse("https://api.telegram.org/") catch unreachable, uri_path_data: ArrayList(u8), poweron: bool = true, server_header_buffer: [4096]u8 = undefined, username: ?[]const u8 = null, id: ?i64 = null, pub fn init(allocator: Allocator, config: Config, db: *DB) !Bot { var uri_path_data = try ArrayList(u8).initCapacity(allocator, 5 + config.bot_token.len); errdefer uri_path_data.deinit(); uri_path_data.appendSliceAssumeCapacity("/bot"); uri_path_data.appendSliceAssumeCapacity(config.bot_token); uri_path_data.appendAssumeCapacity('/'); return .{ .allocator = allocator, .http_client = .{ .allocator = allocator, }, .config = config, .db = db, .uri_path_data = uri_path_data, }; } pub fn deinit(self: *Bot) void { self.http_client.deinit(); self.uri_path_data.deinit(); if (self.username) |username| self.allocator.free(username); self.* = undefined; } pub inline fn answerCallbackQuery(self: *Bot, args: types.AnswerCallbackQueryParams) !void { (try self.post(bool, "answerCallbackQuery", args)).deinit(); } pub inline fn deleteMessage(self: *Bot, args: types.DeleteMessageParams) !void { (try self.post(bool, "deleteMessage", args)).deinit(); } pub inline fn editMessageText(self: *Bot, args: types.EditMessageTextParams) !Parsed(types.Message) { return self.post(types.Message, "editMessageText", args); } pub inline fn editMessageText_(self: *Bot, args: types.EditMessageTextParams) !void { (try self.editMessageText(args)).deinit(); } pub inline fn getId(self: *Bot) !i64 { if (self.id) |id| return id; (try self.getMe()).deinit(); return self.id.?; } pub inline fn getMe(self: *Bot) !Parsed(types.User) { const user = try self.get(types.User, "getMe", null); errdefer user.deinit(); self.id = user.value.id; self.username = try self.allocator.dupe(u8, user.value.username.?); return user; } pub inline fn getMyName(self: *Bot, args: types.GetMyNameParams) !Parsed(types.BotName) { return self.get(types.BotName, "getMyName", args); } pub inline fn getUpdates(self: *Bot, args: types.GetUpdatesParams) !Parsed([]types.Update) { return self.get([]types.Update, "getUpdates", args); } pub inline fn getUsername(self: *Bot) ![]const u8 { if (self.username) |username| return username; (try self.getMe()).deinit(); return self.username.?; } pub inline fn sendAnimation(self: *Bot, args: types.SendAnimationParams) !Parsed(types.Message) { return self.post(types.Message, "sendAnimation", args); } pub inline fn sendAnimation_(self: *Bot, args: types.SendAnimationParams) !void { (try self.sendAnimation(args)).deinit(); } pub inline fn sendMessage(self: *Bot, args: types.SendMessageParams) !Parsed(types.Message) { return self.post(types.Message, "sendMessage", args); } pub inline fn sendMessage_(self: *Bot, args: types.SendMessageParams) !void { (try self.sendMessage(args)).deinit(); } pub inline fn setMyName(self: *Bot, args: types.SetMyNameParams) !void { if (args.name) |new_name| { // Check if the current name isn't the same as what we want to change to const curr_name = try self.getMyName(.{ .language_code = args.language_code }); defer curr_name.deinit(); if (std.mem.eql(u8, curr_name.value.name, new_name)) { return; } } const res = try self.post(bool, "setMyName", args); defer res.deinit(); if (!res.value) { return error.FailedToSetName; } } fn Wrapper(comptime T: type) type { return struct { ok: bool, description: ?[]const u8 = null, result: ?T = null, error_code: ?i64 = null, parameters: ?types.ResponseParameters = null, }; } fn call( self: *Bot, comptime T: type, comptime method: HttpMethod, uri: Uri, data: ?[]const u8, ) !Parsed(T) { var tries: i8 = 0; while (true) : (tries += 1) { if (self.wrappedCall(T, method, uri, data)) |res| { return res; } else |err| { std.log.warn("error when performing call: {}", .{err}); if (tries == 4) { return err; } } } } fn wrappedCall( self: *Bot, comptime T: type, comptime method: HttpMethod, uri: Uri, data: ?[]const u8, ) !Parsed(T) { while (true) { var request = try self.http_client.open(method, uri, .{ .server_header_buffer = &self.server_header_buffer, }); defer request.deinit(); if (data) |s| { request.headers.content_type = .{ .override = "application/json" }; request.transfer_encoding = .{ .content_length = s.len }; } try request.send(); if (data) |s| { try request.writeAll(s); } try request.finish(); try request.wait(); var reader = std.json.reader(self.allocator, request.reader()); defer reader.deinit(); const result = try std.json.parseFromTokenSource( Wrapper(T), self.allocator, &reader, .{ .ignore_unknown_fields = true, .allocate = .alloc_always, }, ); errdefer result.deinit(); if (result.value.parameters) |params| { if (params.retry_after) |sleeptime| { std.log.info("Should sleep for {} seconds", .{sleeptime}); var res = @mulWithOverflow(sleeptime, std.time.ns_per_s); if (res[1] != 0) { res[0] = std.math.maxInt(u64); } std.log.info("Will try to sleep for {} seconds", .{res[0] / std.time.ns_per_s}); std.time.sleep(res[0]); continue; } } if (!result.value.ok or result.value.result == null) { std.log.err("Request failed: {any}", .{result.value}); return error.RequestFailed; } return .{ .arena = result.arena, .value = result.value.result.?, }; } } fn intoQueryString(allocator: Allocator, data: anytype) !?[]u8 { return switch (@typeInfo(@TypeOf(data))) { .null => null, .optional => if (data) |d| intoQueryString(allocator, d) else null, .@"struct" => |s| { var sb = ArrayList(u8).init(allocator); defer sb.deinit(); var counter: usize = 0; inline for (s.fields) |field| { if (!utils.isNull(@field(data, field.name))) { counter += 1; try sb.ensureUnusedCapacity(field.name.len + 2); if (counter != 1) { sb.appendAssumeCapacity('&'); } sb.appendSliceAssumeCapacity(field.name); sb.appendAssumeCapacity('='); const value = try std.json.stringifyAlloc( allocator, @field(data, field.name), .{ .emit_null_optional_fields = false }, ); defer allocator.free(value); try sb.appendSlice(value); } } return try sb.toOwnedSlice(); }, else => @compileError(@typeName(@TypeOf(data)) ++ " not supported"), }; } inline fn get(self: *Bot, Out: type, comptime path: []const u8, args: anytype) !Parsed(Out) { const path_len = self.uri_path_data.items.len; defer self.uri_path_data.shrinkRetainingCapacity(path_len); try self.uri_path_data.appendSlice(path); var uri = self.base_uri; uri.path = .{ .raw = self.uri_path_data.items }; const query = try intoQueryString(self.allocator, args); defer if (query) |q| self.allocator.free(q); if (query) |q| uri.query = .{ .raw = q }; std.log.debug("GET {}", .{uri}); return self.call(Out, .GET, uri, null); } inline fn post(self: *Bot, Out: type, comptime path: []const u8, args: anytype) !Parsed(Out) { const str_data = try std.json.stringifyAlloc(self.allocator, args, .{ .emit_null_optional_fields = false }); defer self.allocator.free(str_data); const path_len = self.uri_path_data.items.len; defer self.uri_path_data.shrinkRetainingCapacity(path_len); try self.uri_path_data.appendSlice(path); var uri = self.base_uri; uri.path = .{ .raw = self.uri_path_data.items }; std.log.debug("POST {}", .{uri}); return self.call(Out, .POST, uri, str_data); }