Convert tabs to spaces
This commit is contained in:
parent
b7868a87c5
commit
d376977326
|
@ -1,7 +1,7 @@
|
|||
listen {
|
||||
type "plaintext"
|
||||
address "::"
|
||||
port 6667
|
||||
type "plaintext"
|
||||
address "::"
|
||||
port 6667
|
||||
}
|
||||
|
||||
# No password set
|
||||
|
|
|
@ -10,64 +10,64 @@ import ircd.server;
|
|||
|
||||
static T tagValueOrNull(T)(Tag tag, string childName)
|
||||
{
|
||||
if(childName !in tag.tags)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return tagValue!T(tag, childName);
|
||||
}
|
||||
if(childName !in tag.tags)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return tagValue!T(tag, childName);
|
||||
}
|
||||
}
|
||||
|
||||
static T tagValue(T)(Tag tag, string childName)
|
||||
{
|
||||
static if(isArray!T && !isSomeString!T)
|
||||
{
|
||||
template U(T : T[])
|
||||
{
|
||||
alias U = T;
|
||||
}
|
||||
static if(isArray!T && !isSomeString!T)
|
||||
{
|
||||
template U(T : T[])
|
||||
{
|
||||
alias U = T;
|
||||
}
|
||||
|
||||
T array = [];
|
||||
T array = [];
|
||||
|
||||
foreach(value; tag.tags[childName][0].values)
|
||||
{
|
||||
array ~= value.get!(U!T);
|
||||
}
|
||||
foreach(value; tag.tags[childName][0].values)
|
||||
{
|
||||
array ~= value.get!(U!T);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
else static if(isIntegral!T && !is(T == int))
|
||||
{
|
||||
return cast(T)tagValue!int(tag, childName);
|
||||
}
|
||||
else
|
||||
{
|
||||
return tag.tags[childName][0].values[0].get!T;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
else static if(isIntegral!T && !is(T == int))
|
||||
{
|
||||
return cast(T)tagValue!int(tag, childName);
|
||||
}
|
||||
else
|
||||
{
|
||||
return tag.tags[childName][0].values[0].get!T;
|
||||
}
|
||||
}
|
||||
|
||||
shared static this()
|
||||
{
|
||||
auto server = new Server();
|
||||
auto server = new Server();
|
||||
|
||||
auto config = parseFile("config.sdl");
|
||||
auto config = parseFile("config.sdl");
|
||||
|
||||
auto pass = config.tagValue!string("pass");
|
||||
server.setPass(pass.empty ? null : pass);
|
||||
auto pass = config.tagValue!string("pass");
|
||||
server.setPass(pass.empty ? null : pass);
|
||||
|
||||
foreach(listenBlock; config.tags.filter!(t => t.getFullName.toString == "listen"))
|
||||
{
|
||||
assert(listenBlock.tagValue!string("type") == "plaintext");
|
||||
foreach(listenBlock; config.tags.filter!(t => t.getFullName.toString == "listen"))
|
||||
{
|
||||
assert(listenBlock.tagValue!string("type") == "plaintext");
|
||||
|
||||
auto addresses = listenBlock.tagValue!(string[])("address");
|
||||
auto port = listenBlock.tagValue!ushort("port");
|
||||
auto addresses = listenBlock.tagValue!(string[])("address");
|
||||
auto port = listenBlock.tagValue!ushort("port");
|
||||
|
||||
foreach(address; addresses)
|
||||
{
|
||||
server.listen(port, address);
|
||||
}
|
||||
}
|
||||
foreach(address; addresses)
|
||||
{
|
||||
server.listen(port, address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,348 +11,348 @@ import ircd.helpers;
|
|||
|
||||
class Channel
|
||||
{
|
||||
string name;
|
||||
string topic = "";
|
||||
|
||||
Connection[] members;
|
||||
char[] modes;
|
||||
char[][Connection] memberModes;
|
||||
string[][char] maskLists;
|
||||
|
||||
string key;
|
||||
Nullable!uint userLimit;
|
||||
Connection[] inviteHolders;
|
||||
|
||||
private Server _server;
|
||||
|
||||
this(string name, Server server)
|
||||
{
|
||||
this.name = name;
|
||||
this._server = server;
|
||||
this.maskLists = ['b' : [], 'e' : [], 'I' : []];
|
||||
}
|
||||
|
||||
void join(Connection connection)
|
||||
{
|
||||
members ~= connection;
|
||||
|
||||
if(members.length == 1)
|
||||
{
|
||||
memberModes[connection] ~= 'o';
|
||||
}
|
||||
else
|
||||
{
|
||||
memberModes[connection] = [];
|
||||
}
|
||||
|
||||
if(inviteHolders.canFind(connection))
|
||||
{
|
||||
inviteHolders = inviteHolders.remove!(c => c == connection);
|
||||
}
|
||||
}
|
||||
|
||||
void part(Connection connection, string partMessage)
|
||||
{
|
||||
foreach(member; members)
|
||||
{
|
||||
if(partMessage !is null)
|
||||
{
|
||||
member.send(Message(connection.prefix, "PART", [name, partMessage], true));
|
||||
}
|
||||
else
|
||||
{
|
||||
member.send(Message(connection.prefix, "PART", [name]));
|
||||
}
|
||||
}
|
||||
|
||||
members = members.remove!(m => m == connection);
|
||||
memberModes.remove(connection);
|
||||
}
|
||||
|
||||
void invite(Connection connection)
|
||||
{
|
||||
inviteHolders ~= connection;
|
||||
}
|
||||
|
||||
void sendNames(Connection connection, bool sendRplEndOfNames = true)
|
||||
{
|
||||
string channelType;
|
||||
|
||||
if(modes.canFind('s'))
|
||||
{
|
||||
channelType = "@";
|
||||
}
|
||||
else if(modes.canFind('p'))
|
||||
{
|
||||
channelType = "*";
|
||||
}
|
||||
else
|
||||
{
|
||||
channelType = "=";
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if(sendRplEndOfNames)
|
||||
{
|
||||
connection.sendRplEndOfNames(name);
|
||||
}
|
||||
}
|
||||
|
||||
void sendPrivMsg(Connection sender, string text)
|
||||
{
|
||||
foreach(member; members.filter!(m => m.nick != sender.nick))
|
||||
{
|
||||
member.send(Message(sender.prefix, "PRIVMSG", [name, text], true));
|
||||
}
|
||||
}
|
||||
|
||||
void sendNotice(Connection sender, string text)
|
||||
{
|
||||
foreach(member; members.filter!(m => m.nick != sender.nick))
|
||||
{
|
||||
member.send(Message(sender.prefix, "NOTICE", [name, text], true));
|
||||
}
|
||||
}
|
||||
|
||||
void sendTopic(Connection connection)
|
||||
{
|
||||
if(topic.empty)
|
||||
{
|
||||
connection.send(Message(_server.name, "331", [connection.nick, name, "No topic is set"]));
|
||||
}
|
||||
else
|
||||
{
|
||||
connection.send(Message(_server.name, "332", [connection.nick, name, topic], true));
|
||||
}
|
||||
}
|
||||
|
||||
void setTopic(Connection connection, string newTopic)
|
||||
{
|
||||
topic = newTopic;
|
||||
|
||||
foreach(member; members)
|
||||
{
|
||||
member.send(Message(connection.prefix, "TOPIC", [name, newTopic], true));
|
||||
}
|
||||
}
|
||||
|
||||
void kick(Connection kicker, Connection user, string comment)
|
||||
{
|
||||
foreach(member; members)
|
||||
{
|
||||
member.send(Message(kicker.prefix, "KICK", [name, user.nick, comment], true));
|
||||
}
|
||||
|
||||
members = members.remove!(m => m == user);
|
||||
memberModes.remove(user);
|
||||
}
|
||||
|
||||
void sendModes(Connection user)
|
||||
{
|
||||
auto specialModes = "";
|
||||
string[] specialModeParameters;
|
||||
|
||||
if(members.canFind(user) && key !is null)
|
||||
{
|
||||
specialModes ~= "k";
|
||||
specialModeParameters ~= key;
|
||||
}
|
||||
|
||||
if(members.canFind(user) && !userLimit.isNull)
|
||||
{
|
||||
import std.conv : to;
|
||||
|
||||
specialModes ~= "l";
|
||||
specialModeParameters ~= userLimit.to!string;
|
||||
}
|
||||
|
||||
user.send(Message(_server.name, "324", [user.nick, name, "+" ~ modes.idup ~ specialModes] ~ specialModeParameters));
|
||||
}
|
||||
|
||||
bool setMemberMode(Connection target, char mode)
|
||||
{
|
||||
if(memberModes[target].canFind(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memberModes[target] ~= mode;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unsetMemberMode(Connection target, char mode)
|
||||
{
|
||||
if(!memberModes[target].canFind(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//NOTE: byCodeUnit is necessary due to auto-decoding (https://wiki.dlang.org/Language_issues#Unicode_and_ranges)
|
||||
import std.utf : byCodeUnit;
|
||||
import std.range : array;
|
||||
memberModes[target] = memberModes[target].byCodeUnit.remove!(m => m == mode).array;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setMode(char mode)
|
||||
{
|
||||
if(modes.canFind(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
modes ~= mode;
|
||||
|
||||
//TODO: If RFC-strictness is off, clear the invite list when +i is set
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unsetMode(char mode)
|
||||
{
|
||||
if(!modes.canFind(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//NOTE: byCodeUnit is necessary due to auto-decoding (https://wiki.dlang.org/Language_issues#Unicode_and_ranges)
|
||||
import std.utf : byCodeUnit;
|
||||
import std.range : array;
|
||||
modes = modes.byCodeUnit.remove!(m => m == mode).array;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool addMaskListEntry(string mask, char mode)
|
||||
{
|
||||
if(maskLists[mode].canFind!(m => m.toIRCLower == mask.toIRCLower))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
maskLists[mode] ~= mask;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool removeMaskListEntry(string mask, char mode)
|
||||
{
|
||||
if(!maskLists[mode].canFind!(m => m.toIRCLower == mask.toIRCLower))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
maskLists[mode] = maskLists[mode].remove!(m => m.toIRCLower == mask.toIRCLower);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void sendBanList(Connection connection)
|
||||
{
|
||||
foreach(entry; maskLists['b'])
|
||||
{
|
||||
connection.send(Message(_server.name, "367", [connection.nick, name, entry], false));
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "368", [connection.nick, name, "End of channel ban list"], true));
|
||||
}
|
||||
|
||||
void sendExceptList(Connection connection)
|
||||
{
|
||||
foreach(entry; maskLists['e'])
|
||||
{
|
||||
connection.send(Message(_server.name, "348", [connection.nick, name, entry], false));
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "349", [connection.nick, name, "End of channel exception list"], true));
|
||||
}
|
||||
|
||||
void sendInviteList(Connection connection)
|
||||
{
|
||||
foreach(entry; maskLists['I'])
|
||||
{
|
||||
connection.send(Message(_server.name, "346", [connection.nick, name, entry], false));
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "347", [connection.nick, name, "End of channel invite list"], true));
|
||||
}
|
||||
|
||||
bool setKey(string key)
|
||||
{
|
||||
this.key = key;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unsetKey(string key)
|
||||
{
|
||||
if(this.key != key)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.key = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void setUserLimit(uint userLimit)
|
||||
{
|
||||
this.userLimit = userLimit;
|
||||
}
|
||||
|
||||
bool unsetUserLimit()
|
||||
{
|
||||
if(userLimit.isNull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
userLimit.nullify();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
string nickPrefix(Connection member)
|
||||
{
|
||||
if(memberModes[member].canFind('o'))
|
||||
{
|
||||
return "@";
|
||||
}
|
||||
else if(memberModes[member].canFind('v'))
|
||||
{
|
||||
return "+";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
string prefixedNick(Connection member) { return nickPrefix(member) ~ member.nick; }
|
||||
|
||||
bool visibleTo(Connection connection)
|
||||
{
|
||||
return members.canFind(connection) || !modes.canFind('s') && !modes.canFind('p');
|
||||
}
|
||||
|
||||
bool canReceiveMessagesFromUser(Connection connection)
|
||||
{
|
||||
if(modes.canFind('n') && !members.canFind(connection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(modes.canFind('m') && nickPrefix(connection).empty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(maskLists['b'].any!(m => connection.matchesMask(m)) && !maskLists['e'].any!(m => connection.matchesMask(m)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
string name;
|
||||
string topic = "";
|
||||
|
||||
Connection[] members;
|
||||
char[] modes;
|
||||
char[][Connection] memberModes;
|
||||
string[][char] maskLists;
|
||||
|
||||
string key;
|
||||
Nullable!uint userLimit;
|
||||
Connection[] inviteHolders;
|
||||
|
||||
private Server _server;
|
||||
|
||||
this(string name, Server server)
|
||||
{
|
||||
this.name = name;
|
||||
this._server = server;
|
||||
this.maskLists = ['b' : [], 'e' : [], 'I' : []];
|
||||
}
|
||||
|
||||
void join(Connection connection)
|
||||
{
|
||||
members ~= connection;
|
||||
|
||||
if(members.length == 1)
|
||||
{
|
||||
memberModes[connection] ~= 'o';
|
||||
}
|
||||
else
|
||||
{
|
||||
memberModes[connection] = [];
|
||||
}
|
||||
|
||||
if(inviteHolders.canFind(connection))
|
||||
{
|
||||
inviteHolders = inviteHolders.remove!(c => c == connection);
|
||||
}
|
||||
}
|
||||
|
||||
void part(Connection connection, string partMessage)
|
||||
{
|
||||
foreach(member; members)
|
||||
{
|
||||
if(partMessage !is null)
|
||||
{
|
||||
member.send(Message(connection.prefix, "PART", [name, partMessage], true));
|
||||
}
|
||||
else
|
||||
{
|
||||
member.send(Message(connection.prefix, "PART", [name]));
|
||||
}
|
||||
}
|
||||
|
||||
members = members.remove!(m => m == connection);
|
||||
memberModes.remove(connection);
|
||||
}
|
||||
|
||||
void invite(Connection connection)
|
||||
{
|
||||
inviteHolders ~= connection;
|
||||
}
|
||||
|
||||
void sendNames(Connection connection, bool sendRplEndOfNames = true)
|
||||
{
|
||||
string channelType;
|
||||
|
||||
if(modes.canFind('s'))
|
||||
{
|
||||
channelType = "@";
|
||||
}
|
||||
else if(modes.canFind('p'))
|
||||
{
|
||||
channelType = "*";
|
||||
}
|
||||
else
|
||||
{
|
||||
channelType = "=";
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if(sendRplEndOfNames)
|
||||
{
|
||||
connection.sendRplEndOfNames(name);
|
||||
}
|
||||
}
|
||||
|
||||
void sendPrivMsg(Connection sender, string text)
|
||||
{
|
||||
foreach(member; members.filter!(m => m.nick != sender.nick))
|
||||
{
|
||||
member.send(Message(sender.prefix, "PRIVMSG", [name, text], true));
|
||||
}
|
||||
}
|
||||
|
||||
void sendNotice(Connection sender, string text)
|
||||
{
|
||||
foreach(member; members.filter!(m => m.nick != sender.nick))
|
||||
{
|
||||
member.send(Message(sender.prefix, "NOTICE", [name, text], true));
|
||||
}
|
||||
}
|
||||
|
||||
void sendTopic(Connection connection)
|
||||
{
|
||||
if(topic.empty)
|
||||
{
|
||||
connection.send(Message(_server.name, "331", [connection.nick, name, "No topic is set"]));
|
||||
}
|
||||
else
|
||||
{
|
||||
connection.send(Message(_server.name, "332", [connection.nick, name, topic], true));
|
||||
}
|
||||
}
|
||||
|
||||
void setTopic(Connection connection, string newTopic)
|
||||
{
|
||||
topic = newTopic;
|
||||
|
||||
foreach(member; members)
|
||||
{
|
||||
member.send(Message(connection.prefix, "TOPIC", [name, newTopic], true));
|
||||
}
|
||||
}
|
||||
|
||||
void kick(Connection kicker, Connection user, string comment)
|
||||
{
|
||||
foreach(member; members)
|
||||
{
|
||||
member.send(Message(kicker.prefix, "KICK", [name, user.nick, comment], true));
|
||||
}
|
||||
|
||||
members = members.remove!(m => m == user);
|
||||
memberModes.remove(user);
|
||||
}
|
||||
|
||||
void sendModes(Connection user)
|
||||
{
|
||||
auto specialModes = "";
|
||||
string[] specialModeParameters;
|
||||
|
||||
if(members.canFind(user) && key !is null)
|
||||
{
|
||||
specialModes ~= "k";
|
||||
specialModeParameters ~= key;
|
||||
}
|
||||
|
||||
if(members.canFind(user) && !userLimit.isNull)
|
||||
{
|
||||
import std.conv : to;
|
||||
|
||||
specialModes ~= "l";
|
||||
specialModeParameters ~= userLimit.to!string;
|
||||
}
|
||||
|
||||
user.send(Message(_server.name, "324", [user.nick, name, "+" ~ modes.idup ~ specialModes] ~ specialModeParameters));
|
||||
}
|
||||
|
||||
bool setMemberMode(Connection target, char mode)
|
||||
{
|
||||
if(memberModes[target].canFind(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memberModes[target] ~= mode;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unsetMemberMode(Connection target, char mode)
|
||||
{
|
||||
if(!memberModes[target].canFind(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//NOTE: byCodeUnit is necessary due to auto-decoding (https://wiki.dlang.org/Language_issues#Unicode_and_ranges)
|
||||
import std.utf : byCodeUnit;
|
||||
import std.range : array;
|
||||
memberModes[target] = memberModes[target].byCodeUnit.remove!(m => m == mode).array;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setMode(char mode)
|
||||
{
|
||||
if(modes.canFind(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
modes ~= mode;
|
||||
|
||||
//TODO: If RFC-strictness is off, clear the invite list when +i is set
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unsetMode(char mode)
|
||||
{
|
||||
if(!modes.canFind(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//NOTE: byCodeUnit is necessary due to auto-decoding (https://wiki.dlang.org/Language_issues#Unicode_and_ranges)
|
||||
import std.utf : byCodeUnit;
|
||||
import std.range : array;
|
||||
modes = modes.byCodeUnit.remove!(m => m == mode).array;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool addMaskListEntry(string mask, char mode)
|
||||
{
|
||||
if(maskLists[mode].canFind!(m => m.toIRCLower == mask.toIRCLower))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
maskLists[mode] ~= mask;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool removeMaskListEntry(string mask, char mode)
|
||||
{
|
||||
if(!maskLists[mode].canFind!(m => m.toIRCLower == mask.toIRCLower))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
maskLists[mode] = maskLists[mode].remove!(m => m.toIRCLower == mask.toIRCLower);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void sendBanList(Connection connection)
|
||||
{
|
||||
foreach(entry; maskLists['b'])
|
||||
{
|
||||
connection.send(Message(_server.name, "367", [connection.nick, name, entry], false));
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "368", [connection.nick, name, "End of channel ban list"], true));
|
||||
}
|
||||
|
||||
void sendExceptList(Connection connection)
|
||||
{
|
||||
foreach(entry; maskLists['e'])
|
||||
{
|
||||
connection.send(Message(_server.name, "348", [connection.nick, name, entry], false));
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "349", [connection.nick, name, "End of channel exception list"], true));
|
||||
}
|
||||
|
||||
void sendInviteList(Connection connection)
|
||||
{
|
||||
foreach(entry; maskLists['I'])
|
||||
{
|
||||
connection.send(Message(_server.name, "346", [connection.nick, name, entry], false));
|
||||
}
|
||||
|
||||
connection.send(Message(_server.name, "347", [connection.nick, name, "End of channel invite list"], true));
|
||||
}
|
||||
|
||||
bool setKey(string key)
|
||||
{
|
||||
this.key = key;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unsetKey(string key)
|
||||
{
|
||||
if(this.key != key)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.key = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void setUserLimit(uint userLimit)
|
||||
{
|
||||
this.userLimit = userLimit;
|
||||
}
|
||||
|
||||
bool unsetUserLimit()
|
||||
{
|
||||
if(userLimit.isNull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
userLimit.nullify();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
string nickPrefix(Connection member)
|
||||
{
|
||||
if(memberModes[member].canFind('o'))
|
||||
{
|
||||
return "@";
|
||||
}
|
||||
else if(memberModes[member].canFind('v'))
|
||||
{
|
||||
return "+";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
string prefixedNick(Connection member) { return nickPrefix(member) ~ member.nick; }
|
||||
|
||||
bool visibleTo(Connection connection)
|
||||
{
|
||||
return members.canFind(connection) || !modes.canFind('s') && !modes.canFind('p');
|
||||
}
|
||||
|
||||
bool canReceiveMessagesFromUser(Connection connection)
|
||||
{
|
||||
if(modes.canFind('n') && !members.canFind(connection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(modes.canFind('m') && nickPrefix(connection).empty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(maskLists['b'].any!(m => connection.matchesMask(m)) && !maskLists['e'].any!(m => connection.matchesMask(m)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,64 +9,64 @@ import std.algorithm : map;
|
|||
@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 || pc != input.front)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
input.popFront();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return input.empty;
|
||||
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 || pc != input.front)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
input.popFront();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return input.empty;
|
||||
}
|
||||
|
||||
@safe pure
|
||||
dchar toIRCLower(dchar input)
|
||||
{
|
||||
import std.uni : toLower;
|
||||
switch(input)
|
||||
{
|
||||
case '[':
|
||||
return '{';
|
||||
case ']':
|
||||
return '}';
|
||||
case '\\':
|
||||
return '|';
|
||||
default:
|
||||
return input.toLower;
|
||||
}
|
||||
import std.uni : toLower;
|
||||
switch(input)
|
||||
{
|
||||
case '[':
|
||||
return '{';
|
||||
case ']':
|
||||
return '}';
|
||||
case '\\':
|
||||
return '|';
|
||||
default:
|
||||
return input.toLower;
|
||||
}
|
||||
}
|
||||
|
||||
@safe pure
|
||||
string toIRCLower(string input)
|
||||
{
|
||||
import std.utf : byChar;
|
||||
return input.map!toIRCLower.byChar.array.idup;
|
||||
import std.utf : byChar;
|
||||
return input.map!toIRCLower.byChar.array.idup;
|
||||
}
|
||||
|
|
|
@ -9,85 +9,85 @@ import std.conv;
|
|||
//TODO: Make this a class
|
||||
struct Message
|
||||
{
|
||||
string prefix;
|
||||
string command;
|
||||
string[] parameters;
|
||||
bool prefixedParameter;
|
||||
string prefix;
|
||||
string command;
|
||||
string[] parameters;
|
||||
bool prefixedParameter;
|
||||
|
||||
//NOTE: The RFCs don't state what this is exactly, but common implementations use the byte count of the message parameters
|
||||
ulong bytes;
|
||||
//NOTE: The RFCs don't state what this is exactly, but common implementations use the byte count of the message parameters
|
||||
ulong bytes;
|
||||
|
||||
static Message fromString(string line)
|
||||
{
|
||||
string prefix = null;
|
||||
if(line.startsWith(':'))
|
||||
{
|
||||
line = line[1 .. $];
|
||||
prefix = line[0 .. line.indexOf(' ')];
|
||||
line = line[prefix.length + 1 .. $];
|
||||
}
|
||||
static Message fromString(string line)
|
||||
{
|
||||
string prefix = null;
|
||||
if(line.startsWith(':'))
|
||||
{
|
||||
line = line[1 .. $];
|
||||
prefix = line[0 .. line.indexOf(' ')];
|
||||
line = line[prefix.length + 1 .. $];
|
||||
}
|
||||
|
||||
//stop early when no space character can be found (message without parameters)
|
||||
if(!line.canFind(' '))
|
||||
{
|
||||
return Message(prefix, line, [], false);
|
||||
}
|
||||
//stop early when no space character can be found (message without parameters)
|
||||
if(!line.canFind(' '))
|
||||
{
|
||||
return Message(prefix, line, [], false);
|
||||
}
|
||||
|
||||
auto command = line[0 .. line.indexOf(' ')];
|
||||
line = line[command.length + 1 .. $];
|
||||
auto bytes = line.length;
|
||||
string[] params = [];
|
||||
bool prefixedParam;
|
||||
while(true)
|
||||
{
|
||||
if(line.startsWith(':'))
|
||||
{
|
||||
params ~= line[1 .. $];
|
||||
prefixedParam = true;
|
||||
break;
|
||||
}
|
||||
else if(line.canFind(' '))
|
||||
{
|
||||
auto param = line[0 .. line.indexOf(' ')];
|
||||
line = line[param.length + 1 .. $];
|
||||
params ~= param;
|
||||
}
|
||||
else
|
||||
{
|
||||
params ~= line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto command = line[0 .. line.indexOf(' ')];
|
||||
line = line[command.length + 1 .. $];
|
||||
auto bytes = line.length;
|
||||
string[] params = [];
|
||||
bool prefixedParam;
|
||||
while(true)
|
||||
{
|
||||
if(line.startsWith(':'))
|
||||
{
|
||||
params ~= line[1 .. $];
|
||||
prefixedParam = true;
|
||||
break;
|
||||
}
|
||||
else if(line.canFind(' '))
|
||||
{
|
||||
auto param = line[0 .. line.indexOf(' ')];
|
||||
line = line[param.length + 1 .. $];
|
||||
params ~= param;
|
||||
}
|
||||
else
|
||||
{
|
||||
params ~= line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Message(prefix, command, params, prefixedParam, bytes);
|
||||
}
|
||||
return Message(prefix, command, params, prefixedParam, bytes);
|
||||
}
|
||||
|
||||
string toString()
|
||||
{
|
||||
auto message = "";
|
||||
if(prefix != null)
|
||||
{
|
||||
message = ":" ~ prefix ~ " ";
|
||||
}
|
||||
string toString()
|
||||
{
|
||||
auto message = "";
|
||||
if(prefix != null)
|
||||
{
|
||||
message = ":" ~ prefix ~ " ";
|
||||
}
|
||||
|
||||
if(parameters.length == 0)
|
||||
{
|
||||
return message ~ command;
|
||||
}
|
||||
if(parameters.length == 0)
|
||||
{
|
||||
return message ~ command;
|
||||
}
|
||||
|
||||
message ~= command ~ " ";
|
||||
if(parameters.length > 1)
|
||||
{
|
||||
message ~= parameters[0 .. $-1].join(' ') ~ " ";
|
||||
}
|
||||
message ~= command ~ " ";
|
||||
if(parameters.length > 1)
|
||||
{
|
||||
message ~= parameters[0 .. $-1].join(' ') ~ " ";
|
||||
}
|
||||
|
||||
if(parameters[$-1].canFind(' ') || prefixedParameter)
|
||||
{
|
||||
message ~= ":";
|
||||
}
|
||||
message ~= parameters[$-1];
|
||||
if(parameters[$-1].canFind(' ') || prefixedParameter)
|
||||
{
|
||||
message ~= ":";
|
||||
}
|
||||
message ~= parameters[$-1];
|
||||
|
||||
return message;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,446 +20,446 @@ import ircd.helpers;
|
|||
|
||||
class Server
|
||||
{
|
||||
Connection[] connections;
|
||||
|
||||
enum creationDate = packageTimestampISO.until('T').text; //TODO: Also show time when RFC-strictness is off
|
||||
enum versionString = "salty-ircd-" ~ packageVersion;
|
||||
|
||||
string name;
|
||||
enum string info = "A salty-ircd server"; //TODO: Make server info configurable
|
||||
|
||||
string motd;
|
||||
|
||||
Channel[] channels;
|
||||
|
||||
private uint[string] _commandUsage;
|
||||
private ulong[string] _commandBytes;
|
||||
|
||||
private string _pass = null;
|
||||
|
||||
private SysTime _startTime;
|
||||
|
||||
this()
|
||||
{
|
||||
name = Socket.hostName;
|
||||
|
||||
readMotd();
|
||||
|
||||
_startTime = Clock.currTime;
|
||||
|
||||
runTask(&pingLoop);
|
||||
}
|
||||
|
||||
private void readMotd()
|
||||
{
|
||||
import std.file : exists, readText;
|
||||
if(exists("motd"))
|
||||
{
|
||||
motd = readText("motd");
|
||||
}
|
||||
}
|
||||
|
||||
private void pingLoop()
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
foreach(connection; connections)
|
||||
{
|
||||
connection.send(Message(null, "PING", [name], true));
|
||||
}
|
||||
sleep(30.seconds);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptConnection(TCPConnection tcpConnection)
|
||||
{
|
||||
auto connection = new Connection(tcpConnection, this);
|
||||
connections ~= connection;
|
||||
connection.handle();
|
||||
connections = connections.filter!(c => c != connection).array;
|
||||
}
|
||||
|
||||
static bool isValidChannelName(string name)
|
||||
{
|
||||
return (name.startsWith('#') || name.startsWith('&')) && name.length <= 200;
|
||||
}
|
||||
|
||||
static bool isValidNick(string name)
|
||||
{
|
||||
import std.ascii : digits, letters;
|
||||
|
||||
if(name.length > 9)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach(i, c; name)
|
||||
{
|
||||
auto allowed = letters ~ "[]\\`_^{|}";
|
||||
if(i > 0)
|
||||
{
|
||||
allowed ~= digits ~ "-";
|
||||
}
|
||||
|
||||
if (!allowed.canFind(c))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isValidUserMask(string mask)
|
||||
{
|
||||
import std.regex : ctRegex, matchFirst;
|
||||
|
||||
auto validMaskRegex = ctRegex!r"^([^!]+)!([^@]+)@(.+)$";
|
||||
return !mask.matchFirst(validMaskRegex).empty;
|
||||
}
|
||||
|
||||
Connection[] findConnectionByNick(string nick)
|
||||
{
|
||||
return connections.find!(c => c.nick.toIRCLower == nick.toIRCLower);
|
||||
}
|
||||
|
||||
bool canFindConnectionByNick(string nick)
|
||||
{
|
||||
return !findConnectionByNick(nick).empty;
|
||||
}
|
||||
|
||||
bool isNickAvailable(string nick)
|
||||
{
|
||||
return !canFindConnectionByNick(nick);
|
||||
}
|
||||
|
||||
Channel[] findChannelByName(string name)
|
||||
{
|
||||
return channels.find!(c => c.name.toIRCLower == name.toIRCLower);
|
||||
}
|
||||
|
||||
bool canFindChannelByName(string name)
|
||||
{
|
||||
return !findChannelByName(name).empty;
|
||||
}
|
||||
|
||||
void join(Connection connection, string channelName)
|
||||
{
|
||||
auto channelRange = findChannelByName(channelName);
|
||||
Channel channel;
|
||||
if(channelRange.empty)
|
||||
{
|
||||
channel = new Channel(channelName, this);
|
||||
channels ~= channel;
|
||||
}
|
||||
else
|
||||
{
|
||||
channel = channelRange[0];
|
||||
}
|
||||
channel.join(connection);
|
||||
|
||||
foreach(member; channel.members)
|
||||
{
|
||||
member.send(Message(connection.prefix, "JOIN", [channelName]));
|
||||
}
|
||||
|
||||
channel.sendNames(connection);
|
||||
|
||||
if(!channel.topic.empty)
|
||||
{
|
||||
channel.sendTopic(connection);
|
||||
}
|
||||
}
|
||||
|
||||
void part(Connection connection, string channelName, string partMessage)
|
||||
{
|
||||
auto channel = connection.channels.array.find!(c => c.name.toIRCLower == channelName.toIRCLower)[0];
|
||||
|
||||
channel.part(connection, partMessage);
|
||||
|
||||
if(channel.members.empty)
|
||||
{
|
||||
channels = channels.remove!(c => c == channel);
|
||||
}
|
||||
}
|
||||
|
||||
void quit(Connection connection, string quitMessage)
|
||||
{
|
||||
Connection[] peers;
|
||||
foreach(channel; connection.channels)
|
||||
{
|
||||
peers ~= channel.members;
|
||||
channel.members = channel.members.remove!(m => m == connection);
|
||||
if(channel.members.empty)
|
||||
{
|
||||
channels = channels.remove!(c => c == channel);
|
||||
}
|
||||
}
|
||||
peers = peers.sort().uniq.filter!(c => c != connection).array;
|
||||
|
||||
foreach(peer; peers)
|
||||
{
|
||||
if(quitMessage !is null)
|
||||
{
|
||||
peer.send(Message(connection.prefix, "QUIT", [quitMessage], true));
|
||||
}
|
||||
else
|
||||
{
|
||||
peer.send(Message(connection.prefix, "QUIT", [connection.nick], true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void whoChannel(Connection origin, string channelName, bool operatorsOnly)
|
||||
{
|
||||
//TODO: Check what RFCs say about secret/private channels
|
||||
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
foreach(c; channel.members.filter!(c => !operatorsOnly || c.isOperator)
|
||||
.filter!(c => c.visibleTo(origin)))
|
||||
{
|
||||
//TODO: Support hop count
|
||||
origin.sendWhoReply(channelName, c, channel.nickPrefix(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 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;
|
||||
auto nickPrefix = c.channels.empty ? "" : c.channels.array[0].nickPrefix(c);
|
||||
//TODO: Support hop count
|
||||
origin.sendWhoReply(channelName, c, nickPrefix, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void privmsgToChannel(Connection sender, string target, string text)
|
||||
{
|
||||
auto channel = findChannelByName(target)[0];
|
||||
channel.sendPrivMsg(sender, text);
|
||||
}
|
||||
|
||||
void privmsgToUser(Connection sender, string target, string text)
|
||||
{
|
||||
auto user = findConnectionByNick(target)[0];
|
||||
user.send(Message(sender.prefix, "PRIVMSG", [target, text], true));
|
||||
}
|
||||
|
||||
void noticeToChannel(Connection sender, string target, string text)
|
||||
{
|
||||
auto channel = findChannelByName(target)[0];
|
||||
channel.sendNotice(sender, text);
|
||||
}
|
||||
|
||||
void noticeToUser(Connection sender, string target, string text)
|
||||
{
|
||||
auto user = findConnectionByNick(target)[0];
|
||||
user.send(Message(sender.prefix, "NOTICE", [target, text], true));
|
||||
}
|
||||
|
||||
void sendChannelTopic(Connection origin, string channelName)
|
||||
{
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
channel.sendTopic(origin);
|
||||
}
|
||||
|
||||
void setChannelTopic(Connection origin, string channelName, string newTopic)
|
||||
{
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
channel.setTopic(origin, newTopic);
|
||||
}
|
||||
|
||||
void sendChannelNames(Connection connection, string channelName)
|
||||
{
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
channel.sendNames(connection);
|
||||
}
|
||||
|
||||
void sendGlobalNames(Connection connection)
|
||||
{
|
||||
foreach(channel; channels.filter!(c => c.visibleTo(connection)))
|
||||
{
|
||||
channel.sendNames(connection, false);
|
||||
}
|
||||
|
||||
auto otherUsers = connections.filter!(c => !c.modes.canFind('i') && c.channels.filter!(ch => !ch.modes.canFind('s') && !ch.modes.canFind('p')).empty);
|
||||
if(!otherUsers.empty)
|
||||
{
|
||||
connection.send(Message(name, "353", [connection.nick, "=", "*", otherUsers.map!(m => m.nick).join(' ')], true));
|
||||
}
|
||||
|
||||
connection.sendRplEndOfNames("*");
|
||||
}
|
||||
|
||||
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.sendRplListEnd();
|
||||
}
|
||||
|
||||
void sendPartialList(Connection connection, string[] channelNames)
|
||||
{
|
||||
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.sendRplListEnd();
|
||||
}
|
||||
|
||||
void sendVersion(Connection connection)
|
||||
{
|
||||
connection.send(Message(name, "351", [connection.nick, versionString ~ ".", name, ""], true));
|
||||
}
|
||||
|
||||
void sendTime(Connection connection)
|
||||
{
|
||||
auto timeString = Clock.currTime.toISOExtString;
|
||||
connection.send(Message(name, "391", [connection.nick, name, timeString], true));
|
||||
}
|
||||
|
||||
void invite(Connection inviter, string target, string channelName)
|
||||
{
|
||||
auto user = findConnectionByNick(target)[0];
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
|
||||
channel.invite(user);
|
||||
|
||||
user.send(Message(inviter.prefix, "INVITE", [user.nick, channelName]));
|
||||
}
|
||||
|
||||
void sendMotd(Connection connection)
|
||||
{
|
||||
connection.send(Message(name, "375", [connection.nick, ":- " ~ name ~ " Message of the day - "], true));
|
||||
foreach(line; motd.splitLines)
|
||||
{
|
||||
//TODO: Implement line wrapping
|
||||
connection.send(Message(name, "372", [connection.nick, ":- " ~ line], true));
|
||||
}
|
||||
connection.send(Message(name, "376", [connection.nick, "End of MOTD command"], true));
|
||||
}
|
||||
|
||||
void sendLusers(Connection connection)
|
||||
{
|
||||
//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));
|
||||
|
||||
if(connections.any!(c => c.isOperator))
|
||||
{
|
||||
connection.send(Message(name, "252", [connection.nick, connections.count!(c => c.isOperator).to!string, "operator(s) online"], true));
|
||||
}
|
||||
|
||||
if(connections.any!(c => !c.registered))
|
||||
{
|
||||
connection.send(Message(name, "253", [connection.nick, connections.count!(c => !c.registered).to!string, "unknown connection(s)"], true));
|
||||
}
|
||||
|
||||
if(channels.length > 0)
|
||||
{
|
||||
connection.send(Message(name, "254", [connection.nick, channels.length.to!string, "channels formed"], true));
|
||||
}
|
||||
|
||||
connection.send(Message(name, "255", [connection.nick, "I have " ~ connections.length.to!string ~ " clients and 1 servers"], true));
|
||||
}
|
||||
|
||||
void ison(Connection connection, string[] nicks)
|
||||
{
|
||||
auto reply = nicks.filter!(n => canFindConnectionByNick(n)).join(' ');
|
||||
|
||||
connection.send(Message(name, "303", [connection.nick, reply], true));
|
||||
}
|
||||
|
||||
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));
|
||||
//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));
|
||||
if(user.isOperator)
|
||||
{
|
||||
connection.send(Message(name, "313", [connection.nick, user.nick, "is an IRC operator"], true));
|
||||
}
|
||||
auto idleSeconds = (Clock.currTime - user.lastMessageTime).total!"seconds";
|
||||
connection.send(Message(name, "317", [connection.nick, user.nick, idleSeconds.to!string, "seconds idle"], true));
|
||||
auto userChannels = user.channels.map!(c => c.nickPrefix(user) ~ c.name).join(' ');
|
||||
connection.send(Message(name, "319", [connection.nick, user.nick, userChannels], true));
|
||||
}
|
||||
|
||||
void kill(Connection killer, string nick, string comment)
|
||||
{
|
||||
auto user = findConnectionByNick(nick)[0];
|
||||
|
||||
user.send(Message(killer.prefix, "KILL", [nick, comment], true));
|
||||
|
||||
quit(user, "Killed by " ~ killer.nick ~ " (" ~ comment ~ ")");
|
||||
|
||||
user.send(Message(null, "ERROR", ["Closing Link: Killed by " ~ killer.nick ~ " (" ~ comment ~ ")"], true));
|
||||
user.closeConnection();
|
||||
}
|
||||
|
||||
void kick(Connection kicker, string channelName, string nick, string comment)
|
||||
{
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
auto user = findConnectionByNick(nick)[0];
|
||||
|
||||
channel.kick(kicker, user, comment);
|
||||
}
|
||||
|
||||
void updateCommandStatistics(Message message)
|
||||
{
|
||||
auto command = message.command.toUpper;
|
||||
if(command !in _commandUsage)
|
||||
{
|
||||
_commandUsage[command] = 0;
|
||||
_commandBytes[command] = 0;
|
||||
}
|
||||
_commandUsage[command]++;
|
||||
_commandBytes[command] += message.bytes;
|
||||
}
|
||||
|
||||
void sendCommandUsage(Connection connection)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
void sendUptime(Connection connection)
|
||||
{
|
||||
import std.format : format;
|
||||
|
||||
auto uptime = (Clock.currTime - _startTime).split!("days", "hours", "minutes", "seconds");
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
void setPass(string pass)
|
||||
{
|
||||
_pass = pass;
|
||||
}
|
||||
|
||||
bool isPassCorrect(string pass)
|
||||
{
|
||||
return pass == _pass;
|
||||
}
|
||||
|
||||
void listen(ushort port = 6667)
|
||||
{
|
||||
listenTCP(port, &acceptConnection);
|
||||
}
|
||||
|
||||
void listen(ushort port, string address)
|
||||
{
|
||||
listenTCP(port, &acceptConnection, address);
|
||||
}
|
||||
Connection[] connections;
|
||||
|
||||
enum creationDate = packageTimestampISO.until('T').text; //TODO: Also show time when RFC-strictness is off
|
||||
enum versionString = "salty-ircd-" ~ packageVersion;
|
||||
|
||||
string name;
|
||||
enum string info = "A salty-ircd server"; //TODO: Make server info configurable
|
||||
|
||||
string motd;
|
||||
|
||||
Channel[] channels;
|
||||
|
||||
private uint[string] _commandUsage;
|
||||
private ulong[string] _commandBytes;
|
||||
|
||||
private string _pass = null;
|
||||
|
||||
private SysTime _startTime;
|
||||
|
||||
this()
|
||||
{
|
||||
name = Socket.hostName;
|
||||
|
||||
readMotd();
|
||||
|
||||
_startTime = Clock.currTime;
|
||||
|
||||
runTask(&pingLoop);
|
||||
}
|
||||
|
||||
private void readMotd()
|
||||
{
|
||||
import std.file : exists, readText;
|
||||
if(exists("motd"))
|
||||
{
|
||||
motd = readText("motd");
|
||||
}
|
||||
}
|
||||
|
||||
private void pingLoop()
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
foreach(connection; connections)
|
||||
{
|
||||
connection.send(Message(null, "PING", [name], true));
|
||||
}
|
||||
sleep(30.seconds);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptConnection(TCPConnection tcpConnection)
|
||||
{
|
||||
auto connection = new Connection(tcpConnection, this);
|
||||
connections ~= connection;
|
||||
connection.handle();
|
||||
connections = connections.filter!(c => c != connection).array;
|
||||
}
|
||||
|
||||
static bool isValidChannelName(string name)
|
||||
{
|
||||
return (name.startsWith('#') || name.startsWith('&')) && name.length <= 200;
|
||||
}
|
||||
|
||||
static bool isValidNick(string name)
|
||||
{
|
||||
import std.ascii : digits, letters;
|
||||
|
||||
if(name.length > 9)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach(i, c; name)
|
||||
{
|
||||
auto allowed = letters ~ "[]\\`_^{|}";
|
||||
if(i > 0)
|
||||
{
|
||||
allowed ~= digits ~ "-";
|
||||
}
|
||||
|
||||
if (!allowed.canFind(c))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isValidUserMask(string mask)
|
||||
{
|
||||
import std.regex : ctRegex, matchFirst;
|
||||
|
||||
auto validMaskRegex = ctRegex!r"^([^!]+)!([^@]+)@(.+)$";
|
||||
return !mask.matchFirst(validMaskRegex).empty;
|
||||
}
|
||||
|
||||
Connection[] findConnectionByNick(string nick)
|
||||
{
|
||||
return connections.find!(c => c.nick.toIRCLower == nick.toIRCLower);
|
||||
}
|
||||
|
||||
bool canFindConnectionByNick(string nick)
|
||||
{
|
||||
return !findConnectionByNick(nick).empty;
|
||||
}
|
||||
|
||||
bool isNickAvailable(string nick)
|
||||
{
|
||||
return !canFindConnectionByNick(nick);
|
||||
}
|
||||
|
||||
Channel[] findChannelByName(string name)
|
||||
{
|
||||
return channels.find!(c => c.name.toIRCLower == name.toIRCLower);
|
||||
}
|
||||
|
||||
bool canFindChannelByName(string name)
|
||||
{
|
||||
return !findChannelByName(name).empty;
|
||||
}
|
||||
|
||||
void join(Connection connection, string channelName)
|
||||
{
|
||||
auto channelRange = findChannelByName(channelName);
|
||||
Channel channel;
|
||||
if(channelRange.empty)
|
||||
{
|
||||
channel = new Channel(channelName, this);
|
||||
channels ~= channel;
|
||||
}
|
||||
else
|
||||
{
|
||||
channel = channelRange[0];
|
||||
}
|
||||
channel.join(connection);
|
||||
|
||||
foreach(member; channel.members)
|
||||
{
|
||||
member.send(Message(connection.prefix, "JOIN", [channelName]));
|
||||
}
|
||||
|
||||
channel.sendNames(connection);
|
||||
|
||||
if(!channel.topic.empty)
|
||||
{
|
||||
channel.sendTopic(connection);
|
||||
}
|
||||
}
|
||||
|
||||
void part(Connection connection, string channelName, string partMessage)
|
||||
{
|
||||
auto channel = connection.channels.array.find!(c => c.name.toIRCLower == channelName.toIRCLower)[0];
|
||||
|
||||
channel.part(connection, partMessage);
|
||||
|
||||
if(channel.members.empty)
|
||||
{
|
||||
channels = channels.remove!(c => c == channel);
|
||||
}
|
||||
}
|
||||
|
||||
void quit(Connection connection, string quitMessage)
|
||||
{
|
||||
Connection[] peers;
|
||||
foreach(channel; connection.channels)
|
||||
{
|
||||
peers ~= channel.members;
|
||||
channel.members = channel.members.remove!(m => m == connection);
|
||||
if(channel.members.empty)
|
||||
{
|
||||
channels = channels.remove!(c => c == channel);
|
||||
}
|
||||
}
|
||||
peers = peers.sort().uniq.filter!(c => c != connection).array;
|
||||
|
||||
foreach(peer; peers)
|
||||
{
|
||||
if(quitMessage !is null)
|
||||
{
|
||||
peer.send(Message(connection.prefix, "QUIT", [quitMessage], true));
|
||||
}
|
||||
else
|
||||
{
|
||||
peer.send(Message(connection.prefix, "QUIT", [connection.nick], true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void whoChannel(Connection origin, string channelName, bool operatorsOnly)
|
||||
{
|
||||
//TODO: Check what RFCs say about secret/private channels
|
||||
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
foreach(c; channel.members.filter!(c => !operatorsOnly || c.isOperator)
|
||||
.filter!(c => c.visibleTo(origin)))
|
||||
{
|
||||
//TODO: Support hop count
|
||||
origin.sendWhoReply(channelName, c, channel.nickPrefix(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 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;
|
||||
auto nickPrefix = c.channels.empty ? "" : c.channels.array[0].nickPrefix(c);
|
||||
//TODO: Support hop count
|
||||
origin.sendWhoReply(channelName, c, nickPrefix, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void privmsgToChannel(Connection sender, string target, string text)
|
||||
{
|
||||
auto channel = findChannelByName(target)[0];
|
||||
channel.sendPrivMsg(sender, text);
|
||||
}
|
||||
|
||||
void privmsgToUser(Connection sender, string target, string text)
|
||||
{
|
||||
auto user = findConnectionByNick(target)[0];
|
||||
user.send(Message(sender.prefix, "PRIVMSG", [target, text], true));
|
||||
}
|
||||
|
||||
void noticeToChannel(Connection sender, string target, string text)
|
||||
{
|
||||
auto channel = findChannelByName(target)[0];
|
||||
channel.sendNotice(sender, text);
|
||||
}
|
||||
|
||||
void noticeToUser(Connection sender, string target, string text)
|
||||
{
|
||||
auto user = findConnectionByNick(target)[0];
|
||||
user.send(Message(sender.prefix, "NOTICE", [target, text], true));
|
||||
}
|
||||
|
||||
void sendChannelTopic(Connection origin, string channelName)
|
||||
{
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
channel.sendTopic(origin);
|
||||
}
|
||||
|
||||
void setChannelTopic(Connection origin, string channelName, string newTopic)
|
||||
{
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
channel.setTopic(origin, newTopic);
|
||||
}
|
||||
|
||||
void sendChannelNames(Connection connection, string channelName)
|
||||
{
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
channel.sendNames(connection);
|
||||
}
|
||||
|
||||
void sendGlobalNames(Connection connection)
|
||||
{
|
||||
foreach(channel; channels.filter!(c => c.visibleTo(connection)))
|
||||
{
|
||||
channel.sendNames(connection, false);
|
||||
}
|
||||
|
||||
auto otherUsers = connections.filter!(c => !c.modes.canFind('i') && c.channels.filter!(ch => !ch.modes.canFind('s') && !ch.modes.canFind('p')).empty);
|
||||
if(!otherUsers.empty)
|
||||
{
|
||||
connection.send(Message(name, "353", [connection.nick, "=", "*", otherUsers.map!(m => m.nick).join(' ')], true));
|
||||
}
|
||||
|
||||
connection.sendRplEndOfNames("*");
|
||||
}
|
||||
|
||||
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.sendRplListEnd();
|
||||
}
|
||||
|
||||
void sendPartialList(Connection connection, string[] channelNames)
|
||||
{
|
||||
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.sendRplListEnd();
|
||||
}
|
||||
|
||||
void sendVersion(Connection connection)
|
||||
{
|
||||
connection.send(Message(name, "351", [connection.nick, versionString ~ ".", name, ""], true));
|
||||
}
|
||||
|
||||
void sendTime(Connection connection)
|
||||
{
|
||||
auto timeString = Clock.currTime.toISOExtString;
|
||||
connection.send(Message(name, "391", [connection.nick, name, timeString], true));
|
||||
}
|
||||
|
||||
void invite(Connection inviter, string target, string channelName)
|
||||
{
|
||||
auto user = findConnectionByNick(target)[0];
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
|
||||
channel.invite(user);
|
||||
|
||||
user.send(Message(inviter.prefix, "INVITE", [user.nick, channelName]));
|
||||
}
|
||||
|
||||
void sendMotd(Connection connection)
|
||||
{
|
||||
connection.send(Message(name, "375", [connection.nick, ":- " ~ name ~ " Message of the day - "], true));
|
||||
foreach(line; motd.splitLines)
|
||||
{
|
||||
//TODO: Implement line wrapping
|
||||
connection.send(Message(name, "372", [connection.nick, ":- " ~ line], true));
|
||||
}
|
||||
connection.send(Message(name, "376", [connection.nick, "End of MOTD command"], true));
|
||||
}
|
||||
|
||||
void sendLusers(Connection connection)
|
||||
{
|
||||
//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));
|
||||
|
||||
if(connections.any!(c => c.isOperator))
|
||||
{
|
||||
connection.send(Message(name, "252", [connection.nick, connections.count!(c => c.isOperator).to!string, "operator(s) online"], true));
|
||||
}
|
||||
|
||||
if(connections.any!(c => !c.registered))
|
||||
{
|
||||
connection.send(Message(name, "253", [connection.nick, connections.count!(c => !c.registered).to!string, "unknown connection(s)"], true));
|
||||
}
|
||||
|
||||
if(channels.length > 0)
|
||||
{
|
||||
connection.send(Message(name, "254", [connection.nick, channels.length.to!string, "channels formed"], true));
|
||||
}
|
||||
|
||||
connection.send(Message(name, "255", [connection.nick, "I have " ~ connections.length.to!string ~ " clients and 1 servers"], true));
|
||||
}
|
||||
|
||||
void ison(Connection connection, string[] nicks)
|
||||
{
|
||||
auto reply = nicks.filter!(n => canFindConnectionByNick(n)).join(' ');
|
||||
|
||||
connection.send(Message(name, "303", [connection.nick, reply], true));
|
||||
}
|
||||
|
||||
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));
|
||||
//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));
|
||||
if(user.isOperator)
|
||||
{
|
||||
connection.send(Message(name, "313", [connection.nick, user.nick, "is an IRC operator"], true));
|
||||
}
|
||||
auto idleSeconds = (Clock.currTime - user.lastMessageTime).total!"seconds";
|
||||
connection.send(Message(name, "317", [connection.nick, user.nick, idleSeconds.to!string, "seconds idle"], true));
|
||||
auto userChannels = user.channels.map!(c => c.nickPrefix(user) ~ c.name).join(' ');
|
||||
connection.send(Message(name, "319", [connection.nick, user.nick, userChannels], true));
|
||||
}
|
||||
|
||||
void kill(Connection killer, string nick, string comment)
|
||||
{
|
||||
auto user = findConnectionByNick(nick)[0];
|
||||
|
||||
user.send(Message(killer.prefix, "KILL", [nick, comment], true));
|
||||
|
||||
quit(user, "Killed by " ~ killer.nick ~ " (" ~ comment ~ ")");
|
||||
|
||||
user.send(Message(null, "ERROR", ["Closing Link: Killed by " ~ killer.nick ~ " (" ~ comment ~ ")"], true));
|
||||
user.closeConnection();
|
||||
}
|
||||
|
||||
void kick(Connection kicker, string channelName, string nick, string comment)
|
||||
{
|
||||
auto channel = findChannelByName(channelName)[0];
|
||||
auto user = findConnectionByNick(nick)[0];
|
||||
|
||||
channel.kick(kicker, user, comment);
|
||||
}
|
||||
|
||||
void updateCommandStatistics(Message message)
|
||||
{
|
||||
auto command = message.command.toUpper;
|
||||
if(command !in _commandUsage)
|
||||
{
|
||||
_commandUsage[command] = 0;
|
||||
_commandBytes[command] = 0;
|
||||
}
|
||||
_commandUsage[command]++;
|
||||
_commandBytes[command] += message.bytes;
|
||||
}
|
||||
|
||||
void sendCommandUsage(Connection connection)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
void sendUptime(Connection connection)
|
||||
{
|
||||
import std.format : format;
|
||||
|
||||
auto uptime = (Clock.currTime - _startTime).split!("days", "hours", "minutes", "seconds");
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
void setPass(string pass)
|
||||
{
|
||||
_pass = pass;
|
||||
}
|
||||
|
||||
bool isPassCorrect(string pass)
|
||||
{
|
||||
return pass == _pass;
|
||||
}
|
||||
|
||||
void listen(ushort port = 6667)
|
||||
{
|
||||
listenTCP(port, &acceptConnection);
|
||||
}
|
||||
|
||||
void listen(ushort port, string address)
|
||||
{
|
||||
listenTCP(port, &acceptConnection, address);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue