Browse Source
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
6 changed files with 360 additions and 42 deletions
@ -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`. |
||||
@ -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); |
||||
}, |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue