diff --git a/source/ircd/connection.d b/source/ircd/connection.d index f2241cd..ff05879 100644 --- a/source/ircd/connection.d +++ b/source/ircd/connection.d @@ -13,6 +13,7 @@ import vibe.stream.operations; import ircd.message; import ircd.server; import ircd.channel; +import ircd.helpers; class Connection { @@ -26,11 +27,12 @@ class Connection string hostname; char[] modes; - @property string mask() { return nick ~ "!" ~ user ~ "@" ~ hostname; } - @property auto channels() { return _server.channels.filter!(c => c.members.canFind(this)); } + @property string mask() { return nick ~ "!" ~ user ~ "@" ~ hostname; } @property bool registered() { return nick !is null && user !is null; } + @property bool isOperator() { return modes.canFind('o') || modes.canFind('O'); } + @property string servername() { return _server.name; } //TODO: Support server linking bool connected; @@ -52,6 +54,11 @@ class Connection return 0; } + bool visibleTo(Connection other) + { + return !modes.canFind('i') || channels.any!(c => c.members.canFind(other)); + } + void send(Message message) { string messageString = message.toString; @@ -127,6 +134,10 @@ class Connection if(!registered) sendErrNotRegistered(); else onPrivMsg(message); break; + case "WHO": + if(!registered) sendErrNotRegistered(); + else onWho(message); + break; default: writeln("unknown command '", message.command, "'"); send(Message(_server.name, "421", [nick, message.command, "Unknown command"])); @@ -287,7 +298,7 @@ class Connection } else { - _server.sendToChannel(this, target, text); + _server.privmsgToChannel(this, target, text); } } else if(Server.isValidNick(target)) @@ -298,7 +309,7 @@ class Connection } else { - _server.sendToUser(this, target, text); + _server.privmsgToUser(this, target, text); } } else @@ -308,6 +319,31 @@ class Connection } } + void onWho(Message message) + { + if(message.parameters.length == 0) + { + _server.whoGlobal(this, "*", false); + } + else + { + auto mask = message.parameters[0]; + auto operatorsOnly = message.parameters.length > 1 && message.parameters[1] == "o"; + + if(_server.isValidChannelName(mask) && _server.channels.canFind!(c => c.name == mask)) + { + _server.whoChannel(this, mask, operatorsOnly); + } + else + { + _server.whoGlobal(this, mask == "0" ? "*" : mask, operatorsOnly); + } + } + + auto name = message.parameters.length == 0 ? "*" : message.parameters[0]; + send(Message(_server.name, "315", [nick, name, "End of WHO list"], true)); + } + void sendErrNoSuchNick(string name) { send(Message(_server.name, "401", [nick, name, "No such nick/channel"], true)); @@ -333,6 +369,15 @@ class Connection send(Message(_server.name, "461", [nick, command, "Not enough parameters"], true)); } + void sendWhoReply(string channel, Connection user, uint hopCount) + { + auto flags = user.modes.canFind('a') ? "G" : "H"; + if(user.isOperator) flags ~= "*"; + //TODO: Add channel prefix + + send(Message(_server.name, "352", [nick, channel, user.user, user.hostname, user.servername, user.nick, flags, hopCount.to!string ~ " " ~ user.realname], true)); + } + void sendWelcome() { send(Message(_server.name, "001", [nick, "Welcome to the Internet Relay Network " ~ mask], true)); diff --git a/source/ircd/helpers.d b/source/ircd/helpers.d new file mode 100644 index 0000000..56cfec2 --- /dev/null +++ b/source/ircd/helpers.d @@ -0,0 +1,43 @@ +module ircd.helpers; + +import std.range; + +//Based on std.path.globMatch (https://github.com/dlang/phobos/blob/v2.073.2/std/path.d#L3164) +//License: Boost License 1.0 (http://www.boost.org/LICENSE_1_0.txt) +//Copyright (c) Lars T. Kyllingstad, Walter Bright +@safe pure +bool wildcardMatch(string input, string pattern) +{ + + foreach (ref pi; 0 .. pattern.length) + { + const pc = pattern[pi]; + switch (pc) + { + case '*': + if (pi + 1 == pattern.length) + return true; + for (; !input.empty; input.popFront()) + { + auto p = input.save; + if (wildcardMatch(p, pattern[pi + 1 .. pattern.length])) + return true; + } + return false; + case '?': + if (input.empty) + return false; + input.popFront(); + break; + default: + if (input.empty) + return false; + if (pc != input.front) + return false; + input.popFront(); + break; + } + } + return input.empty; +} + diff --git a/source/ircd/server.d b/source/ircd/server.d index 81309c8..20bb7cd 100644 --- a/source/ircd/server.d +++ b/source/ircd/server.d @@ -14,6 +14,7 @@ import ircd.packageVersion; import ircd.message; import ircd.connection; import ircd.channel; +import ircd.helpers; class Server { @@ -143,13 +144,39 @@ class Server } } - void sendToChannel(Connection sender, string target, string text) + void whoChannel(Connection origin, string channelName, bool operatorsOnly) + { + //TODO: Check what RFCs say about secret/private channels + + auto channel = channels.find!(c => c.name == channelName)[0]; + foreach(c; channel.members.filter!(c => !operatorsOnly || c.isOperator) + .filter!(c => c.visibleTo(origin))) + { + //TODO: Support hop count + origin.sendWhoReply(channelName, c, 0); + } + } + + void whoGlobal(Connection origin, string mask, bool operatorsOnly) + { + foreach(c; connections.filter!(c => c.visibleTo(origin)) + .filter!(c => !operatorsOnly || c.isOperator) + .filter!(c => [c.hostname, c.servername, c.realname, c.nick].any!(n => wildcardMatch(n, mask)))) + { + //TODO: Don't leak secret/private channels (even if the RFCs don't say anything about it?) + auto channelName = c.channels.empty ? "*" : c.channels.array[0].name; + //TODO: Support hop count + origin.sendWhoReply(channelName, c, 0); + } + } + + void privmsgToChannel(Connection sender, string target, string text) { auto channel = channels.find!(c => c.name == target)[0]; channel.sendPrivMsg(sender, text); } - void sendToUser(Connection sender, string target, string text) + void privmsgToUser(Connection sender, string target, string text) { auto user = connections.find!(c => c.nick == target)[0]; user.send(Message(sender.mask, "PRIVMSG", [target, text], true));