From 936bf470c75a2395fb8a28f689ebe5ea7349e513 Mon Sep 17 00:00:00 2001 From: Jacob Jonsson Date: Sat, 29 Nov 2025 01:05:09 +0100 Subject: [PATCH] wip 2 --- build.zig | 1 + src/bot.zig | 62 +++++++++++++--- src/main.zig | 205 +++++++++++++++------------------------------------ 3 files changed, 113 insertions(+), 155 deletions(-) diff --git a/build.zig b/build.zig index 9ec6c89..72f8337 100644 --- a/build.zig +++ b/build.zig @@ -89,6 +89,7 @@ pub fn build(b: *std.Build) void { }); exe.root_module.addImport("zircon", zircon.module("zircon")); + mod.addImport("zircon", zircon.module("zircon")); exe.linkLibC(); // This declares intent for the executable to be installed into the diff --git a/src/bot.zig b/src/bot.zig index 2a13785..a76e80c 100644 --- a/src/bot.zig +++ b/src/bot.zig @@ -1,12 +1,47 @@ const std = @import("std"); +const zircon = @import("zircon"); pub const Command = union(enum) { - substitute: struct { author: []const u8, needle: []const u8, replacement: []const u8 }, + substitute: struct { author: []const u8, needle: []const u8, replacement: []const u8, all: bool }, + help: void, + + pub fn parse(nick: []const u8, text: []const u8) ?Command { + if (text.len < 2) return null; + + if (text[0] == 's') { + if (text.len == 1) return null; + const delim = text[1]; + var parts = std.mem.splitScalar(u8, text, delim); + _ = parts.next() orelse return null; // skip 's' + const needle = parts.next() orelse return null; + const replacement = parts.next().?; + const flags = parts.next(); + const all = if (flags == null) false else (std.mem.eql(u8, flags.?, "g")); + return Command{ + .substitute = .{ + .author = nick, + .needle = needle, + .replacement = replacement, + .all = all, + }, + }; + } + + if (std.mem.eql(u8, text, "help")) { + return Command.help; + } + + return null; + } }; -pub const Result = union(enum) { - post_message: struct { content: []const u8 }, -}; +const HELP_MESSAGE: []const u8 = + \\Welcome to zigeru! + \\ + \\Commands: + \\!help:\tSee this message + \\s/TYPO/CORRECTION/\tCorrect previous message by replacing TYPO with CORRECTION. +; pub const Error = error{ OutOfMemory, @@ -14,34 +49,43 @@ pub const Error = error{ }; pub const Message = struct { - timestamp: u32, + timestamp: i64, author: []const u8, content: []const u8, }; pub const Bot = struct { backlog: [1024]?Message, + sent_messages: std.ArrayList(u8), top: usize, bottom: usize, allocator: std.mem.Allocator, - pub fn init(allocator: std.mem.Allocator) Bot { + pub fn init(allocator: std.mem.Allocator) Error!Bot { return Bot{ .backlog = .{null} ** 1024, + .sent_messages = try std.ArrayList(u8).initCapacity(allocator, 10), .top = 0, .bottom = 0, .allocator = allocator, }; } - pub fn execute(self: *Bot, cmd: *const Command) Error!Result { + pub fn deinit(self: *Bot) void { + self.sent_messages.deinit(self.allocator); + } + + pub fn execute(self: *Bot, cmd: *const Command, prefix: ?zircon.Prefix, targets: []const u8) Error!zircon.Message { switch (cmd.*) { .substitute => |command| { const prev_msg = self.previous_message_by_author(command.author) orelse return Error.NoMessage; const size = std.mem.replacementSize(u8, prev_msg.content, command.needle, command.replacement); - const output = try self.allocator.alloc(u8, size); + const output = try self.sent_messages.addManyAsSlice(self.allocator, size); _ = std.mem.replace(u8, prev_msg.content, command.needle, command.replacement, output); - return Result{ .post_message = .{ .content = output } }; + return zircon.Message{ .PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = output } }; + }, + .help => { + return zircon.Message{ .PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = HELP_MESSAGE } }; }, } } diff --git a/src/main.zig b/src/main.zig index dd0fd69..2ab092c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,6 +3,7 @@ const zigeru = @import("zigeru"); const zircon = @import("zircon"); const Bot = zigeru.bot.Bot; +const BotCommand = zigeru.bot.Command; var debug_allocator = std.heap.DebugAllocator(.{}).init; @@ -24,16 +25,14 @@ pub fn main() !void { }); defer client.deinit(); - var bot_adapter = BotAdapter.init(allocator); - var adapter = bot_adapter.adapter(); + var bot_adapter = try BotAdapter.init(allocator); + const adapter = bot_adapter.adapter(); // Connect to the IRC server and perform registration. try client.connect(); try client.register(); try client.join("#eru-tests"); - const callback = adapter.callbackFn; - // Enter the main loop that keeps reading incoming IRC messages forever. // The client loop accepts a LoopConfig struct with two optional fields. // @@ -43,29 +42,45 @@ pub fn main() !void { // .msg_callback lets you answer any received IRC messages with another one. // // .spawn_thread lets you tweak if you spawn a thread to run .msg_callback. - client.loop(.{ .msg_callback = callback }) catch |err| { + client.loop(.{ .msg_callback = adapter.callbackFn }) catch |err| { std.debug.print("eru exited with error: {}", .{err}); }; } -const Adapter = struct { - ptr: *anyopaque, - callbackFn: *const fn (zircon.Message) ?zircon.Message, +pub const Adapter = struct { + ptr: ?*anyopaque, + callbackFn: *const fn (?*anyopaque, zircon.Message) ?zircon.Message, }; -const BotAdapter = struct { +pub const BotAdapter = struct { bot: Bot, allocator: std.mem.Allocator, - pub fn init(allocator: std.mem.Allocator) BotAdapter { + pub fn init(allocator: std.mem.Allocator) !BotAdapter { return BotAdapter{ - .bot = Bot.init(allocator), + .bot = try Bot.init(allocator), .allocator = allocator, }; } + pub fn deinit(self: *BotAdapter) void { + self.bot.deinit(); + } + pub fn callback(self: *BotAdapter, message: zircon.Message) ?zircon.Message { - std.debug.print("{} {}", .{ self, message }); + switch (message) { + .PRIVMSG => |msg| { + if (Command.parse(msg.prefix, msg.targets, msg.text)) |command| { + return command.handle(&self.bot); + } + self.bot.hear(zigeru.bot.Message{ + .author = msg.prefix.?.nick orelse "unknown", + .timestamp = std.time.timestamp(), + .content = msg.text, + }); + }, + else => {}, + } return null; } @@ -77,151 +92,49 @@ const BotAdapter = struct { } }; -/// msgCallback is called by zircon.Client.loop when a new IRC message arrives. -/// The message parameter holds the IRC message that arrived from the server. -/// You can switch on the message tagged union to reply based on its kind. -/// On this example we only care about messages of type JOIN, PRIVMSG or PART. -/// To reply to each message we finally return another message to the loop. -fn msgCallback(message: zircon.Message) ?zircon.Message { - std.debug.print("received message: {}\n", .{message}); - switch (message) { - .JOIN => |msg| { - return zircon.Message{ - .PRIVMSG = .{ - .targets = msg.channels, - .text = "Welcome to the channel!", - }, - }; - }, - .PRIVMSG => |msg| { - if (Command.parse(msg.prefix, msg.targets, msg.text)) |command| { - return command.handle(); - } - - return null; - }, - .PART => |msg| { - if (msg.reason) |msg_reason| { - if (std.mem.containsAtLeast(u8, msg_reason, 1, "goodbye")) { - return zircon.Message{ - .PRIVMSG = .{ - .targets = msg.channels, - .text = "Goodbye for you too!", - }, - }; - } - } - }, - .NICK => |msg| { - return zircon.Message{ .PRIVMSG = .{ - .targets = "#geeks", - .text = msg.nickname, - } }; - }, - else => return null, - } - return null; -} - -/// spawnThread is called by zircon.Client.loop to decide when to spawn a thread. -/// The message parameter holds the IRC message that arrived from the server. -/// You can switch on the message tagged union to decide based on its kind. -/// On this example we only care about messages of type PRIVMSG or PART. -/// To spawn a thread we return true to the loop or false otherwise. -/// We should spawn a thread for long running tasks like for instance a bot command. -/// Otherwise we might block the main thread where zircon.Client.loop is running. -fn spawnThread(message: zircon.Message) bool { - switch (message) { - .PRIVMSG => return true, - .PART => return true, - else => return false, - } -} - -/// Command encapsulates each command that our IRC bot supports. pub const Command = struct { - name: CommandName, + command: BotCommand, prefix: ?zircon.Prefix, - params: []const u8, targets: []const u8, - pub const CommandName = enum { - echo, - substitute, - help, - quit, - }; - - pub const Operators = enum { - substitute, - }; - - const map = std.StaticStringMap(Command.CommandName).initComptime(.{ - .{ "echo", CommandName.echo }, - .{ "help", CommandName.help }, - .{ "quit", CommandName.quit }, - }); - - const operators = std.StaticStringMap(Command.CommandName).initComptime(.{ - .{ "s", CommandName.substitute }, - }); - pub fn parse(prefix: ?zircon.Prefix, targets: []const u8, text: []const u8) ?Command { - var iter = std.mem.tokenizeAny(u8, text, &std.ascii.whitespace); - const name = iter.next() orelse return null; - const command_name = map.get(name); - if (command_name == null) { - var parts = std.mem.splitScalar(u8, name, '/'); - const operator = parts.next() orelse return null; - return .{ - .name = operators.get(operator) orelse return null, - .prefix = prefix, - .params = parts.rest(), - .targets = targets, - }; - } - - if (name.len < 2) return null; + const nick = prefix.?.nick.?; + const command = BotCommand.parse(nick, text) orelse return null; return .{ - .name = map.get(name) orelse return null, + .command = command, .prefix = prefix, - .params = iter.rest(), .targets = targets, }; } - pub fn handle(self: Command) ?zircon.Message { - switch (self.name) { - .echo => return echo(self.targets, self.params), - .help => return help(self.prefix, self.targets), - .substitute => unreachable, - .quit => return quit(self.params), - } - } - - fn echo(targets: []const u8, params: []const u8) ?zircon.Message { - return zircon.Message{ - .PRIVMSG = .{ - .targets = targets, - .text = params, - }, - }; - } - - fn help(prefix: ?zircon.Prefix, targets: []const u8) ?zircon.Message { - return zircon.Message{ - .PRIVMSG = .{ - .targets = if (prefix) |p| p.nick orelse targets else targets, - .text = "This is the help message!", - }, - }; - } - - fn quit(params: []const u8) ?zircon.Message { - return zircon.Message{ - .QUIT = .{ - .reason = params, - }, + pub fn handle(self: Command, bot: *Bot) ?zircon.Message { + return bot.execute(&self.command, self.prefix, self.targets) catch |err| { + std.debug.print("Failed to handle {}: {}", .{ self, err }); + return null; }; } }; + +test "substitute" { + 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 = "#test", + .text = "hello world", + }, + }; + _ = bot_adapter.callback(msg); + const cmd_msg = zircon.Message{ + .PRIVMSG = .{ + .prefix = prefix, + .targets = "#test", + .text = "s/world/zig/", + }, + }; + const response = bot_adapter.callback(cmd_msg); + try std.testing.expect(response != null); + try std.testing.expectEqualStrings("hello zig", response.?.PRIVMSG.text); +}