refactor: extract modules from bot.zig

This commit creates a bunch of new modules that contain code and tests
for various concepts/implementations that used to exist inside
bot.zig.

Notable amongst these are:
- buffer.zig, which contain the circular buffer containing both
  backlog and outbox messages.
- parser.zig, which contain the parser used to parse commands from IRC
  messages.
This commit is contained in:
Jacob Jonsson 2026-01-04 23:54:26 +01:00
parent 508e084ddf
commit e1e1938359
Signed by: Jassob
GPG key ID: 7E30B9B047F7202E
7 changed files with 694 additions and 285 deletions

106
src/commands.zig Normal file
View file

@ -0,0 +1,106 @@
const std = @import("std");
const parser = @import("root.zig").parser;
/// UserCommand represents the commands that ordinary IRC users can use.
pub const UserCommand = union(enum) {
/// `s/<old-word>/<new-word>/`
substitute: struct { author: []const u8, needle: []const u8, replacement: []const u8, all: bool = false },
/// !help
help: void,
pub fn parse(nick: []const u8, text: []const u8) ?UserCommand {
const original = parser.init(text);
if (original.consume_str("!help")) |_| {
return .help;
}
if (original.consume_char('s')) |substitute| {
const delim_parser, const delim = substitute.take_char();
if (std.ascii.isAlphanumeric(delim)) {
std.log.debug("parsing substitute command: delimiter cannot be a whitespace: \"{s}\"", .{text});
return null;
}
const typo_parser, const typo = delim_parser.take_until_char(delim);
const correction_parser, const correction = typo_parser.consume_char(delim).?.take_until_char(delim);
if (correction_parser.consume_char(delim) == null) {
std.log.debug("parsing substitute command: missing an ending '/' in \"{s}\"", .{text});
return null;
}
return .{
.substitute = .{
.author = nick,
.needle = typo,
.replacement = correction,
.all = false,
},
};
}
return null;
}
};
/// AdminCommand are commands useful for debugging zigeru, since they
/// are more spammy than others they are separated and only sent to
/// #eru-admin.
pub const AdminCommand = union(enum) {
backlog: struct { history: u16 },
status: void,
join: struct { channel: []const u8 },
err: struct { message: []const u8 },
pub fn parse(text: []const u8) ?AdminCommand {
const original = parser.init(text);
if (original.consume_char('!')) |command| {
if (command.consume_str("status")) |_| {
return .status;
}
if (command.consume_str("join").?.consume_char(' ')) |join| {
if (join.rest[0] != '#') {
return .{ .err = .{ .message = "channels must start with \"#\"" } };
}
return .{ .join = .{ .channel = join.rest } };
}
if (command.consume_str("backlog")) |backlog| {
const history = std.fmt.parseInt(u16, backlog.rest, 10) catch |err| {
std.debug.print("failed to parse int ('{s}') with error: {}\n", .{ backlog.rest, err });
return null;
};
return .{ .backlog = .{ .history = history } };
}
std.log.debug("unknown command: \"{s}\"", .{command.rest});
}
return null;
}
};
test "can parse !help successful" {
try std.testing.expectEqual(
UserCommand.help,
UserCommand.parse("jassob", "!help"),
);
}
test "can parse s/hello/world/ successful" {
try std.testing.expectEqualDeep(
UserCommand{ .substitute = .{
.author = "jassob",
.needle = "hello",
.replacement = "world",
.all = false,
} },
UserCommand.parse("jassob", "s/hello/world/"),
);
}
test "correctly ignores non-messages when trying to parse" {
try std.testing.expectEqualDeep(null, UserCommand.parse("jassob", "Hello, world"));
}
test "parse admin commands" {
const cmd = AdminCommand.parse("!join badchannel") orelse unreachable;
try std.testing.expectEqual(
AdminCommand{ .err = .{ .message = "channels must start with \"#\"" } },
cmd,
);
}