const inline_bots = @import("inline_bots.zig"); const std = @import("std"); const types = @import("types.zig"); const utils = @import("utils.zig"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Bot = @import("Bot.zig"); const Config = @import("Config.zig"); const DB = @import("DB.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 db = try DB.init(config.config.db_path); defer db.deinit(); try db.upgrade(); var bot = try Bot.init(allocator, config.config, &db); 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 reportError(bot: *Bot, evt: anytype, err: anyerror) !void { std.log.err("While handling {}: {}", .{ evt, err }); const evtStr = try std.json.stringifyAlloc(bot.allocator, evt, .{ .whitespace = .indent_2, .emit_null_optional_fields = false, }); defer bot.allocator.free(evtStr); const devMsg = try std.fmt.allocPrint(bot.allocator, "{} while handling\n
{s}
", .{ err, evtStr }); defer bot.allocator.free(devMsg); bot.sendMessage_(.{ .chat_id = bot.config.dev_group, .text = devMsg, .parse_mode = .html, }) catch |err2| { std.log.err("While trying to report the error: {}", .{err2}); return err2; }; } fn wrappedMain(bot: *Bot) !void { try bot.sendMessage_(.{ .chat_id = bot.config.dev_group, .text = "Initializing...", }); try bot.setMyName(.{ .name = bot.config.bot_name }); 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| { onMessage(bot, message) catch |err| { try reportError(bot, message, err); }; } if (update.callback_query) |cb| { onCallbackQuery(bot, cb) catch |err| { try reportError(bot, cb, err); }; } } } // 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 onCallbackQuery(bot: *Bot, cb: types.CallbackQuery) !void { if (cb.data) |cb_data| blk: { if (std.mem.startsWith(u8, cb_data, "bbl:")) { if (cb.from.id != bot.config.owner) { break :blk; } const inline_bot_id = try std.fmt.parseInt(i64, cb_data[4..], 10); try inline_bots.blacklistBot(bot, inline_bot_id); if (cb.message) |msg| { try bot.deleteMessage(.{ .chat_id = msg.chat.id, .message_id = msg.message_id, }); } } else if (std.mem.startsWith(u8, cb_data, "bwl:")) { if (cb.from.id != bot.config.owner) { break :blk; } const inline_bot_id = try std.fmt.parseInt(i64, cb_data[4..], 10); try inline_bots.whitelistBot(bot, inline_bot_id); if (cb.message) |msg| { try bot.deleteMessage(.{ .chat_id = msg.chat.id, .message_id = msg.message_id, }); } } else { break :blk; } return bot.answerCallbackQuery(.{ .callback_query_id = cb.id, .text = "OK", }); } std.log.info("Unrecognised callback query data: {?s}", .{ cb.data }); return bot.answerCallbackQuery(.{ .callback_query_id = cb.id, .text = "Unallowed callback query, don't press the button again", .show_alert = true, }); } fn onMessage(bot: *Bot, msg: types.Message) !void { if (msg.via_bot) |via| { if (!try inline_bots.onInlineBot(bot, msg, via)) { return; } } 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 { if (new_member.id == try bot.getId()) { return; // TODO: // Bot is added to a new group // return bot.sendAnimation_(.{ // .chat_id = msg.chat.id, // // TODO: lol // .animation = "CgACAgQAAx0CVcPEEgACDC5mo7YHMgOE2n3qo3e9UOyd4N-uxQACNAMAAlbuDFMRWj9LxNLBkDUE", // }); } 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!!"); // TODO: // try bot.sendAnimation_(.{ // .chat_id = msg.chat.id, // // TODO: lol // .animation = "CgACAgQAAx0CVcPEEgACC9Vmo6_zCxMp3ZNXSMM1nI6nMkIhgwACNwMAAtDmDFMop6BHmV7lUTUE", // .caption = sb.items, // .parse_mode = .html, // .show_caption_above_media = true, // .reply_parameters = .{ // .allow_sending_without_reply = true, // .message_id = msg.message_id, // .chat_id = msg.chat.id, // }, // }); 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 ")) { const the_text = text[4..]; if (!try utils.isTgWhitespaceStr(the_text)) { const lc = try utils.getLetterCasing(); const uppercased = try lc.toUpperStr(bot.allocator, the_text); defer bot.allocator.free(uppercased); var output = ArrayList(u8).init(bot.allocator); defer output.deinit(); try output.appendSlice(""); try utils.escapeXml(output.writer(), uppercased); try output.appendSlice(""); try bot.sendMessage_(.{ .chat_id = msg.chat.id, .text = output.items, .parse_mode = .html, .reply_parameters = .{ .message_id = msg.message_id, .chat_id = msg.chat.id, }, }); } } else if (std.ascii.eqlIgnoreCase(text, "dio cane")) { 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 = "porco dio", .reply_parameters = .{ .message_id = reply_to, .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.eqlIgnoreCase(text, "porco dio")) { 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 = "dio cane", .reply_parameters = .{ .message_id = reply_to, .chat_id = msg.chat.id, }, }); } else if (std.ascii.startsWithIgnoreCase(text, "say ")) { const the_text = text[4..]; if (!try utils.isTgWhitespaceStr(the_text)) { try bot.sendMessage_(.{ .chat_id = msg.chat.id, .text = the_text, .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, }, }); } }