chore: implement initial bot with substitute command
This commit introduces a Bot struct which is the main type of this project. The Bot holds a backlog of messages to be able to update a typo in a message and so on. So far there is no IRC/TLS implementation, it is only the base logic.
This commit is contained in:
parent
00d2cf3c69
commit
6449a7de2d
6 changed files with 361 additions and 43 deletions
21
README.md
Normal file
21
README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# zigeru
|
||||||
|
|
||||||
|
zigeru is a IRC bot which implements the following commands:
|
||||||
|
|
||||||
|
- `s/OLD/NEW/` -- posts the previous message by the user, where every
|
||||||
|
occurrence of OLD is replaced by NEW.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
To enter into a development shell with all the tools needed for
|
||||||
|
interacting with this project, run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ nix develop
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the tests: `zig build test`.
|
||||||
|
|
||||||
|
To run the binary: `zig build run`.
|
||||||
|
|
||||||
|
To build the binary: `zig build`.
|
||||||
133
flake.lock
generated
133
flake.lock
generated
|
|
@ -1,5 +1,60 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696426674,
|
||||||
|
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1705309234,
|
||||||
|
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"zls",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1761373498,
|
"lastModified": 1761373498,
|
||||||
|
|
@ -16,9 +71,85 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1755704039,
|
||||||
|
"narHash": "sha256-gKlP0LbyJ3qX0KObfIWcp5nbuHSb5EHwIvU6UcNBg2A=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "9cb344e96d5b6918e94e1bca2d9f3ea1e9615545",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-25.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs",
|
||||||
|
"zls": "zls"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zig-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"zls",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1755864794,
|
||||||
|
"narHash": "sha256-hgnov6RLA+DD4Uocs/vCbiH3/3sKvqiJOKHpdhGyVAI=",
|
||||||
|
"owner": "mitchellh",
|
||||||
|
"repo": "zig-overlay",
|
||||||
|
"rev": "5cd601f8760d2383210b7b8c8a45fc79388f3ddf",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "mitchellh",
|
||||||
|
"repo": "zig-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zls": {
|
||||||
|
"inputs": {
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
"zig-overlay": "zig-overlay"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1756048867,
|
||||||
|
"narHash": "sha256-GFzSHUljcxy7sM1PaabbkQUdUnLwpherekPWJFxXtnk=",
|
||||||
|
"owner": "zigtools",
|
||||||
|
"repo": "zls",
|
||||||
|
"rev": "ce6c8f02c78e622421cfc2405c67c5222819ec03",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "zigtools",
|
||||||
|
"ref": "0.15.0",
|
||||||
|
"repo": "zls",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,17 @@
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||||
|
zls.url = "github:zigtools/zls?ref=0.15.0"; # TODO(jsb): Update this when bumping zig version
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs }:
|
outputs = { self, nixpkgs, zls }:
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||||
in {
|
in {
|
||||||
devShells.x86_64-linux.default = pkgs.mkShell {
|
devShells.x86_64-linux.default = pkgs.mkShell {
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
pkgs.zig
|
pkgs.zig
|
||||||
|
zls.packages.x86_64-linux.zls
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
183
src/bot.zig
Normal file
183
src/bot.zig
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const Command = union(enum) {
|
||||||
|
substitute: struct { author: []const u8, needle: []const u8, replacement: []const u8 },
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Result = union(enum) {
|
||||||
|
post_message: struct { content: []const u8 },
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
OutOfMemory,
|
||||||
|
NoMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Message = struct {
|
||||||
|
timestamp: u32,
|
||||||
|
author: []const u8,
|
||||||
|
content: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Bot = struct {
|
||||||
|
backlog: [1024]?Message,
|
||||||
|
top: usize,
|
||||||
|
bottom: usize,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
pub fn new(allocator: std.mem.Allocator) Bot {
|
||||||
|
return Bot{
|
||||||
|
.backlog = .{null} ** 1024,
|
||||||
|
.top = 0,
|
||||||
|
.bottom = 0,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(self: *Bot, cmd: *const Command) Error!Result {
|
||||||
|
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);
|
||||||
|
_ = std.mem.replace(u8, prev_msg.content, command.needle, command.replacement, output);
|
||||||
|
return Result{ .post_message = .{ .content = output } };
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hear(self: *Bot, msg: Message) void {
|
||||||
|
self.backlog[self.top] = msg;
|
||||||
|
self.top = (self.top + 1) % 1024;
|
||||||
|
if (self.top == self.bottom) self.bottom = (self.bottom + 1) % self.backlog.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_message(self: *Bot, comptime pred: *const fn (Message) bool) ?Message {
|
||||||
|
var idx = self.previous_idx(self.top);
|
||||||
|
while (true) : (idx = self.previous_idx(idx)) {
|
||||||
|
if (self.backlog[idx] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const message = self.backlog[idx] orelse unreachable;
|
||||||
|
if (pred(message)) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
if (idx == self.bottom) {
|
||||||
|
// reached the start of the list
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_message_by_author(self: *Bot, author: []const u8) ?Message {
|
||||||
|
var idx = self.previous_idx(self.top);
|
||||||
|
while (true) : (idx = self.previous_idx(idx)) {
|
||||||
|
if (self.backlog[idx] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const message = self.backlog[idx] orelse unreachable;
|
||||||
|
if (std.mem.eql(u8, message.author, author)) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
if (idx == self.bottom) {
|
||||||
|
// reached the start of the list
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "hear_wraps" {
|
||||||
|
var bot = Bot.new(std.testing.allocator);
|
||||||
|
const testMessage = Message{
|
||||||
|
.author = "Jassob",
|
||||||
|
.timestamp = 12345,
|
||||||
|
.content = "All your codebase are belong to us.\n",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (0..1025) |_| {
|
||||||
|
bot.hear(testMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
try std.testing.expect(bot.top == 1);
|
||||||
|
try std.testing.expect(bot.bottom == 2);
|
||||||
|
try std.testing.expect(bot.no_messages() == 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "previous_message" {
|
||||||
|
var bot = Bot.new(std.testing.allocator);
|
||||||
|
|
||||||
|
const callback = struct {
|
||||||
|
fn callback(_: Message) bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}.callback;
|
||||||
|
const prev = bot.previous_message(callback);
|
||||||
|
|
||||||
|
try std.testing.expect(prev == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "previous_message1" {
|
||||||
|
var bot = Bot.new(std.testing.allocator);
|
||||||
|
var contents: [10][]const u8 = .{undefined} ** 10;
|
||||||
|
|
||||||
|
for (0..10) |i| {
|
||||||
|
contents[i] = try std.fmt.allocPrint(std.testing.allocator, "{d}", .{i});
|
||||||
|
const msg = Message{
|
||||||
|
.author = "Jassob",
|
||||||
|
.timestamp = @as(u32, @intCast(i)),
|
||||||
|
.content = contents[i],
|
||||||
|
};
|
||||||
|
bot.hear(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const callback = struct {
|
||||||
|
fn callback(msg: Message) bool {
|
||||||
|
return std.mem.eql(u8, msg.content, "8");
|
||||||
|
}
|
||||||
|
}.callback;
|
||||||
|
const prev = bot.previous_message(callback);
|
||||||
|
|
||||||
|
for (contents) |str| {
|
||||||
|
std.testing.allocator.free(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
try std.testing.expect(prev != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "execute_substitution_no_previous_message" {
|
||||||
|
var bot = Bot.new(std.testing.allocator);
|
||||||
|
const cmd = Command{ .substitute = .{ .author = "jassob", .needle = "What", .replacement = "what" } };
|
||||||
|
try std.testing.expectError(Error.NoMessage, bot.execute(&cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "execute_substitution" {
|
||||||
|
var bot = Bot.new(std.testing.allocator);
|
||||||
|
bot.hear(Message{ .timestamp = 1234, .author = "jassob", .content = "What" });
|
||||||
|
const cmd = Command{ .substitute = .{ .author = "jassob", .needle = "What", .replacement = "what" } };
|
||||||
|
const result = try bot.execute(&cmd);
|
||||||
|
switch (result) {
|
||||||
|
.post_message => |message| {
|
||||||
|
try std.testing.expectEqualDeep(message.content, "what");
|
||||||
|
std.testing.allocator.free(message.content);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/main.zig
41
src/main.zig
|
|
@ -3,25 +3,26 @@ const zigeru = @import("zigeru");
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
// Prints to stderr, ignoring potential errors.
|
// Prints to stderr, ignoring potential errors.
|
||||||
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
|
const message = zigeru.bot.Message{
|
||||||
try zigeru.bufferedPrint();
|
.author = "Jassob",
|
||||||
}
|
.timestamp = 12345,
|
||||||
|
.content = "All your base are belong to us.\n",
|
||||||
test "simple test" {
|
|
||||||
const gpa = std.testing.allocator;
|
|
||||||
var list: std.ArrayList(i32) = .empty;
|
|
||||||
defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
|
|
||||||
try list.append(gpa, 42);
|
|
||||||
try std.testing.expectEqual(@as(i32, 42), list.pop());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "fuzz example" {
|
|
||||||
const Context = struct {
|
|
||||||
fn testOne(context: @This(), input: []const u8) anyerror!void {
|
|
||||||
_ = context;
|
|
||||||
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
|
|
||||||
try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
try std.testing.fuzz(Context{}, Context.testOne, .{});
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = try bot.execute(&command);
|
||||||
|
switch (result) {
|
||||||
|
.post_message => |msg| {
|
||||||
|
std.debug.print("{s}", .{msg.content});
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
src/root.zig
22
src/root.zig
|
|
@ -1,23 +1,3 @@
|
||||||
//! 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.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
pub const bot = @import("bot.zig");
|
||||||
pub fn bufferedPrint() !void {
|
|
||||||
// Stdout is for the actual output of your application, for example if you
|
|
||||||
// are implementing gzip, then only the compressed bytes should be sent to
|
|
||||||
// stdout, not any debugging messages.
|
|
||||||
var stdout_buffer: [1024]u8 = undefined;
|
|
||||||
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
|
|
||||||
const stdout = &stdout_writer.interface;
|
|
||||||
|
|
||||||
try stdout.print("Run `zig build test` to run the tests.\n", .{});
|
|
||||||
|
|
||||||
try stdout.flush(); // Don't forget to flush!
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(a: i32, b: i32) i32 {
|
|
||||||
return a + b;
|
|
||||||
}
|
|
||||||
|
|
||||||
test "basic add functionality" {
|
|
||||||
try std.testing.expect(add(3, 7) == 10);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue