Browse Source

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.
main
Jacob Jonsson 1 month ago
parent
commit
6449a7de2d
Signed by: jassob
GPG Key ID: 7E30B9B047F7202E
  1. 21
      README.md
  2. 133
      flake.lock
  3. 4
      flake.nix
  4. 183
      src/bot.zig
  5. 39
      src/main.zig
  6. 22
      src/root.zig

21
README.md

@ -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

@ -1,5 +1,60 @@
{
"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": {
"locked": {
"lastModified": 1761373498,
@ -16,9 +71,85 @@
"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": {
"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"
}
}
},

4
flake.nix

@ -3,15 +3,17 @@
inputs = {
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
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
devShells.x86_64-linux.default = pkgs.mkShell {
buildInputs = [
pkgs.zig
zls.packages.x86_64-linux.zls
];
};
};

183
src/bot.zig

@ -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);
},
}
}

39
src/main.zig

@ -3,25 +3,26 @@ const zigeru = @import("zigeru");
pub fn main() !void {
// Prints to stderr, ignoring potential errors.
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
try zigeru.bufferedPrint();
}
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());
}
const message = zigeru.bot.Message{
.author = "Jassob",
.timestamp = 12345,
.content = "All your base are belong to us.\n",
};
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));
}
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",
},
};
try std.testing.fuzz(Context{}, Context.testOne, .{});
const result = try bot.execute(&command);
switch (result) {
.post_message => |msg| {
std.debug.print("{s}", .{msg.content});
},
}
}

22
src/root.zig

@ -1,23 +1,3 @@
//! By convention, root.zig is the root source file when making a library.
const std = @import("std");
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);
}
pub const bot = @import("bot.zig");

Loading…
Cancel
Save