Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
Les De Ridder | 8e461051cd | |
Les De Ridder | 5eea8a71f1 | |
Les De Ridder | 95f3d8fabd | |
Les De Ridder | c77709a813 | |
Les De Ridder | d62993c800 | |
Les De Ridder | be963dcf29 |
39
README.md
39
README.md
|
@ -1,19 +1,42 @@
|
|||
# salty-ircd
|
||||
|
||||
salty-ircd is an [Internet Relay Chat](https://en.wikipedia.org/wiki/Internet_Relay_Chat) daemon written in [D](https://dlang.org/).
|
||||
|
||||
## Goals
|
||||
The main goals of salty-ircd are strict RFC compliance and security.
|
||||
|
||||
### RFC compliance
|
||||
salty-ircd aims to be fully compliant with the IRC RFCs (in 'RFC mode'), specifically [RFC 1459](https://tools.ietf.org/html/rfc1459), [RFC 2811](https://tools.ietf.org/html/rfc2811), [RFC 2812](https://tools.ietf.org/html/rfc2812), and [RFC 2813](https://tools.ietf.org/html/rfc2813) (planned).
|
||||
Newer RFCs take precedence over older RFCs. Errata are respected by default, with newer errata (on the same RFC) taking precedence over older errata.
|
||||
salty-ircd aims to be fully compliant with the IRC RFCs (by default), specifically [RFC 1459](https://tools.ietf.org/html/rfc1459), [RFC 2811](https://tools.ietf.org/html/rfc2811), [RFC 2812](https://tools.ietf.org/html/rfc2812), and [RFC 2813](https://tools.ietf.org/html/rfc2813) (planned), including all errata.
|
||||
Newer RFCs take precedence over older RFCs.
|
||||
|
||||
Any additional features breaking RFC compliance will be made available through compile-time options (i.e. 'non-RFC modes').
|
||||
Any additional features breaking RFC compliance are available through compile-time options.
|
||||
|
||||
### Security (non-RFC modes)
|
||||
* salty-ircd will require TLS for all connections. An exception could be made to allow hosting a Tor hidden service.
|
||||
* salty-ircd will require TLS client certificates for authentication.
|
||||
### Security
|
||||
The following rules apply when any compile-time option is enabled (breaking strict RFC compliance):
|
||||
|
||||
* TLS is required for all connections, except connections from localhost (useful for running a Tor hidden service, which already has encryption)
|
||||
* TLS client certificates are required for oper and vhost authentication
|
||||
|
||||
## Building
|
||||
Build dependencies:
|
||||
* A D compiler
|
||||
* dub
|
||||
* fish
|
||||
* git
|
||||
|
||||
Build command:
|
||||
* RFC compliant: `dub build`
|
||||
* Modern (all additional features): `dub build -c=modern`
|
||||
|
||||
The 'modern' configuration aims to be mostly compatible with UnrealIRCd user modes/chars.
|
||||
|
||||
When `-d=ProxyV1` is added to the build command, salty-ircd can accept traffic through the PROXY protocol (version 1), e.g. from an UnrealIRCd server running with [a custom module](https://github.com/lesderid/unrealircd5_proxyv1_copy). Please see this module's readme for more information. Note that this is simply a development/debugging option and should not be used for a production server.
|
||||
|
||||
TODO: Add a method to supply a custom list of compile-time options.
|
||||
|
||||
## Running
|
||||
First, create the configuration file, `config.sdl`. You can find a template in `config.template.sdl`.
|
||||
|
||||
Then, simply run `./out/salty-ircd`.
|
||||
|
||||
## License
|
||||
The source code for salty-ircd is licensed under the [University of Illinois/NCSA Open Source License](LICENSE).
|
||||
[University of Illinois/NCSA Open Source License](LICENSE).
|
||||
|
|
4
dub.sdl
4
dub.sdl
|
@ -11,9 +11,9 @@ preBuildCommands "./generate-version-info.fish"
|
|||
versions "VibeDefaultMain"
|
||||
targetPath "out"
|
||||
|
||||
configuration "application" {
|
||||
configuration "compliant" {
|
||||
}
|
||||
|
||||
configuration "modern" {
|
||||
versions "BasicFixes" "SupportTLS"
|
||||
versions "Modern"
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import ircd.connection;
|
|||
import ircd.server;
|
||||
import ircd.message;
|
||||
import ircd.helpers;
|
||||
import ircd.numerics;
|
||||
|
||||
//TODO: Make this a struct?
|
||||
class Channel
|
||||
|
@ -96,16 +97,14 @@ class Channel
|
|||
|
||||
auto onChannel = members.canFind(connection);
|
||||
|
||||
connection.send(Message(_server.name, "353", [
|
||||
connection.nick, channelType, name,
|
||||
members.filter!(m => onChannel || !m.modes.canFind('i'))
|
||||
.map!(m => prefixedNick(m))
|
||||
.join(' ')
|
||||
], true));
|
||||
connection.sendNumeric!RPL_NAMREPLY(channelType, name,
|
||||
members.filter!(m => onChannel || !m.modes.canFind('i'))
|
||||
.map!(m => prefixedNick(m))
|
||||
.join(' '));
|
||||
|
||||
if (sendRplEndOfNames)
|
||||
{
|
||||
connection.sendRplEndOfNames(name);
|
||||
connection.sendNumeric!RPL_ENDOFNAMES(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,17 +127,9 @@ class Channel
|
|||
void sendTopic(Connection connection)
|
||||
{
|
||||
if (topic.empty)
|
||||
{
|
||||
connection.send(Message(_server.name, "331", [
|
||||
connection.nick, name, "No topic is set"
|
||||
]));
|
||||
}
|
||||
connection.sendNumeric!RPL_NOTOPIC(name);
|
||||
else
|
||||
{
|
||||
connection.send(Message(_server.name, "332", [
|
||||
connection.nick, name, topic
|
||||
], true));
|
||||
}
|
||||
connection.sendNumeric!RPL_TOPIC(name, topic);
|
||||
}
|
||||
|
||||
void setTopic(Connection connection, string newTopic)
|
||||
|
@ -183,9 +174,8 @@ class Channel
|
|||
specialModeParameters ~= memberLimit.to!string;
|
||||
}
|
||||
|
||||
user.send(Message(_server.name, "324", [
|
||||
user.nick, name, "+" ~ modes.idup ~ specialModes
|
||||
] ~ specialModeParameters));
|
||||
user.sendNumeric!RPL_CHANNELMODEIS([name,
|
||||
"+" ~ modes.idup ~ specialModes] ~ specialModeParameters);
|
||||
}
|
||||
|
||||
bool setMemberMode(Connection target, char mode)
|
||||
|
@ -280,42 +270,30 @@ class Channel
|
|||
{
|
||||
foreach (entry; maskLists['b'])
|
||||
{
|
||||
connection.send(Message(_server.name, "367", [
|
||||
connection.nick, name, entry
|
||||
], false));
|
||||
connection.sendNumeric!RPL_BANLIST(name, entry);
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "368", [
|
||||
connection.nick, name, "End of channel ban list"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_ENDOFBANLIST(name);
|
||||
}
|
||||
|
||||
void sendExceptList(Connection connection)
|
||||
{
|
||||
foreach (entry; maskLists['e'])
|
||||
{
|
||||
connection.send(Message(_server.name, "348", [
|
||||
connection.nick, name, entry
|
||||
], false));
|
||||
connection.sendNumeric!RPL_EXCEPTLIST(name, entry);
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "349", [
|
||||
connection.nick, name, "End of channel exception list"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_ENDOFEXCEPTLIST(name);
|
||||
}
|
||||
|
||||
void sendInviteList(Connection connection)
|
||||
{
|
||||
foreach (entry; maskLists['I'])
|
||||
{
|
||||
connection.send(Message(_server.name, "346", [
|
||||
connection.nick, name, entry
|
||||
], false));
|
||||
connection.sendNumeric!RPL_INVITELIST(name, entry);
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "347", [
|
||||
connection.nick, name, "End of channel invite list"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_ENDOFINVITELIST(name);
|
||||
}
|
||||
|
||||
bool setKey(string key)
|
||||
|
|
|
@ -11,6 +11,7 @@ import std.datetime;
|
|||
|
||||
import vibe.core.core;
|
||||
import vibe.core.net;
|
||||
import vibe.core.stream : IOMode;
|
||||
import vibe.stream.operations : readLine;
|
||||
|
||||
import ircd.versionInfo;
|
||||
|
@ -19,6 +20,7 @@ import ircd.message;
|
|||
import ircd.server;
|
||||
import ircd.channel;
|
||||
import ircd.helpers;
|
||||
import ircd.numerics;
|
||||
|
||||
//TODO: Make this a struct?
|
||||
class Connection
|
||||
|
@ -40,6 +42,9 @@ class Connection
|
|||
|
||||
string pass = null;
|
||||
|
||||
debug (ProxyV1)
|
||||
bool proxy;
|
||||
|
||||
@property auto channels()
|
||||
{
|
||||
return _server.channels.filter!(c => c.members.canFind(this));
|
||||
|
@ -121,7 +126,21 @@ class Connection
|
|||
{
|
||||
string messageString = message.toString;
|
||||
writeln("S> " ~ messageString);
|
||||
_connection.write(messageString ~ "\r\n");
|
||||
|
||||
auto messageBytes = cast(const(ubyte)[]) (messageString ~ "\r\n");
|
||||
auto bytesSent = _connection.write(messageBytes, IOMode.once);
|
||||
|
||||
if (bytesSent < 0)
|
||||
{
|
||||
writeln("client disconnected (write error)");
|
||||
closeConnection();
|
||||
}
|
||||
}
|
||||
|
||||
void sendNumeric(alias numeric)(string[] params...)
|
||||
{
|
||||
auto message = Message(_server.name, numeric.number, [nick] ~ params ~ numeric.params);
|
||||
send(message);
|
||||
}
|
||||
|
||||
void closeConnection()
|
||||
|
@ -167,10 +186,19 @@ class Connection
|
|||
continue;
|
||||
}
|
||||
|
||||
debug (ProxyV1)
|
||||
if (message.command == "PROXY")
|
||||
{
|
||||
proxy = true;
|
||||
hostname = getHost(message.parameters[1]);
|
||||
}
|
||||
|
||||
//NOTE: The RFCs don't specify whether commands are case-sensitive
|
||||
version (BasicFixes)
|
||||
{
|
||||
message.command = message.command.map!toUpper.to!string;
|
||||
message.command = message.command
|
||||
.map!toUpper
|
||||
.to!string;
|
||||
}
|
||||
|
||||
//NOTE: The RFCs don't specify what 'being idle' means
|
||||
|
@ -185,7 +213,10 @@ class Connection
|
|||
if (!registered && !["NICK", "USER", "PASS", "PING", "PONG",
|
||||
"QUIT"].canFind(message.command))
|
||||
{
|
||||
sendErrNotRegistered();
|
||||
//NOTE: This actually does not work if NICK hasn't been sent
|
||||
// The first parameter for numerics is the client's nick.
|
||||
// This makes it technically impossible to correctly implement the RFCs.
|
||||
sendNumeric!ERR_NOTREGISTERED();
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -274,23 +305,21 @@ class Connection
|
|||
break;
|
||||
default:
|
||||
writeln("unknown command '", message.command, "'");
|
||||
send(Message(_server.name, "421", [
|
||||
nick, message.command, "Unknown command"
|
||||
]));
|
||||
sendNumeric!ERR_UNKNOWNCOMMAND();
|
||||
continue;
|
||||
}
|
||||
|
||||
_server.updateCommandStatistics(message);
|
||||
}
|
||||
|
||||
onDisconnect();
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
void onNick(Message message)
|
||||
{
|
||||
if (message.parameters.length == 0)
|
||||
{
|
||||
sendErrNoNickGiven();
|
||||
sendNumeric!ERR_NONICKNAMEGIVEN();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -298,17 +327,13 @@ class Connection
|
|||
|
||||
if (!_server.isNickAvailable(newNick) && newNick.toIRCLower != nick.toIRCLower)
|
||||
{
|
||||
send(Message(_server.name, "433", [
|
||||
nick, newNick, "Nickname is already in use"
|
||||
]));
|
||||
sendNumeric!ERR_NICKNAMEINUSE(newNick);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_server.isValidNick(newNick))
|
||||
{
|
||||
send(Message(_server.name, "432", [
|
||||
nick, newNick, "Erroneous nickname"
|
||||
]));
|
||||
sendNumeric!ERR_ERRONEUSNICKNAME(newNick);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -326,7 +351,7 @@ class Connection
|
|||
{
|
||||
sendWelcome();
|
||||
}
|
||||
else if (registrationAttempted)
|
||||
else if (!wasRegistered && registrationAttempted)
|
||||
{
|
||||
onIncorrectPassword();
|
||||
}
|
||||
|
@ -336,15 +361,13 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length < 4)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
if (user !is null)
|
||||
{
|
||||
send(Message(_server.name, "462", [
|
||||
nick, "Unauthorized command (already registered)"
|
||||
], true));
|
||||
sendNumeric!ERR_ALREADYREGISTRED();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -354,7 +377,8 @@ class Connection
|
|||
user = message.parameters[0];
|
||||
modes = modeMaskToModes(message.parameters[1]);
|
||||
realname = message.parameters[3];
|
||||
hostname = getHost();
|
||||
debug (ProxyV1) {}
|
||||
else hostname = getHost();
|
||||
|
||||
if (!wasRegistered && registered)
|
||||
{
|
||||
|
@ -370,15 +394,13 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length < 1)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
if (registered)
|
||||
{
|
||||
send(Message(_server.name, "462", [
|
||||
nick, "Unauthorized command (already registered)"
|
||||
], true));
|
||||
sendNumeric!ERR_ALREADYREGISTRED();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -404,7 +426,7 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length == 0)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -423,7 +445,7 @@ class Connection
|
|||
{
|
||||
if (!Server.isValidChannelName(channelName))
|
||||
{
|
||||
sendErrNoSuchChannel(channelName);
|
||||
sendNumeric!ERR_NOSUCHCHANNEL(channelName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -444,32 +466,24 @@ class Connection
|
|||
if (!channel.memberLimit.isNull
|
||||
&& channel.members.length >= channel.memberLimit.get)
|
||||
{
|
||||
send(Message(_server.name, "471", [
|
||||
nick, channelName, "Cannot join channel (+l)"
|
||||
]));
|
||||
sendNumeric!ERR_CHANNELISFULL(channelName);
|
||||
}
|
||||
else if (channel.modes.canFind('i')
|
||||
&& !(channel.maskLists['I'].any!(m => matchesMask(m))
|
||||
|| channel.inviteHolders.canFind(this)))
|
||||
{
|
||||
send(Message(_server.name, "473", [
|
||||
nick, channelName, "Cannot join channel (+i)"
|
||||
]));
|
||||
sendNumeric!ERR_INVITEONLYCHAN(channelName);
|
||||
}
|
||||
else if (channel.maskLists['b'].any!(m => matchesMask(m))
|
||||
&& !channel.maskLists['e'].any!(m => matchesMask(m))
|
||||
&& !channel.inviteHolders.canFind(this))
|
||||
{
|
||||
send(Message(_server.name, "474", [
|
||||
nick, channelName, "Cannot join channel (+b)"
|
||||
]));
|
||||
sendNumeric!ERR_BANNEDFROMCHAN(channelName);
|
||||
}
|
||||
else if (channel.key !is null && (channelKeys.length < i + 1
|
||||
|| channelKeys[i] != channel.key))
|
||||
{
|
||||
send(Message(_server.name, "475", [
|
||||
nick, channelName, "Cannot join channel (+k)"
|
||||
]));
|
||||
sendNumeric!ERR_BADCHANNELKEY(channelName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -484,7 +498,7 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length == 0)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -493,25 +507,19 @@ class Connection
|
|||
{
|
||||
if (!Server.isValidChannelName(channel))
|
||||
{
|
||||
sendErrNoSuchChannel(channel);
|
||||
sendNumeric!ERR_NOSUCHCHANNEL(channel);
|
||||
}
|
||||
else if (!_server.canFindChannelByName(channel)
|
||||
|| !channels.canFind!(c => c.name.toIRCLower == channel.toIRCLower))
|
||||
{
|
||||
send(Message(_server.name, "442", [
|
||||
nick, channel, "You're not on that channel"
|
||||
], true));
|
||||
sendNumeric!ERR_NOTONCHANNEL(channel);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (message.parameters.length > 1)
|
||||
{
|
||||
_server.part(this, channel, message.parameters[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
_server.part(this, channel, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -524,14 +532,12 @@ class Connection
|
|||
|
||||
if (message.parameters.length == 0)
|
||||
{
|
||||
send(Message(_server.name, "411", [
|
||||
nick, "No recipient given (PRIVMSG)"
|
||||
], true));
|
||||
sendNumeric!ERR_NORECIPIENT_PRIVMSG();
|
||||
return;
|
||||
}
|
||||
if (message.parameters.length == 1)
|
||||
{
|
||||
send(Message(_server.name, "412", [nick, "No text to send"], true));
|
||||
sendNumeric!ERR_NOTEXTTOSEND();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -540,13 +546,11 @@ class Connection
|
|||
auto channelRange = _server.findChannelByName(target);
|
||||
if (channelRange.empty)
|
||||
{
|
||||
sendErrNoSuchNick(target);
|
||||
sendNumeric!ERR_NOSUCHNICK(target);
|
||||
}
|
||||
else if (!channelRange[0].canReceiveMessagesFromUser(this))
|
||||
{
|
||||
send(Message(_server.name, "404", [
|
||||
nick, target, "Cannot send to channel"
|
||||
], true));
|
||||
sendNumeric!ERR_CANNOTSENDTOCHAN(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -557,7 +561,7 @@ class Connection
|
|||
{
|
||||
if (!_server.canFindConnectionByNick(target))
|
||||
{
|
||||
sendErrNoSuchNick(target);
|
||||
sendNumeric!ERR_NOSUCHNICK(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -566,30 +570,35 @@ class Connection
|
|||
auto targetUser = _server.findConnectionByNick(target)[0];
|
||||
if (targetUser.modes.canFind('a'))
|
||||
{
|
||||
sendRplAway(target, targetUser.awayMessage);
|
||||
sendNumeric!RPL_AWAY(target, targetUser.awayMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//is this the right reply?
|
||||
sendErrNoSuchNick(target);
|
||||
sendNumeric!ERR_NOSUCHNICK(target);
|
||||
}
|
||||
}
|
||||
|
||||
void onNotice(Message message)
|
||||
{
|
||||
//TODO: Support special message targets
|
||||
auto target = message.parameters[0];
|
||||
auto text = message.parameters[1];
|
||||
|
||||
//TODO: Figure out what we are allowed to send exactly
|
||||
|
||||
if (message.parameters.length < 2)
|
||||
if (message.parameters.length == 0)
|
||||
{
|
||||
sendNumeric!ERR_NORECIPIENT_NOTICE();
|
||||
return;
|
||||
}
|
||||
auto target = message.parameters[0];
|
||||
|
||||
if (message.parameters.length == 1)
|
||||
{
|
||||
sendNumeric!ERR_NOTEXTTOSEND();
|
||||
return;
|
||||
}
|
||||
auto text = message.parameters[1];
|
||||
|
||||
//TODO: Fix replies
|
||||
if (Server.isValidChannelName(target) && _server.canFindChannelByName(target))
|
||||
{
|
||||
_server.noticeToChannel(this, target, text);
|
||||
|
@ -622,7 +631,7 @@ class Connection
|
|||
}
|
||||
|
||||
auto name = message.parameters.length == 0 ? "*" : message.parameters[0];
|
||||
send(Message(_server.name, "315", [nick, name, "End of WHO list"], true));
|
||||
sendNumeric!RPL_ENDOFWHO(name);
|
||||
}
|
||||
|
||||
void onAway(Message message)
|
||||
|
@ -631,17 +640,13 @@ class Connection
|
|||
{
|
||||
removeMode('a');
|
||||
awayMessage = null;
|
||||
send(Message(_server.name, "305", [
|
||||
nick, "You are no longer marked as being away"
|
||||
], true));
|
||||
sendNumeric!RPL_UNAWAY();
|
||||
}
|
||||
else
|
||||
{
|
||||
modes ~= 'a';
|
||||
awayMessage = message.parameters[0];
|
||||
send(Message(_server.name, "306", [
|
||||
nick, "You have been marked as being away"
|
||||
], true));
|
||||
sendNumeric!RPL_NOWAWAY();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -649,7 +654,7 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length == 0)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -661,15 +666,9 @@ class Connection
|
|||
{
|
||||
//NOTE: The RFCs don't allow ERR_NOSUCHCHANNEL as a response to TOPIC
|
||||
version (BasicFixes)
|
||||
{
|
||||
sendErrNoSuchChannel(channelName);
|
||||
}
|
||||
sendNumeric!ERR_NOSUCHCHANNEL(channelName);
|
||||
else
|
||||
{
|
||||
send(Message(_server.name, "331", [
|
||||
nick, channelName, "No topic is set"
|
||||
], true));
|
||||
}
|
||||
sendNumeric!RPL_NOTOPIC(channelName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -681,13 +680,13 @@ class Connection
|
|||
auto newTopic = message.parameters[1];
|
||||
if (!channels.canFind!(c => c.name.toIRCLower == channelName.toIRCLower))
|
||||
{
|
||||
sendErrNotOnChannel(channelName);
|
||||
sendNumeric!ERR_NOTONCHANNEL(channelName);
|
||||
}
|
||||
else if (channels.find!(c => c.name.toIRCLower == channelName.toIRCLower)
|
||||
.map!(c => c.modes.canFind('t') && !c.memberModes[this].canFind('o'))
|
||||
.array[0])
|
||||
{
|
||||
sendErrChanopPrivsNeeded(channelName);
|
||||
sendNumeric!ERR_CHANOPRIVSNEEDED(channelName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -719,7 +718,7 @@ class Connection
|
|||
}
|
||||
else
|
||||
{
|
||||
sendRplEndOfNames(channelName);
|
||||
sendNumeric!RPL_ENDOFNAMES(channelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -748,7 +747,7 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length < 2)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -756,7 +755,7 @@ class Connection
|
|||
auto targetUserRange = _server.findConnectionByNick(targetNick);
|
||||
if (targetUserRange.empty)
|
||||
{
|
||||
sendErrNoSuchNick(targetNick);
|
||||
sendNumeric!ERR_NOSUCHNICK(targetNick);
|
||||
return;
|
||||
}
|
||||
auto targetUser = targetUserRange[0];
|
||||
|
@ -767,11 +766,11 @@ class Connection
|
|||
{
|
||||
_server.invite(this, targetUser.nick, channelName);
|
||||
|
||||
sendRplInviting(channelName, targetUser.nick);
|
||||
sendNumeric!RPL_INVITING(targetUser.nick, channelName);
|
||||
|
||||
if (targetUser.modes.canFind('a'))
|
||||
{
|
||||
sendRplAway(targetUser.nick, targetUser.awayMessage);
|
||||
sendNumeric!RPL_AWAY(targetUser.nick, targetUser.awayMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -779,28 +778,25 @@ class Connection
|
|||
auto channel = channelRange[0];
|
||||
if (!channel.members.canFind(this))
|
||||
{
|
||||
sendErrNotOnChannel(channel.name);
|
||||
sendNumeric!ERR_NOTONCHANNEL(channel.name);
|
||||
}
|
||||
else if (channel.members.canFind(targetUser))
|
||||
{
|
||||
send(Message(_server.name, "443", [
|
||||
nick, targetUser.nick, channel.name,
|
||||
"is already on channel"
|
||||
], true));
|
||||
sendNumeric!ERR_USERONCHANNEL(targetUser.nick, channel.name);
|
||||
}
|
||||
else if (channel.modes.canFind('i') && !channel.memberModes[this].canFind('o'))
|
||||
{
|
||||
sendErrChanopPrivsNeeded(channel.name);
|
||||
sendNumeric!ERR_CHANOPRIVSNEEDED(channel.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_server.invite(this, targetUser.nick, channel.name);
|
||||
|
||||
sendRplInviting(channel.name, targetUser.nick);
|
||||
sendNumeric!RPL_INVITING(targetUser.nick, channel.name);
|
||||
|
||||
if (targetUser.modes.canFind('a'))
|
||||
{
|
||||
sendRplAway(targetUser.nick, targetUser.awayMessage);
|
||||
sendNumeric!RPL_AWAY(targetUser.nick, targetUser.awayMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -835,7 +831,7 @@ class Connection
|
|||
}
|
||||
else if (_server.motd is null)
|
||||
{
|
||||
send(Message(_server.name, "422", [nick, "MOTD File is missing"], true));
|
||||
sendNumeric!ERR_NOMOTD();
|
||||
return;
|
||||
}
|
||||
_server.sendMotd(this);
|
||||
|
@ -860,7 +856,7 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length < 1)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -876,7 +872,7 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length < 1)
|
||||
{
|
||||
sendErrNoNickGiven();
|
||||
sendNumeric!ERR_NONICKNAMEGIVEN();
|
||||
return;
|
||||
}
|
||||
else if (message.parameters.length > 1)
|
||||
|
@ -890,34 +886,34 @@ class Connection
|
|||
if (!_server.canFindConnectionByNick(mask)
|
||||
|| !_server.findConnectionByNick(mask)[0].visibleTo(this))
|
||||
{
|
||||
sendErrNoSuchNick(mask);
|
||||
sendNumeric!ERR_NOSUCHNICK(mask);
|
||||
}
|
||||
else
|
||||
{
|
||||
_server.whois(this, mask);
|
||||
}
|
||||
|
||||
send(Message(_server.name, "318", [nick, mask, "End of WHOIS list"], true));
|
||||
sendNumeric!RPL_ENDOFWHOIS(mask);
|
||||
}
|
||||
|
||||
void onKill(Message message)
|
||||
{
|
||||
if (!isOperator)
|
||||
{
|
||||
sendErrNoPrivileges();
|
||||
sendNumeric!ERR_NOPRIVILEGES();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.parameters.length < 2)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
auto nick = message.parameters[0];
|
||||
if (!_server.canFindConnectionByNick(nick))
|
||||
{
|
||||
sendErrNoSuchNick(nick);
|
||||
sendNumeric!ERR_NOSUCHNICK(nick);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -930,7 +926,7 @@ class Connection
|
|||
{
|
||||
if (message.parameters.length < 2)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -940,7 +936,7 @@ class Connection
|
|||
|
||||
if (channelList.length != 1 && channelList.length != userList.length)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -954,22 +950,22 @@ class Connection
|
|||
|
||||
if (!_server.canFindChannelByName(channelName))
|
||||
{
|
||||
sendErrNoSuchChannel(channelName);
|
||||
sendNumeric!ERR_NOSUCHCHANNEL(channelName);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto channel = _server.findChannelByName(channelName)[0];
|
||||
if (!channel.members.canFind(this))
|
||||
{
|
||||
sendErrNotOnChannel(channelName);
|
||||
sendNumeric!ERR_NOTONCHANNEL(channelName);
|
||||
}
|
||||
else if (!channel.memberModes[this].canFind('o'))
|
||||
{
|
||||
sendErrChanopPrivsNeeded(channelName);
|
||||
sendNumeric!ERR_CHANOPRIVSNEEDED(channelName);
|
||||
}
|
||||
else if (!channel.members.canFind!(m => m.nick.toIRCLower == nick.toIRCLower))
|
||||
{
|
||||
sendErrUserNotInChannel(nick, channelName);
|
||||
sendNumeric!ERR_USERNOTINCHANNEL(nick, channelName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -983,7 +979,7 @@ class Connection
|
|||
{
|
||||
if (message.parameters.empty)
|
||||
{
|
||||
sendErrNeedMoreParams(message.command);
|
||||
sendNumeric!ERR_NEEDMOREPARAMS(message.command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1001,7 +997,7 @@ class Connection
|
|||
//NOTE: The RFCs don't allow ERR_NOSUCHNICK as a reponse to MODE
|
||||
version (BasicFixes)
|
||||
{
|
||||
sendErrNoSuchNick(target);
|
||||
sendNumeric!ERR_NOSUCHNICK(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1016,24 +1012,20 @@ class Connection
|
|||
version (BasicFixes)
|
||||
{
|
||||
if (message.parameters.length > 1)
|
||||
{
|
||||
send(Message(_server.name, "502", [nick, "Cannot change mode for other users"], true));
|
||||
}
|
||||
sendNumeric!ERR_USERSDONTMATCH();
|
||||
else
|
||||
{
|
||||
send(Message(_server.name, "502", [nick, "Cannot view mode of other users"], true));
|
||||
}
|
||||
sendNumeric!ERR_USERSDONTMATCH_ALT();
|
||||
}
|
||||
else
|
||||
{
|
||||
send(Message(_server.name, "502", [nick, "Cannot change mode for other users"], true));
|
||||
sendNumeric!ERR_USERSDONTMATCH();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.parameters.length == 1)
|
||||
{
|
||||
send(Message(_server.name, "221", [nick, "+" ~ modes.idup]));
|
||||
sendNumeric!RPL_UMODEIS("+" ~ modes.idup);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1045,15 +1037,14 @@ class Connection
|
|||
//NOTE: The RFCs don't specify what should happen on malformed mode operations
|
||||
version (BasicFixes)
|
||||
{
|
||||
sendMalformedMessageError(message.command, "Invalid mode operation: " ~ modeString[0]);
|
||||
sendMalformedMessageError(message.command,
|
||||
"Invalid mode operation: " ~ modeString[0]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (modeString.length == 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto changedModes = modeString[1 .. $];
|
||||
foreach (mode; changedModes)
|
||||
|
@ -1081,9 +1072,7 @@ class Connection
|
|||
//ignore
|
||||
break;
|
||||
default:
|
||||
send(Message(_server.name, "501", [
|
||||
nick, "Unknown MODE flag"
|
||||
], true));
|
||||
sendNumeric!ERR_UMODEUNKNOWNFLAG();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1099,7 +1088,7 @@ class Connection
|
|||
//NOTE: The RFCs don't allow ERR_NOSUCHCHANNEL as a response to MODE
|
||||
version (BasicFixes)
|
||||
{
|
||||
sendErrNoSuchChannel(message.parameters[0]);
|
||||
sendNumeric!ERR_NOSUCHCHANNEL(message.parameters[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -1108,13 +1097,9 @@ class Connection
|
|||
//NOTE: The RFCs are inconsistent on channel mode query syntax for mask list modes
|
||||
// ('MODE #chan +b', but 'MODE #chan e' and 'MODE #chan I')
|
||||
version (BasicFixes)
|
||||
{
|
||||
enum listQueryModes = ["+b", "+e", "+I", "e", "I"];
|
||||
}
|
||||
else
|
||||
{
|
||||
enum listQueryModes = ["+b", "e", "I"];
|
||||
}
|
||||
|
||||
if (message.parameters.length == 1)
|
||||
{
|
||||
|
@ -1140,7 +1125,7 @@ class Connection
|
|||
{
|
||||
if (!channel.memberModes[this].canFind('o'))
|
||||
{
|
||||
sendErrChanopPrivsNeeded(channel.name);
|
||||
sendNumeric!ERR_CHANOPRIVSNEEDED(channel.name);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1153,15 +1138,14 @@ class Connection
|
|||
//NOTE: The RFCs don't specify what should happen on malformed mode operations
|
||||
version (BasicFixes)
|
||||
{
|
||||
sendMalformedMessageError(message.command, "Invalid mode operation: " ~ modeString[0]);
|
||||
sendMalformedMessageError(message.command,
|
||||
"Invalid mode operation: " ~ modeString[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (modeString.length == 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char[] processedModes;
|
||||
string[] processedParameters;
|
||||
|
@ -1174,7 +1158,7 @@ class Connection
|
|||
//TODO: If RFC-strictness is on, limit mode changes with parameter to 3 per command
|
||||
|
||||
case 'o':
|
||||
case 'v':
|
||||
case 'v':
|
||||
if (i + 1 == message.parameters.length)
|
||||
{
|
||||
break Lforeach;
|
||||
|
@ -1184,14 +1168,14 @@ class Connection
|
|||
auto memberRange = _server.findConnectionByNick(memberNick);
|
||||
if (memberRange.empty)
|
||||
{
|
||||
sendErrNoSuchNick(memberNick);
|
||||
sendNumeric!ERR_NOSUCHNICK(memberNick);
|
||||
break Lforeach;
|
||||
}
|
||||
|
||||
auto member = memberRange[0];
|
||||
if (!channel.members.canFind(member))
|
||||
{
|
||||
sendErrUserNotInChannel(memberNick, channel.name);
|
||||
sendNumeric!ERR_USERNOTINCHANNEL(memberNick, channel.name);
|
||||
break Lforeach;
|
||||
}
|
||||
|
||||
|
@ -1226,7 +1210,8 @@ class Connection
|
|||
}
|
||||
else
|
||||
{
|
||||
sendMalformedMessageError(message.command, "Invalid user mask: " ~ mask);
|
||||
sendMalformedMessageError(message.command,
|
||||
"Invalid user mask: " ~ mask);
|
||||
break Lforeach;
|
||||
}
|
||||
}
|
||||
|
@ -1269,20 +1254,14 @@ class Connection
|
|||
if (add)
|
||||
{
|
||||
if (i + 1 == message.parameters.length)
|
||||
{
|
||||
break Lforeach;
|
||||
}
|
||||
|
||||
auto limitString = message.parameters[++i];
|
||||
uint limit = 0;
|
||||
try
|
||||
{
|
||||
limit = limitString.to!uint;
|
||||
}
|
||||
catch (Throwable)
|
||||
{
|
||||
break Lforeach;
|
||||
}
|
||||
|
||||
channel.setMemberLimit(limit);
|
||||
|
||||
|
@ -1298,26 +1277,23 @@ class Connection
|
|||
}
|
||||
break;
|
||||
case 'i':
|
||||
case 'm':
|
||||
case 'n':
|
||||
case 'p':
|
||||
case 's':
|
||||
case 't':
|
||||
case 'm':
|
||||
case 'n':
|
||||
case 'p':
|
||||
case 's':
|
||||
case 't':
|
||||
bool success;
|
||||
if (add)
|
||||
success = channel.setMode(mode);
|
||||
else
|
||||
success = channel.unsetMode(mode);
|
||||
|
||||
if (success)
|
||||
{
|
||||
processedModes ~= mode;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
send(Message(_server.name, "472", [
|
||||
nick, [mode],
|
||||
"is unknown mode char to me for " ~ channel.name
|
||||
], true));
|
||||
sendNumeric!ERR_UNKNOWNMODE([mode],
|
||||
"is unknown mode char to me for " ~ channel.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1363,13 +1339,9 @@ class Connection
|
|||
break;
|
||||
}
|
||||
|
||||
send(Message(_server.name, "219", [
|
||||
nick, [statsLetter].idup, "End of STATS report"
|
||||
], true));
|
||||
sendNumeric!RPL_ENDOFSTATS([statsLetter].idup);
|
||||
}
|
||||
|
||||
//TODO: Refactor numeric replies
|
||||
|
||||
void sendWhoReply(string channel, Connection user, string nickPrefix, uint hopCount)
|
||||
{
|
||||
auto flags = user.modes.canFind('a') ? "G" : "H";
|
||||
|
@ -1377,95 +1349,8 @@ class Connection
|
|||
flags ~= "*";
|
||||
flags ~= nickPrefix;
|
||||
|
||||
send(Message(_server.name, "352", [
|
||||
nick, channel, user.user, user.hostname, user.servername,
|
||||
user.nick, flags, hopCount.to!string ~ " " ~ user.realname
|
||||
], true));
|
||||
}
|
||||
|
||||
void sendRplAway(string target, string message)
|
||||
{
|
||||
send(Message(_server.name, "301", [nick, target, message], true));
|
||||
}
|
||||
|
||||
void sendRplList(string channelName, ulong visibleCount, string topic)
|
||||
{
|
||||
send(Message(_server.name, "322", [
|
||||
nick, channelName, visibleCount.to!string, topic
|
||||
], true));
|
||||
}
|
||||
|
||||
void sendRplListEnd()
|
||||
{
|
||||
send(Message(_server.name, "323", [nick, "End of LIST"], true));
|
||||
}
|
||||
|
||||
void sendRplInviting(string channelName, string name)
|
||||
{
|
||||
//TODO: If errata are being ignored, send nick and channel name in reverse order
|
||||
send(Message(_server.name, "341", [nick, name, channelName]));
|
||||
}
|
||||
|
||||
void sendRplEndOfNames(string channelName)
|
||||
{
|
||||
send(Message(_server.name, "366", [
|
||||
nick, channelName, "End of NAMES list"
|
||||
], true));
|
||||
}
|
||||
|
||||
void sendErrNoSuchNick(string name)
|
||||
{
|
||||
send(Message(_server.name, "401", [nick, name, "No such nick/channel"], true));
|
||||
}
|
||||
|
||||
void sendErrNoSuchChannel(string name)
|
||||
{
|
||||
send(Message(_server.name, "403", [nick, name, "No such channel"], true));
|
||||
}
|
||||
|
||||
void sendErrNoNickGiven()
|
||||
{
|
||||
send(Message(_server.name, "431", [nick, "No nickname given"], true));
|
||||
}
|
||||
|
||||
void sendErrUserNotInChannel(string otherNick, string channel)
|
||||
{
|
||||
send(Message(_server.name, "441", [
|
||||
nick, otherNick, channel, "They aren't on that channel"
|
||||
], true));
|
||||
}
|
||||
|
||||
void sendErrNotOnChannel(string channel)
|
||||
{
|
||||
send(Message(_server.name, "442", [
|
||||
nick, channel, "You're not on that channel"
|
||||
], true));
|
||||
}
|
||||
|
||||
void sendErrNotRegistered()
|
||||
{
|
||||
send(Message(_server.name, "451", ["(You)", "You have not registered"], true));
|
||||
}
|
||||
|
||||
void sendErrNeedMoreParams(string command)
|
||||
{
|
||||
send(Message(_server.name, "461", [
|
||||
nick, command, "Not enough parameters"
|
||||
], true));
|
||||
}
|
||||
|
||||
void sendErrNoPrivileges()
|
||||
{
|
||||
send(Message(_server.name, "481", [
|
||||
nick, "Permission Denied- You're not an IRC operator"
|
||||
], true));
|
||||
}
|
||||
|
||||
void sendErrChanopPrivsNeeded(string channel)
|
||||
{
|
||||
send(Message(_server.name, "482", [
|
||||
nick, channel, "You're not channel operator"
|
||||
], true));
|
||||
sendNumeric!RPL_WHOREPLY(channel, user.user, user.hostname, user.servername,
|
||||
user.nick, flags, hopCount.to!string ~ " " ~ user.realname);
|
||||
}
|
||||
|
||||
void notImplemented(string description)
|
||||
|
@ -1477,7 +1362,9 @@ class Connection
|
|||
|
||||
void sendMalformedMessageError(string command, string description)
|
||||
{
|
||||
send(Message(_server.name, "ERROR", [command, "Malformed message: " ~ description], true));
|
||||
send(Message(_server.name, "ERROR", [
|
||||
command, "Malformed message: " ~ description
|
||||
], true));
|
||||
}
|
||||
|
||||
void sendWelcome()
|
||||
|
@ -1487,35 +1374,29 @@ class Connection
|
|||
enum availableUserModes = "aiwroOs";
|
||||
enum availableChannelModes = "OovaimnqpsrtklbeI";
|
||||
|
||||
send(Message(_server.name, "001", [nick,
|
||||
"Welcome", "to", "the", "Internet", "Relay", "Network", prefix
|
||||
], false));
|
||||
send(Message(_server.name, "002", [nick,
|
||||
"Your", "host", "is", _server.name ~ ",", "running", "version", _server.versionString
|
||||
], false));
|
||||
send(Message(_server.name, "003", [nick,
|
||||
"This", "server", "was", "created", buildDate
|
||||
], false));
|
||||
send(Message(_server.name, "004", [nick,
|
||||
_server.name, _server.versionString, availableUserModes, availableChannelModes
|
||||
], false));
|
||||
sendNumeric!RPL_WELCOME("Welcome", "to", "the", "Internet", "Relay", "Network", prefix);
|
||||
sendNumeric!RPL_YOURHOST("Your", "host", "is", _server.name ~ ",",
|
||||
"running", "version", _server.versionString);
|
||||
sendNumeric!RPL_CREATED("This", "server", "was", "created", buildDate);
|
||||
sendNumeric!RPL_MYINFO(_server.name, _server.versionString,
|
||||
availableUserModes, availableChannelModes);
|
||||
}
|
||||
|
||||
void onIncorrectPassword()
|
||||
{
|
||||
//NOTE: The RFCs don't allow ERR_PASSWDMISMATCH as a response to NICK/USER
|
||||
version (BasicFixes)
|
||||
{
|
||||
send(Message(_server.name, "464", [nick, "Password incorrect"], true));
|
||||
}
|
||||
sendNumeric!ERR_PASSWDMISMATCH();
|
||||
|
||||
//NOTE: The RFCs don't actually specify what should happen here
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
string getHost()
|
||||
string getHost(string addressString = null)
|
||||
{
|
||||
auto address = parseAddress(_connection.remoteAddress.toAddressString);
|
||||
auto address = parseAddress(addressString != null ?
|
||||
addressString :
|
||||
_connection.remoteAddress.toAddressString);
|
||||
auto hostname = address.toHostNameString;
|
||||
if (hostname is null)
|
||||
{
|
||||
|
@ -1536,13 +1417,10 @@ class Connection
|
|||
char[] modes;
|
||||
|
||||
if (mask & 0b100)
|
||||
{
|
||||
modes ~= 'w';
|
||||
}
|
||||
|
||||
if (mask & 0b1000)
|
||||
{
|
||||
modes ~= 'i';
|
||||
}
|
||||
|
||||
return modes;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
module ircd.numerics;
|
||||
|
||||
struct SimpleNumeric
|
||||
{
|
||||
string number;
|
||||
string[] params;
|
||||
}
|
||||
|
||||
private alias N = SimpleNumeric;
|
||||
|
||||
enum : SimpleNumeric
|
||||
{
|
||||
//Command responses
|
||||
RPL_WELCOME = N("001", []),
|
||||
RPL_YOURHOST = N("002", []),
|
||||
RPL_CREATED = N("003", []),
|
||||
RPL_MYINFO = N("004", []),
|
||||
RPL_STATSCOMMANDS = N("212", []),
|
||||
RPL_ENDOFSTATS = N("219", ["End of STATS report"]),
|
||||
RPL_UMODEIS = N("221", []),
|
||||
RPL_STATSUPTIME = N("242", []),
|
||||
RPL_LUSERCLIENT = N("251", []),
|
||||
RPL_LUSEROP = N("252", ["operator(s) online"]),
|
||||
RPL_LUSERUNKNOWN = N("253", ["unknown connection(s)"]),
|
||||
RPL_LUSERCHANNELS = N("254", ["channels formed"]),
|
||||
RPL_LUSERME = N("255", []),
|
||||
RPL_AWAY = N("301", []),
|
||||
RPL_ISON = N("303", []),
|
||||
RPL_UNAWAY = N("305", ["You are no longer marked as being away"]),
|
||||
RPL_NOWAWAY = N("306", ["You have been marked as being away"]),
|
||||
RPL_WHOISUSER = N("311", []),
|
||||
RPL_WHOISSERVER = N("312", []),
|
||||
RPL_WHOISOPERATOR = N("313", ["is an IRC operator"]),
|
||||
RPL_ENDOFWHO = N("315", ["End of WHO list"]),
|
||||
RPL_WHOISIDLE = N("317", ["seconds idle"]),
|
||||
RPL_ENDOFWHOIS = N("318", ["End of WHOIS list"]),
|
||||
RPL_WHOISCHANNELS = N("319", []),
|
||||
RPL_LIST = N("322", []),
|
||||
RPL_LISTEND = N("323", ["End of LIST"]),
|
||||
RPL_CHANNELMODEIS = N("324", []),
|
||||
RPL_NOTOPIC = N("331", ["No topic is set"]),
|
||||
RPL_TOPIC = N("332", []),
|
||||
RPL_INVITING = N("341", []),
|
||||
RPL_INVITELIST = N("346", []),
|
||||
RPL_ENDOFINVITELIST = N("347", ["End of channel invite list"]),
|
||||
RPL_EXCEPTLIST = N("348", []),
|
||||
RPL_ENDOFEXCEPTLIST = N("349", ["End of channel exception list"]),
|
||||
RPL_VERSION = N("351", []),
|
||||
RPL_WHOREPLY = N("352", []),
|
||||
RPL_NAMREPLY = N("353", []),
|
||||
RPL_ENDOFNAMES = N("366", ["End of NAMES list"]),
|
||||
RPL_BANLIST = N("367", []),
|
||||
RPL_ENDOFBANLIST = N("368", ["End of channel ban list"]),
|
||||
RPL_MOTD = N("372", []),
|
||||
RPL_MOTDSTART = N("375", []),
|
||||
RPL_ENDOFMOTD = N("376", ["End of MOTD command"]),
|
||||
RPL_TIME = N("391", []),
|
||||
|
||||
//Error replies
|
||||
ERR_NOSUCHNICK = N("401", ["No such nick/channel"]),
|
||||
ERR_NOSUCHCHANNEL = N("403", ["No such channel"]),
|
||||
ERR_CANNOTSENDTOCHAN = N("404", ["Cannot send to channel"]),
|
||||
ERR_NORECIPIENT_PRIVMSG = N("411", ["No recipient given (PRIVMSG)"]),
|
||||
ERR_NORECIPIENT_NOTICE = N("411", ["No recipient given (NOTICE)"]),
|
||||
ERR_NOTEXTTOSEND = N("412", ["No text to send"]),
|
||||
ERR_UNKNOWNCOMMAND = N("421", ["Unknown command"]),
|
||||
ERR_NOMOTD = N("422", ["MOTD File is missing"]),
|
||||
ERR_NONICKNAMEGIVEN = N("431", ["No nickname given"]),
|
||||
ERR_ERRONEUSNICKNAME = N("432", ["Erroneous nickname"]),
|
||||
ERR_NICKNAMEINUSE = N("433", ["Nickname is already in use"]),
|
||||
ERR_USERNOTINCHANNEL = N("441", ["They aren't on that channel"]),
|
||||
ERR_NOTONCHANNEL = N("442", ["You're not on that channel"]),
|
||||
ERR_USERONCHANNEL = N("443", ["is already on channel"]),
|
||||
ERR_NOTREGISTERED = N("451", ["You have not registered"]),
|
||||
ERR_NEEDMOREPARAMS = N("461", ["Not enough parameters"]),
|
||||
ERR_ALREADYREGISTRED /* sic */ = N("462", ["Unauthorized command (already registered)"]),
|
||||
ERR_PASSWDMISMATCH = N("464", ["Password incorrect"]),
|
||||
ERR_CHANNELISFULL = N("471", ["Cannot join channel (+l)"]),
|
||||
ERR_UNKNOWNMODE = N("472", []),
|
||||
ERR_INVITEONLYCHAN = N("473", ["Cannot join channel (+i)"]),
|
||||
ERR_BANNEDFROMCHAN = N("474", ["Cannot join channel (+b)"]),
|
||||
ERR_BADCHANNELKEY = N("475", ["Cannot join channel (+k)"]),
|
||||
ERR_NOPRIVILEGES = N("481", ["Permission Denied- You're not an IRC operator"]),
|
||||
ERR_CHANOPRIVSNEEDED = N("482", ["You're not channel operator"]),
|
||||
ERR_UMODEUNKNOWNFLAG = N("501", ["Unknown MODE flag"]),
|
||||
ERR_USERSDONTMATCH = N("502", ["Cannot change mode for other users"]),
|
||||
ERR_USERSDONTMATCH_ALT = N("502", ["Cannot view mode of other users"]), //non-standard message (NotStrict-only)
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import ircd.message;
|
|||
import ircd.connection;
|
||||
import ircd.channel;
|
||||
import ircd.helpers;
|
||||
import ircd.numerics;
|
||||
|
||||
//TODO: Make this a struct?
|
||||
class Server
|
||||
|
@ -229,8 +230,8 @@ class Server
|
|||
{
|
||||
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))))
|
||||
.filter!(c => [c.hostname, c.servername, c.realname,
|
||||
c.nick].any!(n => wildcardMatch(n, mask))))
|
||||
{
|
||||
//TODO: Don't leak secret/private channels if RFC-strictness is off (the RFCs don't seem to say anything about it?)
|
||||
auto channelName = c.channels.empty ? "*" : c.channels.array[0].name;
|
||||
|
@ -294,24 +295,23 @@ class Server
|
|||
&& !ch.modes.canFind('p')).empty);
|
||||
if (!otherUsers.empty)
|
||||
{
|
||||
connection.send(Message(name, "353", [
|
||||
connection.nick, "=", "*",
|
||||
otherUsers.map!(m => m.nick).join(' ')
|
||||
], true));
|
||||
connection.sendNumeric!RPL_NAMREPLY("=", "*", otherUsers.map!(m => m.nick).join(' '));
|
||||
}
|
||||
|
||||
connection.sendRplEndOfNames("*");
|
||||
connection.sendNumeric!RPL_ENDOFNAMES("*");
|
||||
}
|
||||
|
||||
void sendFullList(Connection connection)
|
||||
{
|
||||
foreach (channel; channels.filter!(c => c.visibleTo(connection)))
|
||||
{
|
||||
connection.sendRplList(channel.name,
|
||||
channel.members.filter!(m => m.visibleTo(connection))
|
||||
.array.length, channel.topic);
|
||||
connection.sendNumeric!RPL_LIST(channel.name, channel.members
|
||||
.filter!(m => m.visibleTo(connection))
|
||||
.array
|
||||
.length
|
||||
.to!string, channel.topic);
|
||||
}
|
||||
connection.sendRplListEnd();
|
||||
connection.sendNumeric!RPL_LISTEND();
|
||||
}
|
||||
|
||||
void sendPartialList(Connection connection, string[] channelNames)
|
||||
|
@ -319,24 +319,25 @@ class Server
|
|||
foreach (channel; channels.filter!(c => channelNames.canFind(c.name)
|
||||
&& c.visibleTo(connection)))
|
||||
{
|
||||
connection.sendRplList(channel.name,
|
||||
channel.members.filter!(m => m.visibleTo(connection))
|
||||
.array.length, channel.topic);
|
||||
connection.sendNumeric!RPL_LIST(channel.name, channel.members
|
||||
.filter!(m => m.visibleTo(connection))
|
||||
.array
|
||||
.length
|
||||
.to!string, channel.topic);
|
||||
}
|
||||
connection.sendRplListEnd();
|
||||
connection.sendNumeric!RPL_LISTEND();
|
||||
}
|
||||
|
||||
void sendVersion(Connection connection)
|
||||
{
|
||||
connection.send(Message(name, "351", [
|
||||
connection.nick, versionString ~ ".", name, ""
|
||||
], true));
|
||||
//TODO: Include enabled versions in comments?
|
||||
connection.sendNumeric!RPL_VERSION(versionString ~ ".", name, ":");
|
||||
}
|
||||
|
||||
void sendTime(Connection connection)
|
||||
{
|
||||
auto timeString = Clock.currTime.toISOExtString;
|
||||
connection.send(Message(name, "391", [connection.nick, name, timeString], true));
|
||||
connection.sendNumeric!RPL_TIME(name, timeString);
|
||||
}
|
||||
|
||||
void invite(Connection inviter, string target, string channelName)
|
||||
|
@ -351,17 +352,13 @@ class Server
|
|||
|
||||
void sendMotd(Connection connection)
|
||||
{
|
||||
connection.send(Message(name, "375", [
|
||||
connection.nick, ":- " ~ name ~ " Message of the day - "
|
||||
], true));
|
||||
connection.sendNumeric!RPL_MOTDSTART("- " ~ name ~ " Message of the day - ");
|
||||
foreach (line; motd.splitLines)
|
||||
{
|
||||
//TODO: Implement line wrapping
|
||||
connection.send(Message(name, "372", [connection.nick, ":- " ~ line], true));
|
||||
connection.sendNumeric!RPL_MOTD("- " ~ line);
|
||||
}
|
||||
connection.send(Message(name, "376", [
|
||||
connection.nick, "End of MOTD command"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_ENDOFMOTD();
|
||||
}
|
||||
|
||||
void sendLusers(Connection connection)
|
||||
|
@ -369,79 +366,58 @@ class Server
|
|||
//TODO: If RFC-strictness is off, use '1 server' instead of '1 servers' if the network (or the part of the network of the query) has only one server
|
||||
|
||||
//TODO: Support services and multiple servers
|
||||
connection.send(Message(name, "251", [
|
||||
connection.nick,
|
||||
"There are " ~ connections.filter!(c => c.registered)
|
||||
.count
|
||||
.to!string ~ " users and 0 services on 1 servers"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_LUSERCLIENT(
|
||||
"There are " ~ connections.filter!(c => c.registered)
|
||||
.count
|
||||
.to!string ~ " users and 0 services on 1 servers");
|
||||
|
||||
if (connections.any!(c => c.isOperator))
|
||||
{
|
||||
connection.send(Message(name, "252", [
|
||||
connection.nick,
|
||||
connections.count!(c => c.isOperator)
|
||||
.to!string, "operator(s) online"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_LUSEROP(connections.count!(c => c.isOperator)
|
||||
.to!string);
|
||||
}
|
||||
|
||||
if (connections.any!(c => !c.registered))
|
||||
{
|
||||
connection.send(Message(name, "253", [
|
||||
connection.nick,
|
||||
connections.count!(c => !c.registered)
|
||||
.to!string, "unknown connection(s)"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_LUSERUNKNOWN(connections.count!(c => !c.registered)
|
||||
.to!string);
|
||||
}
|
||||
|
||||
if (channels.length > 0)
|
||||
{
|
||||
connection.send(Message(name, "254", [
|
||||
connection.nick, channels.length.to!string,
|
||||
"channels formed"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_LUSERCHANNELS(channels.length.to!string);
|
||||
}
|
||||
|
||||
connection.send(Message(name, "255", [
|
||||
connection.nick,
|
||||
"I have " ~ connections.length.to!string ~ " clients and 1 servers"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_LUSERME(
|
||||
"I have " ~ connections.length.to!string ~ " clients and 1 servers");
|
||||
}
|
||||
|
||||
void ison(Connection connection, string[] nicks)
|
||||
{
|
||||
auto reply = nicks.filter!(n => canFindConnectionByNick(n)).join(' ');
|
||||
|
||||
connection.send(Message(name, "303", [connection.nick, reply], true));
|
||||
connection.sendNumeric!RPL_ISON(reply);
|
||||
}
|
||||
|
||||
void whois(Connection connection, string mask)
|
||||
{
|
||||
auto user = findConnectionByNick(mask)[0];
|
||||
|
||||
connection.send(Message(name, "311", [
|
||||
connection.nick, user.nick, user.user, user.hostname, "*",
|
||||
user.hostname
|
||||
], true));
|
||||
connection.sendNumeric!RPL_WHOISUSER(user.nick, user.user,
|
||||
user.hostname, "*", user.realname);
|
||||
|
||||
//TODO: Send information about the user's actual server (which is not necessarily this one)
|
||||
connection.send(Message(name, "312", [
|
||||
connection.nick, user.nick, name, info
|
||||
], true));
|
||||
connection.sendNumeric!RPL_WHOISSERVER(user.nick, name, info);
|
||||
|
||||
if (user.isOperator)
|
||||
{
|
||||
connection.send(Message(name, "313", [
|
||||
connection.nick, user.nick, "is an IRC operator"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_WHOISOPERATOR(user.nick);
|
||||
}
|
||||
|
||||
auto idleSeconds = (Clock.currTime - user.lastMessageTime).total!"seconds";
|
||||
connection.send(Message(name, "317", [
|
||||
connection.nick, user.nick, idleSeconds.to!string,
|
||||
"seconds idle"
|
||||
], true));
|
||||
connection.sendNumeric!RPL_WHOISIDLE(user.nick, idleSeconds.to!string);
|
||||
|
||||
auto userChannels = user.channels.map!(c => c.nickPrefix(user) ~ c.name).join(' ');
|
||||
connection.send(Message(name, "319", [
|
||||
connection.nick, user.nick, userChannels
|
||||
], true));
|
||||
connection.sendNumeric!RPL_WHOISCHANNELS(user.nick, userChannels);
|
||||
}
|
||||
|
||||
void kill(Connection killer, string nick, string comment)
|
||||
|
@ -482,10 +458,8 @@ class Server
|
|||
foreach (command, count; _commandUsage)
|
||||
{
|
||||
//TODO: Implement remote count
|
||||
connection.send(Message(name, "212", [
|
||||
connection.nick, command, count.to!string,
|
||||
_commandBytes[command].to!string, "0"
|
||||
], false));
|
||||
connection.sendNumeric!RPL_STATSCOMMANDS(command,
|
||||
count.to!string, _commandBytes[command].to!string, "0");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -497,7 +471,7 @@ class Server
|
|||
|
||||
auto uptimeString = format!"Server Up %d days %d:%02d:%02d"(uptime.days,
|
||||
uptime.hours, uptime.minutes, uptime.seconds);
|
||||
connection.send(Message(name, "242", [connection.nick, uptimeString], true));
|
||||
connection.sendNumeric!RPL_STATSUPTIME(uptimeString);
|
||||
}
|
||||
|
||||
void setPass(string pass)
|
||||
|
|
|
@ -27,7 +27,9 @@ version (MaxNickLengthConfigurable) version = NotStrict;
|
|||
|
||||
version (NotStrict)
|
||||
{
|
||||
version (SupportTLS) {}
|
||||
version (SupportTLS)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
static assert(false, "TLS support must be enabled if any non-strict versions are enabled.");
|
||||
|
|
Loading…
Reference in New Issue