第4章:コマンド実装
はじめに
ft_ircでは、接続認証からメッセージ送信まで、さまざまなコマンドを実装する必要があります。本章では、各コマンドの仕様と実装を学びます。
---
1. 認証コマンド
1.1 PASS
構文: PASS <password>
目的:
- サーバー接続時のパスワード認証
- NICK/USER より前に送信する必要がある
エラー:
461 ERR_NEEDMOREPARAMS - パラメータ不足
462 ERR_ALREADYREGISTRED - 既に登録済み
464 ERR_PASSWDMISMATCH - パスワード不一致
void Server::cmdPass(Client* client, const Message& msg) {
// 既に登録済み
if (client->isRegistered()) {
sendToClient(client->getFd(),
":" + _serverName + " 462 " + client->getNickname() +
" :You may not reregister\\r\\n");
return;
}
// パラメータチェック
if (msg.params.empty()) {
sendToClient(client->getFd(),
":" + _serverName + " 461 * PASS :Not enough parameters\\r\\n");
return;
}
// パスワード検証
if (msg.params[0] != _password) {
sendToClient(client->getFd(),
":" + _serverName + " 464 * :Password incorrect\\r\\n");
return;
}
client->setPassReceived();
}
1.2 NICK
構文: NICK <nickname>
目的:
- ニックネームの設定/変更
- 最大9文字(サーバーにより異なる)
- 英数字、- _ [ ] { } \\ ` | のみ使用可
エラー:
431 ERR_NONICKNAMEGIVEN - ニックネームなし
432 ERR_ERRONEUSNICKNAME - 不正なニックネーム
433 ERR_NICKNAMEINUSE - 使用中のニックネーム
void Server::cmdNick(Client* client, const Message& msg) {
// パラメータチェック
if (msg.params.empty()) {
sendToClient(client->getFd(),
":" + _serverName + " 431 * :No nickname given\\r\\n");
return;
}
std::string newNick = msg.params[0];
// ニックネームの検証
if (!isValidNickname(newNick)) {
sendToClient(client->getFd(),
":" + _serverName + " 432 * " + newNick +
" :Erroneous nickname\\r\\n");
return;
}
// 重複チェック
if (_nicknames.find(newNick) != _nicknames.end() &&
_nicknames[newNick] != client) {
sendToClient(client->getFd(),
":" + _serverName + " 433 * " + newNick +
" :Nickname is already in use\\r\\n");
return;
}
std::string oldNick = client->getNickname();
bool wasRegistered = client->isRegistered();
// 古いニックネームをマップから削除
if (!oldNick.empty()) {
_nicknames.erase(oldNick);
}
// 新しいニックネームを設定
client->setNickname(newNick);
_nicknames[newNick] = client;
// 登録済みの場合、ニックネーム変更を通知
if (wasRegistered) {
std::string nickMsg = ":" + oldNick + "!" + client->getUsername() +
"@" + client->getHostname() +
" NICK :" + newNick + "\\r\\n";
// 自分自身に通知
sendToClient(client->getFd(), nickMsg);
// 共有チャンネルのメンバーに通知
std::set<Client*> notified;
const std::set<Channel*>& channels = client->getChannels();
for (std::set<Channel*>::iterator it = channels.begin();
it != channels.end(); ++it) {
const std::set<Client*>& members = (*it)->getMembers();
for (std::set<Client*>::iterator mit = members.begin();
mit != members.end(); ++mit) {
if (*mit != client && notified.find(*mit) == notified.end()) {
sendToClient((*mit)->getFd(), nickMsg);
notified.insert(*mit);
}
}
}
}
// 登録完了チェック
checkRegistration(client);
}
bool Server::isValidNickname(const std::string& nick) {
if (nick.empty() || nick.length() > 9) {
return false;
}
// 最初の文字は英字
if (!std::isalpha(nick[0]) && nick[0] != '_') {
return false;
}
// 残りは英数字と特殊文字
for (size_t i = 1; i < nick.length(); i++) {
char c = nick[i];
if (!std::isalnum(c) && c != '-' && c != '_' &&
c != '[' && c != ']' && c != '{' && c != '}' &&
c != '\\\\' && c != '`' && c != '|') {
return false;
}
}
return true;
}
1.3 USER
構文: USER <username> <mode> <unused> :<realname>
目的:
- ユーザー情報の設定
- 接続時に一度だけ使用
- <mode> と <unused> は通常 "0" と "*"
エラー:
461 ERR_NEEDMOREPARAMS - パラメータ不足
462 ERR_ALREADYREGISTRED - 既に登録済み
void Server::cmdUser(Client* client, const Message& msg) {
// 既に登録済み
if (client->isRegistered()) {
sendToClient(client->getFd(),
":" + _serverName + " 462 " + client->getNickname() +
" :You may not reregister\\r\\n");
return;
}
// パラメータチェック(最低4つ必要)
if (msg.params.size() < 4) {
std::string nick = client->getNickname().empty() ?
"*" : client->getNickname();
sendToClient(client->getFd(),
":" + _serverName + " 461 " + nick +
" USER :Not enough parameters\\r\\n");
return;
}
// ユーザー情報設定
client->setUsername(msg.params[0]);
client->setRealname(msg.params[3]);
// 登録完了チェック
checkRegistration(client);
}
void Server::checkRegistration(Client* client) {
if (client->isRegistered()) {
return;
}
if (!client->isPassReceived()) {
return;
}
if (client->getNickname().empty()) {
return;
}
if (client->getUsername().empty()) {
return;
}
// 登録完了
client->setRegistered();
welcomeClient(client);
}
void Server::welcomeClient(Client* client) {
std::string nick = client->getNickname();
// 001 RPL_WELCOME
sendToClient(client->getFd(),
":" + _serverName + " 001 " + nick +
" :Welcome to the IRC Network, " + nick + "!" +
client->getUsername() + "@" + client->getHostname() + "\\r\\n");
// 002 RPL_YOURHOST
sendToClient(client->getFd(),
":" + _serverName + " 002 " + nick +
" :Your host is " + _serverName + ", running version 1.0\\r\\n");
// 003 RPL_CREATED
sendToClient(client->getFd(),
":" + _serverName + " 003 " + nick +
" :This server was created today\\r\\n");
// 004 RPL_MYINFO
sendToClient(client->getFd(),
":" + _serverName + " 004 " + nick + " " +
_serverName + " 1.0 o itkol\\r\\n");
}
---
2. チャンネルコマンド
2.1 JOIN
構文: JOIN <channel>{,<channel>} [<key>{,<key>}]
目的:
- チャンネルへの参加
- チャンネルが存在しない場合は作成
- 作成者は自動的にオペレータになる
エラー:
403 ERR_NOSUCHCHANNEL - 無効なチャンネル名
405 ERR_TOOMANYCHANNELS - チャンネル数超過
471 ERR_CHANNELISFULL - チャンネルが満員(+l)
473 ERR_INVITEONLYCHAN - 招待制(+i)
475 ERR_BADCHANNELKEY - パスワード不一致(+k)
void Server::cmdJoin(Client* client, const Message& msg) {
if (msg.params.empty()) {
sendToClient(client->getFd(),
":" + _serverName + " 461 " + client->getNickname() +
" JOIN :Not enough parameters\\r\\n");
return;
}
// チャンネル名とキーを分割
std::vector<std::string> channelNames = split(msg.params[0], ',');
std::vector<std::string> keys;
if (msg.params.size() > 1) {
keys = split(msg.params[1], ',');
}
for (size_t i = 0; i < channelNames.size(); i++) {
std::string channelName = channelNames[i];
std::string key = (i < keys.size()) ? keys[i] : "";
joinChannel(client, channelName, key);
}
}
void Server::joinChannel(Client* client, const std::string& channelName,
const std::string& key) {
// チャンネル名の検証
if (channelName.empty() || (channelName[0] != '#' && channelName[0] != '&')) {
sendToClient(client->getFd(),
":" + _serverName + " 403 " + client->getNickname() + " " +
channelName + " :No such channel\\r\\n");
return;
}
// チャンネルを取得または作成
Channel* channel = getChannel(channelName);
bool isNew = (channel == NULL);
if (isNew) {
channel = createChannel(channelName);
}
// 既に参加している場合
if (channel->hasMember(client)) {
return;
}
// チャンネルモードのチェック
if (!isNew) {
// +i (invite-only)
if (channel->hasMode('i') && !channel->isInvited(client)) {
sendToClient(client->getFd(),
":" + _serverName + " 473 " + client->getNickname() + " " +
channelName + " :Cannot join channel (+i)\\r\\n");
return;
}
// +k (key)
if (channel->hasMode('k') && channel->getKey() != key) {
sendToClient(client->getFd(),
":" + _serverName + " 475 " + client->getNickname() + " " +
channelName + " :Cannot join channel (+k)\\r\\n");
return;
}
// +l (limit)
if (channel->hasMode('l') &&
channel->getMemberCount() >= channel->getLimit()) {
sendToClient(client->getFd(),
":" + _serverName + " 471 " + client->getNickname() + " " +
channelName + " :Cannot join channel (+l)\\r\\n");
return;
}
}
// チャンネルに参加
channel->addMember(client);
client->joinChannel(channel);
// 招待リストから削除
channel->removeInvite(client);
// 新規作成の場合、オペレータ権限を付与
if (isNew) {
channel->addOperator(client);
}
// JOINメッセージをチャンネルに送信
std::string joinMsg = ":" + client->getPrefix() + " JOIN " +
channelName + "\\r\\n";
sendToChannel(channel, joinMsg, NULL);
// トピックを送信
if (!channel->getTopic().empty()) {
sendToClient(client->getFd(),
":" + _serverName + " 332 " + client->getNickname() + " " +
channelName + " :" + channel->getTopic() + "\\r\\n");
}
// メンバーリストを送信
sendChannelNames(client, channel);
}
void Server::sendChannelNames(Client* client, Channel* channel) {
std::string names;
const std::set<Client*>& members = channel->getMembers();
for (std::set<Client*>::iterator it = members.begin();
it != members.end(); ++it) {
if (!names.empty()) {
names += " ";
}
if (channel->isOperator(*it)) {
names += "@";
}
names += (*it)->getNickname();
}
// 353 RPL_NAMREPLY
sendToClient(client->getFd(),
":" + _serverName + " 353 " + client->getNickname() + " = " +
channel->getName() + " :" + names + "\\r\\n");
// 366 RPL_ENDOFNAMES
sendToClient(client->getFd(),
":" + _serverName + " 366 " + client->getNickname() + " " +
channel->getName() + " :End of /NAMES list\\r\\n");
}
2.2 PART
構文: PART <channel>{,<channel>} [<reason>]
目的:
- チャンネルからの退出
エラー:
403 ERR_NOSUCHCHANNEL - チャンネルが存在しない
442 ERR_NOTONCHANNEL - チャンネルに参加していない
void Server::cmdPart(Client* client, const Message& msg) {
if (msg.params.empty()) {
sendToClient(client->getFd(),
":" + _serverName + " 461 " + client->getNickname() +
" PART :Not enough parameters\\r\\n");
return;
}
std::vector<std::string> channelNames = split(msg.params[0], ',');
std::string reason = (msg.params.size() > 1) ? msg.params[1] : "";
for (size_t i = 0; i < channelNames.size(); i++) {
partChannel(client, channelNames[i], reason);
}
}
void Server::partChannel(Client* client, const std::string& channelName,
const std::string& reason) {
Channel* channel = getChannel(channelName);
if (!channel) {
sendToClient(client->getFd(),
":" + _serverName + " 403 " + client->getNickname() + " " +
channelName + " :No such channel\\r\\n");
return;
}
if (!channel->hasMember(client)) {
sendToClient(client->getFd(),
":" + _serverName + " 442 " + client->getNickname() + " " +
channelName + " :You're not on that channel\\r\\n");
return;
}
// PARTメッセージを送信
std::string partMsg = ":" + client->getPrefix() + " PART " + channelName;
if (!reason.empty()) {
partMsg += " :" + reason;
}
partMsg += "\\r\\n";
sendToChannel(channel, partMsg, NULL);
// チャンネルから退出
channel->removeMember(client);
client->leaveChannel(channel);
// 空になったチャンネルを削除
if (channel->isEmpty()) {
removeChannel(channelName);
}
}
---
3. メッセージコマンド
3.1 PRIVMSG
構文: PRIVMSG <target> :<message>
目的:
- チャンネルまたはユーザーにメッセージを送信
エラー:
401 ERR_NOSUCHNICK - 対象が存在しない
404 ERR_CANNOTSENDTOCHAN - チャンネルに送信できない
411 ERR_NORECIPIENT - 宛先がない
412 ERR_NOTEXTTOSEND - メッセージがない
void Server::cmdPrivmsg(Client* client, const Message& msg) {
if (msg.params.empty()) {
sendToClient(client->getFd(),
":" + _serverName + " 411 " + client->getNickname() +
" :No recipient given (PRIVMSG)\\r\\n");
return;
}
if (msg.params.size() < 2 || msg.params[1].empty()) {
sendToClient(client->getFd(),
":" + _serverName + " 412 " + client->getNickname() +
" :No text to send\\r\\n");
return;
}
std::string target = msg.params[0];
std::string text = msg.params[1];
// チャンネルへのメッセージ
if (target[0] == '#' || target[0] == '&') {
Channel* channel = getChannel(target);
if (!channel) {
sendToClient(client->getFd(),
":" + _serverName + " 403 " + client->getNickname() + " " +
target + " :No such channel\\r\\n");
return;
}
if (!channel->hasMember(client)) {
sendToClient(client->getFd(),
":" + _serverName + " 404 " + client->getNickname() + " " +
target + " :Cannot send to channel\\r\\n");
return;
}
// チャンネルメンバーに送信(送信者以外)
std::string privmsgMsg = ":" + client->getPrefix() +
" PRIVMSG " + target + " :" + text + "\\r\\n";
sendToChannel(channel, privmsgMsg, client);
}
// ユーザーへのプライベートメッセージ
else {
Client* targetClient = getClientByNick(target);
if (!targetClient) {
sendToClient(client->getFd(),
":" + _serverName + " 401 " + client->getNickname() + " " +
target + " :No such nick/channel\\r\\n");
return;
}
std::string privmsgMsg = ":" + client->getPrefix() +
" PRIVMSG " + target + " :" + text + "\\r\\n";
sendToClient(targetClient->getFd(), privmsgMsg);
}
}
---
4. その他のコマンド
4.1 QUIT
void Server::cmdQuit(Client* client, const Message& msg) {
std::string reason = (msg.params.size() > 0) ? msg.params[0] : "Quit";
// QUITメッセージを作成
std::string quitMsg = ":" + client->getPrefix() + " QUIT :" + reason + "\\r\\n";
// 共有チャンネルのメンバーに通知
std::set<Client*> notified;
const std::set<Channel*>& channels = client->getChannels();
for (std::set<Channel*>::iterator it = channels.begin();
it != channels.end(); ++it) {
const std::set<Client*>& members = (*it)->getMembers();
for (std::set<Client*>::iterator mit = members.begin();
mit != members.end(); ++mit) {
if (*mit != client && notified.find(*mit) == notified.end()) {
sendToClient((*mit)->getFd(), quitMsg);
notified.insert(*mit);
}
}
}
// クライアントを切断
handleClientDisconnect(client->getFd());
}
4.2 PING/PONG
void Server::cmdPing(Client* client, const Message& msg) {
std::string token = (msg.params.size() > 0) ? msg.params[0] : "";
sendToClient(client->getFd(),
"PONG " + _serverName + " :" + token + "\\r\\n");
}
---
5. ヘルパー関数
5.1 文字列分割
std::vector<std::string> split(const std::string& str, char delimiter) {
std::vector<std::string> result;
std::stringstream ss(str);
std::string item;
while (std::getline(ss, item, delimiter)) {
if (!item.empty()) {
result.push_back(item);
}
}
return result;
}
5.2 大文字小文字を無視した比較
bool iequals(const std::string& a, const std::string& b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); i++) {
if (std::tolower(a[i]) != std::tolower(b[i])) {
return false;
}
}
return true;
}
---
まとめ
本章で学んだこと:
- 認証コマンド: PASS, NICK, USER
- チャンネルコマンド: JOIN, PART
- メッセージコマンド: PRIVMSG
- その他: QUIT, PING/PONG
- エラー処理: 数値リプライ
- ヘルパー関数: 文字列操作
次章では、チャンネル管理とオペレータコマンドを学びます。