Compare commits
18 commits
chore/brea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bd3d7b909a | |||
| 1addcc7ae3 | |||
| 2e1416eb21 | |||
| 7d444204ef | |||
| b72d584996 | |||
| 1d44645451 | |||
| bd1891521e | |||
| 49a6b79fd9 | |||
| 958c00ce6f | |||
| 4d1f22194c | |||
| 4f2b9cbce2 | |||
| b0f0daa19d | |||
| a05229f72d | |||
| 6796a62a5f | |||
| 8b15398196 | |||
| 4e11cc9ea1 | |||
| d237ba9e8a | |||
| e1e1938359 |
8 changed files with 922 additions and 341 deletions
30
README.md
30
README.md
|
|
@ -5,6 +5,31 @@ zigeru is a IRC bot which implements the following commands:
|
||||||
- `s/OLD/NEW/` -- posts the previous message by the user, where every
|
- `s/OLD/NEW/` -- posts the previous message by the user, where every
|
||||||
occurrence of OLD is replaced by NEW.
|
occurrence of OLD is replaced by NEW.
|
||||||
|
|
||||||
|
```
|
||||||
|
00:04 <jassob> hello, world
|
||||||
|
00:05 <jassob> s/world/IRC/
|
||||||
|
00:05 <@eru> jassob: "hello, IRC"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `!help` -- post a usage string as a response.
|
||||||
|
|
||||||
|
```
|
||||||
|
00:05 <jassob> !help
|
||||||
|
00:05 <@eru> Send `s/TYPO/CORRECTION/` to replace TYPO with CORRECTION in your last message.
|
||||||
|
```
|
||||||
|
|
||||||
|
- `!join #CHANNEL` -- make zigeru join #CHANNEL.
|
||||||
|
|
||||||
|
```
|
||||||
|
# in #eru-test2
|
||||||
|
00:06 <jassob> !join #eru-test3
|
||||||
|
|
||||||
|
# in #eru-test3
|
||||||
|
00:06 --> jassob (~u@6wh6sdzhnfjx4.dtek.se) has joined #eru-test3
|
||||||
|
00:06 -- Channel #eru-test3: 2 nicks (0 owners, 0 admins, 1 op, 0 halfops, 0 voiced, 1 regular)
|
||||||
|
00:06 -- Channel created on ons, 11 mar 2026 00:06:44
|
||||||
|
```
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
To enter into a development shell with all the tools needed for
|
To enter into a development shell with all the tools needed for
|
||||||
|
|
@ -20,7 +45,4 @@ To run the binary: `zig build run`.
|
||||||
|
|
||||||
To build the binary: `zig build`.
|
To build the binary: `zig build`.
|
||||||
|
|
||||||
## TO DO
|
To build the binary statically: `zig build -Dtarget=x86_64-linux-musl`
|
||||||
|
|
||||||
- [ ] Add IRC support
|
|
||||||
- [ ] Add TLS support
|
|
||||||
|
|
|
||||||
111
build.zig
111
build.zig
|
|
@ -1,12 +1,16 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Import = std.Build.Module.Import;
|
||||||
|
const ResolvedTarget = std.Build.ResolvedTarget;
|
||||||
|
const Run = std.Build.Step.Run;
|
||||||
|
|
||||||
// Although this function looks imperative, it does not perform the build
|
// Although this function looks imperative, it does not perform the build
|
||||||
// directly and instead it mutates the build graph (`b`) that will be then
|
// directly and instead it mutates the build graph (`b`) that will be then
|
||||||
// executed by an external runner. The functions in `std.Build` implement a DSL
|
// executed by an external runner. The functions in `std.Build` implement a DSL
|
||||||
// for defining build steps and express dependencies between them, allowing the
|
// for defining build steps and express dependencies between them, allowing the
|
||||||
// build runner to parallelize the build automatically (and the cache system to
|
// build runner to parallelize the build automatically (and the cache system to
|
||||||
// know when a step doesn't need to be re-run).
|
// know when a step doesn't need to be re-run).
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) !void {
|
||||||
// Standard target options allow the person running `zig build` to choose
|
// Standard target options allow the person running `zig build` to choose
|
||||||
// what target to build for. Here we do not override the defaults, which
|
// what target to build for. Here we do not override the defaults, which
|
||||||
// means any target is allowed, and the default is native. Other options
|
// means any target is allowed, and the default is native. Other options
|
||||||
|
|
@ -106,7 +110,7 @@ pub fn build(b: *std.Build) void {
|
||||||
// This will evaluate the `run` step rather than the default step.
|
// This will evaluate the `run` step rather than the default step.
|
||||||
// For a top level step to actually do something, it must depend on other
|
// For a top level step to actually do something, it must depend on other
|
||||||
// steps (e.g. a Run step, as we will see in a moment).
|
// steps (e.g. a Run step, as we will see in a moment).
|
||||||
const run_step = b.step("run", "Run the app");
|
const runStep = b.step("run", "Run the app");
|
||||||
|
|
||||||
// This creates a RunArtifact step in the build graph. A RunArtifact step
|
// This creates a RunArtifact step in the build graph. A RunArtifact step
|
||||||
// invokes an executable compiled by Zig. Steps will only be executed by the
|
// invokes an executable compiled by Zig. Steps will only be executed by the
|
||||||
|
|
@ -114,61 +118,51 @@ pub fn build(b: *std.Build) void {
|
||||||
// or if another step depends on it, so it's up to you to define when and
|
// or if another step depends on it, so it's up to you to define when and
|
||||||
// how this Run step will be executed. In our case we want to run it when
|
// how this Run step will be executed. In our case we want to run it when
|
||||||
// the user runs `zig build run`, so we create a dependency link.
|
// the user runs `zig build run`, so we create a dependency link.
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
const runCmd = b.addRunArtifact(exe);
|
||||||
run_step.dependOn(&run_cmd.step);
|
runStep.dependOn(&runCmd.step);
|
||||||
|
|
||||||
// By making the run step depend on the default step, it will be run from the
|
// By making the run step depend on the default step, it will be run from the
|
||||||
// installation directory rather than directly from within the cache directory.
|
// installation directory rather than directly from within the cache directory.
|
||||||
run_cmd.step.dependOn(b.getInstallStep());
|
runCmd.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
// This allows the user to pass arguments to the application in the build
|
// This allows the user to pass arguments to the application in the build
|
||||||
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||||
if (b.args) |args| {
|
if (b.args) |args| {
|
||||||
run_cmd.addArgs(args);
|
runCmd.addArgs(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an executable that will run `test` blocks from the provided module.
|
|
||||||
// Here `mod` needs to define a target, which is why earlier we made sure to
|
|
||||||
// set the releative field.
|
|
||||||
const mod_tests = b.addTest(.{
|
|
||||||
.root_module = mod,
|
|
||||||
});
|
|
||||||
|
|
||||||
// A run step that will run the test executable.
|
|
||||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
|
||||||
|
|
||||||
const bot_tests = b.addTest(.{
|
|
||||||
.root_module = b.addModule("bot", .{
|
|
||||||
.target = target,
|
|
||||||
.root_source_file = b.path("src/bot.zig"),
|
|
||||||
.imports = &.{
|
|
||||||
.{
|
|
||||||
.name = "zircon",
|
|
||||||
.module = zircon.module("zircon"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const run_bot_tests = b.addRunArtifact(bot_tests);
|
|
||||||
|
|
||||||
// Creates an executable that will run `test` blocks from the executable's
|
|
||||||
// root module. Note that test executables only test one module at a time,
|
|
||||||
// hence why we have to create two separate ones.
|
|
||||||
const exe_tests = b.addTest(.{
|
|
||||||
.root_module = exe.root_module,
|
|
||||||
});
|
|
||||||
|
|
||||||
// A run step that will run the second test executable.
|
|
||||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
|
||||||
|
|
||||||
// A top level step for running all tests. dependOn can be called multiple
|
// A top level step for running all tests. dependOn can be called multiple
|
||||||
// times and since the two run steps do not depend on one another, this will
|
// times and since the two run steps do not depend on one another, this will
|
||||||
// make the two of them run in parallel.
|
// make the two of them run in parallel.
|
||||||
const test_step = b.step("test", "Run tests");
|
const testStep = b.step("test", "Run tests");
|
||||||
test_step.dependOn(&run_mod_tests.step);
|
|
||||||
test_step.dependOn(&run_exe_tests.step);
|
const testRunArtifacts: [6]*Run = .{
|
||||||
test_step.dependOn(&run_bot_tests.step);
|
// Creates an executable that will run `test` blocks from the provided module.
|
||||||
|
// Here `mod` needs to define a target, which is why earlier we made sure to
|
||||||
|
// set the releative field.
|
||||||
|
b.addRunArtifact(b.addTest(.{
|
||||||
|
.root_module = mod,
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Creates an executable that will run `test` blocks from the executable's
|
||||||
|
// root module. Note that test executables only test one module at a time,
|
||||||
|
// hence why we have to create two separate ones.
|
||||||
|
b.addRunArtifact(b.addTest(.{
|
||||||
|
.root_module = exe.root_module,
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Our bot tests needs zircon module import,
|
||||||
|
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.
|
||||||
|
try testRun(b, target, "buffer"),
|
||||||
|
try testRun(b, target, "parser"),
|
||||||
|
try testRun(b, target, "commands"),
|
||||||
|
};
|
||||||
|
for (testRunArtifacts) |test_run_artifact| {
|
||||||
|
testStep.dependOn(&test_run_artifact.step);
|
||||||
|
}
|
||||||
|
|
||||||
// Just like flags, top level steps are also listed in the `--help` menu.
|
// Just like flags, top level steps are also listed in the `--help` menu.
|
||||||
//
|
//
|
||||||
|
|
@ -182,3 +176,30 @@ pub fn build(b: *std.Build) void {
|
||||||
// Lastly, the Zig build system is relatively simple and self-contained,
|
// Lastly, the Zig build system is relatively simple and self-contained,
|
||||||
// and reading its source code will allow you to master it.
|
// and reading its source code will allow you to master it.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates a Run reference that can be depended on when creating a
|
||||||
|
// test step.
|
||||||
|
//
|
||||||
|
// Assumes there exists a file called "src/$name.zig" that contains
|
||||||
|
// tests.
|
||||||
|
fn testRun(b: *std.Build, target: ?ResolvedTarget, name: []const u8) error{OutOfMemory}!*Run {
|
||||||
|
return testRunWithImports(b, target, name, &.{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a Run reference that can be depended on when creating a
|
||||||
|
// test step, for modules with external imports.
|
||||||
|
//
|
||||||
|
// Assumes there exists a file called "src/$name.zig" that contains
|
||||||
|
// tests.
|
||||||
|
fn testRunWithImports(b: *std.Build, target: ?ResolvedTarget, name: []const u8, imports: []const Import) error{OutOfMemory}!*Run {
|
||||||
|
return b.addRunArtifact(b.addTest(.{
|
||||||
|
.root_module = b.addModule(
|
||||||
|
name,
|
||||||
|
.{
|
||||||
|
.target = target,
|
||||||
|
.root_source_file = b.path(try std.mem.concat(b.allocator, u8, &.{ "src/", name, ".zig" })),
|
||||||
|
.imports = imports,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
|
||||||
463
src/bot.zig
463
src/bot.zig
|
|
@ -1,190 +1,122 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const zircon = @import("zircon");
|
|
||||||
|
|
||||||
pub const Parser = struct {
|
const zigeru = @import("root.zig");
|
||||||
original: []const u8,
|
const Buffer = zigeru.buffer.Buffer;
|
||||||
rest: []const u8,
|
const UserCommand = zigeru.commands.UserCommand;
|
||||||
end_idx: usize,
|
const AdminCommand = zigeru.commands.AdminCommand;
|
||||||
|
|
||||||
fn seek(self: *const Parser, skip: usize) Parser {
|
|
||||||
return .{ .original = self.original, .rest = self.rest[skip..], .end_idx = self.end_idx + skip };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init(s: []const u8) Parser {
|
|
||||||
return .{
|
|
||||||
.original = s,
|
|
||||||
.end_idx = 0,
|
|
||||||
.rest = s,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_char(self: *const Parser, c: u8) ?Parser {
|
|
||||||
if (self.rest[0] != c) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return self.seek(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_str(self: *const Parser, s: []const u8) ?Parser {
|
|
||||||
const len = s.len;
|
|
||||||
if (self.rest.len < len) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!std.mem.eql(u8, self.rest[0..len], s)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return self.seek(len);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_until_char(self: *const Parser, c: u8) struct { Parser, []const u8 } {
|
|
||||||
const idx = std.mem.indexOfScalar(u8, self.rest, c) orelse unreachable;
|
|
||||||
return .{ self.seek(idx), self.rest[0..idx] };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_char(self: *const Parser) struct { Parser, u8 } {
|
|
||||||
return .{ self.seek(1), self.rest[0] };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parsed(self: *const Parser) []const u8 {
|
|
||||||
return self.original[0..self.end_idx];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Command represents the commands that ordinary IRC users can use.
|
|
||||||
pub const Command = 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) ?Command {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const HELP_MESSAGE: []const u8 = "Send `s/TYPO/CORRECTION/` to replace TYPO with CORRECTION in your last message.";
|
const HELP_MESSAGE: []const u8 = "Send `s/TYPO/CORRECTION/` to replace TYPO with CORRECTION in your last message.";
|
||||||
|
|
||||||
pub const Error = error{ OutOfMemory, NoMessage, WriteFailed };
|
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 {
|
pub const Message = struct {
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
targets: []const u8,
|
targets: []const u8,
|
||||||
author: []const u8,
|
author: []const u8,
|
||||||
content: []const u8,
|
content: []const u8,
|
||||||
|
|
||||||
pub fn init_owned(allocator: std.mem.Allocator, timestamp: i64, author: []const u8, targets: []const u8, content: []const u8) Error!Message {
|
pub fn initOwned(
|
||||||
return .{
|
allocator: std.mem.Allocator,
|
||||||
.timestamp = timestamp,
|
timestamp: i64,
|
||||||
.targets = try allocator.dupe(u8, targets),
|
author: []const u8,
|
||||||
.author = try allocator.dupe(u8, author),
|
targets: []const u8,
|
||||||
.content = try allocator.dupe(u8, content),
|
content: []const u8,
|
||||||
};
|
) Error!*Message {
|
||||||
|
const message = try allocator.create(Message);
|
||||||
|
message.timestamp = timestamp;
|
||||||
|
message.targets = try allocator.dupe(u8, targets);
|
||||||
|
message.author = try allocator.dupe(u8, author);
|
||||||
|
message.content = try allocator.dupe(u8, content);
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *const Message, allocator: std.mem.Allocator) void {
|
pub fn deinit(self: *const Message, allocator: std.mem.Allocator) void {
|
||||||
allocator.free(self.author);
|
allocator.free(self.author);
|
||||||
allocator.free(self.content);
|
allocator.free(self.content);
|
||||||
allocator.free(self.targets);
|
allocator.free(self.targets);
|
||||||
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 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 {
|
pub const Bot = struct {
|
||||||
backlog: [1024]?*const Message,
|
backlog: Buffer(*const Message, 1024),
|
||||||
sent_messages: std.ArrayList([]u8),
|
outbox: Buffer([]u8, 1024),
|
||||||
top: usize,
|
|
||||||
bottom: usize,
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
// deinit function for backlog messages
|
||||||
|
fn deinitBacklogSlot(allocator: std.mem.Allocator, item: *const Message) void {
|
||||||
|
item.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// deinit function for outbox messages.
|
||||||
|
fn deinitSentMessage(allocator: std.mem.Allocator, item: []u8) void {
|
||||||
|
allocator.free(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// init initializes a Bot with fixed size backlog and outbox
|
||||||
|
// buffers.
|
||||||
|
//
|
||||||
|
// Both buffers own their contents and deinits any slots that gets
|
||||||
|
// overwritten.
|
||||||
pub fn init(allocator: std.mem.Allocator) Error!Bot {
|
pub fn init(allocator: std.mem.Allocator) Error!Bot {
|
||||||
return Bot{
|
var bot = Bot{
|
||||||
.backlog = .{null} ** 1024,
|
|
||||||
.sent_messages = try std.ArrayList([]u8).initCapacity(allocator, 10),
|
|
||||||
.top = 0,
|
|
||||||
.bottom = 0,
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
|
.backlog = undefined,
|
||||||
|
.outbox = undefined,
|
||||||
};
|
};
|
||||||
|
bot.backlog = .initWithClosure(allocator, &Bot.deinitBacklogSlot);
|
||||||
|
bot.outbox = .initWithClosure(allocator, &Bot.deinitSentMessage);
|
||||||
|
return bot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deinits self's buffers and their contents.
|
||||||
|
//
|
||||||
|
// NOTE: does not deinit self.
|
||||||
pub fn deinit(self: *Bot) void {
|
pub fn deinit(self: *Bot) void {
|
||||||
for (self.sent_messages.items) |item| {
|
self.outbox.deinit();
|
||||||
self.allocator.free(item);
|
self.backlog.deinit();
|
||||||
}
|
|
||||||
self.sent_messages.deinit(self.allocator);
|
|
||||||
|
|
||||||
var idx = self.previous_idx(self.top);
|
|
||||||
while (idx != self.bottom) : (idx = self.previous_idx(idx)) {
|
|
||||||
if (self.backlog[idx]) |message| {
|
|
||||||
message.deinit(self.allocator);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(self: *Bot, cmd: *const Command, prefix: ?zircon.Prefix, targets: []const u8) Error!zircon.Message {
|
pub fn hear(self: *Bot, message: *const Message) ?Error!Response {
|
||||||
|
// Store the message to keep track of the allocation
|
||||||
|
defer self.store(message);
|
||||||
|
|
||||||
|
if (UserCommand.parse(message.author, message.content)) |cmd| {
|
||||||
|
return self.execute(&cmd, message.targets);
|
||||||
|
}
|
||||||
|
if (AdminCommand.parse(message.content)) |cmd| {
|
||||||
|
return self.execute_admin(&cmd, "#eru-admin");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(self: *Bot, cmd: *const UserCommand, targets: []const u8) Error!Response {
|
||||||
switch (cmd.*) {
|
switch (cmd.*) {
|
||||||
.substitute => |command| {
|
.substitute => |command| {
|
||||||
const prev_msg = self.previous_message_by_author(command.author, targets) orelse return Error.NoMessage;
|
const prev_msg = self.previous_message_by_author(
|
||||||
|
command.author,
|
||||||
|
targets,
|
||||||
|
) orelse return Error.NoMessage;
|
||||||
|
if (std.mem.count(u8, prev_msg.content, command.needle) == 0) {
|
||||||
|
return Error.NoMessage;
|
||||||
|
}
|
||||||
const output = try std.mem.replaceOwned(
|
const output = try std.mem.replaceOwned(
|
||||||
u8,
|
u8,
|
||||||
self.allocator,
|
self.allocator,
|
||||||
|
|
@ -193,158 +125,213 @@ pub const Bot = struct {
|
||||||
command.replacement,
|
command.replacement,
|
||||||
);
|
);
|
||||||
defer self.allocator.free(output);
|
defer self.allocator.free(output);
|
||||||
const quoted_output = try std.fmt.allocPrint(self.allocator, "{s}: \"{s}\"", .{ command.author, output });
|
const quoted_output = try std.fmt.allocPrint(
|
||||||
try self.sent_messages.append(self.allocator, quoted_output);
|
self.allocator,
|
||||||
return zircon.Message{
|
"{s}: \"{s}\"",
|
||||||
.PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = quoted_output },
|
.{ command.author, output },
|
||||||
};
|
);
|
||||||
|
self.outbox.append(quoted_output);
|
||||||
|
return .{ .privmsg = .{
|
||||||
|
.targets = targets,
|
||||||
|
.text = quoted_output,
|
||||||
|
} };
|
||||||
},
|
},
|
||||||
.help => {
|
.help => {
|
||||||
return zircon.Message{
|
return .{ .privmsg = .{
|
||||||
.PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = HELP_MESSAGE },
|
.targets = targets,
|
||||||
};
|
.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.*) {
|
switch (cmd.*) {
|
||||||
.status => {
|
.status => {
|
||||||
const msg = try std.fmt.allocPrint(
|
const msg = try std.fmt.allocPrint(
|
||||||
self.allocator,
|
self.allocator,
|
||||||
"heard messages: {}, sent messages: {}, top: {}, bottom: {}",
|
"heard messages: {}, sent messages: {}",
|
||||||
.{ self.no_messages(), self.sent_messages.items.len, self.top, self.bottom },
|
.{ self.backlog.len(), self.outbox.len() },
|
||||||
);
|
);
|
||||||
return .{ .PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = msg } };
|
return .{ .privmsg = .{ .targets = targets, .text = msg } };
|
||||||
},
|
},
|
||||||
.join => |msg| {
|
.join => |msg| {
|
||||||
std.log.debug("received join request: channel \"{s}\"", .{msg.channel});
|
std.log.debug("received join request: channel \"{s}\"", .{msg.channel});
|
||||||
return .{ .JOIN = .{ .prefix = prefix, .channels = msg.channel } };
|
return .{ .join = .{ .channels = msg.channel } };
|
||||||
},
|
},
|
||||||
.backlog => |backlog| {
|
.backlog => |backlog| {
|
||||||
if (self.top == self.bottom) {
|
if (self.backlog.len() == 0) {
|
||||||
return Error.NoMessage;
|
return Error.NoMessage;
|
||||||
}
|
}
|
||||||
if (backlog.history > self.no_messages()) {
|
if (backlog.history >= self.backlog.len()) {
|
||||||
return Error.NoMessage;
|
return Error.NoMessage;
|
||||||
}
|
}
|
||||||
const idx = self.previous_idx(self.top - backlog.history);
|
if (self.backlog.getBackwards(backlog.history)) |message| {
|
||||||
if (self.backlog[idx]) |message| {
|
|
||||||
const quoted_output = try std.fmt.allocPrint(
|
const quoted_output = try std.fmt.allocPrint(
|
||||||
self.allocator,
|
self.allocator,
|
||||||
"backlog {}: author: \"{s}\", content: \"{s}\"",
|
"backlog {}: author: \"{s}\", content: \"{s}\"",
|
||||||
.{ backlog.history, message.author, message.content },
|
.{ backlog.history, message.author, message.content },
|
||||||
);
|
);
|
||||||
try self.sent_messages.append(self.allocator, quoted_output);
|
self.outbox.append(quoted_output);
|
||||||
return .{
|
return .{
|
||||||
.PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = quoted_output },
|
.privmsg = .{ .targets = targets, .text = quoted_output },
|
||||||
};
|
};
|
||||||
} else return Error.NoMessage;
|
} else return Error.NoMessage;
|
||||||
},
|
},
|
||||||
.err => |err| {
|
.err => |err| {
|
||||||
return .{ .PRIVMSG = .{ .targets = targets, .prefix = prefix, .text = err.message } };
|
return .{ .privmsg = .{ .targets = targets, .text = err.message } };
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hear(self: *Bot, msg: *const Message) void {
|
// store a message in backlog, potentially overwriting oldest message.
|
||||||
self.backlog[self.top] = msg;
|
pub fn store(self: *Bot, msg: *const Message) void {
|
||||||
self.top = (self.top + 1) % self.backlog.len;
|
self.backlog.append(msg);
|
||||||
if (self.top == self.bottom) {
|
|
||||||
self.bottom = (self.bottom + 1) % self.backlog.len;
|
|
||||||
// free old message
|
|
||||||
self.allocator.free(self.backlog[self.top].?.author);
|
|
||||||
self.allocator.free(self.backlog[self.top].?.content);
|
|
||||||
self.backlog[self.top] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn no_messages(self: *Bot) usize {
|
|
||||||
if (self.top < self.bottom) {
|
|
||||||
// we've bounded around, the backlog is full.
|
|
||||||
return self.backlog.len;
|
|
||||||
}
|
|
||||||
return self.top - self.bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn previous_idx(self: *Bot, idx: usize) usize {
|
|
||||||
if (idx == 0) {
|
|
||||||
return self.backlog.len - 1;
|
|
||||||
}
|
|
||||||
return (idx - 1) % self.backlog.len;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_message_by_author(self: *Bot, author: []const u8, targets: []const u8) ?*const Message {
|
fn previous_message_by_author(self: *Bot, author: []const u8, targets: []const u8) ?*const Message {
|
||||||
var idx = self.previous_idx(self.top);
|
var iter = self.backlog.iterateReverse();
|
||||||
while (true) : (idx = self.previous_idx(idx)) {
|
while (iter.prev()) |message| {
|
||||||
if (self.backlog[idx] == null) {
|
if (std.mem.eql(u8, message.author, author) and
|
||||||
return null;
|
std.mem.eql(u8, message.targets, targets))
|
||||||
}
|
{
|
||||||
const message = self.backlog[idx] orelse unreachable;
|
|
||||||
if (std.mem.eql(u8, message.author, author) and std.mem.eql(u8, message.targets, targets)) {
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
if (idx == self.bottom) {
|
|
||||||
// reached the start of the list
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
test "hear_wraps" {
|
fn newTestMessage(allocator: std.mem.Allocator, content: []const u8) !*Message {
|
||||||
|
return try Message.initOwned(
|
||||||
|
allocator,
|
||||||
|
12345,
|
||||||
|
"jassob",
|
||||||
|
"#test",
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "deiniting an owned Message leaks no memory" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
const testMessage = try newTestMessage(allocator, "test");
|
||||||
|
testMessage.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "deiniting an inited Bot leaks no memory" {
|
||||||
|
var bot = try Bot.init(std.testing.allocator);
|
||||||
|
bot.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "hear and deinit has no leaks" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var bot = try Bot.init(allocator);
|
||||||
|
defer bot.deinit();
|
||||||
|
|
||||||
|
const testMessage = try newTestMessage(allocator, "test");
|
||||||
|
try std.testing.expectEqual(null, bot.hear(testMessage));
|
||||||
|
|
||||||
|
try std.testing.expectEqual(0, bot.backlog.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a few hears and deinit has no leaks" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var bot = try Bot.init(allocator);
|
||||||
|
defer bot.deinit();
|
||||||
|
|
||||||
|
for (0..2) |_| {
|
||||||
|
const testMessage = try newTestMessage(std.testing.allocator, "test");
|
||||||
|
_ = bot.hear(testMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
try std.testing.expectEqual(1, bot.backlog.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "hear wraps" {
|
||||||
var bot = try Bot.init(std.testing.allocator);
|
var bot = try Bot.init(std.testing.allocator);
|
||||||
defer bot.deinit();
|
defer bot.deinit();
|
||||||
|
|
||||||
for (0..1025) |_| {
|
for (0..1025) |_| {
|
||||||
const testMessage = try Message.init_owned(
|
const testMessage = try newTestMessage(std.testing.allocator, "test");
|
||||||
std.testing.allocator,
|
_ = bot.hear(testMessage);
|
||||||
12345,
|
|
||||||
"jassob",
|
|
||||||
"#test",
|
|
||||||
"All your codebase are belong to us.\n",
|
|
||||||
);
|
|
||||||
bot.hear(&testMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try std.testing.expect(bot.top == 1);
|
try std.testing.expectEqual(0, bot.backlog.top);
|
||||||
try std.testing.expect(bot.bottom == 2);
|
try std.testing.expectEqual(1, bot.backlog.bottom());
|
||||||
try std.testing.expect(bot.no_messages() == 1024);
|
try std.testing.expectEqual(1024, bot.backlog.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "execute_substitution_no_previous_message" {
|
test "execute substitution no previous message" {
|
||||||
var bot = try Bot.init(std.testing.allocator);
|
var bot = try Bot.init(std.testing.allocator);
|
||||||
defer bot.deinit();
|
defer bot.deinit();
|
||||||
const cmd = Command{ .substitute = .{ .author = "jassob", .needle = "What", .replacement = "what" } };
|
|
||||||
try std.testing.expectError(Error.NoMessage, bot.execute(&cmd, null, "#test"));
|
const substitution = try newTestMessage(std.testing.allocator, "s/What/what/");
|
||||||
|
try std.testing.expectError(Error.NoMessage, bot.hear(substitution).?);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "execute_substitution" {
|
test "execute substitution" {
|
||||||
var bot = try Bot.init(std.testing.allocator);
|
const allocator = std.testing.allocator;
|
||||||
|
var bot = try Bot.init(allocator);
|
||||||
defer bot.deinit();
|
defer bot.deinit();
|
||||||
|
|
||||||
// hear original message with typo
|
// hear original message with typo
|
||||||
const msg = try Message.init_owned(
|
const msg = try newTestMessage(allocator, "What");
|
||||||
std.testing.allocator,
|
try std.testing.expectEqual(null, bot.hear(msg));
|
||||||
1234,
|
|
||||||
"jassob",
|
|
||||||
"#test",
|
|
||||||
"What",
|
|
||||||
);
|
|
||||||
bot.hear(&msg);
|
|
||||||
|
|
||||||
// execute substitution
|
// execute substitution
|
||||||
const cmd = Command{
|
const sub = try newTestMessage(allocator, "s/What/what/");
|
||||||
.substitute = .{ .author = "jassob", .needle = "What", .replacement = "what" },
|
const response = try bot.hear(sub).?;
|
||||||
};
|
|
||||||
const response = try bot.execute(&cmd, null, "#test");
|
|
||||||
|
|
||||||
// expect response matching the correct message
|
// expect response matching the correct message
|
||||||
switch (response) {
|
switch (response) {
|
||||||
.PRIVMSG => |message| {
|
.privmsg => |message| {
|
||||||
try std.testing.expectEqualDeep(message.text, "jassob: \"what\"");
|
try std.testing.expectEqualDeep(message.text, "jassob: \"what\"");
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "execute substitution with no matching needle" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var bot = try Bot.init(allocator);
|
||||||
|
defer bot.deinit();
|
||||||
|
|
||||||
|
// hear original message
|
||||||
|
const msg = try newTestMessage(allocator, "original");
|
||||||
|
try std.testing.expectEqual(null, bot.hear(msg));
|
||||||
|
|
||||||
|
// execute substitution
|
||||||
|
const sub = try newTestMessage(allocator, "s/something else/weird/");
|
||||||
|
try std.testing.expectError(Error.NoMessage, bot.hear(sub).?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "recursive substitutions does not cause issues" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var bot = try Bot.init(allocator);
|
||||||
|
defer bot.deinit();
|
||||||
|
|
||||||
|
// hear original message
|
||||||
|
const msg = try newTestMessage(allocator, "original");
|
||||||
|
try std.testing.expectEqual(null, bot.hear(msg));
|
||||||
|
|
||||||
|
// execute substitution
|
||||||
|
const sub = try newTestMessage(allocator, "s/original/something else/");
|
||||||
|
switch (try bot.hear(sub).?) {
|
||||||
|
.privmsg => |message| {
|
||||||
|
try std.testing.expectEqualDeep("jassob: \"something else\"", message.text);
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute second substitution
|
||||||
|
const sub2 = try newTestMessage(
|
||||||
|
allocator,
|
||||||
|
"s|s/original/something else/|something else|",
|
||||||
|
);
|
||||||
|
switch (try bot.hear(sub2).?) {
|
||||||
|
.privmsg => |message| {
|
||||||
|
try std.testing.expectEqualDeep("jassob: \"something else\"", message.text);
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
264
src/buffer.zig
Normal file
264
src/buffer.zig
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Buffer represents a non-allocating, circular buffer of a fixed size.
|
||||||
|
//
|
||||||
|
// It is possible to store owned data in Buffer and have it
|
||||||
|
// automatically deinited when it is overwritten by supplying a custom
|
||||||
|
// ItemDeinitClosure, using .init_with_closure.
|
||||||
|
pub fn Buffer(comptime T: type, comptime length: usize) type {
|
||||||
|
return struct {
|
||||||
|
items: [length]?T,
|
||||||
|
top: usize,
|
||||||
|
insertions: usize,
|
||||||
|
deinit_func: ?*const fn (allocator: std.mem.Allocator, item: T) void,
|
||||||
|
allocator: ?std.mem.Allocator,
|
||||||
|
|
||||||
|
pub fn init() Buffer(T, length) {
|
||||||
|
return .{
|
||||||
|
.items = .{null} ** length,
|
||||||
|
.top = 0,
|
||||||
|
.insertions = 0,
|
||||||
|
.deinit_func = null,
|
||||||
|
.allocator = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initWithClosure(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
deinit_func: *const fn (allocator: std.mem.Allocator, item: T) void,
|
||||||
|
) Buffer(T, length) {
|
||||||
|
var buf = Buffer(T, length).init();
|
||||||
|
buf.allocator = allocator;
|
||||||
|
buf.deinit_func = deinit_func;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canDeinit(self: *const Buffer(T, length)) bool {
|
||||||
|
return self.allocator != null and self.deinit_func != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinitItem(self: *Buffer(T, length), item: T) void {
|
||||||
|
if (!self.canDeinit()) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.deinit_func.?(self.allocator.?, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(self: *const Buffer(T, length)) usize {
|
||||||
|
return (self.top + 1) % length;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev(self: *const Buffer(T, length)) usize {
|
||||||
|
if (self.top == 0) {
|
||||||
|
return length - 1;
|
||||||
|
}
|
||||||
|
return self.top - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reached_end(self: *const Buffer(T, length)) bool {
|
||||||
|
return self.insertions >= length;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(self: *Buffer(T, length), item: T) void {
|
||||||
|
if (self.insertions > 0) {
|
||||||
|
self.top = self.next();
|
||||||
|
}
|
||||||
|
if (self.insertions >= length) {
|
||||||
|
// free old bottom item
|
||||||
|
self.deinitItem(self.items[self.top].?);
|
||||||
|
self.items[self.top] = null;
|
||||||
|
}
|
||||||
|
self.items[self.top] = item;
|
||||||
|
self.insertions = self.insertions + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(self: *const Buffer(T, length)) usize {
|
||||||
|
return @min(self.insertions, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bottom(self: *const Buffer(T, length)) usize {
|
||||||
|
if (self.reached_end()) {
|
||||||
|
return self.next();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *const Buffer(T, length), index: usize) ?T {
|
||||||
|
const idx = (self.bottom() + index) % self.items.len;
|
||||||
|
if (index > self.insertions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return self.items[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getBackwards(self: *const Buffer(T, length), offset: usize) ?T {
|
||||||
|
if (offset >= self.insertions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return self.items[(self.top - offset) % self.items.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iterate(self: *const Buffer(T, length)) BufferIterator(T, length) {
|
||||||
|
return .{
|
||||||
|
.buffer = self,
|
||||||
|
.index = self.bottom(),
|
||||||
|
.seen_first_item = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iterateReverse(self: *const Buffer(T, length)) BufferIterator(T, length) {
|
||||||
|
return .{
|
||||||
|
.buffer = self,
|
||||||
|
.index = self.top,
|
||||||
|
.seen_first_item = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Buffer(T, length)) void {
|
||||||
|
if (!self.canDeinit()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (self.items, 0..) |item, idx| {
|
||||||
|
if (item != null) {
|
||||||
|
self.deinitItem(item.?);
|
||||||
|
self.items[idx] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn BufferIterator(comptime T: type, comptime length: usize) type {
|
||||||
|
return struct {
|
||||||
|
buffer: *const Buffer(T, length),
|
||||||
|
index: usize,
|
||||||
|
seen_first_item: bool,
|
||||||
|
|
||||||
|
pub fn next(self: *BufferIterator(T, length)) ?T {
|
||||||
|
if (self.seen_first_item and self.index == self.buffer.bottom()) {
|
||||||
|
// We've reached the top, return null.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const item = self.buffer.items[self.index];
|
||||||
|
self.index = (self.index + 1) % length;
|
||||||
|
if (!self.seen_first_item) {
|
||||||
|
self.seen_first_item = true;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(self: *BufferIterator(T, length)) ?T {
|
||||||
|
if (self.seen_first_item and self.index == self.buffer.top) {
|
||||||
|
// We've reached the top, return null.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const item = self.buffer.items[self.index];
|
||||||
|
if (self.index == 0) {
|
||||||
|
self.index = length - 1;
|
||||||
|
} else {
|
||||||
|
self.index = self.index - 1;
|
||||||
|
}
|
||||||
|
if (!self.seen_first_item) {
|
||||||
|
self.seen_first_item = true;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "init is empty" {
|
||||||
|
const buffer = Buffer(u8, 10).init();
|
||||||
|
// len is 0 in initial buffer
|
||||||
|
try std.testing.expectEqual(0, buffer.len());
|
||||||
|
var iter = buffer.iterate();
|
||||||
|
if (iter.next()) |_| {
|
||||||
|
@panic("unexpected message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "reached_end is true after we've inserted length items" {
|
||||||
|
var buffer = Buffer(u8, 2).init();
|
||||||
|
buffer.append(1);
|
||||||
|
try std.testing.expect(!buffer.reached_end());
|
||||||
|
buffer.append(2);
|
||||||
|
try std.testing.expectEqual(2, buffer.insertions);
|
||||||
|
try std.testing.expect(buffer.reached_end());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "append adds item" {
|
||||||
|
var buffer = Buffer(u8, 10).init();
|
||||||
|
buffer.append(10);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(1, buffer.len());
|
||||||
|
var iter = buffer.iterate();
|
||||||
|
try std.testing.expectEqual(10, iter.next().?);
|
||||||
|
try std.testing.expectEqual(null, iter.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "append bounds" {
|
||||||
|
var buffer = Buffer(usize, 10).init();
|
||||||
|
|
||||||
|
// append 0 - 9 to buffer
|
||||||
|
for (0..10) |i| {
|
||||||
|
buffer.append(i);
|
||||||
|
}
|
||||||
|
// we expect 0 - 9 to exist inside buffer
|
||||||
|
var iter = buffer.iterate();
|
||||||
|
for (0..10) |i| {
|
||||||
|
try std.testing.expectEqual(i, iter.next().?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// append 11, will overwrite 0.
|
||||||
|
buffer.append(11);
|
||||||
|
|
||||||
|
// reset iterator
|
||||||
|
iter = buffer.iterate();
|
||||||
|
|
||||||
|
// values 1-9 are present
|
||||||
|
for (1..10) |i| {
|
||||||
|
try std.testing.expectEqual(i, iter.next().?);
|
||||||
|
}
|
||||||
|
// and so is value 11
|
||||||
|
try std.testing.expectEqual(11, iter.next().?);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit_byte_slice(allocator: std.mem.Allocator, item: []u8) void {
|
||||||
|
allocator.free(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "deiniting allocating buffer does not leak" {
|
||||||
|
// create closure
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// create buffer with closure
|
||||||
|
var buffer = Buffer([]u8, 3).initWithClosure(allocator, &deinit_byte_slice);
|
||||||
|
defer buffer.deinit();
|
||||||
|
|
||||||
|
// add less elements than length
|
||||||
|
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{1}));
|
||||||
|
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{2}));
|
||||||
|
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{3}));
|
||||||
|
|
||||||
|
try std.testing.expectEqual(3, buffer.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "wrapping allocating buffer does not leak" {
|
||||||
|
// create closure
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// create buffer with closure
|
||||||
|
var buffer = Buffer([]u8, 3).initWithClosure(allocator, &deinit_byte_slice);
|
||||||
|
defer buffer.deinit();
|
||||||
|
|
||||||
|
// add an element more than length
|
||||||
|
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{1}));
|
||||||
|
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{2}));
|
||||||
|
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{3}));
|
||||||
|
buffer.append(try std.fmt.allocPrint(allocator, "item {}", .{4}));
|
||||||
|
|
||||||
|
// length is still 3 because we have removed the first element
|
||||||
|
try std.testing.expectEqual(3, buffer.len());
|
||||||
|
try std.testing.expectEqual(4, buffer.insertions);
|
||||||
|
try std.testing.expect(buffer.reached_end());
|
||||||
|
}
|
||||||
144
src/commands.zig
Normal file
144
src/commands.zig
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
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/<old-word>/<new-word>/`
|
||||||
|
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);
|
||||||
|
}
|
||||||
157
src/main.zig
157
src/main.zig
|
|
@ -1,13 +1,11 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const zircon = @import("zircon");
|
|
||||||
const zigeru = @import("zigeru");
|
const zigeru = @import("zigeru");
|
||||||
|
const bot = zigeru.bot;
|
||||||
const Bot = zigeru.bot.Bot;
|
const Bot = zigeru.bot.Bot;
|
||||||
const Error = zigeru.bot.Error;
|
const UserCommand = zigeru.commands.UserCommand;
|
||||||
const BotCommand = zigeru.bot.Command;
|
const AdminCommand = zigeru.commands.AdminCommand;
|
||||||
const AdminCommand = zigeru.bot.AdminCommand;
|
const zircon = @import("zircon");
|
||||||
const BotMessage = zigeru.bot.Message;
|
|
||||||
|
|
||||||
var debug_allocator = std.heap.DebugAllocator(.{}).init;
|
var debug_allocator = std.heap.DebugAllocator(.{}).init;
|
||||||
|
|
||||||
|
|
@ -44,11 +42,16 @@ pub fn main() !void {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Adapter = struct {
|
/// BotAdapter is the closure that we register in zircon as the
|
||||||
ptr: *anyopaque,
|
/// message callback.
|
||||||
callbackFn: *const fn (*anyopaque, zircon.Message) ?zircon.Message,
|
///
|
||||||
};
|
/// Whenever a message is received by the zircon client it will invoke
|
||||||
|
/// BotAdapter.callback (through some indirection) with the received
|
||||||
|
/// message.
|
||||||
|
///
|
||||||
|
/// The main responsibility of BotAdapter is to serve as the
|
||||||
|
/// translation layer between our own internal types and the zircon
|
||||||
|
/// IRC types.
|
||||||
pub const BotAdapter = struct {
|
pub const BotAdapter = struct {
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
|
@ -64,29 +67,48 @@ pub const BotAdapter = struct {
|
||||||
self.bot.deinit();
|
self.bot.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn callback(self: *BotAdapter, message: zircon.Message) ?zircon.Message {
|
/// callback gets called for every message that we receive.
|
||||||
|
///
|
||||||
|
/// This is where we can extend the bot to support more types of
|
||||||
|
/// messages if needed.
|
||||||
|
///
|
||||||
|
/// See
|
||||||
|
/// - https://modern.ircdocs.horse/, for what kinds of messages exists in the IRC protocol documentation,
|
||||||
|
/// - https://github.com/Jassob/zircon/blob/main/src/message.zig, for zircon documentation.
|
||||||
|
///
|
||||||
|
/// NOTE: This function does not have a self-parameter, this is
|
||||||
|
/// because this function is called as a "generic" function
|
||||||
|
/// that is parameterized in the pointer argument (otherwise
|
||||||
|
/// zircon library would not be able to be reused).
|
||||||
|
///
|
||||||
|
/// That's why the first thing we do is perform some casting
|
||||||
|
/// magic to convert our pointer back to a BotAdapter
|
||||||
|
/// pointer. This is safe as long as we know that we
|
||||||
|
/// registered a BotAdapter closure and there are no other
|
||||||
|
/// callbacks registered.
|
||||||
|
pub fn callback(ptr: *anyopaque, message: zircon.Message) ?zircon.Message {
|
||||||
|
const self: *@This() = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
switch (message) {
|
switch (message) {
|
||||||
.PRIVMSG => |msg| {
|
.PRIVMSG => |msg| {
|
||||||
std.log.debug(
|
const nick = nickFromPrefix(msg.prefix);
|
||||||
"received message: nick {?s}, user: {?s}, host: {?s}, targets: {s}, text: {s}",
|
|
||||||
.{ msg.prefix.?.nick, msg.prefix.?.user, msg.prefix.?.host, msg.targets, msg.text },
|
// create message
|
||||||
);
|
const bot_message = bot.Message.initOwned(
|
||||||
const nick = if (msg.prefix) |prefix| if (prefix.nick) |nick| nick else "unknown" else "unknown";
|
|
||||||
if (BotCommand.parse(nick, msg.text)) |cmd| {
|
|
||||||
return self.bot.execute(&cmd, msg.prefix, 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);
|
|
||||||
}
|
|
||||||
const bot_msg = BotMessage.init_owned(
|
|
||||||
self.allocator,
|
self.allocator,
|
||||||
std.time.timestamp(),
|
std.time.timestamp(),
|
||||||
nick,
|
nick,
|
||||||
msg.targets,
|
msg.targets,
|
||||||
msg.text,
|
msg.text,
|
||||||
) catch |err| return report_error(err);
|
) catch |err| return reportError(err);
|
||||||
self.bot.hear(&bot_msg);
|
|
||||||
|
// send message to bot
|
||||||
|
const response = self.bot.hear(bot_message) orelse {
|
||||||
return null;
|
return null;
|
||||||
|
} catch |err| {
|
||||||
|
return reportError(err);
|
||||||
|
};
|
||||||
|
return toIRC(response);
|
||||||
},
|
},
|
||||||
.JOIN => |msg| {
|
.JOIN => |msg| {
|
||||||
std.log.debug("received join message: channels {s}", .{msg.channels});
|
std.log.debug("received join message: channels {s}", .{msg.channels});
|
||||||
|
|
@ -99,48 +121,77 @@ pub const BotAdapter = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report_error(err: Error) zircon.Message {
|
/// report errors as private message to admin channel.
|
||||||
|
fn reportError(err: bot.Error) zircon.Message {
|
||||||
const err_msg = switch (err) {
|
const err_msg = switch (err) {
|
||||||
Error.NoMessage => "no matching message",
|
bot.Error.NoMessage => "no matching message",
|
||||||
Error.OutOfMemory => "out of memory",
|
bot.Error.OutOfMemory => "out of memory",
|
||||||
Error.WriteFailed => "write failed",
|
bot.Error.WriteFailed => "write failed",
|
||||||
};
|
};
|
||||||
return .{ .PRIVMSG = .{ .prefix = null, .targets = "#eru-admin", .text = err_msg } };
|
return .{ .PRIVMSG = .{ .prefix = null, .targets = "#eru-admin", .text = err_msg } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn erased_callback(self: *anyopaque, message: zircon.Message) ?zircon.Message {
|
|
||||||
const a: *@This() = @ptrCast(@alignCast(self));
|
|
||||||
return a.callback(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn closure(self: *BotAdapter) zircon.MessageClosure {
|
pub fn closure(self: *BotAdapter) zircon.MessageClosure {
|
||||||
return .{
|
return .{
|
||||||
.ptr = self,
|
.ptr = self,
|
||||||
.callbackFn = BotAdapter.erased_callback,
|
.callbackFn = BotAdapter.callback,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// nickFromPrefix returns a nick if it exists in the prefix or
|
||||||
|
/// "unknown" otherwise.
|
||||||
|
fn nickFromPrefix(prefix: ?zircon.Prefix) []const u8 {
|
||||||
|
if (prefix == null or prefix.?.nick == null) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
return prefix.?.nick.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// toIRC converts a bot response and converts it to a IRC message.
|
||||||
|
fn toIRC(response: bot.Response) 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 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn priv_msg(text: []const u8) zircon.Message {
|
||||||
|
return .{ .PRIVMSG = .{
|
||||||
|
.prefix = .{ .nick = "jassob", .host = "localhost", .user = "jassob" },
|
||||||
|
.targets = "#test",
|
||||||
|
.text = text,
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
test "substitute" {
|
test "substitute" {
|
||||||
|
var adapter = try BotAdapter.init(std.testing.allocator);
|
||||||
|
defer adapter.deinit();
|
||||||
|
if (BotAdapter.callback(&adapter, priv_msg("hello world"))) |_| {
|
||||||
|
@panic("unexpected response");
|
||||||
|
}
|
||||||
|
const response = BotAdapter.callback(&adapter, priv_msg("s/world/zig/"));
|
||||||
|
|
||||||
|
try std.testing.expect(response != null);
|
||||||
|
try std.testing.expectEqualStrings(
|
||||||
|
"jassob: \"hello zig\"",
|
||||||
|
response.?.PRIVMSG.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "get empty backlog message" {
|
||||||
var bot_adapter = try BotAdapter.init(std.testing.allocator);
|
var bot_adapter = try BotAdapter.init(std.testing.allocator);
|
||||||
defer bot_adapter.deinit();
|
defer bot_adapter.deinit();
|
||||||
const prefix = zircon.Prefix{ .nick = "jassob", .user = "jassob", .host = "localhost" };
|
|
||||||
const msg = zircon.Message{
|
const msg = zircon.Message{
|
||||||
.PRIVMSG = .{
|
.PRIVMSG = .{ .prefix = null, .targets = "#eru-admin", .text = "!backlog 0" },
|
||||||
.prefix = prefix,
|
|
||||||
.targets = "#test",
|
|
||||||
.text = "hello world",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
_ = bot_adapter.callback(msg);
|
|
||||||
const cmd_msg = zircon.Message{
|
try std.testing.expectEqualDeep(
|
||||||
.PRIVMSG = .{
|
"no matching message",
|
||||||
.prefix = prefix,
|
BotAdapter.callback(&bot_adapter, msg).?.PRIVMSG.text,
|
||||||
.targets = "#test",
|
);
|
||||||
.text = "s/world/zig/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const response = bot_adapter.callback(cmd_msg);
|
|
||||||
try std.testing.expect(response != null);
|
|
||||||
try std.testing.expectEqualStrings("jassob: \"hello zig\"", response.?.PRIVMSG.text);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
89
src/parser.zig
Normal file
89
src/parser.zig
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn init(s: []const u8) Parser {
|
||||||
|
return .init(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Parser = struct {
|
||||||
|
original: []const u8,
|
||||||
|
rest: []const u8,
|
||||||
|
end_idx: usize,
|
||||||
|
|
||||||
|
// Initializes a Parser for s.
|
||||||
|
pub fn init(s: []const u8) Parser {
|
||||||
|
return .{
|
||||||
|
.original = s,
|
||||||
|
.end_idx = 0,
|
||||||
|
.rest = s,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek the parser window of the text forward skip bytes and return a new Parser.
|
||||||
|
pub fn seek(self: *const Parser, skip: usize) Parser {
|
||||||
|
return .{ .original = self.original, .rest = self.rest[skip..], .end_idx = self.end_idx + skip };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to consume at least one whitespace character from the input text.
|
||||||
|
pub fn consume_space(self: *const Parser) ?Parser {
|
||||||
|
if (!std.ascii.isWhitespace(self.rest[0])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (self.rest[1..], 1..) |c, idx| {
|
||||||
|
if (!std.ascii.isWhitespace(c)) {
|
||||||
|
return self.seek(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.seek(self.rest.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to consume a character c.
|
||||||
|
pub fn consume_char(self: *const Parser, c: u8) ?Parser {
|
||||||
|
if (self.rest[0] != c) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return self.seek(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to consume a string s.
|
||||||
|
pub fn consume_str(self: *const Parser, s: []const u8) ?Parser {
|
||||||
|
const len = s.len;
|
||||||
|
if (self.rest.len < len) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!std.mem.eql(u8, self.rest[0..len], s)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return self.seek(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the next occurrence of c (idx) in the current parser
|
||||||
|
// window and extracts it.
|
||||||
|
//
|
||||||
|
// Returns a new parser window that starts after idx and the
|
||||||
|
// extracted byte slice.
|
||||||
|
pub fn take_until_char(self: *const Parser, c: u8) ?struct { Parser, []const u8 } {
|
||||||
|
if (std.mem.indexOfScalar(u8, self.rest, c)) |idx| {
|
||||||
|
return .{ self.seek(idx), self.rest[0..idx] };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the current character and advance the parser one step.
|
||||||
|
pub fn take_char(self: *const Parser) struct { Parser, u8 } {
|
||||||
|
return .{ self.seek(1), self.rest[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the currently accepted text.
|
||||||
|
pub fn parsed(self: *const Parser) []const u8 {
|
||||||
|
return self.original[0..self.end_idx];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "parser can skip whitespace" {
|
||||||
|
var parser = init("Hello, World");
|
||||||
|
parser = parser.consume_str("Hello,").?;
|
||||||
|
parser = parser.consume_space().?;
|
||||||
|
parser = parser.consume_str("World").?;
|
||||||
|
|
||||||
|
try std.testing.expectEqual("Hello, World", parser.parsed());
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
//! By convention, root.zig is the root source file when making a library.
|
//! By convention, root.zig is the root source file when making a library.
|
||||||
|
|
||||||
pub const bot = @import("bot.zig");
|
pub const bot = @import("bot.zig");
|
||||||
|
pub const buffer = @import("buffer.zig");
|
||||||
|
pub const parser = @import("parser.zig");
|
||||||
|
pub const commands = @import("commands.zig");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue