zigeru/src/main.zig
Jacob Jonsson 958c00ce6f
refactor(bot): rename bot.hear to bot.store
Clearer what it actually does (i.e. record a message in the backlog)
and also unlocks that name for a dispatch function where we hear any
message and decide what to do with it. Currently that dispatch logic
lives inside BotAdapter in main.zig and that is not ideal.
2026-03-15 13:50:17 +01:00

170 lines
5.7 KiB
Zig

const std = @import("std");
const zigeru = @import("zigeru");
const Bot = zigeru.bot.Bot;
const Error = zigeru.bot.Error;
const UserCommand = zigeru.commands.UserCommand;
const AdminCommand = zigeru.commands.AdminCommand;
const BotMessage = zigeru.bot.Message;
const BotResponse = zigeru.bot.Response;
const zircon = @import("zircon");
var debug_allocator = std.heap.DebugAllocator(.{}).init;
pub fn main() !void {
const allocator = debug_allocator.allocator();
defer _ = debug_allocator.deinit();
var channels: [0][]const u8 = .{};
// Create a zircon.Client with a given configuration.
var client = try zircon.Client.init(allocator, .{
.user = "eru",
.nick = "eru",
.real_name = "Eru (zigeru) bot",
.server = "irc.dtek.se",
.port = 6697,
.tls = true,
.channels = &channels,
});
defer client.deinit();
var bot_adapter = try BotAdapter.init(allocator);
defer bot_adapter.deinit();
client.register_message_closure(bot_adapter.closure());
// Connect to the IRC server and perform registration.
try client.connect();
try client.register();
try client.join("#eru-tests");
try client.join("#eru-admin");
client.loop(.{}) catch |err| {
std.debug.print("eru exited with error: {}\n", .{err});
return;
};
}
pub const Adapter = struct {
ptr: *anyopaque,
callbackFn: *const fn (*anyopaque, zircon.Message) ?zircon.Message,
};
pub const BotAdapter = struct {
bot: Bot,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) !BotAdapter {
return BotAdapter{
.bot = try Bot.init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *BotAdapter) void {
self.bot.deinit();
}
pub fn callback(self: *BotAdapter, message: zircon.Message) ?zircon.Message {
switch (message) {
.PRIVMSG => |msg| {
std.log.debug(
"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 },
);
const nick = if (msg.prefix) |prefix| if (prefix.nick) |nick| nick else "unknown" else "unknown";
if (UserCommand.parse(nick, msg.text)) |cmd| {
return toIRC(self.bot.execute(&cmd, msg.targets) catch |err| return report_error(err));
}
if (AdminCommand.parse(msg.text)) |cmd| {
return toIRC(self.bot.execute_admin(&cmd, "#eru-admin") catch |err| return report_error(err));
}
const bot_msg = BotMessage.init_owned(
self.allocator,
std.time.timestamp(),
nick,
msg.targets,
msg.text,
) catch |err| return report_error(err);
self.bot.store(bot_msg);
return null;
},
.JOIN => |msg| {
std.log.debug("received join message: channels {s}", .{msg.channels});
return null;
},
else => {
std.log.debug("received unknown message {}", .{message});
return null;
},
}
}
fn report_error(err: Error) zircon.Message {
const err_msg = switch (err) {
Error.NoMessage => "no matching message",
Error.OutOfMemory => "out of memory",
Error.WriteFailed => "write failed",
};
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 {
return .{
.ptr = self,
.callbackFn = BotAdapter.erased_callback,
};
}
};
/// toIRC converts a bot response and converts it to a IRC message.
fn toIRC(response: BotResponse) 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 },
},
}
}
test "substitute" {
var bot_adapter = try BotAdapter.init(std.testing.allocator);
defer bot_adapter.deinit();
const prefix = zircon.Prefix{ .nick = "jassob", .user = "jassob", .host = "localhost" };
const msg = zircon.Message{
.PRIVMSG = .{
.prefix = prefix,
.targets = "#test",
.text = "hello world",
},
};
if (bot_adapter.callback(msg)) |_| {
@panic("unexpected response");
}
const cmd_msg = zircon.Message{
.PRIVMSG = .{
.prefix = prefix,
.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);
}
test "get empty backlog message" {
var bot_adapter = try BotAdapter.init(std.testing.allocator);
defer bot_adapter.deinit();
const prefix = zircon.Prefix{ .nick = "jassob", .user = "jassob", .host = "localhost" };
const msg = zircon.Message{
.PRIVMSG = .{ .prefix = prefix, .targets = "#eru-admin", .text = "!backlog 0" },
};
try std.testing.expectEqualDeep("no matching message", bot_adapter.callback(msg).?.PRIVMSG.text);
}