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/main.zig | 353 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 src/main.zig (limited to 'src/main.zig') 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, + }, + }); + } +} -- cgit v1.2.3