This is the second last project of the 42cursus' common core. The goal of this project is to create an IRC server in C++98. Now, we should be able to study protocols, open sockets, and handle multiple connections at the same time. This project is a great opportunity to learn about network programming and to understand how servers and clients communicate with each other. Does it sound simple? Well, it's not. But, for me at least, was a lot of fun.
For the mandatory part, you must follow these rules:
- You must implement an IRC server in C++98;
- You mustn't develop a client, but must choose an existing one as reference;
- You mustn't handle server-server communication;
- The server must be able to handle several clients at the same time, and all communication must be done via TCP/IP (v4 or v6);
- Forking is not allowed;
- All I/O operations must be non-blocking;
- Only 1 poll() is allowed;
- You must be able to authenticate, set a nickname, a username, join a channel, send and receive private messages;
- You must handle the following commands: KICK, INVITE, TOPIC, MODE (with i, t, k, o and l parameters);
For the bonus part, you must follow these rules:
- Handle file transfers;
- A bot;
So, like in every single project so far, the real question here is: where do we start?
I think a good way to approach this project is to try and understand everything we can about servers and clients. How to connect them, how to make them interact with each other and, most importantly, what types of "rules" these connections follow - also known as protocols.
In simple terms, a server is a computer program or a device that provides any sort of service to other programs or devices, known as clients. These services can be anything from sharing data or resources to performing computations for the client. APIs, for example, are a type of server that provides a set of rules for how software programs can interact with each other. Databases are also servers, as they provide a service to store and retrieve data.
One important thing to have in mind is that servers are not necessarily on the same machine as the client. They can be located anywhere in the world, as long as they have ways to establish a connection with the client.
The most common way to connect a client to a server is through the TCP/IP protocol. This particular protocol is the most widely used protocol in the world, and it governs how data is sent and received over the internet.
It's not for us, in this project, to understand in depth how TCP/IP works. However, it is important to understand what protocols are, and how they work.
A protocol is a set of rules that govern how data is transmitted between devices in a network. It's like a language that is used to allow communication. In order to establish a connection, both the client and the server must speak the same language.
Protocols are conventions. Most of them, today, are standardized, which means that they are widely accepted and used. We can find protocols for almost everything, from how to send an email to how to connect to a server.
The Request for Comments (RFC) documents are a series (with thousands of entries) of documents that describe the protocols, technologies, and policies used in the internet. They are the result of the work of the Internet Engineering Task Force (IETF), and they are the standardization documents of the internet.
All (or almost all) protocols used in the internet are described in RFC documents. They are, for lack of a better word, the "bible" of the internet.
So, that means that if you want (and you do want) to create a server from scratch - and in our case, an IRC server, then you should probably start by reading the RFC documents that describe the IRC protocol. To be more precise, the RFC 1459 and the RFC 2810 to 2813.
Again, it's not for us to understand in depth how these protocols work. But it's important to understand that they exist, and that they are the rules that we must follow in order to create our IRC server (especially because we are going to be able to connect to a real IRC client in the process).
But after all, what in the world is an IRC server?
The Internet Relay Chat (IRC) is a protocol that allows users to communicate with each other in real-time. It's a text-based protocol, and it's been around since 1988. It's one of the oldest protocols still in use today.
With an IRC server, users must be able to connect to it, and then they can join channels, send private messages, and perform a series of other actions. The server is responsible for handling all these actions, and for sending the messages to the right clients, either in channels, privately, or to the server itself, in order to perform some action.
That's what we are going to develop the logic behind the IRC server's resposes to the clients' actions. How to store the users' information, how to handle the channels, and how to send and receive messages, either in channels or privately.
But first, we must understand how to create a server in C++.
The first real technical challenge of this project is to create a server in C++.
For starters, we must be able to connect two programs through a network. This is done through sockets.
A socket is one endpoint of a two-way communication link between two programs running on the network. A socket is bound to a port number so that the TCP layer can identify the application that data is destined to. Sounds complicated? Surprisingly, it's not.
Imagine that a socket is like a telephone. When you want to call someone, you pick up the phone and dial the number. The other person picks up the phone and answers. The connection is established, and you can start talking.
In the same way, when you - a client - want to connect to a server, you create a socket and connect it to the server's address and port. The server accepts the connection, and the connection is established. You can start sending and receiving data.
A socket is the combination of an IP address and a port number. The IP address is the address of the server, and the port number is the "door" that you want to knock on - or the number of the extension that you want to call.
The same way, the client creates a socket and connects it to the server's address and port, the server creates a socket and binds it to a port. When a client connects to the server, the server accepts the connection and creates a new socket to communicate with the client.
Since we won't be creating a client in this project, we are going to focus on the server's side of the connection. But, either way, we will need to extract the client's address and port in order to send and receive data correctly.
Regardless of the programming language, creating a socket is a relatively simple process, and it's supported by most operating systems.
In C++, we can create a socket using the socket() function, which is part of the sys/socket.h library. This function returns a file descriptor that we can use to send and receive data.
This is important to understand: under the hood, a socket is a file descriptor. This means that we can use the same functions that we use to read and write files to send and receive data through a socket.
When calling the right functions, the operating system takes care of the rest. The kernel will be the one responsible for handling the data that is sent and received through the socket. It's the kernel that sends the data to the network, and it's the kernel that receives the data from the network. Our program only needs to read and write to the correct file descriptor.
In C++, we will need to use the following functions to enable the full capabilities of the socket:
socket()
: to create the socket;bind()
: to bind the socket to a port;listen()
: to listen for incoming connections;accept()
: to accept an incoming connection;
Each one of these functions has its own parameters and return values, and we must fully understand how to use them.
int socket(int domain, int type, int protocol);
The socket()
function creates a new socket and returns a file descriptor that we can use to send and receive data.
-
The
domain
parameter specifies the protocol family that we want to use. In our case, we are going to use theAF_INET
protocol family, which is the most common protocol family used in the internet. This protocol family is used with theIPv4
addresses. If you want to use theIPv6
protocol family, you should use theAF_INET6
protocol family. The macros for these protocol families are defined in thesys/socket.h
library. -
The
type
parameter specifies the type of the socket. We are going to use theSOCK_STREAM
type, which is a reliable two-way connection-based byte stream. This means that the data is sent and received in the same order that it was sent, and that the data is not lost or duplicated. If you want to use a connectionless datagram service, you should use theSOCK_DGRAM
type. -
The
protocol
parameter specifies the protocol that we want to use. We are going to use the0
protocol, which is the default protocol for the given domain and type. If you want to use a specific protocol, you should use thegetprotobyname()
function to get the protocol number.
After calling the socket()
function, we will have a file descriptor that we can use to send and receive data.
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Now, we need to bind the socket to a port. That's the difference between a regular socket and a server socket. A regular socket is not bound to any port, and it's not listening for incoming connections. A server socket is bound to a port, and it's listening for incoming connections.
In clients, for example, we don't need to bind the socket to a port. We only need to connect the socket to the server's address and port - we usually use the function connect()
to do that.
In servers, however, we need to bind the socket to a port. That means that the server will be able to listen for incoming connections on that port.
The bind()
function takes three parameters:
-
The
sockfd
parameter is the file descriptor of the socket that we want to bind to a port; -
The
addr
parameter is a pointer to asockaddr
structure that contains the address and port that we want to bind the socket to. This structure is a generic structure that can be used with any protocol family. In our case, we are going to use thesockaddr_in
structure, which is specific to theAF_INET
protocol family. This structure contains thesin_family
member, which is the protocol family, thesin_port
member, which is the port number, and thesin_addr
member, which is the address. Thesin_addr
member is usually set toINADDR_ANY
, which means that the socket will be bound to all available interfaces. Thesin_port
member is set to the port number that we want to bind the socket to. Both of these members must be in network byte order, which means that we must use thehtons()
function to convert the port number to network byte order. -
The
addrlen
parameter is the size of theaddr
parameter. This parameter is usually set tosizeof(struct sockaddr_in)
.
After calling the bind()
function, the socket will be bound to the specified port. However, it's not yet listening for incoming connections.
int listen(int sockfd, int backlog);
Now, we need to listen for incoming connections. That means that the server will be able to accept incoming connections on the specified port.
The listen()
function takes two parameters:
-
The
sockfd
parameter is the file descriptor of the socket that we want to listen for incoming connections; -
The
backlog
parameter is the maximum length of the queue of pending connections. This parameter is usually set to5
. You can even set it to any value that you want, but most operating systems will silently truncate it to5
.
After calling the listen()
function, the socket will be listening for incoming connections. However, it's not yet accepting incoming connections.
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
Finally, we need to accept incoming connections. That means that any client that attempts to connect to the server will be able to do so.
The accept()
function takes three parameters:
-
The
sockfd
parameter is the file descriptor of the socket that we want to accept incoming connections; -
The
addr
parameter is a pointer to asockaddr
structure that will contain the address and port of the client that is connecting to the server. This structure is a generic structure that can be used with any protocol family. In our case, we are going to use thesockaddr_in
structure, which is specific to theAF_INET
protocol family. This structure contains thesin_family
member, which is the protocol family, thesin_port
member, which is the port number, and thesin_addr
member, which is the address. Thesin_port
member is set to the port number of the client that is connecting to the server. Thesin_addr
member is set to the address of the client that is connecting to the server. Both of these members will be in network byte order, which means that we must use thentohs()
function to convert the port number to host byte order, and theinet_ntoa()
function to convert the address to a string. Theaddr
parameter can be set toNULL
if we don't want to know the address and port of the client that is connecting to the server. -
The
addrlen
parameter is the size of theaddr
parameter. This parameter is usually set tosizeof(struct sockaddr_in)
.
After calling the accept()
function, we will have a new file descriptor that we can use to send and receive data with the client that is connecting to the server.
The next challenge is to make the I/O operations non-blocking. This is important because we want to be able to handle multiple connections at the same time. If we use blocking I/O operations, the program will stop and wait for the data to be sent or received. That means that we will only be able to handle one connection at a time.
We could, theoretically, create a new thread for each connection. However, this is not a good idea. Threads are expensive, and they consume a lot of resources. If we have a lot of connections, we will have a lot of threads, and the program will consume a lot of resources.
The best way to handle multiple connections at the same time is to use non-blocking I/O operations. This means that the program will not stop and wait for the data to be sent or received. Instead, the program will continue to run, and we will be able to handle multiple connections at the same time.
The accept()
function is a blocking function. That means that the program will stop and wait for a client to connect to the server.
We can, however, make the accept()
function non-blocking by using the fcntl()
function to set the O_NONBLOCK
flag on the socket. This flag will make the accept()
function return immediately if there are no pending connections.
int fcntl(int fd, int cmd, ... /* arg */ );
The fcntl()
function is used to perform various operations on a file descriptor. It takes three parameters:
-
The
fd
parameter is the file descriptor that we want to perform the operation on; -
The
cmd
parameter is the operation that we want to perform. We can use theF_SETFL
command to set the file status flags. We can use the bitwise OR operator to set theO_NONBLOCK
flag on the socket. -
The
arg
parameter is the value that we want to set. We can use theO_NONBLOCK
flag to make theaccept()
function non-blocking.
After calling the fcntl()
function, the accept()
function will return immediately if there are no pending connections.
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
The poll()
function is a non-blocking function that is used to monitor file descriptors for I/O events. It takes three parameters:
-
The
fds
parameter is a pointer to an array ofpollfd
structures.struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
- The
fd
member is the file descriptor that we want to monitor. - The
events
member is the events that we want to monitor. There are macros for different events, such asPOLLIN
for data to be read,POLLOUT
for data to be written, andPOLLERR
for an error condition. We can use the bitwise OR operator to monitor multiple events at the same time. - The
revents
member is the events that occurred. This member is set by the kernel, and we can use the bitwise AND operator to check if a specific event occurred.
- The
-
The
nfds
parameter is the number of file descriptors that we want to monitor. -
The
timeout
parameter is the maximum amount of time, in milliseconds, that we want to wait for an event to occur. This parameter is usually set to-1
, which means that we want to wait indefinitely (making it a blocking function all the same). You can even set it to0
, which means that we don't want to wait at all.
The poll()
function returns the number of file descriptors that have events that occurred, 0
if no events occurred before the timeout, or -1
if an error occurred.
The most important thing to understand about servers is this: In order to accept multiple clients, we need to create multiple sockets. Each one of these sockets will be responsible for handling the connection with a single client.
There's one socket - called the server socket - that is responsible for listening for incoming connections. When a client connects to the server, the server socket will accept the connection and create a new socket to communicate with the client.
These other sockets - called client sockets - are responsible for handling the connection with the client. They are created by the server socket, and they are used to send and receive data with the client.
This is important to understand: the server socket is not used to send and receive data with the client. It's only used to accept incoming connections. The client sockets are used to send and receive data with the client.
Now, we know how servers and clients communicate with each other. We know how to create a server in C++, and we know how to handle multiple connections at the same time. This, however, is just the very first step.
In order for our server to be considered an IRC server, it must learn how "to speak" the IRC protocol. This is the core of the project, and where the real challenge lies.
The IRC protocol is a text-based protocol. That means that all communication is done through text messages. These messages are sent and received through the sockets, and they follow a specific set of rules.
The IRC protocol is described in the RFC 1459 and the RFC 2810 to 2813. These documents describe the rules that we must follow in order to create an IRC server. We won't need (God forbid!) to implement the entire protocol. That would be a nightmare. But we must implement the most important parts of it.
We also need to choose a client to use as a reference. In our case, we chose to deal with the open-source IRC client HexChat
. This client is available for Windows, macOS, and Linux, and it's a great reference for us to understand how the IRC protocol works.
It's important to have in mind that, although the RFC documents are an important baseline for stablishing comunication between the server and the client, each client and each server may have their own set of rules and abstractions that are not necessarily described in the RFC documents.
In our case, our server must ensure the best and most noise-free possible comunication with the client of our choice, but it does not garantee that it will be the case for any other client. During the evaluation process, we must use the client that we chose as a reference to test the server's responses to the client's actions.
So, why HexChat
? Because it's open-source, and because it's available for all major operating systems. This means that we can use it to test the server's responses to the client's actions, regardless of the operating system that we are using. It's also one of the most popular IRC clients, and it's widely used by the IRC community.
To download HexChat
, you can visit the official HexChat website.
When a new client connects to the server, the client sends a series of messages to the server who must, in turn, respond appropriately.
This is the main core of the project: understanding the messages that the client sends to the server, and responding to these messages with the appropriate replies. To do so, you must use as references:
- the RFC documents that describe the IRC protocol;
- the IRC client that you chose as a reference.
Like we said before, our program must be able to open a socket, bind it to a port, listen for incoming connections, and accept incoming connections.
Once the connection is established, the client sends a series of messages to the server. These messages follow a very specific format.
So, for starters, let's break down the protocol syntax:
- The messages that the client sends to the server are called commands;
- The server must respond to these commands with the appropriate replies;
All messages are sent and received through the sockets. The messages are sent as strings, and they follow a specific format. The messages are separated by a \n
and a \r
character.
The syntax for all commands is as follows:
:prefix command [parameters] :trailing
-
The
prefix
is an optional field that contains the address of the client that is sending the message. This field is usually set to the client's nickname, but it can be set to the client's username or hostname. Theprefix
is identified by the:
character. For example::nick!user@host
or:irc.example.com
; -
The
command
is the command that the client is sending to the server. This field is mandatory, and it must be a valid command. For example:NICK
,USER
,JOIN
,PRIVMSG
,QUIT
,KICK
,INVITE
,TOPIC
,MODE
,WHOIS
,WHO
,LIST
,NAMES
; -
The
parameters
are the parameters of the command. This field can contain zero or more parameters, depending on the command. The parameters are separated by a space character. For example:#channel
,nickname
,message
; -
The
trailing
is the last parameter of the command, and it's optional. This is important: the trailing is part of the command itself, but it's main difference is that it can contain one or more words, all considered as a single parameter. The:
character is used to identify the begginning of thetrailing
parameter, and everything that comes after it is considered part of thetrailing
. For example::Hello, world!
;
During the parsing of the messages, the server must be able to identify the prefix
, the command
, and all the parameters
, including the trailing
one.
In the other end, the server must respond to the client's commands in a way that the client will understand. Clients have a set expectations regarding replies formatting, coding and timing. Understand that was one of the most challenging parts of the project, but it was also one of the most rewarding.
The syntax for replies can differ depending on the type of command and context. For these project, we were able to identify the following:
When the server needs to send a reply directly to a single client, usually involving a command that is not in the scope of channels or private users. We send this message on behalf of the server, directly to the client who requested the action.
For example:
# Reply to a /USER command
:irc.example.com 001 nick :Welcome to the Internet Relay Network nick!user@host
# Reply to a /LIST command
:irc.example.com 322 nick :#channel 1 :Welcome to the channel
# Reply to a /OPER command
:irc.example.com 381 nick :You are now an IRC operator
-
The
prefix
is the server's address::irc.example.com
; -
The
code
is a three-digit number that identifies the type of reply, either to identify sucess or failure in processing the request. The001
, for example, is the welcome message. For each code, the client can choose to perform a different action. Programatically, there's no library macro with the codes, so you must define them yourself using the RFC documents; -
The
nickname
is the client's nickname:nick
; -
The
trailing
is the message that the server is sending to the client:Welcome to the Internet Relay Network nick!user@host
. For each type of response, the message can be slightly different. Read the documentation carefully.
When the a user attempts to perform an action that involves a channel, the server must send a reply to the channel's users. HexChat
uses these replies to perform underlying actions to the Client's Interface. We must respect this format rigorously.
For example:
# Reply to a /JOIN command
:nick!user@host JOIN #channel
# Reply to a /PRIVMSG command
:nick!user@host PRIVMSG #channel :Hello, world!
# Reply to a /MODE command
:nick!user@host MODE #channel +o otheruser
-
The
prefix
is the client's address, formatted as:nick!user@host
. It shows who's the user performing the action; -
The
command
is the command that the client is sending to the server, all in uppercase:JOIN
,PRIVMSG
,MODE
, etc.; -
The
channel
is the channel that the client is sending the message to:#channel
or#channel:hostname
. Pay attention to the format the Client is expecting. In our case,HexChat
expects the hostname to be included in the channel name, otherwise it does not recognize the commands in the GUI; -
The
parameters
may be the flag of the command, or the nickname of the user that is being kicked, or even a trailing message. It depends on the command;
When a user sends a private message to another user, the server must send a reply to the recipient. This reply is similar to the channel replies, but it's sent directly to the recipient.
For example:
# Reply to a /PRIVMSG command
:nick!user@host PRIVMSG otheruser :Hello, world!
# Invite a user to a channel
:nick!user@host INVITE otheruser #channel
-
The
prefix
is the client's address, formatted as:nick!user@host
. It shows who's the user sending the message; -
The
command
is the command that the client is sending to the server, all in uppercase:PRIVMSG
,INVITE
, etc.; -
The
recipient
is the nickname of the user that is receiving the message:otheruser
; -
The
parameters
depend on the command. It may be the message that the user is sending, like atrailing
string, or the channel that the user is inviting the recipient to;
It's important to notice that in some very rare cases of Client->Client communication, replies do not go through the server for processing. Some features, like file transfers, are handled directly by the clients. In these cases, the server must be able to handle the commands, but it does not need to send replies to the clients.
However, in most cases, the server will have to handle the commands passed by the clients and send the appropriate replies. In the INVITE
command, for example, the server receives a very similar message: INVITE otheruser #channel
. The server must then handle the appropriate logic of invitation, and respond to the client with almost the same message: :nick!user@host INVITE otheruser #channel
.
First things first, let's see how Hexchat
works to log in to an IRC server.
When you open HexChat, you are presented with a simnple window with some fields to be filled. The program is pretty straightforward.
First, you must fill information about Nick Name, Second and Third Choices (in case the nickname is already in use in the server), and User Name.
Then, on the Network tab, you must add a new server. On the Network tab, set up you server's name, address and port. Due to the project's guidelines, we must also set a password in your server, so you must inform it in the Password field.
After all this information is set, you can click Connect.
The first thing the client does when connecting to the server is to authenticate. HexChat
, specifically, sends the following messages to the server:
CAP LS <version>
PASS <password>
NICK <nickname>
USER <username> 0 * :<realname>
For each one of this commands, your server must respond with the appropriate reply.
Even though the CAP
command (as many others) is not mandatory, we chose to implement all commands the server receives spontaneously from HexChat
. This is a good practice, as it allows the server to be more flexible and to handle more commands in the future.
The PASS
command is used to authenticate the client via password. The NICK
and USER
commands are used to register the client with the server. Only when the client is properly authenticated and registered, the server must respond with the 001
reply, welcoming the client to the server.
The
CAP
command is used to negotiate capabilities with the server. The client sends theCAP LS
command to list the available capabilities, and the server responds with theCAP
command to list the capabilities that are enabled.An appropriate reply to the
CAP LS
command involves listing the capabilities that are available. In our case, we chose to list no particular capabilities. This part is up to you.In our case, we chose to inform the client back that no particular capabilities are enabled. Do with this part as you please.
The
PASS
command is used to authenticate the client via password. If you typed in the password in theHexchat
Network tab, the client will send this password to the server.In our case, we chose to deal with authentication through a boolean flag. If the password is correct, the flag is set to true. Only if the client is also registered, the server responds with the
001
reply, welcoming the client to the server.In case the password is incorrect, the server must respond with the
464
reply, informing the client that the password is incorrect. Other then that, we chose to disconnect the client from the server due to authentication failure. This part is up to you.
The
NICK
command is used to set the client's nickname, and you may or may not consider it part of the registration process. The difference is that the client can change the nickname at any time by sending theNICK
command again. However, there cannot exist a client with no nick, neither a client with the same nick as another client.
The
USER
command is used to set the client's username and realname. TheUSER
command is mandatory, and it is good practice to be sent after theNICK
command, although not necessarily mandatory. TheUSER
command is used to register the client with the server.In our case, we chose to only consider a client registered if the
username
,realname
, andnickname
are all set. However, due to particularities of theHexChat
client, the NICK command might fail before the USER command is sent. This is up to you to decide how to handle.
Once the client is properly authenticated and registered, the server must respond with the 001
reply, welcoming the client to the server.
:irc.example.com 001 nick :Welcome to the Internet Relay Network nick!user@host
HexChat
does a few extra configurations steps to the GUI after this reply, so you better consider it as a mandatory step.
We are (obviously) not going to implement the entire IRC protocol. That would be a nightmare.
However, some commands are mandatory for our project's success. Others, just very welcome.
The
JOIN
command is used to join a channel. The client sends theJOIN
command to the server, and the server responds with the appropriate replies. It's important to have in mind thatHexChat
uses the server's reply to the join command to perform actions in the Graphical User Interface.Also, the
JOIN
command is a Client->Channel command, so the server must send the reply to all the channel's users.We also chose to handle the
JOIN
command in order to create channels that do not exist. That's only possible if the client who's joining the channel is also a server operator.This is not necessarily the default behavior of an IRC server, but it's a good practice to allow the creation of channels on the fly and it makes our server more flexible. Choose to handle that as you please.
The
PRIVMSG
command is used to send a message to a user or a channel. The client sends thePRIVMSG
command to the server, and the server responds with the appropriate replies. It's important to have in mind thatHexChat
uses the server's reply to the PRIVMSG command to perform actions in the Graphical User Interface as well, like opening a new tab for the user, or displaying the message in the channel appropriately.
The
WHOIS
command is used to get information about a user. The client sends theWHOIS
command to the server, and the server responds with the appropriate replies.Although not mandatory, the
WHOIS
command is a very welcome feature to implement, specially inHexChat
, where there's a button directly in the private chat tab to get information about that user.
The
OPER
command is used to authenticate the client as an IRC operator. That means that the client has special privileges, and can perform actions that regular users cannot. In our case, it means that only Operators can create new channels with the /join command, edit channel's modes without necessarily being a channel operator themselves and include other users to the server's operator list.It's important to note that Operators might have different privileges in different servers. This is completely up to you to decide how to handle this command in a fine-grained way in your own server.
The
PING
command is used to check if the client is still connected to the server. The client may send thePING
command to the server, and the server must respond with thePONG
command. InHexChat
, it's a way to test latency and connection.It was not mandatory for the project to handle the
PING
command, but it was simple enough, and proved very useful. We also took the oportunity to handle the PING command sent between clients, to check connectivity.
The
LIST
command is not mandatory at all, but super cool and simple to implement to checkHexChat
GUI's full potential. The client sends theLIST
command to the server, and the server must respond with the appropriate replies. TheLIST
command is used to list all the channels that are available on the server, to any client correctly registered.
The
QUIT
command is used to disconnect the client from the server. The client sends theQUIT
command to the server, and the server must respond with the appropriate replies. InHexChat
, it's a way to disconnect from the server.
The
MODE
command is used to see or change channel's modes. Out of every command implemented so far, we found these one to be the most challenging. We must deal with different modes, code modularity and the fact thatHexChat
must interpret the server's reply to the MODE command to perform actions in the GUI in a more fine-grained way.We chose - and that is completely up to our own interpretation - that not only channel operators can change the channel's modes. Server operators can too - and that's the only channel specific command that they can perform. We did this so there's a way to add channel operators to channels that lost theirs, for some reason.
There are five modes we must handle:
+o
/-o
to give/remove operator status to a user;+t
/-t
to allow/prevent only operators to change the topic;+i
/-i
to set/unset the channel as invite-only;+k
/-k
to set/unset a password to the channel;+l
/-l
to set/unset the channel's user limit.Obviously, there are a few other modes we did not ever dare to approach. These five were enough to make us sweat.
The
KICK
command is used to kick a user from a channel. The client sends theKICK
command to the server, and the server must respond with the appropriate replies. To kick someone from a channel, the client must be a channel operator.
The
INVITE
command is used to invite a user to a channel. The client sends theINVITE
command to the server, and the server must respond with the appropriate replies. To invite someone to a channel, the client must be a channel operator.That's actually a pretty cool feature to implement on
HexChat
, since the GUI takes care of abstracting the process of accepting the invitation and joining the channel by simply right-clicking the invitation message.
The
TOPIC
command is used to set the channel's topic. The client sends theTOPIC
command to the server, and the server must respond with the appropriate replies. To set the channel's topic, the client must be a channel operator.Another cool feature to implement on
HexChat
, since we have a dedicated space for the channel's topic, and can change it at any time.
The
WHO
andNAMES
commands are used to list the users that are in a channel. Although pretty similar in purpose, they have different outputs. TheWHO
command lists the users in a channel, along with a bunch of other information. The list is given in separate messages, one for each user. TheNAMES
command lists the users in a channel, along with their modes. The list is given in a single message. It's one way forHexChat
to update the GUI with the users in the channel.
For the first part of the Bonus part, the file transfer feature, you'll find that it's probably the easiest bonus to ever implement in your whole 42 common core experience. That is because, if you implemented the PRIVMSG
command correctly, you already have the file transfer feature implemented.
No? Really?
Yes, really.
Just try it. Send a file to a friend in a private chat. It's that simple.
That's only possible because file transfers in HexChat
do not go over to the server. They are handled directly by the clients. The server must be able to handle the commands, but it does not need to send replies regarding the FTP.
Easy peasy.
Now, the second part of the Bonus, the Bot feature, is a little bit more challenging. How much really depends on how much time you want to spend on it.
Regular bots are usually implemented as separate clients, with their own sockets and connections to the server. They are usually used to perform automated tasks, like sending messages, or responding to commands.
Did we want to develop a whole client? Hell no.
So, we chose to implement a bot that is part of the server. It is, most specifically, dealt in the Commands class, and implemented as a separate Client object. Nothing too fancy, but it does its job pretty well.
Now, if you want to grow on it, expand its capabilities, choose to connect to APIs and so on, maybe an in-server bot is not the best choice. But for the project's sake, it's more than enough.