summaryrefslogtreecommitdiff
path: root/src/Bot.zig
diff options
context:
space:
mode:
authorGravatar Uko Kokņevičs2024-07-20 17:22:25 +0300
committerGravatar Uko Kokņevičs2024-07-20 17:22:25 +0300
commitc70ffd095a6de5cd5b872796a0d82a8c5afc1511 (patch)
tree56183274b05a294e357bad4d06b523472a1c4a4a /src/Bot.zig
downloadukkobot-c70ffd095a6de5cd5b872796a0d82a8c5afc1511.tar.gz
ukkobot-c70ffd095a6de5cd5b872796a0d82a8c5afc1511.tar.xz
ukkobot-c70ffd095a6de5cd5b872796a0d82a8c5afc1511.zip
Initial commit
Diffstat (limited to 'src/Bot.zig')
-rw-r--r--src/Bot.zig239
1 files changed, 239 insertions, 0 deletions
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 @@
1const types = @import("types.zig");
2const std = @import("std");
3
4const Allocator = std.mem.Allocator;
5const ArrayList = std.ArrayList;
6const Bot = @This();
7const Config = @import("Config.zig");
8const HttpClient = std.http.Client;
9const HttpMethod = std.http.Method;
10const Parsed = std.json.Parsed;
11const Uri = std.Uri;
12
13allocator: Allocator,
14http_client: HttpClient,
15config: Config,
16base_uri: Uri = Uri.parse("https://api.telegram.org/") catch unreachable,
17uri_path_data: ArrayList(u8),
18poweron: bool = true,
19server_header_buffer: [4096]u8 = undefined,
20username: ?[]const u8 = null,
21
22pub fn init(allocator: Allocator, config: Config) !Bot {
23 var uri_path_data = try ArrayList(u8).initCapacity(allocator, 5 + config.bot_token.len);
24 errdefer uri_path_data.deinit();
25
26 uri_path_data.appendSliceAssumeCapacity("/bot");
27 uri_path_data.appendSliceAssumeCapacity(config.bot_token);
28 uri_path_data.appendAssumeCapacity('/');
29
30 return .{
31 .allocator = allocator,
32 .http_client = .{
33 .allocator = allocator,
34 },
35 .config = config,
36 .uri_path_data = uri_path_data,
37 };
38}
39
40pub fn deinit(self: *Bot) void {
41 self.http_client.deinit();
42 self.uri_path_data.deinit();
43 if (self.username) |username| self.allocator.free(username);
44
45 self.* = undefined;
46}
47
48pub inline fn editMessageText(self: *Bot, args: types.EditMessageTextParams) !Parsed(types.Message) {
49 return self.post(types.Message, "editMessageText", args);
50}
51
52pub inline fn editMessageText_(self: *Bot, args: types.EditMessageTextParams) !void {
53 (try self.editMessageText(args)).deinit();
54}
55
56pub inline fn getMe(self: *Bot) !Parsed(types.User) {
57 return self.get(types.User, "getMe", null);
58}
59
60pub inline fn getMyName(self: *Bot, args: types.GetMyNameParams) !Parsed(types.BotName) {
61 return self.get(types.BotName, "getMyName", args);
62}
63
64pub inline fn getUpdates(self: *Bot, args: types.GetUpdatesParams) !Parsed([]types.Update) {
65 return self.get([]types.Update, "getUpdates", args);
66}
67
68pub inline fn getUsername(self: *Bot) ![]const u8 {
69 if (self.username) |username| return username;
70 const user = try self.getMe();
71 defer user.deinit();
72 self.username = user.value.username;
73 return self.username.?;
74}
75
76pub inline fn sendMessage(self: *Bot, args: types.SendMessageParams) !Parsed(types.Message) {
77 return self.post(types.Message, "sendMessage", args);
78}
79
80pub inline fn sendMessage_(self: *Bot, args: types.SendMessageParams) !void {
81 (try self.sendMessage(args)).deinit();
82}
83
84pub inline fn setMyName(self: *Bot, args: types.SetMyNameParams) !void {
85 if (args.name) |new_name| {
86 // Check if the current name isn't the same as what we want to change to
87 const curr_name = try self.getMyName(.{ .language_code = args.language_code });
88 defer curr_name.deinit();
89
90 if (std.mem.eql(u8, curr_name.value.name, new_name)) {
91 return;
92 }
93 }
94
95 const res = try self.post(bool, "setMyName", args);
96 defer res.deinit();
97 if (!res.value) {
98 return error.FailedToSetName;
99 }
100}
101
102fn Wrapper(comptime T: type) type {
103 return struct {
104 ok: bool,
105 description: ?[]const u8 = null,
106 result: ?T = null,
107 error_code: ?i64 = null,
108 parameters: ?types.ResponseParameters = null,
109 };
110}
111
112fn call(
113 self: *Bot,
114 comptime T: type,
115 comptime method: HttpMethod,
116 uri: Uri,
117 data: ?[]const u8,
118) !Parsed(T) {
119 var request = try self.http_client.open(method, uri, .{
120 .server_header_buffer = &self.server_header_buffer,
121 });
122 defer request.deinit();
123
124 if (data) |s| {
125 request.headers.content_type = .{ .override = "application/json" };
126 request.transfer_encoding = .{ .content_length = s.len };
127 }
128 try request.send();
129
130 if (data) |s| {
131 try request.writeAll(s);
132 }
133 try request.finish();
134
135 try request.wait();
136
137 var reader = std.json.reader(self.allocator, request.reader());
138 defer reader.deinit();
139
140 const result = try std.json.parseFromTokenSource(
141 Wrapper(T),
142 self.allocator,
143 &reader,
144 .{
145 .ignore_unknown_fields = true,
146 .allocate = .alloc_always,
147 },
148 );
149 errdefer result.deinit();
150
151 if (!result.value.ok or result.value.result == null) {
152 std.log.err("Request failed: {any}", .{result.value});
153 return error.RequestFailed;
154 }
155
156 return .{
157 .arena = result.arena,
158 .value = result.value.result.?,
159 };
160}
161
162inline fn isNull(value: anytype) bool {
163 return switch (@typeInfo(@TypeOf(value))) {
164 .Null => true,
165 .Optional => value == null,
166 else => false,
167 };
168}
169
170fn intoQueryString(allocator: Allocator, data: anytype) !?[]u8 {
171 return switch (@typeInfo(@TypeOf(data))) {
172 .Null => null,
173 .Optional => if (data) |d| intoQueryString(allocator, d) else null,
174 .Struct => |s| {
175 var sb = ArrayList(u8).init(allocator);
176 defer sb.deinit();
177
178 var counter: usize = 0;
179
180 inline for (s.fields) |field| {
181 if (!isNull(@field(data, field.name))) {
182 counter += 1;
183
184 try sb.ensureUnusedCapacity(field.name.len + 2);
185 if (counter != 1) {
186 sb.appendAssumeCapacity('&');
187 }
188
189 sb.appendSliceAssumeCapacity(field.name);
190 sb.appendAssumeCapacity('=');
191
192 const value = try std.json.stringifyAlloc(
193 allocator,
194 @field(data, field.name),
195 .{ .emit_null_optional_fields = false },
196 );
197 defer allocator.free(value);
198 try sb.appendSlice(value);
199 }
200 }
201
202 return try sb.toOwnedSlice();
203 },
204 else => @compileError(@typeName(@TypeOf(data)) ++ " not supported"),
205 };
206}
207
208inline fn get(self: *Bot, Out: type, comptime path: []const u8, args: anytype) !Parsed(Out) {
209 const path_len = self.uri_path_data.items.len;
210 defer self.uri_path_data.shrinkRetainingCapacity(path_len);
211
212 try self.uri_path_data.appendSlice(path);
213
214 var uri = self.base_uri;
215 uri.path = .{ .raw = self.uri_path_data.items };
216
217 const query = try intoQueryString(self.allocator, args);
218 defer if (query) |q| self.allocator.free(q);
219 if (query) |q| uri.query = .{ .raw = q };
220
221 std.log.debug("GET {}", .{uri});
222 return self.call(Out, .GET, uri, null);
223}
224
225inline fn post(self: *Bot, Out: type, comptime path: []const u8, args: anytype) !Parsed(Out) {
226 const str_data = try std.json.stringifyAlloc(self.allocator, args, .{ .emit_null_optional_fields = false });
227 defer self.allocator.free(str_data);
228
229 const path_len = self.uri_path_data.items.len;
230 defer self.uri_path_data.shrinkRetainingCapacity(path_len);
231
232 try self.uri_path_data.appendSlice(path);
233
234 var uri = self.base_uri;
235 uri.path = .{ .raw = self.uri_path_data.items };
236
237 std.log.debug("POST {}", .{uri});
238 return self.call(Out, .POST, uri, str_data);
239}