From bd1891521e2089c368251c80f839daa46e8c886b Mon Sep 17 00:00:00 2001 From: Jacob Jonsson Date: Sun, 15 Mar 2026 14:35:51 +0100 Subject: [PATCH] refactor: move message dispatch logic to bot This commit moves the logic that governs what action to take when a message is heard by the bot from the BotAdapter (which should be a layer only responsible for translating IRC messages to our internal representation) to the Bot. This makes it possible to test full conversations in the bot tests. --- src/bot.zig | 68 +++++++++++++++++++++++++++++++++------------------- src/main.zig | 29 ++++++++++------------ 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/bot.zig b/src/bot.zig index 26d1ea6..137813e 100644 --- a/src/bot.zig +++ b/src/bot.zig @@ -94,6 +94,19 @@ pub const Bot = struct { self.backlog.deinit(); } + pub fn hear(self: *Bot, message: *const Message) ?Error!Response { + // Store the message to keep track of the allocation + defer self.store(message); + + if (UserCommand.parse(message.author, message.content)) |cmd| { + return self.execute(&cmd, message.targets); + } + if (AdminCommand.parse(message.content)) |cmd| { + return self.execute_admin(&cmd, "#eru-admin"); + } + return null; + } + pub fn execute(self: *Bot, cmd: *const UserCommand, targets: []const u8) Error!Response { switch (cmd.*) { .substitute => |command| { @@ -216,7 +229,7 @@ test "hear and deinit has no leaks" { defer bot.deinit(); const testMessage = try newTestMessage(allocator, "test"); - bot.store(testMessage); + try std.testing.expectEqual(null, bot.hear(testMessage)); try std.testing.expectEqual(0, bot.backlog.top); } @@ -228,7 +241,7 @@ test "a few hears and deinit has no leaks" { for (0..2) |_| { const testMessage = try newTestMessage(std.testing.allocator, "test"); - bot.store(testMessage); + _ = bot.hear(testMessage); } try std.testing.expectEqual(1, bot.backlog.top); @@ -240,7 +253,7 @@ test "hear wraps" { for (0..1025) |_| { const testMessage = try newTestMessage(std.testing.allocator, "test"); - bot.store(testMessage); + _ = bot.hear(testMessage); } try std.testing.expectEqual(0, bot.backlog.top); @@ -251,12 +264,9 @@ test "hear wraps" { test "execute substitution no previous message" { var bot = try Bot.init(std.testing.allocator); defer bot.deinit(); - const cmd = UserCommand{ .substitute = .{ - .author = "jassob", - .needle = "What", - .replacement = "what", - } }; - try std.testing.expectError(Error.NoMessage, bot.execute(&cmd, "#test")); + + const substitution = try newTestMessage(std.testing.allocator, "s/What/what/"); + try std.testing.expectError(Error.NoMessage, bot.hear(substitution).?); } test "execute substitution" { @@ -266,11 +276,11 @@ test "execute substitution" { // hear original message with typo const msg = try newTestMessage(allocator, "What"); - bot.store(msg); + try std.testing.expectEqual(null, bot.hear(msg)); // execute substitution - const cmd = UserCommand.init_substitute("jassob", "What", "what", false); - const response = try bot.execute(&cmd, "#test"); + const sub = try newTestMessage(allocator, "s/What/what/"); + const response = try bot.hear(sub).?; // expect response matching the correct message switch (response) { @@ -282,29 +292,31 @@ test "execute substitution" { } test "execute substitution with no matching needle" { - var bot = try Bot.init(std.testing.allocator); + const allocator = std.testing.allocator; + var bot = try Bot.init(allocator); defer bot.deinit(); // hear original message - const msg = try newTestMessage(std.testing.allocator, "original"); - bot.store(msg); + const msg = try newTestMessage(allocator, "original"); + try std.testing.expectEqual(null, bot.hear(msg)); // execute substitution - const cmd = UserCommand.init_substitute("jassob", "something else", "weird", false); - try std.testing.expectError(Error.NoMessage, bot.execute(&cmd, "#test")); + const sub = try newTestMessage(allocator, "s/something else/weird/"); + try std.testing.expectError(Error.NoMessage, bot.hear(sub).?); } test "recursive substitutions does not cause issues" { - var bot = try Bot.init(std.testing.allocator); + const allocator = std.testing.allocator; + var bot = try Bot.init(allocator); defer bot.deinit(); // hear original message - const msg = try newTestMessage(std.testing.allocator, "original"); - bot.store(msg); + const msg = try newTestMessage(allocator, "original"); + try std.testing.expectEqual(null, bot.hear(msg)); // execute substitution - const cmd = UserCommand.init_substitute("jassob", "original", "something else", false); - switch (try bot.execute(&cmd, "#test")) { + const sub = try newTestMessage(allocator, "s/original/something else/"); + switch (try bot.hear(sub).?) { .privmsg => |message| { try std.testing.expectEqualDeep("jassob: \"something else\"", message.text); }, @@ -312,6 +324,14 @@ test "recursive substitutions does not cause issues" { } // execute second substitution - const cmd2 = UserCommand.init_substitute("jassob", "s/original/something else/", "something else", false); - try std.testing.expectError(Error.NoMessage, bot.execute(&cmd2, "#test")); + const sub2 = try newTestMessage( + allocator, + "s|s/original/something else/|something else|", + ); + switch (try bot.hear(sub2).?) { + .privmsg => |message| { + try std.testing.expectEqualDeep("jassob: \"something else\"", message.text); + }, + else => unreachable, + } } diff --git a/src/main.zig b/src/main.zig index 43f2880..54e48ea 100644 --- a/src/main.zig +++ b/src/main.zig @@ -67,26 +67,24 @@ pub const BotAdapter = struct { pub fn callback(self: *BotAdapter, message: zircon.Message) ?zircon.Message { switch (message) { .PRIVMSG => |msg| { - std.log.debug( - "received message: nick {?s}, user: {?s}, host: {?s}, targets: {s}, text: {s}", - .{ msg.prefix.?.nick, msg.prefix.?.user, msg.prefix.?.host, msg.targets, msg.text }, - ); - const nick = if (msg.prefix) |prefix| if (prefix.nick) |nick| nick else "unknown" else "unknown"; - if (UserCommand.parse(nick, msg.text)) |cmd| { - return toIRC(self.bot.execute(&cmd, msg.targets) catch |err| return report_error(err)); - } - if (AdminCommand.parse(msg.text)) |cmd| { - return toIRC(self.bot.execute_admin(&cmd, "#eru-admin") catch |err| return report_error(err)); - } - const bot_msg = BotMessage.init_owned( + const nick = if (msg.prefix != null and msg.prefix.?.nick != null) msg.prefix.?.nick.? else "unknown"; + + // create message + const bot_message = BotMessage.init_owned( self.allocator, std.time.timestamp(), nick, msg.targets, msg.text, ) catch |err| return report_error(err); - self.bot.store(bot_msg); - return null; + + // send message to bot + const response = self.bot.hear(bot_message) orelse { + return null; + } catch |err| { + return report_error(err); + }; + return toIRC(response); }, .JOIN => |msg| { std.log.debug("received join message: channels {s}", .{msg.channels}); @@ -162,9 +160,8 @@ test "substitute" { test "get empty backlog message" { var bot_adapter = try BotAdapter.init(std.testing.allocator); defer bot_adapter.deinit(); - const prefix = zircon.Prefix{ .nick = "jassob", .user = "jassob", .host = "localhost" }; const msg = zircon.Message{ - .PRIVMSG = .{ .prefix = prefix, .targets = "#eru-admin", .text = "!backlog 0" }, + .PRIVMSG = .{ .prefix = null, .targets = "#eru-admin", .text = "!backlog 0" }, }; try std.testing.expectEqualDeep("no matching message", bot_adapter.callback(msg).?.PRIVMSG.text); }