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.
170 lines
5.7 KiB
Zig
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);
|
|
}
|