Add basic channel support

This commit is contained in:
Les De Ridder 2017-03-14 02:45:11 +01:00
parent 10c1da7581
commit dad9ac49f9
No known key found for this signature in database
GPG Key ID: 5EC132DFA85DB372
4 changed files with 210 additions and 20 deletions

40
source/ircd/channel.d Normal file
View File

@ -0,0 +1,40 @@
module ircd.channel;
import std.algorithm;
import std.string;
import ircd.connection;
import ircd.server;
import ircd.message;
class Channel
{
private string _name;
Connection[] members;
Connection owner;
private Server _server;
this(string name, Connection owner, Server server)
{
this._name = name;
this.owner = owner;
this.members = [owner];
this._server = server;
}
@property
string name()
{
return _name;
}
void sendNames(Connection connection)
{
enum channelType = "="; //TODO: Support secret and private channels
connection.send(Message(_server.name, "353", [channelType, name, members.map!(m => m.nick).join(' ')], true));
connection.send(Message(_server.name, "366", [name, "End of NAMES list"], true));
}
}

View File

@ -2,12 +2,16 @@ module ircd.connection;
import std.stdio;
import std.string;
import std.algorithm;
import std.range;
import std.conv;
import vibe.core.core;
import vibe.stream.operations;
import ircd.message;
import ircd.server;
import ircd.channel;
class Connection
{
@ -18,6 +22,11 @@ class Connection
string nick;
string user;
string realname;
string hostname = "HOSTNAME";
@property string mask() { return nick ~ "!" ~ user ~ "@" ~ hostname; }
@property Channel[] channels() { return _server.channels.filter!(c => c.members.canFind(this)).array; }
bool connected;
@ -25,58 +34,148 @@ class Connection
{
_connection = connection;
_server = server;
connected = _connection.connected;
}
void send(Message message)
{
string messageString = message.toString;
writeln("S> " ~ messageString);
writeln("S> " ~ messageString ~ " (" ~ nick.to!string ~ ")");
_connection.write(messageString ~ "\r\n");
}
void onDisconnect()
{
writeln("client disconnected");
}
void handle()
{
connected = true;
while(connected)
{
auto message = Message.fromString((cast(string)_connection.readLine()).chomp);
Message message = void;
try
{
message = Message.fromString((cast(string)_connection.readLine()).chomp);
}
catch(Throwable)
{
//TODO: The actual Throwable could be useful?
connected = _connection.connected;
continue;
}
writeln("C> " ~ message.toString);
//TODO: If RFC-strictness is off, ignore case
switch(message.command)
{
case "NICK":
//TODO: Check availablity and validity etc.
nick = message.parameters[0];
onNick(message);
break;
case "USER":
user = message.parameters[0];
realname = message.parameters[3];
writeln("mode: " ~ message.parameters[1]);
writeln("unused: " ~ message.parameters[2]);
send(Message("localhost", "001", [nick, "Welcome to the Internet Relay Network " ~ nick ~ "!" ~ user ~ "@hostname"], true));
send(Message("localhost", "002", [nick, "Your host is " ~ _server.name ~ ", running version " ~ _server.versionString], true));
send(Message("localhost", "003", [nick, "This server was created " ~ _server.creationDate], true));
send(Message("localhost", "004", [nick, _server.name, _server.versionString, "w", "snt"]));
onUser(message);
break;
case "PING":
send(Message(null, "PONG", [message.parameters[0]], true));
//TODO: Connection timeout when we don't get a PONG
send(Message(_server.name, "PONG", [_server.name, message.parameters[0]], true));
break;
case "PONG":
//TODO: Handle pong
break;
case "QUIT":
send(Message(null, "ERROR", ["Bye!"]));
connected = false;
onQuit(message);
break;
case "JOIN":
onJoin(message);
break;
case "PART":
onPart(message);
break;
default:
writeln("unknown command '", message.command, "'");
send(Message(_server.name, "421", ["Unknown command"]));
break;
}
}
onDisconnect();
}
void onNick(Message message)
{
auto newNick = message.parameters[0];
if(nick !is null)
{
foreach(connection; channels.map!(c => c.members).fold!((a, b) => a ~ b).sort().uniq.filter!(c => c != this))
{
connection.send(Message(nick, "NICK", [newNick]));
}
send(Message(nick, "NICK", [newNick]));
}
//TODO: Check availablity and validity etc.
nick = newNick;
}
void onUser(Message message)
{
//TODO: Parse user mode
user = message.parameters[0];
writeln("mode: " ~ message.parameters[1]);
writeln("unused: " ~ message.parameters[2]);
realname = message.parameters[3];
send(Message(_server.name, "001", [nick, "Welcome to the Internet Relay Network " ~ mask], true));
send(Message(_server.name, "002", [nick, "Your host is " ~ _server.name ~ ", running version " ~ _server.versionString], true));
send(Message(_server.name, "003", [nick, "This server was created " ~ _server.creationDate], true));
send(Message(_server.name, "004", [nick, _server.name, _server.versionString, "w", "snt"]));
}
void onQuit(Message message)
{
connected = false;
send(Message(_server.name, "ERROR", ["Bye!"]));
}
void onJoin(Message message)
{
auto channel = message.parameters[0];
if(!Server.isValidChannelName(channel))
{
send(Message(_server.name, "403", [channel, "No such channel"], true));
}
else
{
_server.join(this, channel);
}
}
void onPart(Message message)
{
//TODO: Support channel lists
//TODO: Check if user is member of channel(s)
auto channel = message.parameters[0];
if(!Server.isValidChannelName(channel))
{
send(Message(_server.name, "403", [channel, "No such channel"], true));
}
else if(!channels.canFind!(c => c.name == channel))
{
send(Message(_server.name, "442", [channel, "You're not on that channel"], true));
}
else
{
if(message.parameters.length > 1)
{
_server.part(this, channel, message.parameters[1]);
}
else
{
_server.part(this, channel, null);
}
}
}
}

View File

@ -6,6 +6,7 @@ import std.array;
import std.algorithm;
import std.conv;
//TODO: Make this a class
struct Message
{
string prefix;

View File

@ -13,6 +13,7 @@ import ircd.packageVersion;
import ircd.message;
import ircd.connection;
import ircd.channel;
class Server
{
@ -23,6 +24,8 @@ class Server
string name;
Channel[] channels;
this()
{
name = Socket.hostName;
@ -36,9 +39,9 @@ class Server
{
foreach(connection; connections)
{
connection.send(Message(null, "PING", [connection.nick]));
connection.send(Message(null, "PING", [name], true));
}
sleep(10.seconds);
sleep(30.seconds);
}
}
@ -50,6 +53,53 @@ class Server
connections = connections.filter!(c => c != connection).array;
}
static bool isValidChannelName(string name)
{
return (name.startsWith('#') || name.startsWith('&')) && name.length <= 200;
}
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);
}
void part(Connection connection, string channelName, string partMessage)
{
auto channel = connection.channels.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);
}
void listen(ushort port = 6667)
{
listenTCP(port, &acceptConnection);