From c70ffd095a6de5cd5b872796a0d82a8c5afc1511 Mon Sep 17 00:00:00 2001 From: Uko Kokņevičs Date: Sat, 20 Jul 2024 17:22:25 +0300 Subject: Initial commit --- src/Bot.zig | 239 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 src/Bot.zig (limited to 'src/Bot.zig') diff --git a/src/Bot.zig b/src/Bot.zig new file mode 100644 index 0000000..d40a4a0 --- /dev/null +++ b/src/Bot.zig @@ -0,0 +1,239 @@ +const types = @import("types.zig"); +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const Bot = @This(); +const Config = @import("Config.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, +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, + +pub fn init(allocator: Allocator, config: Config) !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, + .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 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 getMe(self: *Bot) !Parsed(types.User) { + return self.get(types.User, "getMe", null); +} + +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; + const user = try self.getMe(); + defer user.deinit(); + self.username = user.value.username; + return self.username.?; +} + +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 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.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.?, + }; +} + +inline fn isNull(value: anytype) bool { + return switch (@typeInfo(@TypeOf(value))) { + .Null => true, + .Optional => value == null, + else => false, + }; +} + +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 (!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); +} -- cgit v1.2.3