2017-03-11 17:45:49 +01:00
module ircd.server ;
import std.stdio ;
import std.algorithm ;
import std.range ;
import std.conv ;
import std.socket ;
import core.time ;
2017-03-22 17:33:56 +01:00
import std.datetime ;
2017-04-07 08:08:24 +02:00
import std.string ;
2017-03-11 17:45:49 +01:00
import vibe.core.core ;
import ircd.packageVersion ;
import ircd.message ;
import ircd.connection ;
2017-03-14 02:45:11 +01:00
import ircd.channel ;
2017-03-19 22:43:52 +01:00
import ircd.helpers ;
2017-03-11 17:45:49 +01:00
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 ;
2017-04-24 06:20:18 +02:00
enum string info = "A salty-ircd server" ; //TODO: Make server info configurable
2017-03-11 17:45:49 +01:00
2017-04-07 08:08:24 +02:00
string motd ;
2017-03-14 02:45:11 +01:00
Channel [ ] channels ;
2017-03-11 17:45:49 +01:00
this ( )
{
name = Socket . hostName ;
2017-04-07 08:08:24 +02:00
readMotd ( ) ;
2017-03-11 17:45:49 +01:00
runTask ( & pingLoop ) ;
}
2017-04-07 08:08:24 +02:00
private void readMotd ( )
{
import std.file : exists , readText ;
if ( exists ( "motd" ) )
{
motd = readText ( "motd" ) ;
}
}
2017-03-11 17:45:49 +01:00
private void pingLoop ( )
{
while ( true )
{
foreach ( connection ; connections )
{
2017-03-14 02:45:11 +01:00
connection . send ( Message ( null , "PING" , [ name ] , true ) ) ;
2017-03-11 17:45:49 +01:00
}
2017-03-14 02:45:11 +01:00
sleep ( 30. seconds ) ;
2017-03-11 17:45:49 +01:00
}
}
private void acceptConnection ( TCPConnection tcpConnection )
{
auto connection = new Connection ( tcpConnection , this ) ;
connections ~ = connection ;
connection . handle ( ) ;
connections = connections . filter ! ( c = > c ! = connection ) . array ;
}
2017-03-14 02:45:11 +01:00
static bool isValidChannelName ( string name )
{
return ( name . startsWith ( '#' ) | | name . startsWith ( '&' ) ) & & name . length < = 200 ;
}
2017-03-15 22:35:15 +01:00
static bool isValidNick ( string name )
{
2017-04-10 02:56:31 +02:00
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 ;
2017-03-15 22:35:15 +01:00
}
2017-04-13 22:20:49 +02:00
Connection [ ] findConnectionByNick ( string nick )
{
return connections . find ! ( c = > c . nick . toIRCLower = = nick . toIRCLower ) ;
}
bool canFindConnectionByNick ( string nick )
{
return ! findConnectionByNick ( nick ) . empty ;
}
2017-03-17 02:17:23 +01:00
bool isNickAvailable ( string nick )
{
2017-04-13 22:20:49 +02:00
return ! canFindConnectionByNick ( nick ) ;
}
Channel [ ] findChannelByName ( string name )
{
return channels . find ! ( c = > c . name . toIRCLower = = name . toIRCLower ) ;
}
bool canFindChannelByName ( string name )
{
2017-04-30 21:03:18 +02:00
return ! findChannelByName ( name ) . empty ;
2017-03-17 02:17:23 +01:00
}
2017-03-14 02:45:11 +01:00
void join ( Connection connection , string channelName )
{
2017-04-13 22:20:49 +02:00
auto channelRange = findChannelByName ( channelName ) ;
2017-03-14 02:45:11 +01:00
Channel channel ;
if ( channelRange . empty )
{
2017-04-30 21:05:41 +02:00
channel = new Channel ( channelName , this ) ;
2017-03-14 02:45:11 +01:00
channels ~ = channel ;
}
else
{
channel = channelRange [ 0 ] ;
}
2017-04-30 21:05:41 +02:00
channel . join ( connection ) ;
2017-03-14 02:45:11 +01:00
foreach ( member ; channel . members )
{
member . send ( Message ( connection . mask , "JOIN" , [ channelName ] ) ) ;
}
channel . sendNames ( connection ) ;
2017-03-21 01:28:39 +01:00
if ( ! channel . topic . empty )
{
channel . sendTopic ( connection ) ;
}
2017-03-14 02:45:11 +01:00
}
void part ( Connection connection , string channelName , string partMessage )
{
2017-04-13 22:20:49 +02:00
auto channel = connection . channels . array . find ! ( c = > c . name . toIRCLower = = channelName . toIRCLower ) [ 0 ] ;
2017-03-14 02:45:11 +01:00
2017-04-30 22:41:48 +02:00
channel . part ( connection , partMessage ) ;
2017-03-14 02:45:11 +01:00
2017-04-30 22:41:48 +02:00
if ( channel . members . empty )
2017-03-14 17:18:35 +01:00
{
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 ) ;
2017-04-30 22:41:48 +02:00
if ( channel . members . empty )
2017-03-14 17:18:35 +01:00
{
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
{
2017-03-14 17:45:28 +01:00
peer . send ( Message ( connection . mask , "QUIT" , [ connection . nick ] , true ) ) ;
2017-03-14 17:18:35 +01:00
}
}
2017-03-14 02:45:11 +01:00
}
2017-03-19 22:43:52 +01:00
void whoChannel ( Connection origin , string channelName , bool operatorsOnly )
{
//TODO: Check what RFCs say about secret/private channels
2017-04-13 22:20:49 +02:00
auto channel = findChannelByName ( channelName ) [ 0 ] ;
2017-03-19 22:43:52 +01:00
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 ) ) ) )
{
2017-03-21 01:28:39 +01:00
//TODO: Don't leak secret/private channels if RFC-strictness is off (the RFCs don't seem to say anything about it?)
2017-03-19 22:43:52 +01:00
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 )
2017-03-15 22:35:15 +01:00
{
2017-04-13 22:20:49 +02:00
auto channel = findChannelByName ( target ) [ 0 ] ;
2017-03-15 22:35:15 +01:00
channel . sendPrivMsg ( sender , text ) ;
}
2017-03-19 22:43:52 +01:00
void privmsgToUser ( Connection sender , string target , string text )
2017-03-15 22:35:15 +01:00
{
2017-04-13 22:20:49 +02:00
auto user = findConnectionByNick ( target ) [ 0 ] ;
2017-03-15 22:35:15 +01:00
user . send ( Message ( sender . mask , "PRIVMSG" , [ target , text ] , true ) ) ;
}
2017-03-20 05:35:26 +01:00
void noticeToChannel ( Connection sender , string target , string text )
{
2017-04-13 22:20:49 +02:00
auto channel = findChannelByName ( target ) [ 0 ] ;
2017-03-20 05:35:26 +01:00
channel . sendNotice ( sender , text ) ;
}
void noticeToUser ( Connection sender , string target , string text )
{
2017-04-13 22:20:49 +02:00
auto user = findConnectionByNick ( target ) [ 0 ] ;
2017-03-20 05:35:26 +01:00
user . send ( Message ( sender . mask , "NOTICE" , [ target , text ] , true ) ) ;
}
2017-03-21 01:28:39 +01:00
void sendChannelTopic ( Connection origin , string channelName )
{
2017-04-13 22:20:49 +02:00
auto channel = findChannelByName ( channelName ) [ 0 ] ;
2017-03-21 01:28:39 +01:00
channel . sendTopic ( origin ) ;
}
void setChannelTopic ( Connection origin , string channelName , string newTopic )
{
2017-04-13 22:20:49 +02:00
auto channel = findChannelByName ( channelName ) [ 0 ] ;
2017-03-21 01:28:39 +01:00
channel . setTopic ( origin , newTopic ) ;
}
2017-03-21 02:24:33 +01:00
void sendChannelNames ( Connection connection , string channelName )
{
2017-04-13 22:20:49 +02:00
auto channel = findChannelByName ( channelName ) [ 0 ] ;
2017-03-21 02:24:33 +01:00
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 ( "*" ) ;
}
2017-03-22 16:20:31 +01:00
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 ( ) ;
}
2017-03-22 17:16:43 +01:00
void sendVersion ( Connection connection )
{
connection . send ( Message ( name , "351" , [ connection . nick , versionString ~ "." , name , "" ] , true ) ) ;
}
2017-03-22 17:33:56 +01:00
void sendTime ( Connection connection )
{
auto timeString = Clock . currTime . toISOExtString ;
connection . send ( Message ( name , "391" , [ connection . nick , name , timeString ] , true ) ) ;
}
2017-03-22 16:58:03 +01:00
void invite ( Connection inviter , string target , string channelName )
{
2017-04-24 06:20:59 +02:00
auto user = findConnectionByNick ( target ) [ 0 ] ;
2017-03-22 16:58:03 +01:00
user . send ( Message ( inviter . mask , "INVITE" , [ user . nick , channelName ] ) ) ;
}
2017-04-07 08:08:24 +02:00
void sendMotd ( Connection connection )
{
2017-04-10 02:56:31 +02:00
connection . send ( Message ( name , "375" , [ connection . nick , ":- " ~ name ~ " Message of the day - " ] , true ) ) ;
foreach ( line ; motd . splitLines )
2017-04-07 08:08:24 +02:00
{
2017-04-10 02:56:31 +02:00
//TODO: Implement line wrapping
connection . send ( Message ( name , "372" , [ connection . nick , ":- " ~ line ] , true ) ) ;
2017-04-07 08:08:24 +02:00
}
2017-04-10 02:56:31 +02:00
connection . send ( Message ( name , "376" , [ connection . nick , "End of MOTD command" ] , true ) ) ;
2017-04-07 08:08:24 +02:00
}
2017-04-10 05:07:19 +02:00
void sendLusers ( Connection connection )
{
2017-04-13 01:46:10 +02:00
//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
2017-04-10 05:07:19 +02:00
//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 ) ) ;
}
2017-04-13 01:46:10 +02:00
void ison ( Connection connection , string [ ] nicks )
{
2017-04-13 22:20:49 +02:00
auto reply = nicks . filter ! ( n = > canFindConnectionByNick ( n ) ) . join ( ' ' ) ;
2017-04-13 01:46:10 +02:00
connection . send ( Message ( name , "303" , [ connection . nick , reply ] , true ) ) ;
}
2017-04-24 06:20:18 +02:00
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 ) ) ;
//TODO: Prepend nick prefix (i.e. '@' or '+') when applicable
auto userChannels = user . channels . map ! ( c = > c . name ) . join ( ' ' ) ;
connection . send ( Message ( name , "319" , [ connection . nick , user . nick , userChannels ] , true ) ) ;
}
2017-04-24 06:57:21 +02:00
void kill ( Connection killer , string nick , string comment )
{
auto user = findConnectionByNick ( nick ) [ 0 ] ;
user . send ( Message ( killer . mask , "KILL" , [ nick , comment ] , true ) ) ;
//TODO: Find out if any RFC specifies a QUIT message
quit ( user , "Killed by " ~ killer . nick ~ " (" ~ comment ~ ")" ) ;
//TODO: Find out if what we have to send here
user . send ( Message ( null , "ERROR" , [ "Closing Link: Killed by " ~ killer . nick ~ " (" ~ comment ~ ")" ] , true ) ) ;
user . closeConnection ( ) ;
}
2017-04-30 22:41:48 +02:00
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 ) ;
}
2017-03-11 17:45:49 +01:00
void listen ( ushort port = 6667 )
{
listenTCP ( port , & acceptConnection ) ;
}
void listen ( ushort port , string address )
{
listenTCP ( port , & acceptConnection , address ) ;
}
}