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.ascii.eqlIgnoreCase(text, "forgor")) { try bot.sendMessage_(.{ .chat_id = msg.chat.id, .text = "💀", .reply_parameters = .{ .message_id = msg.message_id, .chat_id = msg.chat.id, }, }); } else if (std.ascii.eqlIgnoreCase(text, "huh")) { try bot.sendMessage_(.{ .chat_id = msg.chat.id, .text = "idgi", .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, "msginfo")) { if (msg.reply_to_message) |replied| { const str_data = try std.json.stringifyAlloc(bot.allocator, replied.*, .{ .whitespace = .indent_2, .emit_null_optional_fields = false, }); defer bot.allocator.free(str_data); try bot.sendMessage_(.{ .chat_id = msg.chat.id, .text = str_data, .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(); var sb = ArrayList(u8).init(bot.allocator); defer sb.deinit(); try sb.writer().print("Pong!\nSend time: ...", .{}); 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!\n\nSend time: {d}ms", .{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, }, }); } }