diff options
| author | 2024-07-20 17:22:25 +0300 | |
|---|---|---|
| committer | 2024-07-20 17:22:25 +0300 | |
| commit | c70ffd095a6de5cd5b872796a0d82a8c5afc1511 (patch) | |
| tree | 56183274b05a294e357bad4d06b523472a1c4a4a /src/main.zig | |
| download | ukkobot-c70ffd095a6de5cd5b872796a0d82a8c5afc1511.tar.gz ukkobot-c70ffd095a6de5cd5b872796a0d82a8c5afc1511.tar.xz ukkobot-c70ffd095a6de5cd5b872796a0d82a8c5afc1511.zip | |
Initial commit
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 353 |
1 files changed, 353 insertions, 0 deletions
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 @@ | |||
| 1 | const types = @import("types.zig"); | ||
| 2 | const std = @import("std"); | ||
| 3 | |||
| 4 | const Allocator = std.mem.Allocator; | ||
| 5 | const ArrayList = std.ArrayList; | ||
| 6 | const Bot = @import("Bot.zig"); | ||
| 7 | const Config = @import("Config.zig"); | ||
| 8 | const GPA = std.heap.GeneralPurposeAllocator(.{}); | ||
| 9 | |||
| 10 | pub fn main() !void { | ||
| 11 | defer std.log.info("We're done", .{}); | ||
| 12 | |||
| 13 | var gpa = GPA{}; | ||
| 14 | const allocator = gpa.allocator(); | ||
| 15 | defer _ = gpa.deinit(); | ||
| 16 | |||
| 17 | // Load config | ||
| 18 | var config = try Config.load(allocator, "config.default.json"); | ||
| 19 | defer config.deinit(); | ||
| 20 | try config.merge("config.json"); | ||
| 21 | |||
| 22 | var bot = try Bot.init(allocator, config.config); | ||
| 23 | defer bot.deinit(); | ||
| 24 | |||
| 25 | // TODO: Catch fatal errors, report them | ||
| 26 | try wrappedMain(&bot); | ||
| 27 | } | ||
| 28 | |||
| 29 | fn loadConfig(allocator: Allocator, filename: []const u8) !std.json.Parsed(Config) { | ||
| 30 | const file = try std.fs.cwd().openFile(filename, .{}); | ||
| 31 | defer file.close(); | ||
| 32 | |||
| 33 | var reader = std.json.reader(allocator, file.reader()); | ||
| 34 | defer reader.deinit(); | ||
| 35 | |||
| 36 | return try std.json.parseFromTokenSource( | ||
| 37 | Config, | ||
| 38 | allocator, | ||
| 39 | &reader, | ||
| 40 | .{ | ||
| 41 | .duplicate_field_behavior = .use_last, | ||
| 42 | .ignore_unknown_fields = true, | ||
| 43 | .allocate = .alloc_always, | ||
| 44 | }, | ||
| 45 | ); | ||
| 46 | } | ||
| 47 | |||
| 48 | fn wrappedMain(bot: *Bot) !void { | ||
| 49 | try bot.sendMessage_(.{ | ||
| 50 | .chat_id = bot.config.dev_group, | ||
| 51 | .text = "Initializing...", | ||
| 52 | }); | ||
| 53 | |||
| 54 | try bot.setMyName(.{ .name = "Ukko's bot" }); | ||
| 55 | |||
| 56 | var gup = types.GetUpdatesParams{ .timeout = 60 }; | ||
| 57 | while (bot.poweron) { | ||
| 58 | // TODO: Catch major errors, report them (and crash after 5 of them or so) | ||
| 59 | const updates = try bot.getUpdates(gup); | ||
| 60 | defer updates.deinit(); | ||
| 61 | for (updates.value) |update| { | ||
| 62 | defer gup.offset = update.update_id + 1; | ||
| 63 | |||
| 64 | if (update.message) |message| { | ||
| 65 | // TODO: Catch minor errors, report them | ||
| 66 | try onMessage(bot, message); | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | // one last getUpdates to make sure offset is saved | ||
| 72 | gup.timeout = 0; | ||
| 73 | gup.limit = 1; | ||
| 74 | (try bot.getUpdates(gup)).deinit(); | ||
| 75 | |||
| 76 | try bot.sendMessage_(.{ | ||
| 77 | .chat_id = bot.config.dev_group, | ||
| 78 | .text = "Shutting down...", | ||
| 79 | }); | ||
| 80 | } | ||
| 81 | |||
| 82 | fn onMessage(bot: *Bot, msg: types.Message) !void { | ||
| 83 | if (msg.text) |text| { | ||
| 84 | try onTextMessage(bot, msg, text); | ||
| 85 | } | ||
| 86 | |||
| 87 | if (msg.new_chat_members) |new_chat_members| { | ||
| 88 | for (new_chat_members) |new_chat_member| { | ||
| 89 | try onNewMember(bot, msg, new_chat_member); | ||
| 90 | } | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | fn onNewMember(bot: *Bot, msg: types.Message, new_member: types.User) !void { | ||
| 95 | var sb = ArrayList(u8).init(bot.allocator); | ||
| 96 | defer sb.deinit(); | ||
| 97 | |||
| 98 | const w = sb.writer(); | ||
| 99 | |||
| 100 | try w.writeAll("Hello there, "); | ||
| 101 | try new_member.writeFormattedName(w); | ||
| 102 | try w.writeAll("! Be on your bestest behaviour now!!"); | ||
| 103 | |||
| 104 | try bot.sendMessage_(.{ | ||
| 105 | .chat_id = msg.chat.id, | ||
| 106 | .text = sb.items, | ||
| 107 | .parse_mode = .html, | ||
| 108 | .reply_parameters = .{ | ||
| 109 | .allow_sending_without_reply = true, | ||
| 110 | .message_id = msg.message_id, | ||
| 111 | .chat_id = msg.chat.id, | ||
| 112 | }, | ||
| 113 | }); | ||
| 114 | } | ||
| 115 | |||
| 116 | fn isBadText(text: []const u8) bool { | ||
| 117 | _ = text; | ||
| 118 | return false; | ||
| 119 | } | ||
| 120 | |||
| 121 | fn onTextMessage(bot: *Bot, msg: types.Message, text: []const u8) !void { | ||
| 122 | if (isBadText(text)) { | ||
| 123 | // TODO: Delete message, mute & warn user | ||
| 124 | // 0 current warns: 5 minute mute, +1 warn | ||
| 125 | // 1 current warn : 10 minute mute, +1 warn | ||
| 126 | // 2 current warns: 30 minute mute, +1 warn | ||
| 127 | // 3 current warns: 1 hour mute, +1 warn | ||
| 128 | // 4 current warns: 1 day mute, +1 warn | ||
| 129 | // 5 current warns: Ban | ||
| 130 | // | ||
| 131 | // warn gets removed after a month of no warns | ||
| 132 | // | ||
| 133 | // Lines to say in response: | ||
| 134 | // Your head will be my new trophy! | ||
| 135 | // Your cursed bloodline ends here! | ||
| 136 | // This is the end of you, s'wit! | ||
| 137 | // Your life's end is approaching. | ||
| 138 | // Surrender your life to me and I will end your pain! | ||
| 139 | // Your pain is nearing an end. | ||
| 140 | // May our Lords be merciful! | ||
| 141 | // You have sealed your fate! | ||
| 142 | // You cannot escape the righteous! | ||
| 143 | // You will pay with your blood! | ||
| 144 | // You will die. | ||
| 145 | // There is no escape. | ||
| 146 | // Die, fetcher. | ||
| 147 | // I shall enjoy watching you take your last breath. | ||
| 148 | // You'll soon be nothing more than a bad memory! | ||
| 149 | // You will die in disgrace. | ||
| 150 | // I'll see you dead. | ||
| 151 | // One of us will die here and it won't be me. | ||
| 152 | // You don't deserve to live. | ||
| 153 | // Surrender now and I might let you live! | ||
| 154 | // I will bathe in your blood. | ||
| 155 | // Your bones will be my dinner. | ||
| 156 | // So small and tasty. I will enjoy eating you. | ||
| 157 | return; | ||
| 158 | } | ||
| 159 | |||
| 160 | if (msg.entities) |entities| { | ||
| 161 | for (entities) |entity| { | ||
| 162 | if (entity.type == .bot_command and entity.offset == 0) { | ||
| 163 | const cmd = try entity.extract(text); | ||
| 164 | try onTextCommand(bot, msg, text, cmd); | ||
| 165 | } | ||
| 166 | } | ||
| 167 | } | ||
| 168 | |||
| 169 | if (std.mem.eql(u8, text, ":3")) { | ||
| 170 | try bot.sendMessage_(.{ | ||
| 171 | .chat_id = msg.chat.id, | ||
| 172 | .text = ">:3", | ||
| 173 | .reply_parameters = .{ | ||
| 174 | .message_id = msg.message_id, | ||
| 175 | .chat_id = msg.chat.id, | ||
| 176 | }, | ||
| 177 | }); | ||
| 178 | } else if (std.mem.eql(u8, text, ">:3")) { | ||
| 179 | try bot.sendMessage_(.{ | ||
| 180 | .chat_id = msg.chat.id, | ||
| 181 | .text = "<b>>:3</b>", | ||
| 182 | .parse_mode = .html, | ||
| 183 | .reply_parameters = .{ | ||
| 184 | .message_id = msg.message_id, | ||
| 185 | .chat_id = msg.chat.id, | ||
| 186 | }, | ||
| 187 | }); | ||
| 188 | } else if (std.ascii.startsWithIgnoreCase(text, "big ")) { | ||
| 189 | var output = try bot.allocator.alloc(u8, text.len + 3); | ||
| 190 | defer bot.allocator.free(output); | ||
| 191 | |||
| 192 | std.mem.copyForwards(u8, output, "<b>"); | ||
| 193 | _ = std.ascii.upperString(output[3..], text[4..]); | ||
| 194 | std.mem.copyForwards(u8, output[output.len - 4 ..], "</b>"); | ||
| 195 | |||
| 196 | try bot.sendMessage_(.{ | ||
| 197 | .chat_id = msg.chat.id, | ||
| 198 | .text = output, | ||
| 199 | .parse_mode = .html, | ||
| 200 | .reply_parameters = .{ | ||
| 201 | .message_id = msg.message_id, | ||
| 202 | .chat_id = msg.chat.id, | ||
| 203 | }, | ||
| 204 | }); | ||
| 205 | } else if (std.mem.eql(u8, text, "H")) { | ||
| 206 | try bot.sendMessage_(.{ | ||
| 207 | .chat_id = msg.chat.id, | ||
| 208 | .text = "<code>Randomly selected reminder that h > H.</code>", | ||
| 209 | .parse_mode = .html, | ||
| 210 | .reply_parameters = .{ | ||
| 211 | .message_id = msg.message_id, | ||
| 212 | .chat_id = msg.chat.id, | ||
| 213 | }, | ||
| 214 | }); | ||
| 215 | } else if (std.ascii.startsWithIgnoreCase(text, "say ")) { | ||
| 216 | try bot.sendMessage_(.{ | ||
| 217 | .chat_id = msg.chat.id, | ||
| 218 | .text = text[4..], | ||
| 219 | .reply_parameters = .{ | ||
| 220 | .message_id = msg.message_id, | ||
| 221 | .chat_id = msg.chat.id, | ||
| 222 | }, | ||
| 223 | }); | ||
| 224 | } else if (std.ascii.eqlIgnoreCase(text, "uwu")) { | ||
| 225 | try bot.sendMessage_(.{ | ||
| 226 | .chat_id = msg.chat.id, | ||
| 227 | .text = "OwO", | ||
| 228 | .reply_parameters = .{ | ||
| 229 | .message_id = msg.message_id, | ||
| 230 | .chat_id = msg.chat.id, | ||
| 231 | }, | ||
| 232 | }); | ||
| 233 | } else if (std.ascii.eqlIgnoreCase(text, "waow")) { | ||
| 234 | const reply_to = if (msg.reply_to_message) |r| | ||
| 235 | r.message_id | ||
| 236 | else | ||
| 237 | msg.message_id; | ||
| 238 | |||
| 239 | try bot.sendMessage_(.{ | ||
| 240 | .chat_id = msg.chat.id, | ||
| 241 | .text = "BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED", | ||
| 242 | .reply_parameters = .{ | ||
| 243 | .message_id = reply_to, | ||
| 244 | .chat_id = msg.chat.id, | ||
| 245 | }, | ||
| 246 | }); | ||
| 247 | } else if (std.ascii.eqlIgnoreCase(text, "what")) { | ||
| 248 | var sb = try ArrayList(u8).initCapacity(bot.allocator, 9); | ||
| 249 | defer sb.deinit(); | ||
| 250 | |||
| 251 | if (text[0] == 'w') { | ||
| 252 | sb.appendSliceAssumeCapacity("g"); | ||
| 253 | } else { | ||
| 254 | sb.appendSliceAssumeCapacity("G"); | ||
| 255 | } | ||
| 256 | if (text[1] == 'h') { | ||
| 257 | sb.appendSliceAssumeCapacity("ood "); | ||
| 258 | } else { | ||
| 259 | sb.appendSliceAssumeCapacity("OOD "); | ||
| 260 | } | ||
| 261 | if (text[2] == 'a') { | ||
| 262 | sb.appendSliceAssumeCapacity("gir"); | ||
| 263 | } else { | ||
| 264 | sb.appendSliceAssumeCapacity("GIR"); | ||
| 265 | } | ||
| 266 | if (text[3] == 't') { | ||
| 267 | sb.appendSliceAssumeCapacity("l"); | ||
| 268 | } else { | ||
| 269 | sb.appendSliceAssumeCapacity("L"); | ||
| 270 | } | ||
| 271 | |||
| 272 | try bot.sendMessage_(.{ | ||
| 273 | .chat_id = msg.chat.id, | ||
| 274 | .text = sb.items, | ||
| 275 | .reply_parameters = .{ | ||
| 276 | .message_id = msg.message_id, | ||
| 277 | .chat_id = msg.chat.id, | ||
| 278 | }, | ||
| 279 | }); | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | fn onTextCommand(bot: *Bot, msg: types.Message, text: []const u8, cmd: []const u8) !void { | ||
| 284 | _ = text; | ||
| 285 | |||
| 286 | const simple_cmd = if (std.mem.indexOfScalar(u8, cmd, '@')) |idx| blk: { | ||
| 287 | const cmd_username = cmd[idx + 1 ..]; | ||
| 288 | if (!std.mem.eql(u8, cmd_username, try bot.getUsername())) { | ||
| 289 | return; | ||
| 290 | } | ||
| 291 | break :blk cmd[1..idx]; | ||
| 292 | } else cmd[1..]; | ||
| 293 | |||
| 294 | // TODO: StaticStringMap :) | ||
| 295 | if (std.mem.eql(u8, simple_cmd, "chatid")) { | ||
| 296 | var sb = ArrayList(u8).init(bot.allocator); | ||
| 297 | defer sb.deinit(); | ||
| 298 | try sb.writer().print("<code>{}</code>", .{msg.chat.id}); | ||
| 299 | |||
| 300 | try bot.sendMessage_(.{ | ||
| 301 | .chat_id = msg.chat.id, | ||
| 302 | .text = sb.items, | ||
| 303 | .parse_mode = .html, | ||
| 304 | .reply_parameters = .{ | ||
| 305 | .message_id = msg.message_id, | ||
| 306 | .chat_id = msg.chat.id, | ||
| 307 | }, | ||
| 308 | }); | ||
| 309 | } else if (std.mem.eql(u8, simple_cmd, "ping")) { | ||
| 310 | var timer = try std.time.Timer.start(); | ||
| 311 | |||
| 312 | const recv = msg.date - @as(u64, @intCast(std.time.timestamp())); | ||
| 313 | |||
| 314 | var sb = ArrayList(u8).init(bot.allocator); | ||
| 315 | defer sb.deinit(); | ||
| 316 | try sb.writer().print("Pong!\nReceive time: {}s\nSend time: ...", .{recv}); | ||
| 317 | |||
| 318 | const reply = try bot.sendMessage(.{ | ||
| 319 | .chat_id = msg.chat.id, | ||
| 320 | .text = sb.items, | ||
| 321 | .reply_parameters = .{ | ||
| 322 | .message_id = msg.message_id, | ||
| 323 | .chat_id = msg.chat.id, | ||
| 324 | }, | ||
| 325 | }); | ||
| 326 | defer reply.deinit(); | ||
| 327 | |||
| 328 | const send = @as(f64, @floatFromInt(timer.read())) / std.time.ns_per_ms; | ||
| 329 | sb.clearRetainingCapacity(); | ||
| 330 | try sb.writer().print("Pong!\nReceive time: {}s\nSend time: {d}ms", .{ recv, send }); | ||
| 331 | |||
| 332 | try bot.editMessageText_(.{ | ||
| 333 | .chat_id = reply.value.chat.id, | ||
| 334 | .message_id = reply.value.message_id, | ||
| 335 | .text = sb.items, | ||
| 336 | }); | ||
| 337 | } else if (std.mem.eql(u8, simple_cmd, "shutdown")) blk: { | ||
| 338 | if (msg.from == null or msg.from.?.id != bot.config.owner) { | ||
| 339 | break :blk; | ||
| 340 | } | ||
| 341 | |||
| 342 | bot.poweron = false; | ||
| 343 | try bot.sendMessage_(.{ | ||
| 344 | .chat_id = msg.chat.id, | ||
| 345 | .text = "Initialising shutdown...", | ||
| 346 | .reply_parameters = .{ | ||
| 347 | .allow_sending_without_reply = true, | ||
| 348 | .message_id = msg.message_id, | ||
| 349 | .chat_id = msg.chat.id, | ||
| 350 | }, | ||
| 351 | }); | ||
| 352 | } | ||
| 353 | } | ||