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