From 4d1f22194c2e23cca925c7a24c64142b9fe18ef2 Mon Sep 17 00:00:00 2001 From: Jacob Jonsson Date: Sun, 15 Mar 2026 13:47:57 +0100 Subject: [PATCH] refactor(bot): introduce response type The response type holds represents the way a bot can respond to a message it executes and allows us to move the IRC dependency out of bot and into only the main module. --- build.zig | 7 +---- src/bot.zig | 80 ++++++++++++++++++++++------------------------------ src/main.zig | 17 +++++++++-- 3 files changed, 50 insertions(+), 54 deletions(-) diff --git a/build.zig b/build.zig index 789e0fc..cbe6e17 100644 --- a/build.zig +++ b/build.zig @@ -152,12 +152,7 @@ pub fn build(b: *std.Build) !void { })), // Our bot tests needs zircon module import, - try testRunWithImports(b, target, "bot", &.{ - .{ - .name = "zircon", - .module = zircon.module("zircon"), - }, - }), + try testRun(b, target, "bot"), // Our module tests for each module we want to add. If // breaking out new functionality to a module, remember to // bump the length of the array above. diff --git a/src/bot.zig b/src/bot.zig index f8c9d9e..ee3ab49 100644 --- a/src/bot.zig +++ b/src/bot.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const zircon = @import("zircon"); const zigeru = @import("root.zig"); const Buffer = zigeru.buffer.Buffer; @@ -10,6 +9,10 @@ const HELP_MESSAGE: []const u8 = "Send `s/TYPO/CORRECTION/` to replace TYPO with pub const Error = error{ OutOfMemory, NoMessage, WriteFailed }; +/// Message represents a message received by the bot. +/// +/// It is what we store in the backlog and what the substitutions are +/// run on. pub const Message = struct { timestamp: i64, targets: []const u8, @@ -39,6 +42,19 @@ pub const Message = struct { } }; +/// Responses from hearing a Message. +/// +/// Response represents the kind of responses the bot can make when +/// hearing a message. +/// +/// Responses can be both administrative (i.e. a join-request for a +/// channel), or actual responses to a message (e.g. the result of a +/// substitution). +pub const Response = union(enum) { + join: struct { channels: []const u8 }, + privmsg: struct { targets: []const u8, text: []const u8 }, +}; + pub const Bot = struct { backlog: Buffer(*const Message, 1024), outbox: Buffer([]u8, 1024), @@ -78,12 +94,7 @@ pub const Bot = struct { self.backlog.deinit(); } - pub fn execute( - self: *Bot, - cmd: *const UserCommand, - prefix: ?zircon.Prefix, - targets: []const u8, - ) Error!zircon.Message { + pub fn execute(self: *Bot, cmd: *const UserCommand, targets: []const u8) Error!Response { switch (cmd.*) { .substitute => |command| { const prev_msg = self.previous_message_by_author( @@ -107,28 +118,21 @@ pub const Bot = struct { .{ command.author, output }, ); self.outbox.append(quoted_output); - return zircon.Message{ .PRIVMSG = .{ + return .{ .privmsg = .{ .targets = targets, - .prefix = prefix, .text = quoted_output, } }; }, .help => { - return zircon.Message{ .PRIVMSG = .{ + return .{ .privmsg = .{ .targets = targets, - .prefix = prefix, .text = HELP_MESSAGE, } }; }, } } - pub fn execute_admin( - self: *Bot, - cmd: *const AdminCommand, - prefix: ?zircon.Prefix, - targets: []const u8, - ) Error!zircon.Message { + pub fn execute_admin(self: *Bot, cmd: *const AdminCommand, targets: []const u8) Error!Response { switch (cmd.*) { .status => { const msg = try std.fmt.allocPrint( @@ -136,11 +140,11 @@ pub const Bot = struct { "heard messages: {}, sent messages: {}", .{ self.backlog.len(), self.outbox.len() }, ); - return .{ .PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = msg } }; + return .{ .privmsg = .{ .targets = targets, .text = msg } }; }, .join => |msg| { std.log.debug("received join request: channel \"{s}\"", .{msg.channel}); - return .{ .JOIN = .{ .prefix = prefix, .channels = msg.channel } }; + return .{ .join = .{ .channels = msg.channel } }; }, .backlog => |backlog| { if (self.backlog.len() == 0) { @@ -157,18 +161,18 @@ pub const Bot = struct { ); self.outbox.append(quoted_output); return .{ - .PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = quoted_output }, + .privmsg = .{ .targets = targets, .text = quoted_output }, }; } else return Error.NoMessage; }, .err => |err| { - return .{ .PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = err.message } }; + return .{ .privmsg = .{ .targets = targets, .text = err.message } }; }, } } - // hear a message and store it in backlog, potentially overwriting oldest message. - pub fn hear(self: *Bot, msg: *const Message) void { + // store a message in backlog, potentially overwriting oldest message. + pub fn store(self: *Bot, msg: *const Message) void { self.backlog.append(msg); } @@ -252,11 +256,7 @@ test "execute substitution no previous message" { .needle = "What", .replacement = "what", } }; - try std.testing.expectError(Error.NoMessage, bot.execute( - &cmd, - null, - "#test", - )); + try std.testing.expectError(Error.NoMessage, bot.execute(&cmd, "#test")); } test "execute substitution" { @@ -270,11 +270,11 @@ test "execute substitution" { // execute substitution const cmd = UserCommand.init_substitute("jassob", "What", "what", false); - const response = try bot.execute(&cmd, null, "#test"); + const response = try bot.execute(&cmd, "#test"); // expect response matching the correct message switch (response) { - .PRIVMSG => |message| { + .privmsg => |message| { try std.testing.expectEqualDeep(message.text, "jassob: \"what\""); }, else => unreachable, @@ -291,11 +291,7 @@ test "execute substitution with no matching needle" { // execute substitution const cmd = UserCommand.init_substitute("jassob", "something else", "weird", false); - try std.testing.expectError(Error.NoMessage, bot.execute( - &cmd, - null, - "#test", - )); + try std.testing.expectError(Error.NoMessage, bot.execute(&cmd, "#test")); } test "recursive substitutions does not cause issues" { @@ -308,12 +304,8 @@ test "recursive substitutions does not cause issues" { // execute substitution const cmd = UserCommand.init_substitute("jassob", "original", "something else", false); - switch (try bot.execute( - &cmd, - null, - "#test", - )) { - .PRIVMSG => |message| { + switch (try bot.execute(&cmd, "#test")) { + .privmsg => |message| { try std.testing.expectEqualDeep("jassob: \"something else\"", message.text); }, else => unreachable, @@ -321,9 +313,5 @@ 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, - null, - "#test", - )); + try std.testing.expectError(Error.NoMessage, bot.execute(&cmd2, "#test")); } diff --git a/src/main.zig b/src/main.zig index f484f2e..2ef1980 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ const Error = zigeru.bot.Error; const UserCommand = zigeru.commands.UserCommand; const AdminCommand = zigeru.commands.AdminCommand; const BotMessage = zigeru.bot.Message; +const BotResponse = zigeru.bot.Response; const zircon = @import("zircon"); var debug_allocator = std.heap.DebugAllocator(.{}).init; @@ -72,10 +73,10 @@ pub const BotAdapter = struct { ); const nick = if (msg.prefix) |prefix| if (prefix.nick) |nick| nick else "unknown" else "unknown"; if (UserCommand.parse(nick, msg.text)) |cmd| { - return self.bot.execute(&cmd, msg.prefix, msg.targets) catch |err| return report_error(err); + return toIRC(self.bot.execute(&cmd, msg.targets) catch |err| return report_error(err)); } if (AdminCommand.parse(msg.text)) |cmd| { - return self.bot.execute_admin(&cmd, msg.prefix, "#eru-admin") catch |err| return report_error(err); + return toIRC(self.bot.execute_admin(&cmd, "#eru-admin") catch |err| return report_error(err)); } const bot_msg = BotMessage.init_owned( self.allocator, @@ -120,6 +121,18 @@ pub const BotAdapter = struct { } }; +/// toIRC converts a bot response and converts it to a IRC message. +fn toIRC(response: BotResponse) zircon.Message { + switch (response) { + .join => |join| return .{ + .JOIN = .{ .prefix = null, .channels = join.channels }, + }, + .privmsg => |msg| return .{ + .PRIVMSG = .{ .prefix = null, .targets = msg.targets, .text = msg.text }, + }, + } +} + test "substitute" { var bot_adapter = try BotAdapter.init(std.testing.allocator); defer bot_adapter.deinit();