From 59293d16900454562d2ebf615dc6caccefcb66ee Mon Sep 17 00:00:00 2001 From: Jacob Jonsson Date: Fri, 28 Nov 2025 23:04:53 +0100 Subject: [PATCH] wip --- build.zig | 8 ++ build.zig.zon | 41 +-------- flake.nix | 1 + src/bot.zig | 3 +- src/main.zig | 237 ++++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 232 insertions(+), 58 deletions(-) diff --git a/build.zig b/build.zig index dcf34bf..9ec6c89 100644 --- a/build.zig +++ b/build.zig @@ -83,6 +83,14 @@ pub fn build(b: *std.Build) void { }), }); + const zircon = b.dependency("zircon", .{ + .target = target, + .optimize = optimize, + }); + + exe.root_module.addImport("zircon", zircon.module("zircon")); + exe.linkLibC(); + // This declares intent for the executable to be installed into the // install prefix when running `zig build` (i.e. when executing the default // step). By default the install prefix is `zig-out/` but can be overridden diff --git a/build.zig.zon b/build.zig.zon index d42f7a4..c1d37fd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -32,44 +32,11 @@ // Once all dependencies are fetched, `zig build` no longer requires // internet connectivity. .dependencies = .{ - // See `zig fetch --save ` for a command-line interface for adding dependencies. - //.example = .{ - // // When updating this field to a new URL, be sure to delete the corresponding - // // `hash`, otherwise you are communicating that you expect to find the old hash at - // // the new URL. If the contents of a URL change this will result in a hash mismatch - // // which will prevent zig from using it. - // .url = "https://example.com/foo.tar.gz", - // - // // This is computed from the file contents of the directory of files that is - // // obtained after fetching `url` and applying the inclusion rules given by - // // `paths`. - // // - // // This field is the source of truth; packages do not come from a `url`; they - // // come from a `hash`. `url` is just one of many possible mirrors for how to - // // obtain a package matching this `hash`. - // // - // // Uses the [multihash](https://multiformats.io/multihash/) format. - // .hash = "...", - // - // // When this is provided, the package is found in a directory relative to the - // // build root. In this case the package's hash is irrelevant and therefore not - // // computed. This field and `url` are mutually exclusive. - // .path = "foo", - // - // // When this is set to `true`, a package is declared to be lazily - // // fetched. This makes the dependency only get fetched if it is - // // actually used. - // .lazy = false, - //}, + .zircon = .{ + .url = "git+https://github.com/Jassob/zircon.git#aefa32c0f77a3943e61eb90cf92bee8d0d636f7f", + .hash = "zircon-0.6.0-_NQ2hZrfAADXdGbbqGQ4ibVnsbXCFVWfLdXyEYGAaAvL", + }, }, - // Specifies the set of files and directories that are included in this package. - // Only files and directories listed here are included in the `hash` that - // is computed for this package. Only files listed here will remain on disk - // when using the zig package manager. As a rule of thumb, one should list - // files required for compilation plus any license(s). - // Paths are relative to the build root. Use the empty string (`""`) to refer to - // the build root itself. - // A directory listed here means that all files within, recursively, are included. .paths = .{ "build.zig", "build.zig.zon", diff --git a/flake.nix b/flake.nix index 17e2e91..d93b5c9 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,7 @@ pkgs.zig zls.packages.x86_64-linux.zls ]; + shellHook = "exec zsh"; }; }; } diff --git a/src/bot.zig b/src/bot.zig index cf86d8e..2a13785 100644 --- a/src/bot.zig +++ b/src/bot.zig @@ -25,7 +25,7 @@ pub const Bot = struct { bottom: usize, allocator: std.mem.Allocator, - pub fn new(allocator: std.mem.Allocator) Bot { + pub fn init(allocator: std.mem.Allocator) Bot { return Bot{ .backlog = .{null} ** 1024, .top = 0, @@ -43,7 +43,6 @@ pub const Bot = struct { _ = std.mem.replace(u8, prev_msg.content, command.needle, command.replacement, output); return Result{ .post_message = .{ .content = output } }; }, - else => unreachable, } } diff --git a/src/main.zig b/src/main.zig index 931ed37..dd0fd69 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,28 +1,227 @@ const std = @import("std"); const zigeru = @import("zigeru"); +const zircon = @import("zircon"); + +const Bot = zigeru.bot.Bot; + +var debug_allocator = std.heap.DebugAllocator(.{}).init; pub fn main() !void { - // Prints to stderr, ignoring potential errors. - const message = zigeru.bot.Message{ - .author = "Jassob", - .timestamp = 12345, - .content = "All your base are belong to us.\n", - }; + const allocator = debug_allocator.allocator(); + defer _ = debug_allocator.deinit(); - var gpa = std.heap.DebugAllocator(.{}){}; - var bot = zigeru.bot.Bot.new(gpa.allocator()); - bot.hear(message); - const command = zigeru.bot.Command{ - .substitute = .{ - .author = "Jassob", - .needle = "base", - .replacement = "codebase", - }, + var channels: [0][]const u8 = .{}; + + // Create a zircon.Client with a given configuration. + var client = try zircon.Client.init(allocator, .{ + .user = "eru", + .nick = "eru", + .real_name = "Eru (zigeru) bot", + .server = "irc.dtek.se", + .port = 6697, + .tls = true, + .channels = &channels, + }); + defer client.deinit(); + + var bot_adapter = BotAdapter.init(allocator); + var 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. + // + // These two fields, .msg_callback and .spawn_thread are callback pointers. + // You set them to custom functions you define to customize the main loop. + // + // .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| { + std.debug.print("eru exited with error: {}", .{err}); }; - const result = try bot.execute(&command); - switch (result) { - .post_message => |msg| { - std.debug.print("{s}", .{msg.content}); +} + +const Adapter = struct { + ptr: *anyopaque, + callbackFn: *const fn (zircon.Message) ?zircon.Message, +}; + +const BotAdapter = struct { + bot: Bot, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) BotAdapter { + return BotAdapter{ + .bot = Bot.init(allocator), + .allocator = allocator, + }; + } + + pub fn callback(self: *BotAdapter, message: zircon.Message) ?zircon.Message { + std.debug.print("{} {}", .{ self, message }); + return null; + } + + pub fn adapter(self: *BotAdapter) Adapter { + return .{ + .ptr = self, + .callbackFn = self.callback, + }; + } +}; + +/// 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, + 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; + return .{ + .name = map.get(name) orelse return null, + .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, + }, + }; + } +};