salty-ircd/source/ircd/server.d

326 lines
8.1 KiB
D

module ircd.server;
import std.stdio;
import std.algorithm;
import std.range;
import std.conv;
import std.socket;
import core.time;
import std.datetime;
import std.string;
import vibe.core.core;
import ircd.packageVersion;
import ircd.message;
import ircd.connection;
import ircd.channel;
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;
string motd;
Channel[] channels;
this()
{
name = Socket.hostName;
readMotd();
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 = allowed ~ digits ~ "-";
if (!allowed.canFind(c))
return false;
}
return true;
}
bool isNickAvailable(string nick)
{
return !connections.canFind!(c => c.nick == nick);
}
void join(Connection connection, string channelName)
{
auto channelRange = channels.find!(c => c.name == channelName);
Channel channel;
if(channelRange.empty)
{
channel = new Channel(channelName, connection, this);
channels ~= channel;
}
else
{
channel = channelRange[0];
channel.members ~= connection;
}
foreach(member; channel.members)
{
member.send(Message(connection.mask, "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 == channelName)[0];
foreach(member; channel.members)
{
if(partMessage !is null)
{
member.send(Message(connection.mask, "PART", [channelName, partMessage], true));
}
else
{
member.send(Message(connection.mask, "PART", [channelName]));
}
}
channel.members = channel.members.remove!(m => m == connection);
if(channel.members.length == 0)
{
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.length == 0)
{
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.mask, "QUIT", [quitMessage], true));
}
else
{
peer.send(Message(connection.mask, "QUIT", [connection.nick], true));
}
}
}
void whoChannel(Connection origin, string channelName, bool operatorsOnly)
{
//TODO: Check what RFCs say about secret/private channels
auto channel = channels.find!(c => c.name == channelName)[0];
foreach(c; channel.members.filter!(c => !operatorsOnly || c.isOperator)
.filter!(c => c.visibleTo(origin)))
{
//TODO: Support hop count
origin.sendWhoReply(channelName, c, 0);
}
}
void whoGlobal(Connection origin, string mask, bool operatorsOnly)
{
foreach(c; connections.filter!(c => c.visibleTo(origin))
.filter!(c => !operatorsOnly || c.isOperator)
.filter!(c => [c.hostname, c.servername, c.realname, c.nick].any!(n => wildcardMatch(n, mask))))
{
//TODO: Don't leak secret/private channels 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;
//TODO: Support hop count
origin.sendWhoReply(channelName, c, 0);
}
}
void privmsgToChannel(Connection sender, string target, string text)
{
auto channel = channels.find!(c => c.name == target)[0];
channel.sendPrivMsg(sender, text);
}
void privmsgToUser(Connection sender, string target, string text)
{
auto user = connections.find!(c => c.nick == target)[0];
user.send(Message(sender.mask, "PRIVMSG", [target, text], true));
}
void noticeToChannel(Connection sender, string target, string text)
{
auto channel = channels.find!(c => c.name == target)[0];
channel.sendNotice(sender, text);
}
void noticeToUser(Connection sender, string target, string text)
{
auto user = connections.find!(c => c.nick == target)[0];
user.send(Message(sender.mask, "NOTICE", [target, text], true));
}
void sendChannelTopic(Connection origin, string channelName)
{
auto channel = channels.find!(c => c.name == channelName)[0];
channel.sendTopic(origin);
}
void setChannelTopic(Connection origin, string channelName, string newTopic)
{
auto channel = channels.find!(c => c.name == channelName)[0];
channel.setTopic(origin, newTopic);
}
void sendChannelNames(Connection connection, string channelName)
{
auto channel = channels.find!(c => c.name == 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 = connections.find!(c => c.nick = target)[0];
user.send(Message(inviter.mask, "INVITE", [user.nick, channelName]));
}
void sendMotd(Connection connection)
{
if(motd !is null)
{
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));
}
else
{
connection.send(Message(name, "422", [connection.nick, "MOTD File is missing"], true));
}
}
void listen(ushort port = 6667)
{
listenTCP(port, &acceptConnection);
}
void listen(ushort port, string address)
{
listenTCP(port, &acceptConnection, address);
}
}