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 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 reportError(bot: *Bot, msg: types.Message, err: anyerror) !void { std.log.err("While handling {}: {}", .{ msg, err }); const msgStr = try std.json.stringifyAlloc(bot.allocator, msg, .{ .whitespace = .indent_2, .emit_null_optional_fields = false, }); defer bot.allocator.free(msgStr); const devMsg = try std.fmt.allocPrint(bot.allocator, "{} while handling\n
{s}
", .{ err, msgStr }); 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 = "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| { onMessage(bot, message) catch |err| { try reportError(bot, message, 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...", }); } const allowed_inline_bots = [_]i64{ 90832338, // @vid 109158646, // @bing 114528005, // @pic 140267078, // @gif 154595593, // @wiki 184730458, // @UnitConversionBot 296635833, // @lastfmrobot 595898211, // @DeezerMusicBot 870410041, // @HowGayBot 7904498194, // @tanstiktokbot }; comptime { std.testing.expect(std.sort.isSorted( i64, &allowed_inline_bots, {}, std.sort.asc(i64), )) catch unreachable; } fn orderI64(a: i64, b: i64) std.math.Order { return std.math.order(a, b); } fn isAllowedInlineBot(id: i64) bool { return std.sort.binarySearch( i64, &allowed_inline_bots, id, orderI64, ) != null; } fn onMessage(bot: *Bot, msg: types.Message) !void { if (msg.via_bot) |via| { if (!isAllowedInlineBot(via.id)) { std.log.info("Deleting an unallowed inline bot message from {?s} {}", .{ via.username, via.id }); try bot.deleteMessage(.{ .chat_id = msg.chat.id, .message_id = msg.message_id, }); const text = try std.fmt.allocPrint(bot.allocator, "Deleted a message sent by inline bot @{?s} {}", .{ via.username, via.id }); defer bot.allocator.free(text); try bot.sendMessage_(.{ .chat_id = bot.config.dev_group, .text = text, .parse_mode = .html, }); 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, "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 ")) { 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, }, }); } }