const std = @import("std"); const Parser = @import("root.zig").parser.Parser; /// UserCommand represents the commands that ordinary IRC users can use. pub const UserCommand = union(enum) { /// `s///` substitute: struct { author: []const u8, needle: []const u8, replacement: []const u8, all: bool = false }, /// !help help: void, pub fn init_substitute(author: []const u8, needle: []const u8, replacement: []const u8, all: bool) UserCommand { return .{ .substitute = .{ .author = author, .needle = needle, .replacement = replacement, .all = all, } }; } 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 log_prefix = "parsing substitute command"; var parser = substitute; parser, const delim = parser.take_char(); if (std.ascii.isAlphanumeric(delim)) { std.log.debug("{s}: delimiter cannot be a whitespace: \"{s}\"", .{ log_prefix, text }); return null; } var result = parser.take_until_char(delim); if (result == null) { std.log.debug( "{s}: cannot find typo, expecting a message on the form 's{}TYPO{}CORRECTION{}', but got {s}", .{ log_prefix, delim, delim, delim, text }, ); return null; } parser, const typo = result.?; result = parser.consume_char(delim).?.take_until_char(delim); if (result == null) { std.log.debug("{s}: missing an ending '/' in \"{s}\"", .{ log_prefix, text }); return null; } parser, const correction = result.?; return .init_substitute(nick, typo, correction, false); } 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 "can parse s/hello/world and report failure" { try std.testing.expectEqualDeep(null, UserCommand.parse("jassob", "s/hello/world")); } test "can parse s/hello|world| and report failure" { try std.testing.expectEqualDeep(null, 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")); } /// 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")) |join| { if (join.consume_space()) |channel| { if (channel.rest[0] != '#') { return .{ .err = .{ .message = "channels must start with \"#\"" } }; } return .{ .join = .{ .channel = join.rest } }; } } if (command.consume_str("backlog")) |backlog| { if (backlog.consume_space()) |history| { const historyOffset = std.fmt.parseInt(u16, history.rest, 10) catch |err| { std.debug.print("failed to parse int ('{s}') with error: {}\n", .{ history.rest, err }); return null; }; return .{ .backlog = .{ .history = historyOffset } }; } } std.log.debug("unknown command: \"{s}\"", .{command.rest}); } return null; } }; test "parse admin commands" { const cmd = AdminCommand.parse("!join badchannel") orelse unreachable; try std.testing.expectEqual( AdminCommand{ .err = .{ .message = "channels must start with \"#\"" } }, cmd, ); } test "parse backlog admin commands" { const cmd = AdminCommand.parse("!backlog 1") orelse unreachable; try std.testing.expectEqual( AdminCommand{ .backlog = .{ .history = 1 } }, cmd, ); } test "parse unknown admin commands" { const cmd = AdminCommand.parse("!history 1"); try std.testing.expectEqual(null, cmd); }