第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
  • エラー処理: 数値リプライ
  • ヘルパー関数: 文字列操作

次章では、チャンネル管理とオペレータコマンドを学びます。