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 +++++++++++++++++++ src/Config.zig | 83 +++++++ src/json.zig | 108 +++++++++ src/main.zig | 353 ++++++++++++++++++++++++++++ src/textutils.zig | 16 ++ src/types.zig | 99 ++++++++ src/types/Animation.zig | 11 + src/types/Audio.zig | 11 + src/types/BotName.zig | 1 + src/types/BusinessConnection.zig | 8 + src/types/BusinessMessagesDeleted.zig | 5 + src/types/CallbackGame.zig | 1 + src/types/CallbackQuery.zig | 10 + src/types/Chat.zig | 14 ++ src/types/ChatBackground.zig | 3 + src/types/ChatBoost.zig | 6 + src/types/ChatBoostAdded.zig | 1 + src/types/ChatBoostRemoved.zig | 7 + src/types/ChatBoostUpdated.zig | 5 + src/types/ChatInviteLink.zig | 11 + src/types/ChatJoinRequest.zig | 10 + src/types/ChatMemberUpdated.zig | 13 + src/types/ChatShared.zig | 7 + src/types/ChosenInlineResult.zig | 8 + src/types/Contact.zig | 5 + src/types/Dice.zig | 2 + src/types/Document.zig | 8 + src/types/EditMessageTextParams.zig | 16 ++ src/types/EncryptedCredentials.zig | 3 + src/types/EncryptedPassportElement.zig | 30 +++ src/types/ExternalReplyInfo.zig | 47 ++++ src/types/File.zig | 4 + src/types/ForumTopicClosed.zig | 1 + src/types/ForumTopicCreated.zig | 3 + src/types/ForumTopicEdited.zig | 2 + src/types/ForumTopicReopened.zig | 1 + src/types/Game.zig | 10 + src/types/GeneralForumTopicHidden.zig | 1 + src/types/GeneralForumTopicUnhidden.zig | 1 + src/types/GetMyNameParams.zig | 1 + src/types/GetUpdatesParams.zig | 4 + src/types/Giveaway.zig | 10 + src/types/GiveawayCompleted.zig | 5 + src/types/GiveawayCreated.zig | 1 + src/types/GiveawayWinners.zig | 14 ++ src/types/InlineKeyboardButton.zig | 15 ++ src/types/InlineKeyboardMarkup.zig | 3 + src/types/InlineQuery.zig | 17 ++ src/types/Invoice.zig | 5 + src/types/LinkPreviewOptions.zig | 5 + src/types/Location.zig | 6 + src/types/LoginUrl.zig | 4 + src/types/MaskPosition.zig | 11 + src/types/Message.zig | 139 +++++++++++ src/types/MessageAutoDeleteTimerChanged.zig | 1 + src/types/MessageEntity.zig | 66 ++++++ src/types/MessageReactionCountUpdated.zig | 7 + src/types/MessageReactionUpdated.zig | 11 + src/types/OrderInfo.zig | 6 + src/types/PaidMediaInfo.zig | 4 + src/types/PassportData.zig | 5 + src/types/PassportFile.zig | 4 + src/types/PhotoSize.zig | 5 + src/types/Poll.zig | 22 ++ src/types/PollAnswer.zig | 7 + src/types/PollOption.zig | 5 + src/types/PreCheckoutQuery.zig | 10 + src/types/ProximityAlertTriggered.zig | 5 + src/types/ReactionCount.zig | 4 + src/types/RefundedPayment.zig | 5 + src/types/ReplyParameters.zig | 9 + src/types/ResponseParameters.zig | 2 + src/types/SendMessageParams.zig | 18 ++ src/types/SetMyNameParams.zig | 2 + src/types/SharedUser.zig | 7 + src/types/ShippingAddress.zig | 6 + src/types/ShippingQuery.zig | 7 + src/types/Sticker.zig | 25 ++ src/types/Story.zig | 4 + src/types/SuccessfulPayment.zig | 9 + src/types/SwitchInlineQueryChosenChat.zig | 5 + src/types/TextQuote.zig | 6 + src/types/Update.zig | 41 ++++ src/types/User.zig | 32 +++ src/types/UsersShared.zig | 4 + src/types/Venue.zig | 9 + src/types/Video.zig | 11 + src/types/VideoChatEnded.zig | 1 + src/types/VideoChatParticipantsInvited.zig | 3 + src/types/VideoChatScheduled.zig | 1 + src/types/VideoChatStarted.zig | 1 + src/types/VideoNote.zig | 8 + src/types/Voice.zig | 5 + src/types/WebAppData.zig | 2 + src/types/WebAppInfo.zig | 1 + src/types/WriteAccessAllowed.zig | 3 + src/types/background_fill.zig | 17 ++ src/types/background_type.zig | 30 +++ src/types/chat_boost_source.zig | 20 ++ src/types/chat_member.zig | 63 +++++ src/types/message_origin.zig | 29 +++ src/types/paid_media.zig | 21 ++ src/types/parse_mode.zig | 5 + src/types/reaction_type.zig | 13 + 104 files changed, 1991 insertions(+) create mode 100644 src/Bot.zig create mode 100644 src/Config.zig create mode 100644 src/json.zig create mode 100644 src/main.zig create mode 100644 src/textutils.zig create mode 100644 src/types.zig create mode 100644 src/types/Animation.zig create mode 100644 src/types/Audio.zig create mode 100644 src/types/BotName.zig create mode 100644 src/types/BusinessConnection.zig create mode 100644 src/types/BusinessMessagesDeleted.zig create mode 100644 src/types/CallbackGame.zig create mode 100644 src/types/CallbackQuery.zig create mode 100644 src/types/Chat.zig create mode 100644 src/types/ChatBackground.zig create mode 100644 src/types/ChatBoost.zig create mode 100644 src/types/ChatBoostAdded.zig create mode 100644 src/types/ChatBoostRemoved.zig create mode 100644 src/types/ChatBoostUpdated.zig create mode 100644 src/types/ChatInviteLink.zig create mode 100644 src/types/ChatJoinRequest.zig create mode 100644 src/types/ChatMemberUpdated.zig create mode 100644 src/types/ChatShared.zig create mode 100644 src/types/ChosenInlineResult.zig create mode 100644 src/types/Contact.zig create mode 100644 src/types/Dice.zig create mode 100644 src/types/Document.zig create mode 100644 src/types/EditMessageTextParams.zig create mode 100644 src/types/EncryptedCredentials.zig create mode 100644 src/types/EncryptedPassportElement.zig create mode 100644 src/types/ExternalReplyInfo.zig create mode 100644 src/types/File.zig create mode 100644 src/types/ForumTopicClosed.zig create mode 100644 src/types/ForumTopicCreated.zig create mode 100644 src/types/ForumTopicEdited.zig create mode 100644 src/types/ForumTopicReopened.zig create mode 100644 src/types/Game.zig create mode 100644 src/types/GeneralForumTopicHidden.zig create mode 100644 src/types/GeneralForumTopicUnhidden.zig create mode 100644 src/types/GetMyNameParams.zig create mode 100644 src/types/GetUpdatesParams.zig create mode 100644 src/types/Giveaway.zig create mode 100644 src/types/GiveawayCompleted.zig create mode 100644 src/types/GiveawayCreated.zig create mode 100644 src/types/GiveawayWinners.zig create mode 100644 src/types/InlineKeyboardButton.zig create mode 100644 src/types/InlineKeyboardMarkup.zig create mode 100644 src/types/InlineQuery.zig create mode 100644 src/types/Invoice.zig create mode 100644 src/types/LinkPreviewOptions.zig create mode 100644 src/types/Location.zig create mode 100644 src/types/LoginUrl.zig create mode 100644 src/types/MaskPosition.zig create mode 100644 src/types/Message.zig create mode 100644 src/types/MessageAutoDeleteTimerChanged.zig create mode 100644 src/types/MessageEntity.zig create mode 100644 src/types/MessageReactionCountUpdated.zig create mode 100644 src/types/MessageReactionUpdated.zig create mode 100644 src/types/OrderInfo.zig create mode 100644 src/types/PaidMediaInfo.zig create mode 100644 src/types/PassportData.zig create mode 100644 src/types/PassportFile.zig create mode 100644 src/types/PhotoSize.zig create mode 100644 src/types/Poll.zig create mode 100644 src/types/PollAnswer.zig create mode 100644 src/types/PollOption.zig create mode 100644 src/types/PreCheckoutQuery.zig create mode 100644 src/types/ProximityAlertTriggered.zig create mode 100644 src/types/ReactionCount.zig create mode 100644 src/types/RefundedPayment.zig create mode 100644 src/types/ReplyParameters.zig create mode 100644 src/types/ResponseParameters.zig create mode 100644 src/types/SendMessageParams.zig create mode 100644 src/types/SetMyNameParams.zig create mode 100644 src/types/SharedUser.zig create mode 100644 src/types/ShippingAddress.zig create mode 100644 src/types/ShippingQuery.zig create mode 100644 src/types/Sticker.zig create mode 100644 src/types/Story.zig create mode 100644 src/types/SuccessfulPayment.zig create mode 100644 src/types/SwitchInlineQueryChosenChat.zig create mode 100644 src/types/TextQuote.zig create mode 100644 src/types/Update.zig create mode 100644 src/types/User.zig create mode 100644 src/types/UsersShared.zig create mode 100644 src/types/Venue.zig create mode 100644 src/types/Video.zig create mode 100644 src/types/VideoChatEnded.zig create mode 100644 src/types/VideoChatParticipantsInvited.zig create mode 100644 src/types/VideoChatScheduled.zig create mode 100644 src/types/VideoChatStarted.zig create mode 100644 src/types/VideoNote.zig create mode 100644 src/types/Voice.zig create mode 100644 src/types/WebAppData.zig create mode 100644 src/types/WebAppInfo.zig create mode 100644 src/types/WriteAccessAllowed.zig create mode 100644 src/types/background_fill.zig create mode 100644 src/types/background_type.zig create mode 100644 src/types/chat_boost_source.zig create mode 100644 src/types/chat_member.zig create mode 100644 src/types/message_origin.zig create mode 100644 src/types/paid_media.zig create mode 100644 src/types/parse_mode.zig create mode 100644 src/types/reaction_type.zig (limited to 'src') 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); +} diff --git a/src/Config.zig b/src/Config.zig new file mode 100644 index 0000000..4deebbc --- /dev/null +++ b/src/Config.zig @@ -0,0 +1,83 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const Config = @This(); + +pub const Wrapper = struct { + arena: ArenaAllocator, + config: Config, + + pub fn deinit(self: Wrapper) void { + self.arena.deinit(); + } + + pub fn merge(self: *Wrapper, filename: []const u8) !void { + const file = try std.fs.cwd().openFile(filename, .{}); + defer file.close(); + + const allocator = self.arena.allocator(); + + var reader = std.json.reader(allocator, file.reader()); + defer reader.deinit(); + + const new_config = try std.json.parseFromTokenSourceLeaky( + Nullable, + allocator, + &reader, + .{ + .duplicate_field_behavior = .use_last, + .ignore_unknown_fields = true, + .allocate = .alloc_always, + }, + ); + + inline for (std.meta.fields(Config)) |field| { + if (@field(new_config, field.name)) |value| { + @field(self.config, field.name) = value; + } + } + } +}; + +bot_token: []const u8, +dev_group: i64, +owner: i64, + +pub fn load(parent_allocator: Allocator, filename: []const u8) !Wrapper { + const file = try std.fs.cwd().openFile(filename, .{}); + defer file.close(); + + var arena = ArenaAllocator.init(parent_allocator); + errdefer arena.deinit(); + const allocator = arena.allocator(); + + var reader = std.json.reader(allocator, file.reader()); + defer reader.deinit(); + + const config = try std.json.parseFromTokenSourceLeaky( + Config, + allocator, + &reader, + .{ + .duplicate_field_behavior = .use_last, + .ignore_unknown_fields = true, + .allocate = .alloc_always, + }, + ); + + return .{ .arena = arena, .config = config }; +} + +const Nullable = blk: { + const template = @typeInfo(Config); + var fields: [template.Struct.fields.len]std.builtin.Type.StructField = undefined; + for (template.Struct.fields, 0..) |template_field, field_idx| { + fields[field_idx] = template_field; + fields[field_idx].type = ?template_field.type; + } + var new = template; + new.Struct.fields = &fields; + new.Struct.decls = &.{}; + break :blk @Type(new); +}; diff --git a/src/json.zig b/src/json.zig new file mode 100644 index 0000000..9252344 --- /dev/null +++ b/src/json.zig @@ -0,0 +1,108 @@ +// TODO: ALSO IMPLEMENT jsonStringify !!! + +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ParseError = std.json.ParseError; +const ParseFromValueError = std.json.ParseFromValueError; +const ParseOptions = std.json.ParseOptions; +const Value = std.json.Value; + +pub fn JsonParse(T: type) type { + const f = struct { + pub fn f(allocator: Allocator, source: anytype, options: ParseOptions) ParseError(@TypeOf(source.*))!T { + _ = allocator; + _ = options; + unreachable; + } + }.f; + return @TypeOf(f); +} + +pub fn makeJsonParse(T: type) JsonParse(T) { + comptime if (!std.meta.hasFn(T, "jsonParseFromValue")) { + @compileError("Did you forget `pub const jsonParseFromValue = json.makeJsonParseFromValue(" ++ @typeName(T) ++ ") ?"); + }; + + return struct { + pub fn jsonParse(allocator: Allocator, source: anytype, options: ParseOptions) ParseError(@TypeOf(source.*))!T { + const value = try std.json.innerParse(Value, allocator, source, options); + return try T.jsonParseFromValue(allocator, value, options); + } + }.jsonParse; +} + +pub fn JsonParseFromValue(T: type) type { + const f = struct { + pub fn f(allocator: Allocator, source: Value, options: ParseOptions) ParseFromValueError!T { + _ = allocator; + _ = source; + _ = options; + unreachable; + } + }.f; + return @TypeOf(f); +} + +pub fn makeJsonParseFromValue(T: type) JsonParseFromValue(T) { + return makeJsonParseFromValueWithTag(T, "type"); +} + +pub fn makeJsonParseFromValueWithTag(T: type, comptime tag: [:0]const u8) JsonParseFromValue(T) { + const union_info = switch (@typeInfo(T)) { + .Union => |info| blk: { + if (info.tag_type == null) { + @compileError("Only tagged unions supported, got '" ++ @typeName(T) ++ "'"); + } + break :blk info; + }, + else => @compileError("Only unions supported, got '" ++ @typeName(T) ++ "'"), + }; + + const TagType = union_info.tag_type.?; + + const Base = blk: { + const info = std.builtin.Type{ .Struct = .{ + .layout = .auto, + .fields = &.{.{ + .name = tag, + .type = TagType, + .default_value = null, + .is_comptime = false, + .alignment = 0, + }}, + .decls = &.{}, + .is_tuple = false, + } }; + break :blk @Type(info); + }; + + return struct { + pub fn jsonParseFromValue( + allocator: Allocator, + source: Value, + options: ParseOptions, + ) ParseFromValueError!T { + const new_options = blk: { + var opt = options; + opt.ignore_unknown_fields = true; + break :blk opt; + }; + + const base = try std.json.innerParseFromValue(Base, allocator, source, new_options); + const typeName = @tagName(@field(base, tag)); + // TODO: Upgrade to StaticStringMap + inline for (union_info.fields) |field_info| { + if (std.mem.eql(u8, typeName, field_info.name)) { + return @unionInit( + T, + field_info.name, + try std.json.innerParseFromValue(field_info.type, allocator, source, new_options), + ); + } + } + + unreachable; + } + }.jsonParseFromValue; +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..0c524a7 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,353 @@ +const types = @import("types.zig"); +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const Bot = @import("Bot.zig"); +const Config = @import("Config.zig"); +const GPA = std.heap.GeneralPurposeAllocator(.{}); + +pub fn main() !void { + defer std.log.info("We're done", .{}); + + var gpa = GPA{}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + // Load config + var config = try Config.load(allocator, "config.default.json"); + defer config.deinit(); + try config.merge("config.json"); + + var bot = try Bot.init(allocator, config.config); + defer bot.deinit(); + + // TODO: Catch fatal errors, report them + try wrappedMain(&bot); +} + +fn loadConfig(allocator: Allocator, filename: []const u8) !std.json.Parsed(Config) { + const file = try std.fs.cwd().openFile(filename, .{}); + defer file.close(); + + var reader = std.json.reader(allocator, file.reader()); + defer reader.deinit(); + + return try std.json.parseFromTokenSource( + Config, + allocator, + &reader, + .{ + .duplicate_field_behavior = .use_last, + .ignore_unknown_fields = true, + .allocate = .alloc_always, + }, + ); +} + +fn wrappedMain(bot: *Bot) !void { + try bot.sendMessage_(.{ + .chat_id = bot.config.dev_group, + .text = "Initializing...", + }); + + try bot.setMyName(.{ .name = "Ukko's bot" }); + + var gup = types.GetUpdatesParams{ .timeout = 60 }; + while (bot.poweron) { + // TODO: Catch major errors, report them (and crash after 5 of them or so) + const updates = try bot.getUpdates(gup); + defer updates.deinit(); + for (updates.value) |update| { + defer gup.offset = update.update_id + 1; + + if (update.message) |message| { + // TODO: Catch minor errors, report them + try onMessage(bot, message); + } + } + } + + // one last getUpdates to make sure offset is saved + gup.timeout = 0; + gup.limit = 1; + (try bot.getUpdates(gup)).deinit(); + + try bot.sendMessage_(.{ + .chat_id = bot.config.dev_group, + .text = "Shutting down...", + }); +} + +fn onMessage(bot: *Bot, msg: types.Message) !void { + if (msg.text) |text| { + try onTextMessage(bot, msg, text); + } + + if (msg.new_chat_members) |new_chat_members| { + for (new_chat_members) |new_chat_member| { + try onNewMember(bot, msg, new_chat_member); + } + } +} + +fn onNewMember(bot: *Bot, msg: types.Message, new_member: types.User) !void { + var sb = ArrayList(u8).init(bot.allocator); + defer sb.deinit(); + + const w = sb.writer(); + + try w.writeAll("Hello there, "); + try new_member.writeFormattedName(w); + try w.writeAll("! Be on your bestest behaviour now!!"); + + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = sb.items, + .parse_mode = .html, + .reply_parameters = .{ + .allow_sending_without_reply = true, + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); +} + +fn isBadText(text: []const u8) bool { + _ = text; + return false; +} + +fn onTextMessage(bot: *Bot, msg: types.Message, text: []const u8) !void { + if (isBadText(text)) { + // TODO: Delete message, mute & warn user + // 0 current warns: 5 minute mute, +1 warn + // 1 current warn : 10 minute mute, +1 warn + // 2 current warns: 30 minute mute, +1 warn + // 3 current warns: 1 hour mute, +1 warn + // 4 current warns: 1 day mute, +1 warn + // 5 current warns: Ban + // + // warn gets removed after a month of no warns + // + // Lines to say in response: + // Your head will be my new trophy! + // Your cursed bloodline ends here! + // This is the end of you, s'wit! + // Your life's end is approaching. + // Surrender your life to me and I will end your pain! + // Your pain is nearing an end. + // May our Lords be merciful! + // You have sealed your fate! + // You cannot escape the righteous! + // You will pay with your blood! + // You will die. + // There is no escape. + // Die, fetcher. + // I shall enjoy watching you take your last breath. + // You'll soon be nothing more than a bad memory! + // You will die in disgrace. + // I'll see you dead. + // One of us will die here and it won't be me. + // You don't deserve to live. + // Surrender now and I might let you live! + // I will bathe in your blood. + // Your bones will be my dinner. + // So small and tasty. I will enjoy eating you. + return; + } + + if (msg.entities) |entities| { + for (entities) |entity| { + if (entity.type == .bot_command and entity.offset == 0) { + const cmd = try entity.extract(text); + try onTextCommand(bot, msg, text, cmd); + } + } + } + + if (std.mem.eql(u8, text, ":3")) { + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = ">:3", + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } else if (std.mem.eql(u8, text, ">:3")) { + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = ">:3", + .parse_mode = .html, + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } else if (std.ascii.startsWithIgnoreCase(text, "big ")) { + var output = try bot.allocator.alloc(u8, text.len + 3); + defer bot.allocator.free(output); + + std.mem.copyForwards(u8, output, ""); + _ = std.ascii.upperString(output[3..], text[4..]); + std.mem.copyForwards(u8, output[output.len - 4 ..], ""); + + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = output, + .parse_mode = .html, + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } else if (std.mem.eql(u8, text, "H")) { + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = "Randomly selected reminder that h > H.", + .parse_mode = .html, + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } else if (std.ascii.startsWithIgnoreCase(text, "say ")) { + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = text[4..], + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } else if (std.ascii.eqlIgnoreCase(text, "uwu")) { + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = "OwO", + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } else if (std.ascii.eqlIgnoreCase(text, "waow")) { + const reply_to = if (msg.reply_to_message) |r| + r.message_id + else + msg.message_id; + + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = "BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED", + .reply_parameters = .{ + .message_id = reply_to, + .chat_id = msg.chat.id, + }, + }); + } else if (std.ascii.eqlIgnoreCase(text, "what")) { + var sb = try ArrayList(u8).initCapacity(bot.allocator, 9); + defer sb.deinit(); + + if (text[0] == 'w') { + sb.appendSliceAssumeCapacity("g"); + } else { + sb.appendSliceAssumeCapacity("G"); + } + if (text[1] == 'h') { + sb.appendSliceAssumeCapacity("ood "); + } else { + sb.appendSliceAssumeCapacity("OOD "); + } + if (text[2] == 'a') { + sb.appendSliceAssumeCapacity("gir"); + } else { + sb.appendSliceAssumeCapacity("GIR"); + } + if (text[3] == 't') { + sb.appendSliceAssumeCapacity("l"); + } else { + sb.appendSliceAssumeCapacity("L"); + } + + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = sb.items, + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } +} + +fn onTextCommand(bot: *Bot, msg: types.Message, text: []const u8, cmd: []const u8) !void { + _ = text; + + const simple_cmd = if (std.mem.indexOfScalar(u8, cmd, '@')) |idx| blk: { + const cmd_username = cmd[idx + 1 ..]; + if (!std.mem.eql(u8, cmd_username, try bot.getUsername())) { + return; + } + break :blk cmd[1..idx]; + } else cmd[1..]; + + // TODO: StaticStringMap :) + if (std.mem.eql(u8, simple_cmd, "chatid")) { + var sb = ArrayList(u8).init(bot.allocator); + defer sb.deinit(); + try sb.writer().print("{}", .{msg.chat.id}); + + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = sb.items, + .parse_mode = .html, + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } else if (std.mem.eql(u8, simple_cmd, "ping")) { + var timer = try std.time.Timer.start(); + + const recv = msg.date - @as(u64, @intCast(std.time.timestamp())); + + var sb = ArrayList(u8).init(bot.allocator); + defer sb.deinit(); + try sb.writer().print("Pong!\nReceive time: {}s\nSend time: ...", .{recv}); + + const reply = try bot.sendMessage(.{ + .chat_id = msg.chat.id, + .text = sb.items, + .reply_parameters = .{ + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + defer reply.deinit(); + + const send = @as(f64, @floatFromInt(timer.read())) / std.time.ns_per_ms; + sb.clearRetainingCapacity(); + try sb.writer().print("Pong!\nReceive time: {}s\nSend time: {d}ms", .{ recv, send }); + + try bot.editMessageText_(.{ + .chat_id = reply.value.chat.id, + .message_id = reply.value.message_id, + .text = sb.items, + }); + } else if (std.mem.eql(u8, simple_cmd, "shutdown")) blk: { + if (msg.from == null or msg.from.?.id != bot.config.owner) { + break :blk; + } + + bot.poweron = false; + try bot.sendMessage_(.{ + .chat_id = msg.chat.id, + .text = "Initialising shutdown...", + .reply_parameters = .{ + .allow_sending_without_reply = true, + .message_id = msg.message_id, + .chat_id = msg.chat.id, + }, + }); + } +} diff --git a/src/textutils.zig b/src/textutils.zig new file mode 100644 index 0000000..41dd5f5 --- /dev/null +++ b/src/textutils.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; + +pub fn escapeXml(writer: anytype, text: []const u8) !void { + for (text) |ch| { + try switch (ch) { + '<' => writer.writeAll("<"), + '>' => writer.writeAll(">"), + '&' => writer.writeAll("&"), + '"' => writer.writeAll("""), + else => writer.writeByte(ch), + }; + } +} diff --git a/src/types.zig b/src/types.zig new file mode 100644 index 0000000..ab1ab20 --- /dev/null +++ b/src/types.zig @@ -0,0 +1,99 @@ +pub const Animation = @import("types/Animation.zig"); +pub const Audio = @import("types/Audio.zig"); +pub const BackgroundFill = @import("types/background_fill.zig").BackgroundFill; +pub const BackgroundType = @import("types/background_type.zig").BackgroundType; +pub const BotName = @import("types/BotName.zig"); +pub const BusinessConnection = @import("types/BusinessConnection.zig"); +pub const BusinessMessagesDeleted = @import("types/BusinessMessagesDeleted.zig"); +pub const CallbackGame = @import("types/CallbackGame.zig"); +pub const CallbackQuery = @import("types/CallbackQuery.zig"); +pub const Chat = @import("types/Chat.zig"); +pub const ChatBackground = @import("types/ChatBackground.zig"); +pub const ChatBoost = @import("types/ChatBoost.zig"); +pub const ChatBoostAdded = @import("types/ChatBoostAdded.zig"); +pub const ChatBoostRemoved = @import("types/ChatBoostRemoved.zig"); +pub const ChatBoostSource = @import("types/chat_boost_source.zig").ChatBoostSource; +pub const ChatBoostUpdated = @import("types/ChatBoostUpdated.zig"); +pub const ChatInviteLink = @import("types/ChatInviteLink.zig"); +pub const ChatJoinRequest = @import("types/ChatJoinRequest.zig"); +pub const ChatMember = @import("types/chat_member.zig").ChatMember; +pub const ChatMemberUpdated = @import("types/ChatMemberUpdated.zig"); +pub const ChatShared = @import("types/ChatShared.zig"); +pub const ChosenInlineResult = @import("types/ChosenInlineResult.zig"); +pub const Contact = @import("types/Contact.zig"); +pub const Dice = @import("types/Dice.zig"); +pub const Document = @import("types/Document.zig"); +pub const EncryptedCredentials = @import("types/EncryptedCredentials.zig"); +pub const EncryptedPassportElement = @import("types/EncryptedPassportElement.zig"); +pub const ExternalReplyInfo = @import("types/ExternalReplyInfo.zig"); +pub const File = @import("types/File.zig"); +pub const ForumTopicClosed = @import("types/ForumTopicClosed.zig"); +pub const ForumTopicCreated = @import("types/ForumTopicCreated.zig"); +pub const ForumTopicEdited = @import("types/ForumTopicEdited.zig"); +pub const ForumTopicReopened = @import("types/ForumTopicReopened.zig"); +pub const Game = @import("types/Game.zig"); +pub const GeneralForumTopicHidden = @import("types/GeneralForumTopicHidden.zig"); +pub const GeneralForumTopicUnhidden = @import("types/GeneralForumTopicUnhidden.zig"); +pub const Giveaway = @import("types/Giveaway.zig"); +pub const GiveawayCompleted = @import("types/GiveawayCompleted.zig"); +pub const GiveawayCreated = @import("types/GiveawayCreated.zig"); +pub const GiveawayWinners = @import("types/GiveawayWinners.zig"); +pub const InlineKeyboardButton = @import("types/InlineKeyboardButton.zig"); +pub const InlineKeyboardMarkup = @import("types/InlineKeyboardMarkup.zig"); +pub const InlineQuery = @import("types/InlineQuery.zig"); +pub const Invoice = @import("types/Invoice.zig"); +pub const LinkPreviewOptions = @import("types/LinkPreviewOptions.zig"); +pub const Location = @import("types/Location.zig"); +pub const LoginUrl = @import("types/LoginUrl.zig"); +pub const MaskPosition = @import("types/MaskPosition.zig"); +pub const MaybeInaccessibleMessage = Message; +pub const Message = @import("types/Message.zig"); +pub const MessageAutoDeleteTimerChanged = @import("types/MessageAutoDeleteTimerChanged.zig"); +pub const MessageEntity = @import("types/MessageEntity.zig"); +pub const MessageOrigin = @import("types/message_origin.zig").MessageOrigin; +pub const MessageReactionCountUpdated = @import("types/MessageReactionCountUpdated.zig"); +pub const MessageReactionUpdated = @import("types/MessageReactionUpdated.zig"); +pub const OrderInfo = @import("types/OrderInfo.zig"); +pub const PaidMedia = @import("types/paid_media.zig").PaidMedia; +pub const PaidMediaInfo = @import("types/PaidMediaInfo.zig"); +pub const PassportData = @import("types/PassportData.zig"); +pub const PassportFile = @import("types/PassportFile.zig"); +pub const PhotoSize = @import("types/PhotoSize.zig"); +pub const Poll = @import("types/Poll.zig"); +pub const PollAnswer = @import("types/PollAnswer.zig"); +pub const PollOption = @import("types/PollOption.zig"); +pub const PreCheckoutQuery = @import("types/PreCheckoutQuery.zig"); +pub const ProximityAlertTriggered = @import("types/ProximityAlertTriggered.zig"); +pub const ReactionCount = @import("types/ReactionCount.zig"); +pub const ReactionType = @import("types/reaction_type.zig").ReactionType; +pub const RefundedPayment = @import("types/RefundedPayment.zig"); +pub const ReplyParameters = @import("types/ReplyParameters.zig"); +pub const ResponseParameters = @import("types/ResponseParameters.zig"); +pub const SharedUser = @import("types/SharedUser.zig"); +pub const ShippingAddress = @import("types/ShippingAddress.zig"); +pub const ShippingQuery = @import("types/ShippingQuery.zig"); +pub const Sticker = @import("types/Sticker.zig"); +pub const Story = @import("types/Story.zig"); +pub const SuccessfulPayment = @import("types/SuccessfulPayment.zig"); +pub const SwitchInlineQueryChosenChat = @import("types/SwitchInlineQueryChosenChat.zig"); +pub const TextQuote = @import("types/TextQuote.zig"); +pub const Update = @import("types/Update.zig"); +pub const User = @import("types/User.zig"); +pub const UsersShared = @import("types/UsersShared.zig"); +pub const Venue = @import("types/Venue.zig"); +pub const Video = @import("types/Video.zig"); +pub const VideoChatEnded = @import("types/VideoChatEnded.zig"); +pub const VideoChatParticipantsInvited = @import("types/VideoChatParticipantsInvited.zig"); +pub const VideoChatScheduled = @import("types/VideoChatScheduled.zig"); +pub const VideoChatStarted = @import("types/VideoChatStarted.zig"); +pub const VideoNote = @import("types/VideoNote.zig"); +pub const Voice = @import("types/Voice.zig"); +pub const WebAppData = @import("types/WebAppData.zig"); +pub const WebAppInfo = @import("types/WebAppInfo.zig"); +pub const WriteAccessAllowed = @import("types/WriteAccessAllowed.zig"); + +pub const EditMessageTextParams = @import("types/EditMessageTextParams.zig"); +pub const GetMyNameParams = @import("types/GetMyNameParams.zig"); +pub const GetUpdatesParams = @import("types/GetUpdatesParams.zig"); +pub const SendMessageParams = @import("types/SendMessageParams.zig"); +pub const SetMyNameParams = @import("types/SetMyNameParams.zig"); diff --git a/src/types/Animation.zig b/src/types/Animation.zig new file mode 100644 index 0000000..f5d6efc --- /dev/null +++ b/src/types/Animation.zig @@ -0,0 +1,11 @@ +const PhotoSize = @import("PhotoSize.zig"); + +file_id: []const u8, +file_unique_id: []const u8, +width: u64, +height: u64, +duration: u64, +thumbnail: ?[]PhotoSize = null, +file_name: ?[]const u8 = null, +mime_type: ?[]const u8 = null, +file_size: ?u64 = null, diff --git a/src/types/Audio.zig b/src/types/Audio.zig new file mode 100644 index 0000000..1914088 --- /dev/null +++ b/src/types/Audio.zig @@ -0,0 +1,11 @@ +const PhotoSize = @import("PhotoSize.zig"); + +file_id: []const u8, +file_unique_id: []const u8, +duration: u64, +performer: ?[]const u8 = null, +title: ?[]const u8 = null, +file_name: ?[]const u8 = null, +mime_type: ?[]const u8 = null, +file_size: ?u64 = null, +thumbnail: ?PhotoSize = null, diff --git a/src/types/BotName.zig b/src/types/BotName.zig new file mode 100644 index 0000000..61a2cb6 --- /dev/null +++ b/src/types/BotName.zig @@ -0,0 +1 @@ +name: []const u8, diff --git a/src/types/BusinessConnection.zig b/src/types/BusinessConnection.zig new file mode 100644 index 0000000..0801235 --- /dev/null +++ b/src/types/BusinessConnection.zig @@ -0,0 +1,8 @@ +const User = @import("User.zig"); + +id: []const u8, +user: User, +user_chat_id: i64, +date: u64, +can_reply: bool, +is_enabled: bool, diff --git a/src/types/BusinessMessagesDeleted.zig b/src/types/BusinessMessagesDeleted.zig new file mode 100644 index 0000000..ef55793 --- /dev/null +++ b/src/types/BusinessMessagesDeleted.zig @@ -0,0 +1,5 @@ +const Chat = @import("Chat.zig"); + +business_connection_id: []const u8, +chat: Chat, +message_ids: []u64, diff --git a/src/types/CallbackGame.zig b/src/types/CallbackGame.zig new file mode 100644 index 0000000..2f72c10 --- /dev/null +++ b/src/types/CallbackGame.zig @@ -0,0 +1 @@ +// meant to be empty diff --git a/src/types/CallbackQuery.zig b/src/types/CallbackQuery.zig new file mode 100644 index 0000000..8fb96c3 --- /dev/null +++ b/src/types/CallbackQuery.zig @@ -0,0 +1,10 @@ +const MaybeInaccessibleMessage = @import("Message.zig"); +const User = @import("User.zig"); + +id: []const u8, +from: User, +message: ?MaybeInaccessibleMessage = null, +inline_message_id: ?[]const u8 = null, +chat_instance: []const u8, +data: ?[]const u8 = null, +game_short_name: ?[]const u8 = null, diff --git a/src/types/Chat.zig b/src/types/Chat.zig new file mode 100644 index 0000000..f44e480 --- /dev/null +++ b/src/types/Chat.zig @@ -0,0 +1,14 @@ +pub const Type = enum { + private, + group, + supergroup, + channel, +}; + +id: i64, +type: Type, +title: ?[]const u8 = null, +username: ?[]const u8 = null, +first_name: ?[]const u8 = null, +last_name: ?[]const u8 = null, +is_forum: bool = false, diff --git a/src/types/ChatBackground.zig b/src/types/ChatBackground.zig new file mode 100644 index 0000000..34d2e15 --- /dev/null +++ b/src/types/ChatBackground.zig @@ -0,0 +1,3 @@ +const BackgroundType = @import("background_type.zig").BackgroundType; + +type: BackgroundType, diff --git a/src/types/ChatBoost.zig b/src/types/ChatBoost.zig new file mode 100644 index 0000000..353805b --- /dev/null +++ b/src/types/ChatBoost.zig @@ -0,0 +1,6 @@ +const ChatBoostSource = @import("chat_boost_source.zig").ChatBoostSource; + +boost_id: []const u8, +add_date: u64, +expiration_date: u64, +source: ChatBoostSource, diff --git a/src/types/ChatBoostAdded.zig b/src/types/ChatBoostAdded.zig new file mode 100644 index 0000000..487ae70 --- /dev/null +++ b/src/types/ChatBoostAdded.zig @@ -0,0 +1 @@ +boost_count: u64, diff --git a/src/types/ChatBoostRemoved.zig b/src/types/ChatBoostRemoved.zig new file mode 100644 index 0000000..0d61bc5 --- /dev/null +++ b/src/types/ChatBoostRemoved.zig @@ -0,0 +1,7 @@ +const Chat = @import("Chat.zig"); +const ChatBoostSource = @import("chat_boost_source.zig").ChatBoostSource; + +chat: Chat, +boost_id: []const u8, +remove_date: u64, +source: ChatBoostSource, diff --git a/src/types/ChatBoostUpdated.zig b/src/types/ChatBoostUpdated.zig new file mode 100644 index 0000000..e7ad33c --- /dev/null +++ b/src/types/ChatBoostUpdated.zig @@ -0,0 +1,5 @@ +const Chat = @import("Chat.zig"); +const ChatBoost = @import("ChatBoost.zig"); + +chat: Chat, +boost: ChatBoost, diff --git a/src/types/ChatInviteLink.zig b/src/types/ChatInviteLink.zig new file mode 100644 index 0000000..41a1f44 --- /dev/null +++ b/src/types/ChatInviteLink.zig @@ -0,0 +1,11 @@ +const User = @import("User.zig"); + +invite_link: []const u8, +creator: User, +creates_join_request: bool, +is_primary: bool, +is_revoked: bool, +name: ?[]const u8, +expire_date: ?u64, +member_limit: ?u64, +pending_join_request_count: ?u64, diff --git a/src/types/ChatJoinRequest.zig b/src/types/ChatJoinRequest.zig new file mode 100644 index 0000000..f08e070 --- /dev/null +++ b/src/types/ChatJoinRequest.zig @@ -0,0 +1,10 @@ +const Chat = @import("Chat.zig"); +const ChatInviteLink = @import("ChatInviteLink.zig"); +const User = @import("User.zig"); + +chat: Chat, +from: User, +user_chat_id: i64, +date: u64, +bio: ?[]const u8, +invite_link: ?ChatInviteLink, diff --git a/src/types/ChatMemberUpdated.zig b/src/types/ChatMemberUpdated.zig new file mode 100644 index 0000000..8df56ec --- /dev/null +++ b/src/types/ChatMemberUpdated.zig @@ -0,0 +1,13 @@ +const Chat = @import("Chat.zig"); +const ChatInviteLink = @import("ChatInviteLink.zig"); +const ChatMember = @import("chat_member.zig").ChatMember; +const User = @import("User.zig"); + +chat: Chat, +from: User, +date: u64, +old_chat_member: ChatMember, +new_chat_member: ChatMember, +invite_link: ?ChatInviteLink = null, +via_join_request: bool = false, +via_chat_folder_invite_link: bool = false, diff --git a/src/types/ChatShared.zig b/src/types/ChatShared.zig new file mode 100644 index 0000000..7b49f02 --- /dev/null +++ b/src/types/ChatShared.zig @@ -0,0 +1,7 @@ +const PhotoSize = @import("PhotoSize.zig"); + +request_id: u64, +chat_id: i64, +title: ?[]const u8 = null, +username: ?[]const u8 = null, +photo: ?[]PhotoSize = null, diff --git a/src/types/ChosenInlineResult.zig b/src/types/ChosenInlineResult.zig new file mode 100644 index 0000000..e0528cd --- /dev/null +++ b/src/types/ChosenInlineResult.zig @@ -0,0 +1,8 @@ +const Location = @import("Location.zig"); +const User = @import("User.zig"); + +result_id: []const u8, +from: User, +location: ?Location = null, +inline_message_id: ?[]const u8, +query: []const u8, diff --git a/src/types/Contact.zig b/src/types/Contact.zig new file mode 100644 index 0000000..e3000fa --- /dev/null +++ b/src/types/Contact.zig @@ -0,0 +1,5 @@ +phone_number: []const u8, +first_name: []const u8, +last_name: ?[]const u8 = null, +user_id: ?[]u64 = null, +vcard: ?[]const u8 = null, diff --git a/src/types/Dice.zig b/src/types/Dice.zig new file mode 100644 index 0000000..9aa6b06 --- /dev/null +++ b/src/types/Dice.zig @@ -0,0 +1,2 @@ +emoji: []const u8, +value: u8, diff --git a/src/types/Document.zig b/src/types/Document.zig new file mode 100644 index 0000000..811273d --- /dev/null +++ b/src/types/Document.zig @@ -0,0 +1,8 @@ +const PhotoSize = @import("PhotoSize.zig"); + +file_id: []const u8, +file_unique_id: []const u8, +thumbnail: ?PhotoSize = null, +file_name: ?[]const u8, +mime_type: ?[]const u8, +file_size: ?u64 = null, diff --git a/src/types/EditMessageTextParams.zig b/src/types/EditMessageTextParams.zig new file mode 100644 index 0000000..3e7701f --- /dev/null +++ b/src/types/EditMessageTextParams.zig @@ -0,0 +1,16 @@ +const InlineKeyboardMarkup = @import("InlineKeyboardMarkup.zig"); +const LinkPreviewOptions = @import("LinkPreviewOptions.zig"); +const MessageEntity = @import("MessageEntity.zig"); +const ParseMode = @import("parse_mode.zig").ParseMode; + +business_connection_id: ?[]const u8 = null, +// TODO: Integer or String +chat_id: i64, +message_id: u64, +// TODO: Make a different call for this bcs it returns just true :D +// inline_message_id: ?u64 = null, +text: []const u8, +parse_mode: ?ParseMode = null, +entities: ?[]MessageEntity = null, +link_preview_options: ?LinkPreviewOptions = null, +reply_markup: ?InlineKeyboardMarkup = null, diff --git a/src/types/EncryptedCredentials.zig b/src/types/EncryptedCredentials.zig new file mode 100644 index 0000000..6c87689 --- /dev/null +++ b/src/types/EncryptedCredentials.zig @@ -0,0 +1,3 @@ +data: []const u8, +hash: []const u8, +secret: []const u8, diff --git a/src/types/EncryptedPassportElement.zig b/src/types/EncryptedPassportElement.zig new file mode 100644 index 0000000..ca0e2ed --- /dev/null +++ b/src/types/EncryptedPassportElement.zig @@ -0,0 +1,30 @@ +// TODO: Turn this into a tagged union + +const PassportFile = @import("PassportFile.zig"); + +pub const Type = enum { + personal_details, + passport, + driver_license, + identity_card, + internal_passport, + address, + utility_bill, + bank_statement, + rental_agreement, + passport_registration, + temporary_registration, + phone_number, + email, +}; + +type: Type, +data: ?[]const u8, +phone_number: ?[]const u8, +email: ?[]const u8, +files: ?[]PassportFile, +front_side: ?PassportFile, +reverse_side: ?PassportFile, +selfie: ?PassportFile, +translation: ?[]PassportFile, +hash: []const u8, diff --git a/src/types/ExternalReplyInfo.zig b/src/types/ExternalReplyInfo.zig new file mode 100644 index 0000000..89aa004 --- /dev/null +++ b/src/types/ExternalReplyInfo.zig @@ -0,0 +1,47 @@ +const Animation = @import("Animation.zig"); +const Audio = @import("Audio.zig"); +const Chat = @import("Chat.zig"); +const Contact = @import("Contact.zig"); +const Dice = @import("Dice.zig"); +const Document = @import("Document.zig"); +const Game = @import("Game.zig"); +const Giveaway = @import("Giveaway.zig"); +const GiveawayWinners = @import("GiveawayWinners.zig"); +const Invoice = @import("Invoice.zig"); +const LinkPreviewOptions = @import("LinkPreviewOptions.zig"); +const Location = @import("Location.zig"); +const MessageOrigin = @import("message_origin.zig").MessageOrigin; +const PaidMediaInfo = @import("PaidMediaInfo.zig"); +const Poll = @import("Poll.zig"); +const PhotoSize = @import("PhotoSize.zig"); +const Sticker = @import("Sticker.zig"); +const Story = @import("Story.zig"); +const Venue = @import("Venue.zig"); +const Video = @import("Video.zig"); +const VideoNote = @import("VideoNote.zig"); +const Voice = @import("Voice.zig"); + +origin: MessageOrigin, +chat: ?Chat = null, +message_id: ?u64 = null, +link_preview_options: ?LinkPreviewOptions = null, +animation: ?Animation = null, +audio: ?Audio = null, +document: ?Document = null, +paid_media: ?PaidMediaInfo = null, +photo: ?[]PhotoSize = null, +sticker: ?Sticker = null, +story: ?Story = null, +video: ?Video = null, +video_note: ?VideoNote = null, +voice: ?Voice = null, +has_media_spoiler: bool = false, +contact: ?Contact = null, +dice: ?Dice = null, +game: ?Game = null, +giveaway: ?Giveaway = null, +giveaway_winners: ?GiveawayWinners = null, +invoice: ?Invoice = null, +location: ?Location = null, +poll: ?Poll = null, +venue: ?Venue = null, diff --git a/src/types/File.zig b/src/types/File.zig new file mode 100644 index 0000000..3555bc9 --- /dev/null +++ b/src/types/File.zig @@ -0,0 +1,4 @@ +file_id: []const u8, +file_unique_id: []const u8, +file_size: ?u64 = null, +file_path: ?[]const u8 = null, diff --git a/src/types/ForumTopicClosed.zig b/src/types/ForumTopicClosed.zig new file mode 100644 index 0000000..2f72c10 --- /dev/null +++ b/src/types/ForumTopicClosed.zig @@ -0,0 +1 @@ +// meant to be empty diff --git a/src/types/ForumTopicCreated.zig b/src/types/ForumTopicCreated.zig new file mode 100644 index 0000000..2429de7 --- /dev/null +++ b/src/types/ForumTopicCreated.zig @@ -0,0 +1,3 @@ +name: []const u8, +icon_color: u24, +icon_custom_emoji_id: ?[]const u8 = null, diff --git a/src/types/ForumTopicEdited.zig b/src/types/ForumTopicEdited.zig new file mode 100644 index 0000000..097acb3 --- /dev/null +++ b/src/types/ForumTopicEdited.zig @@ -0,0 +1,2 @@ +name: ?[]const u8 = null, +icon_custom_emoji_id: ?[]const u8 = null, diff --git a/src/types/ForumTopicReopened.zig b/src/types/ForumTopicReopened.zig new file mode 100644 index 0000000..2f72c10 --- /dev/null +++ b/src/types/ForumTopicReopened.zig @@ -0,0 +1 @@ +// meant to be empty diff --git a/src/types/Game.zig b/src/types/Game.zig new file mode 100644 index 0000000..3738284 --- /dev/null +++ b/src/types/Game.zig @@ -0,0 +1,10 @@ +const Animation = @import("Animation.zig"); +const MessageEntity = @import("MessageEntity.zig"); +const PhotoSize = @import("PhotoSize.zig"); + +title: []const u8, +description: []const u8, +photo: []PhotoSize, +text: ?[]const u8 = null, +text_entities: ?[]MessageEntity = null, +animation: ?Animation = null, diff --git a/src/types/GeneralForumTopicHidden.zig b/src/types/GeneralForumTopicHidden.zig new file mode 100644 index 0000000..2f72c10 --- /dev/null +++ b/src/types/GeneralForumTopicHidden.zig @@ -0,0 +1 @@ +// meant to be empty diff --git a/src/types/GeneralForumTopicUnhidden.zig b/src/types/GeneralForumTopicUnhidden.zig new file mode 100644 index 0000000..2f72c10 --- /dev/null +++ b/src/types/GeneralForumTopicUnhidden.zig @@ -0,0 +1 @@ +// meant to be empty diff --git a/src/types/GetMyNameParams.zig b/src/types/GetMyNameParams.zig new file mode 100644 index 0000000..2cb38fa --- /dev/null +++ b/src/types/GetMyNameParams.zig @@ -0,0 +1 @@ +language_code: ?[]const u8 = null diff --git a/src/types/GetUpdatesParams.zig b/src/types/GetUpdatesParams.zig new file mode 100644 index 0000000..0a4e333 --- /dev/null +++ b/src/types/GetUpdatesParams.zig @@ -0,0 +1,4 @@ +offset: ?u64 = null, +limit: ?u64 = null, +timeout: ?u64 = null, +allowed_updates: ?[]const u8 = null, diff --git a/src/types/Giveaway.zig b/src/types/Giveaway.zig new file mode 100644 index 0000000..d1cfa7e --- /dev/null +++ b/src/types/Giveaway.zig @@ -0,0 +1,10 @@ +const Chat = @import("Chat.zig"); + +chats: []Chat, +winners_selection_date: u64, +winner_count: u64, +only_new_members: bool = false, +has_public_winners: bool = false, +prize_description: ?[]const u8 = null, +country_codes: ?[]const u8 = null, +premium_subscription_month_count: ?u64 = null, diff --git a/src/types/GiveawayCompleted.zig b/src/types/GiveawayCompleted.zig new file mode 100644 index 0000000..9e07ad9 --- /dev/null +++ b/src/types/GiveawayCompleted.zig @@ -0,0 +1,5 @@ +const Message = @import("Message.zig"); + +winner_count: u64, +unclaimed_prize_count: ?u64 = null, +giveaway_message: ?Message = null, diff --git a/src/types/GiveawayCreated.zig b/src/types/GiveawayCreated.zig new file mode 100644 index 0000000..2f72c10 --- /dev/null +++ b/src/types/GiveawayCreated.zig @@ -0,0 +1 @@ +// meant to be empty diff --git a/src/types/GiveawayWinners.zig b/src/types/GiveawayWinners.zig new file mode 100644 index 0000000..b99a7d2 --- /dev/null +++ b/src/types/GiveawayWinners.zig @@ -0,0 +1,14 @@ +const Chat = @import("Chat.zig"); +const User = @import("User.zig"); + +chat: Chat, +giveaway_message_id: u64, +winners_selection_date: u64, +winner_count: u64, +winners: []User, +additional_chat_count: ?u64 = null, +premium_subscription_month_count: ?u64 = null, +unclaimed_prize_count: ?u64 = null, +only_new_members: bool = false, +was_refunded: bool = false, +prize_description: ?[]const u8 = null, diff --git a/src/types/InlineKeyboardButton.zig b/src/types/InlineKeyboardButton.zig new file mode 100644 index 0000000..be156b4 --- /dev/null +++ b/src/types/InlineKeyboardButton.zig @@ -0,0 +1,15 @@ +const CallbackGame = @import("CallbackGame.zig"); +const LoginUrl = @import("LoginUrl.zig"); +const SwitchInlineQueryChosenChat = @import("SwitchInlineQueryChosenChat.zig"); +const WebAppInfo = @import("WebAppInfo.zig"); + +text: []const u8, +url: ?[]const u8 = null, +callback_data: ?[]const u8 = null, +web_app: ?WebAppInfo = null, +login_url: ?LoginUrl = null, +switch_inline_query: ?[]const u8 = null, +switch_inline_query_current_chat: ?[]const u8 = null, +switch_inline_query_chosen_chat: ?SwitchInlineQueryChosenChat = null, +callback_game: ?CallbackGame = null, +pay: bool = false, diff --git a/src/types/InlineKeyboardMarkup.zig b/src/types/InlineKeyboardMarkup.zig new file mode 100644 index 0000000..388d4fc --- /dev/null +++ b/src/types/InlineKeyboardMarkup.zig @@ -0,0 +1,3 @@ +const InlineKeyboardButton = @import("InlineKeyboardButton.zig"); + +inline_keyboard: [][]InlineKeyboardButton, diff --git a/src/types/InlineQuery.zig b/src/types/InlineQuery.zig new file mode 100644 index 0000000..7cbbe20 --- /dev/null +++ b/src/types/InlineQuery.zig @@ -0,0 +1,17 @@ +const Location = @import("Location.zig"); +const User = @import("User.zig"); + +pub const ChatType = enum { + sender, + private, + group, + supergroup, + channel, +}; + +id: []const u8, +from: User, +query: []const u8, +offset: []const u8, +chat_type: ?ChatType, +location: ?Location, diff --git a/src/types/Invoice.zig b/src/types/Invoice.zig new file mode 100644 index 0000000..5ff6bfd --- /dev/null +++ b/src/types/Invoice.zig @@ -0,0 +1,5 @@ +title: []const u8, +description: []const u8, +start_parameter: []const u8, +currency: []const u8, +total_amount: u64, diff --git a/src/types/LinkPreviewOptions.zig b/src/types/LinkPreviewOptions.zig new file mode 100644 index 0000000..c8258e7 --- /dev/null +++ b/src/types/LinkPreviewOptions.zig @@ -0,0 +1,5 @@ +is_disabled: ?bool = null, +url: ?[]const u8 = null, +prefer_small_media: ?bool = null, +prefer_large_media: ?bool = null, +show_above_text: ?bool = null, diff --git a/src/types/Location.zig b/src/types/Location.zig new file mode 100644 index 0000000..1fef4e3 --- /dev/null +++ b/src/types/Location.zig @@ -0,0 +1,6 @@ +latitude: f32, +longitude: f32, +horizontal_accuracy: ?f32 = null, +live_period: ?u64 = null, +heading: ?u64 = null, +proximity_alert_radius: ?u64 = null, diff --git a/src/types/LoginUrl.zig b/src/types/LoginUrl.zig new file mode 100644 index 0000000..37b89f6 --- /dev/null +++ b/src/types/LoginUrl.zig @@ -0,0 +1,4 @@ +url: []const u8, +forward_text: ?[]const u8 = null, +bot_username: ?[]const u8 = null, +request_write_access: ?[]const u8 = null, diff --git a/src/types/MaskPosition.zig b/src/types/MaskPosition.zig new file mode 100644 index 0000000..ba668c4 --- /dev/null +++ b/src/types/MaskPosition.zig @@ -0,0 +1,11 @@ +pub const Point = enum { + forehead, + eyes, + mouth, + chin, +}; + +point: Point, +x_shift: f32, +y_shift: f32, +scale: f32, diff --git a/src/types/Message.zig b/src/types/Message.zig new file mode 100644 index 0000000..448ca86 --- /dev/null +++ b/src/types/Message.zig @@ -0,0 +1,139 @@ +const Animation = @import("Animation.zig"); +const Audio = @import("Audio.zig"); +const Chat = @import("Chat.zig"); +const ChatBackground = @import("ChatBackground.zig"); +const ChatBoostAdded = @import("ChatBoostAdded.zig"); +const ChatShared = @import("ChatShared.zig"); +const Contact = @import("Contact.zig"); +const Dice = @import("Dice.zig"); +const Document = @import("Document.zig"); +const ExternalReplyInfo = @import("ExternalReplyInfo.zig"); +const ForumTopicClosed = @import("ForumTopicClosed.zig"); +const ForumTopicCreated = @import("ForumTopicCreated.zig"); +const ForumTopicEdited = @import("ForumTopicEdited.zig"); +const ForumTopicReopened = @import("ForumTopicReopened.zig"); +const Game = @import("Game.zig"); +const GeneralForumTopicHidden = @import("GeneralForumTopicHidden.zig"); +const GeneralForumTopicUnhidden = @import("GeneralForumTopicUnhidden.zig"); +const Giveaway = @import("Giveaway.zig"); +const GiveawayCompleted = @import("GiveawayCompleted.zig"); +const GiveawayCreated = @import("GiveawayCreated.zig"); +const GiveawayWinners = @import("GiveawayWinners.zig"); +const InlineKeyboardMarkup = @import("InlineKeyboardMarkup.zig"); +const Invoice = @import("Invoice.zig"); +const LinkPreviewOptions = @import("LinkPreviewOptions.zig"); +const Location = @import("Location.zig"); +const MaybeInaccessibleMessage = @This(); +const Message = @This(); +const MessageAutoDeleteTimerChanged = @import("MessageAutoDeleteTimerChanged.zig"); +const MessageEntity = @import("MessageEntity.zig"); +const MessageOrigin = @import("message_origin.zig").MessageOrigin; +const PaidMediaInfo = @import("PaidMediaInfo.zig"); +const PassportData = @import("PassportData.zig"); +const PhotoSize = @import("PhotoSize.zig"); +const Poll = @import("Poll.zig"); +const ProximityAlertTriggered = @import("ProximityAlertTriggered.zig"); +const RefundedPayment = @import("RefundedPayment.zig"); +const Sticker = @import("Sticker.zig"); +const Story = @import("Story.zig"); +const SuccessfulPayment = @import("SuccessfulPayment.zig"); +const TextQuote = @import("TextQuote.zig"); +const User = @import("User.zig"); +const UsersShared = @import("UsersShared.zig"); +const Venue = @import("Venue.zig"); +const Video = @import("Video.zig"); +const VideoChatEnded = @import("VideoChatEnded.zig"); +const VideoChatParticipantsInvited = @import("VideoChatParticipantsInvited.zig"); +const VideoChatScheduled = @import("VideoChatScheduled.zig"); +const VideoChatStarted = @import("VideoChatStarted.zig"); +const VideoNote = @import("VideoNote.zig"); +const Voice = @import("Voice.zig"); +const WebAppData = @import("WebAppData.zig"); +const WriteAccessAllowed = @import("WriteAccessAllowed.zig"); + +message_id: u64, +message_thread_id: ?u64 = null, +from: ?User = null, +sender_chat: ?Chat = null, +sender_boost_count: ?u64 = null, +sender_business_bot: ?User = null, +// If this is a MaybeInaccessibleMessage this will be 0 if this is inaccessible +date: u64, +business_connection_id: ?[]const u8 = null, +chat: Chat, +forward_origin: ?MessageOrigin = null, +is_topic_message: bool = false, +is_automatic_forward: bool = false, +reply_to_message: ?*Message = null, +external_reply: ?ExternalReplyInfo = null, +quote: ?TextQuote = null, +reply_to_story: ?Story = null, +via_bot: ?User = null, +edit_date: ?u64 = null, +has_protected_content: bool = false, +is_from_offline: bool = false, +media_group_id: ?[]const u8 = null, +author_signature: ?[]const u8 = null, +text: ?[]const u8 = null, +entities: ?[]MessageEntity = null, +link_preview_options: ?LinkPreviewOptions = null, +effect_id: ?[]const u8 = null, +animation: ?Animation = null, +audio: ?Audio = null, +document: ?Document = null, +paid_media: ?PaidMediaInfo = null, +photo: ?[]PhotoSize = null, +sticker: ?Sticker = null, +story: ?Story = null, +video: ?Video = null, +video_note: ?VideoNote = null, +voice: ?Voice = null, +caption: ?[]const u8 = null, +caption_entities: ?[]MessageEntity = null, +show_caption_above_media: bool = false, +has_media_spoiler: bool = false, +contact: ?Contact = null, +dice: ?Dice = null, +game: ?Game = null, +poll: ?Poll = null, +venue: ?Venue = null, +location: ?Location = null, +new_chat_members: ?[]User = null, +left_chat_member: ?User = null, +new_chat_title: ?[]const u8 = null, +new_chat_photo: ?[]PhotoSize = null, +delete_chat_photo: bool = false, +group_chat_created: bool = false, +supergroup_chat_created: bool = false, +channel_chat_created: bool = false, +message_auto_delete_timer_changed: ?MessageAutoDeleteTimerChanged = null, +migrate_to_chat_id: ?i64 = null, +migrate_from_chat_id: ?i64 = null, +pinned_message: ?*MaybeInaccessibleMessage = null, +invoice: ?Invoice = null, +successful_payment: ?SuccessfulPayment = null, +refunded_payment: ?RefundedPayment = null, +users_shared: ?UsersShared = null, +chat_shared: ?ChatShared = null, +connected_website: ?[]const u8 = null, +write_access_allowed: ?WriteAccessAllowed = null, +passport_data: ?PassportData = null, +proximity_alert_triggered: ?ProximityAlertTriggered = null, +boost_added: ?ChatBoostAdded = null, +chat_background_set: ?ChatBackground = null, +forum_topic_created: ?ForumTopicCreated = null, +forum_topic_edited: ?ForumTopicEdited = null, +forum_topic_closed: ?ForumTopicClosed = null, +forum_topic_reopened: ?ForumTopicReopened = null, +general_forum_topic_hidden: ?GeneralForumTopicHidden = null, +general_forum_topic_unhidden: ?GeneralForumTopicUnhidden = null, +giveaway_created: ?GiveawayCreated = null, +giveaway: ?Giveaway = null, +giveaway_winners: ?GiveawayWinners = null, +giveaway_completed: ?*GiveawayCompleted = null, +video_chat_scheduled: ?VideoChatScheduled = null, +video_chat_started: ?VideoChatStarted = null, +video_chat_ended: ?VideoChatEnded = null, +video_chat_participants_invited: ?VideoChatParticipantsInvited = null, +web_app_data: ?WebAppData = null, +reply_markup: ?InlineKeyboardMarkup = null, diff --git a/src/types/MessageAutoDeleteTimerChanged.zig b/src/types/MessageAutoDeleteTimerChanged.zig new file mode 100644 index 0000000..c73e682 --- /dev/null +++ b/src/types/MessageAutoDeleteTimerChanged.zig @@ -0,0 +1 @@ +message_auto_delete_time: u64, diff --git a/src/types/MessageEntity.zig b/src/types/MessageEntity.zig new file mode 100644 index 0000000..1c44d20 --- /dev/null +++ b/src/types/MessageEntity.zig @@ -0,0 +1,66 @@ +const std = @import("std"); + +const MessageEntity = @This(); +const User = @import("User.zig"); +const Utf8View = std.unicode.Utf8View; + +pub const Type = enum { + mention, + hashtag, + cashtag, + bot_command, + url, + email, + phone_number, + bold, + italic, + underline, + strikethrough, + spoiler, + blockquote, + expandable_blockquote, + code, + pre, + text_link, + text_mention, + custom_emoji, +}; + +type: Type, +offset: u64, +length: u64, +url: ?[]const u8 = null, +user: ?User = null, +language: ?[]const u8 = null, +custom_emoji_id: ?[]const u8 = null, + +pub fn extract(self: MessageEntity, src: []const u8) ![]const u8 { + if (self.length == 0) { + return ""; + } + + var utf8 = (try Utf8View.init(src)).iterator(); + var i: usize = 0; + + const start = if (i >= self.offset) + utf8.i + else blk: { + while (utf8.nextCodepoint()) |cp| { + i += std.unicode.utf16CodepointSequenceLength(cp) catch unreachable; + if (i >= self.offset) { + break :blk utf8.i; + } + } + return ""; + }; + + i = 0; + while (utf8.nextCodepoint()) |cp| { + i += std.unicode.utf16CodepointSequenceLength(cp) catch unreachable; + if (i >= self.length) { + return src[start..utf8.i]; + } + } + + return src[start..]; +} diff --git a/src/types/MessageReactionCountUpdated.zig b/src/types/MessageReactionCountUpdated.zig new file mode 100644 index 0000000..48a9f24 --- /dev/null +++ b/src/types/MessageReactionCountUpdated.zig @@ -0,0 +1,7 @@ +const Chat = @import("Chat.zig"); +const ReactionCount = @import("ReactionCount.zig"); + +chat: Chat, +message_id: u64, +date: u64, +reactions: []ReactionCount, diff --git a/src/types/MessageReactionUpdated.zig b/src/types/MessageReactionUpdated.zig new file mode 100644 index 0000000..7da679b --- /dev/null +++ b/src/types/MessageReactionUpdated.zig @@ -0,0 +1,11 @@ +const Chat = @import("Chat.zig"); +const ReactionType = @import("reaction_type.zig").ReactionType; +const User = @import("User.zig"); + +chat: Chat, +message_id: u64, +user: ?User = null, +actor_chat: ?Chat = null, +date: u64, +old_reaction: []ReactionType, +new_reaction: []ReactionType, diff --git a/src/types/OrderInfo.zig b/src/types/OrderInfo.zig new file mode 100644 index 0000000..3f94376 --- /dev/null +++ b/src/types/OrderInfo.zig @@ -0,0 +1,6 @@ +const ShippingAddress = @import("ShippingAddress.zig"); + +name: ?[]const u8 = null, +phone_number: ?[]const u8 = null, +email: ?[]const u8 = null, +shipping_address: ?ShippingAddress = null, diff --git a/src/types/PaidMediaInfo.zig b/src/types/PaidMediaInfo.zig new file mode 100644 index 0000000..6de7447 --- /dev/null +++ b/src/types/PaidMediaInfo.zig @@ -0,0 +1,4 @@ +const PaidMedia = @import("paid_media.zig").PaidMedia; + +star_count: u64, +paid_media: []PaidMedia, diff --git a/src/types/PassportData.zig b/src/types/PassportData.zig new file mode 100644 index 0000000..679534c --- /dev/null +++ b/src/types/PassportData.zig @@ -0,0 +1,5 @@ +const EncryptedCredentials = @import("EncryptedCredentials.zig"); +const EncryptedPassportElement = @import("EncryptedPassportElement.zig"); + +data: []EncryptedPassportElement, +credentials: EncryptedCredentials, diff --git a/src/types/PassportFile.zig b/src/types/PassportFile.zig new file mode 100644 index 0000000..2b208fa --- /dev/null +++ b/src/types/PassportFile.zig @@ -0,0 +1,4 @@ +file_id: []const u8, +file_unique_id: []const u8, +file_size: u64, +file_date: u64, diff --git a/src/types/PhotoSize.zig b/src/types/PhotoSize.zig new file mode 100644 index 0000000..887b530 --- /dev/null +++ b/src/types/PhotoSize.zig @@ -0,0 +1,5 @@ +file_id: []const u8, +file_unique_id: []const u8, +width: u64, +height: u64, +file_size: ?u64 = null, diff --git a/src/types/Poll.zig b/src/types/Poll.zig new file mode 100644 index 0000000..ccc3b68 --- /dev/null +++ b/src/types/Poll.zig @@ -0,0 +1,22 @@ +const MessageEntity = @import("MessageEntity.zig"); +const PollOption = @import("PollOption.zig"); + +pub const Type = enum { + regular, + quiz, +}; + +id: []const u8, +question: []const u8, +question_entities: ?[]MessageEntity = null, +options: []PollOption, +total_voter_count: u64, +is_closed: bool, +is_anonymous: bool, +type: Type, +allows_multiple_answers: bool, +correct_option_id: ?u64 = null, +explanation: ?[]const u8 = null, +explanation_entities: ?[]MessageEntity = null, +open_period: ?u64 = null, +close_date: ?u64 = null, diff --git a/src/types/PollAnswer.zig b/src/types/PollAnswer.zig new file mode 100644 index 0000000..7d4985c --- /dev/null +++ b/src/types/PollAnswer.zig @@ -0,0 +1,7 @@ +const Chat = @import("Chat.zig"); +const User = @import("User.zig"); + +poll_id: []const u8, +voter_chat: ?Chat, +user: ?User, +option_ids: []u64, diff --git a/src/types/PollOption.zig b/src/types/PollOption.zig new file mode 100644 index 0000000..870bfee --- /dev/null +++ b/src/types/PollOption.zig @@ -0,0 +1,5 @@ +const MessageEntity = @import("MessageEntity.zig"); + +text: []const u8, +text_entities: ?[]MessageEntity = null, +voter_count: u64, diff --git a/src/types/PreCheckoutQuery.zig b/src/types/PreCheckoutQuery.zig new file mode 100644 index 0000000..48e013f --- /dev/null +++ b/src/types/PreCheckoutQuery.zig @@ -0,0 +1,10 @@ +const OrderInfo = @import("OrderInfo.zig"); +const User = @import("User.zig"); + +id: []const u8, +from: User, +currency: []const u8, +total_amount: u64, +invoice_payload: []const u8, +shipping_option_id: ?[]const u8, +order_info: ?OrderInfo, diff --git a/src/types/ProximityAlertTriggered.zig b/src/types/ProximityAlertTriggered.zig new file mode 100644 index 0000000..db5159d --- /dev/null +++ b/src/types/ProximityAlertTriggered.zig @@ -0,0 +1,5 @@ +const User = @import("User.zig"); + +traveler: User, +watcher: User, +distance: u64, diff --git a/src/types/ReactionCount.zig b/src/types/ReactionCount.zig new file mode 100644 index 0000000..7c6a0bc --- /dev/null +++ b/src/types/ReactionCount.zig @@ -0,0 +1,4 @@ +const ReactionType = @import("reaction_type.zig").ReactionType; + +type: ReactionType, +total_count: u64, diff --git a/src/types/RefundedPayment.zig b/src/types/RefundedPayment.zig new file mode 100644 index 0000000..31f04c0 --- /dev/null +++ b/src/types/RefundedPayment.zig @@ -0,0 +1,5 @@ +currency: []const u8, +total_amount: u64, +invoice_payload: []const u8, +telegram_payment_charge_id: []const u8, +provider_payment_charge_id: []const u8, diff --git a/src/types/ReplyParameters.zig b/src/types/ReplyParameters.zig new file mode 100644 index 0000000..57d75c2 --- /dev/null +++ b/src/types/ReplyParameters.zig @@ -0,0 +1,9 @@ +message_id: u64, +// TODO: Integer OR String +chat_id: ?i64 = null, +allow_sending_without_reply: ?bool = null, +quote: ?[]const u8 = null, +// one of quote_parse_mode or quote_entities +quote_parse_mode: ?[]const u8 = null, +quote_entities: ?[]const u8 = null, +quote_position: ?u64 = null, diff --git a/src/types/ResponseParameters.zig b/src/types/ResponseParameters.zig new file mode 100644 index 0000000..655c4f9 --- /dev/null +++ b/src/types/ResponseParameters.zig @@ -0,0 +1,2 @@ +migrate_to_chat_id: ?i64 = null, +retry_after: ?i64 = null, diff --git a/src/types/SendMessageParams.zig b/src/types/SendMessageParams.zig new file mode 100644 index 0000000..8c84940 --- /dev/null +++ b/src/types/SendMessageParams.zig @@ -0,0 +1,18 @@ +const LinkPreviewOptions = @import("LinkPreviewOptions.zig"); +const MessageEntity = @import("MessageEntity.zig"); +const ParseMode = @import("parse_mode.zig").ParseMode; +const ReplyParameters = @import("ReplyParameters.zig"); + +business_connection_id: ?[]const u8 = null, +// TODO: Integer or String +chat_id: i64, +message_thread_id: ?u64 = null, +text: []const u8, +parse_mode: ?ParseMode = null, +entities: ?[]MessageEntity = null, +link_preview_options: ?LinkPreviewOptions = null, +disable_notification: ?bool = null, +protect_content: ?bool = null, +message_effect_id: ?[]const u8 = null, +reply_parameters: ?ReplyParameters = null, +// TODO: reply_markup: InlineKeyboardMarkup OR ReplyKeyboardMarkup OR ReplyKeyboardRemove OR ForceReply diff --git a/src/types/SetMyNameParams.zig b/src/types/SetMyNameParams.zig new file mode 100644 index 0000000..0bd2f06 --- /dev/null +++ b/src/types/SetMyNameParams.zig @@ -0,0 +1,2 @@ +name: ?[]const u8 = null, +language_code: ?[]const u8 = null, diff --git a/src/types/SharedUser.zig b/src/types/SharedUser.zig new file mode 100644 index 0000000..4afe053 --- /dev/null +++ b/src/types/SharedUser.zig @@ -0,0 +1,7 @@ +const PhotoSize = @import("PhotoSize.zig"); + +user_id: u64, +first_name: ?[]const u8 = null, +last_name: ?[]const u8 = null, +username: ?[]const u8 = null, +photo: ?[]PhotoSize = null, diff --git a/src/types/ShippingAddress.zig b/src/types/ShippingAddress.zig new file mode 100644 index 0000000..f4203fe --- /dev/null +++ b/src/types/ShippingAddress.zig @@ -0,0 +1,6 @@ +country_code: []const u8, +state: []const u8, +city: []const u8, +street_line1: []const u8, +street_line2: []const u8, +post_code: []const u8, diff --git a/src/types/ShippingQuery.zig b/src/types/ShippingQuery.zig new file mode 100644 index 0000000..15abcda --- /dev/null +++ b/src/types/ShippingQuery.zig @@ -0,0 +1,7 @@ +const ShippingAddress = @import("ShippingAddress.zig"); +const User = @import("User.zig"); + +id: []const u8, +from: User, +invoice_payload: []const u8, +shipping_address: ShippingAddress, diff --git a/src/types/Sticker.zig b/src/types/Sticker.zig new file mode 100644 index 0000000..e28df69 --- /dev/null +++ b/src/types/Sticker.zig @@ -0,0 +1,25 @@ +const File = @import("File.zig"); +const MaskPosition = @import("MaskPosition.zig"); +const PhotoSize = @import("PhotoSize.zig"); + +pub const Type = enum { + regular, + mask, + custom_emoji, +}; + +file_id: []const u8, +file_unique_id: []const u8, +type: Type, +width: u64, +height: u64, +is_animated: bool = false, +is_video: bool = false, +thumbnail: ?PhotoSize = null, +emoji: ?[]const u8 = null, +set_name: ?[]const u8 = null, +premium_animation: ?File = null, +mask_position: ?MaskPosition = null, +custom_emoji_id: ?[]const u8 = null, +needs_repainting: bool = false, +file_size: ?u64 = null, diff --git a/src/types/Story.zig b/src/types/Story.zig new file mode 100644 index 0000000..479b7a9 --- /dev/null +++ b/src/types/Story.zig @@ -0,0 +1,4 @@ +const Chat = @import("Chat.zig"); + +chat: Chat, +id: u64, diff --git a/src/types/SuccessfulPayment.zig b/src/types/SuccessfulPayment.zig new file mode 100644 index 0000000..c508f48 --- /dev/null +++ b/src/types/SuccessfulPayment.zig @@ -0,0 +1,9 @@ +const OrderInfo = @import("OrderInfo.zig"); + +currency: []const u8, +total_amount: u64, +invoice_payload: []const u8, +shipping_option_id: ?[]const u8 = null, +order_info: ?OrderInfo = null, +telegram_payment_charge_id: []const u8, +provider_payment_charge_id: []const u8, diff --git a/src/types/SwitchInlineQueryChosenChat.zig b/src/types/SwitchInlineQueryChosenChat.zig new file mode 100644 index 0000000..860f2d9 --- /dev/null +++ b/src/types/SwitchInlineQueryChosenChat.zig @@ -0,0 +1,5 @@ +query: ?[]const u8, +allow_user_chats: ?bool = null, +allow_bot_chats: ?bool = null, +allow_group_chats: ?bool = null, +allow_channel_chats: ?bool = null, diff --git a/src/types/TextQuote.zig b/src/types/TextQuote.zig new file mode 100644 index 0000000..6dc6be2 --- /dev/null +++ b/src/types/TextQuote.zig @@ -0,0 +1,6 @@ +const MessageEntity = @import("MessageEntity.zig"); + +text: []const u8, +entities: ?[]MessageEntity = null, +position: u64, +is_manual: bool = false, diff --git a/src/types/Update.zig b/src/types/Update.zig new file mode 100644 index 0000000..0491110 --- /dev/null +++ b/src/types/Update.zig @@ -0,0 +1,41 @@ +const BusinessConnection = @import("BusinessConnection.zig"); +const BusinessMessagesDeleted = @import("BusinessMessagesDeleted.zig"); +const CallbackQuery = @import("CallbackQuery.zig"); +const ChatBoostRemoved = @import("ChatBoostRemoved.zig"); +const ChatBoostUpdated = @import("ChatBoostUpdated.zig"); +const ChatJoinRequest = @import("ChatJoinRequest.zig"); +const ChatMemberUpdated = @import("ChatMemberUpdated.zig"); +const ChosenInlineResult = @import("ChosenInlineResult.zig"); +const InlineQuery = @import("InlineQuery.zig"); +const Message = @import("Message.zig"); +const MessageReactionCountUpdated = @import("MessageReactionCountUpdated.zig"); +const MessageReactionUpdated = @import("MessageReactionUpdated.zig"); +const Poll = @import("Poll.zig"); +const PollAnswer = @import("PollAnswer.zig"); +const PreCheckoutQuery = @import("PreCheckoutQuery.zig"); +const ShippingQuery = @import("ShippingQuery.zig"); + +// TODO: Make this into a tagged union +update_id: u64, +message: ?Message = null, +edited_message: ?Message = null, +channel_post: ?Message = null, +edited_channel_post: ?Message = null, +business_connection: ?BusinessConnection = null, +business_message: ?Message = null, +edited_business_message: ?Message = null, +deleted_business_messages: ?BusinessMessagesDeleted = null, +message_reaction: ?MessageReactionUpdated = null, +message_reaction_count: ?MessageReactionCountUpdated = null, +inline_query: ?InlineQuery = null, +chosen_inline_result: ?ChosenInlineResult = null, +callback_query: ?CallbackQuery = null, +shipping_query: ?ShippingQuery = null, +pre_checkout_query: ?PreCheckoutQuery = null, +poll: ?Poll = null, +poll_answer: ?PollAnswer = null, +my_chat_member: ?ChatMemberUpdated = null, +chat_member: ?ChatMemberUpdated = null, +chat_join_request: ?ChatJoinRequest = null, +chat_boost: ?ChatBoostUpdated = null, +removed_chat_boost: ?ChatBoostRemoved = null, diff --git a/src/types/User.zig b/src/types/User.zig new file mode 100644 index 0000000..dc06097 --- /dev/null +++ b/src/types/User.zig @@ -0,0 +1,32 @@ +const textutils = @import("../textutils.zig"); + +const User = @This(); + +id: i64, +is_bot: bool, +first_name: []const u8, +last_name: ?[]const u8 = null, +username: ?[]const u8 = null, +language_code: ?[]const u8 = null, +is_premium: bool = false, +added_to_attachment_menu: bool = false, +can_join_groups: bool = false, +can_read_all_group_messages: bool = false, +supports_inline_queries: bool = false, +can_connect_to_business: bool = false, + +pub fn writeFormattedName(self: User, w: anytype) !void { + try w.print("", .{self.id}); + try textutils.escapeXml(w, self.first_name); + if (self.last_name) |last_name| { + try w.writeByte(' '); + try textutils.escapeXml(w, last_name); + } + try w.writeAll(""); + + if (self.username) |username| { + try w.writeAll(" @"); + try textutils.escapeXml(w, username); + } + try w.print(" [{}]", .{self.id}); +} diff --git a/src/types/UsersShared.zig b/src/types/UsersShared.zig new file mode 100644 index 0000000..0fff785 --- /dev/null +++ b/src/types/UsersShared.zig @@ -0,0 +1,4 @@ +const SharedUser = @import("SharedUser.zig"); + +request_id: u64, +users: []SharedUser, diff --git a/src/types/Venue.zig b/src/types/Venue.zig new file mode 100644 index 0000000..6ae61f3 --- /dev/null +++ b/src/types/Venue.zig @@ -0,0 +1,9 @@ +const Location = @import("Location.zig"); + +location: Location, +title: []const u8, +address: []const u8, +foursquare_id: ?[]const u8 = null, +foursquare_type: ?[]const u8 = null, +google_place_id: ?[]const u8 = null, +google_place_type: ?[]const u8 = null, diff --git a/src/types/Video.zig b/src/types/Video.zig new file mode 100644 index 0000000..8330637 --- /dev/null +++ b/src/types/Video.zig @@ -0,0 +1,11 @@ +const PhotoSize = @import("PhotoSize.zig"); + +file_id: []const u8, +file_unique_id: []const u8, +width: u64, +height: u64, +duration: u64, +thumbnail: ?PhotoSize = null, +file_name: ?[]const u8 = null, +mime_type: ?[]const u8 = null, +file_size: ?u64 = null, diff --git a/src/types/VideoChatEnded.zig b/src/types/VideoChatEnded.zig new file mode 100644 index 0000000..748bf44 --- /dev/null +++ b/src/types/VideoChatEnded.zig @@ -0,0 +1 @@ +duration: u64, diff --git a/src/types/VideoChatParticipantsInvited.zig b/src/types/VideoChatParticipantsInvited.zig new file mode 100644 index 0000000..22a46f7 --- /dev/null +++ b/src/types/VideoChatParticipantsInvited.zig @@ -0,0 +1,3 @@ +const User = @import("User.zig"); + +users: []User, diff --git a/src/types/VideoChatScheduled.zig b/src/types/VideoChatScheduled.zig new file mode 100644 index 0000000..a55dfe3 --- /dev/null +++ b/src/types/VideoChatScheduled.zig @@ -0,0 +1 @@ +start_date: u64, diff --git a/src/types/VideoChatStarted.zig b/src/types/VideoChatStarted.zig new file mode 100644 index 0000000..2f72c10 --- /dev/null +++ b/src/types/VideoChatStarted.zig @@ -0,0 +1 @@ +// meant to be empty diff --git a/src/types/VideoNote.zig b/src/types/VideoNote.zig new file mode 100644 index 0000000..aadb318 --- /dev/null +++ b/src/types/VideoNote.zig @@ -0,0 +1,8 @@ +const PhotoSize = @import("PhotoSize.zig"); + +file_id: []const u8, +file_unique_id: []const u8, +length: u64, +duration: u64, +thumbnail: ?PhotoSize = null, +file_size: ?u64 = null, diff --git a/src/types/Voice.zig b/src/types/Voice.zig new file mode 100644 index 0000000..609ee0b --- /dev/null +++ b/src/types/Voice.zig @@ -0,0 +1,5 @@ +file_id: []const u8, +file_unique_id: []const u8, +duration: u64, +mime_type: ?[]const u8 = null, +file_size: ?u64 = null, diff --git a/src/types/WebAppData.zig b/src/types/WebAppData.zig new file mode 100644 index 0000000..e853fdc --- /dev/null +++ b/src/types/WebAppData.zig @@ -0,0 +1,2 @@ +data: []const u8, +button_text: []const u8, diff --git a/src/types/WebAppInfo.zig b/src/types/WebAppInfo.zig new file mode 100644 index 0000000..02fcc5e --- /dev/null +++ b/src/types/WebAppInfo.zig @@ -0,0 +1 @@ +url: []const u8, diff --git a/src/types/WriteAccessAllowed.zig b/src/types/WriteAccessAllowed.zig new file mode 100644 index 0000000..2ff3259 --- /dev/null +++ b/src/types/WriteAccessAllowed.zig @@ -0,0 +1,3 @@ +from_request: ?bool = null, +web_app_name: ?[]const u8 = null, +from_attachment_menu: ?bool = null, diff --git a/src/types/background_fill.zig b/src/types/background_fill.zig new file mode 100644 index 0000000..86aaa59 --- /dev/null +++ b/src/types/background_fill.zig @@ -0,0 +1,17 @@ +const json = @import("../json.zig"); + +pub const BackgroundFill = union(enum) { + solid: struct { + color: u24, + }, + gradient: struct { + top_color: u24, + bottom_color: u24, + rotation_angle: u16, + }, + freeform_gradient: struct { + colors: []const u24, + }, + + pub const jsonParse = json.makeJsonParse(BackgroundFill); +}; diff --git a/src/types/background_type.zig b/src/types/background_type.zig new file mode 100644 index 0000000..220465a --- /dev/null +++ b/src/types/background_type.zig @@ -0,0 +1,30 @@ +const json = @import("../json.zig"); + +const BackgroundFill = @import("background_fill.zig").BackgroundFill; +const Document = @import("Document.zig"); + +pub const BackgroundType = union(enum) { + fill: struct { + fill: BackgroundFill, + dark_theme_dimming: u8, + }, + wallpaper: struct { + document: Document, + dark_theme_dimming: u8, + is_blurred: bool = false, + is_moving: bool = false, + }, + pattern: struct { + document: Document, + fill: BackgroundFill, + intensity: u8, + is_inverted: bool = false, + is_moving: bool = false, + }, + chat_theme: struct { + theme_name: []const u8, + }, + + pub const jsonParse = json.makeJsonParse(BackgroundType); + pub const jsonParseFromValue = json.makeJsonParseFromValue(BackgroundType); +}; diff --git a/src/types/chat_boost_source.zig b/src/types/chat_boost_source.zig new file mode 100644 index 0000000..ead19a0 --- /dev/null +++ b/src/types/chat_boost_source.zig @@ -0,0 +1,20 @@ +const json = @import("../json.zig"); + +const User = @import("User.zig"); + +pub const ChatBoostSource = union(enum) { + premium: struct { + user: User, + }, + gift_code: struct { + user: User, + }, + giveaway: struct { + giveaway_message_id: u64, + user: ?User = null, + is_unclaimed: bool = false, + }, + + pub const jsonParse = json.makeJsonParse(ChatBoostSource); + pub const jsonParseFromValue = json.makeJsonParseFromValue(ChatBoostSource); +}; diff --git a/src/types/chat_member.zig b/src/types/chat_member.zig new file mode 100644 index 0000000..7650299 --- /dev/null +++ b/src/types/chat_member.zig @@ -0,0 +1,63 @@ +const json = @import("../json.zig"); + +const User = @import("User.zig"); + +pub const ChatMember = union(enum) { + creator: struct { + user: User, + is_anonymous: bool, + custom_title: ?[]const u8 = null, + }, + administrator: struct { + user: User, + can_be_edited: bool, + is_anonymous: bool, + can_manage_chat: bool, + can_delete_messages: bool, + can_manage_video_chats: bool, + can_restrict_members: bool, + can_promote_members: bool, + can_change_info: bool, + can_invite_users: bool, + can_post_stories: bool, + can_edit_stories: bool, + can_delete_stories: bool, + can_post_messages: ?bool = null, + can_edit_messages: ?bool = null, + can_pin_messages: ?bool = null, + can_manage_topics: ?bool = null, + custom_title: ?[]const u8 = null, + }, + member: struct { + user: User, + }, + restricted: struct { + user: User, + is_member: bool, + can_send_messages: bool, + can_send_audios: bool, + can_send_documents: bool, + can_send_photos: bool, + can_send_videos: bool, + can_send_video_notes: bool, + can_send_voice_notes: bool, + can_send_polls: bool, + can_send_other_messages: bool, + can_add_web_page_previews: bool, + can_change_info: bool, + can_invite_users: bool, + can_pin_messages: bool, + can_manage_topics: bool, + until_date: u64, + }, + left: struct { + user: User, + }, + banned: struct { + user: User, + until_date: u64, + }, + + pub const jsonParse = json.makeJsonParse(ChatMember); + pub const jsonParseFromValue = json.makeJsonParseFromValueWithTag(ChatMember, "status"); +}; diff --git a/src/types/message_origin.zig b/src/types/message_origin.zig new file mode 100644 index 0000000..5ca1f9f --- /dev/null +++ b/src/types/message_origin.zig @@ -0,0 +1,29 @@ +const json = @import("../json.zig"); + +const Chat = @import("Chat.zig"); +const User = @import("User.zig"); + +pub const MessageOrigin = union(enum) { + user: struct { + date: u64, + sender_user: User, + }, + hidden_user: struct { + date: u64, + sender_user_name: []const u8, + }, + chat: struct { + date: u64, + sender_chat: Chat, + author_signature: ?[]const u8, + }, + channel: struct { + date: u64, + chat: Chat, + message_id: u64, + author_signature: ?[]const u8, + }, + + pub const jsonParse = json.makeJsonParse(MessageOrigin); + pub const jsonParseFromValue = json.makeJsonParseFromValue(MessageOrigin); +}; diff --git a/src/types/paid_media.zig b/src/types/paid_media.zig new file mode 100644 index 0000000..3b50250 --- /dev/null +++ b/src/types/paid_media.zig @@ -0,0 +1,21 @@ +const json = @import("../json.zig"); + +const PhotoSize = @import("PhotoSize.zig"); +const Video = @import("Video.zig"); + +pub const PaidMedia = union(enum) { + preview: struct { + width: ?u64 = null, + height: ?u64 = null, + duration: ?u64 = null, + }, + photo: struct { + photo: []PhotoSize, + }, + video: struct { + video: Video, + }, + + pub const jsonParse = json.makeJsonParse(PaidMedia); + pub const jsonParseFromValue = json.makeJsonParseFromValue(PaidMedia); +}; diff --git a/src/types/parse_mode.zig b/src/types/parse_mode.zig new file mode 100644 index 0000000..7285c64 --- /dev/null +++ b/src/types/parse_mode.zig @@ -0,0 +1,5 @@ +pub const ParseMode = enum { + markdownv2, + html, + markdown, +}; diff --git a/src/types/reaction_type.zig b/src/types/reaction_type.zig new file mode 100644 index 0000000..416def9 --- /dev/null +++ b/src/types/reaction_type.zig @@ -0,0 +1,13 @@ +const json = @import("../json.zig"); + +pub const ReactionType = union(enum) { + emoji: struct { + emoji: []const u8, + }, + custom_emoji: struct { + custom_emoji_id: []const u8, + }, + + pub const jsonParse = json.makeJsonParse(ReactionType); + pub const jsonParseFromValue = json.makeJsonParseFromValue(ReactionType); +}; -- cgit v1.2.3