第5章:チャンネル管理

はじめに

チャンネルはIRCの中核機能です。本章では、Channelクラスの設計、オペレータコマンド(KICK, INVITE, TOPIC, MODE)の実装を学びます。

---

1. Channelクラス

1.1 設計

#include <set>
#include <map>
#include <string>

class Channel {
private:
    std::string _name;
    std::string _topic;
    std::string _key;           // パスワード(+k)

    std::set<Client*> _members;
    std::set<Client*> _operators;
    std::set<Client*> _inviteList;

    bool _inviteOnly;           // +i
    bool _topicRestricted;      // +t
    size_t _userLimit;          // +l

public:
    Channel(const std::string& name);
    ~Channel();

    // アクセサ
    const std::string& getName() const { return _name; }
    const std::string& getTopic() const { return _topic; }
    const std::string& getKey() const { return _key; }
    size_t getLimit() const { return _userLimit; }
    size_t getMemberCount() const { return _members.size(); }
    const std::set<Client*>& getMembers() const { return _members; }

    // 設定
    void setTopic(const std::string& topic) { _topic = topic; }
    void setKey(const std::string& key) { _key = key; }
    void setLimit(size_t limit) { _userLimit = limit; }

    // メンバー管理
    void addMember(Client* client);
    void removeMember(Client* client);
    bool hasMember(Client* client) const;
    bool isEmpty() const { return _members.empty(); }

    // オペレータ管理
    void addOperator(Client* client);
    void removeOperator(Client* client);
    bool isOperator(Client* client) const;

    // 招待管理
    void addInvite(Client* client);
    void removeInvite(Client* client);
    bool isInvited(Client* client) const;

    // モード管理
    bool hasMode(char mode) const;
    void setMode(char mode, bool enable);
    std::string getModeString() const;
};

1.2 実装

Channel::Channel(const std::string& name)
    : _name(name),
      _inviteOnly(false),
      _topicRestricted(false),
      _userLimit(0) {
}

Channel::~Channel() {
}

void Channel::addMember(Client* client) {
    _members.insert(client);
}

void Channel::removeMember(Client* client) {
    _members.erase(client);
    _operators.erase(client);
    _inviteList.erase(client);
}

bool Channel::hasMember(Client* client) const {
    return _members.find(client) != _members.end();
}

void Channel::addOperator(Client* client) {
    _operators.insert(client);
}

void Channel::removeOperator(Client* client) {
    _operators.erase(client);
}

bool Channel::isOperator(Client* client) const {
    return _operators.find(client) != _operators.end();
}

void Channel::addInvite(Client* client) {
    _inviteList.insert(client);
}

void Channel::removeInvite(Client* client) {
    _inviteList.erase(client);
}

bool Channel::isInvited(Client* client) const {
    return _inviteList.find(client) != _inviteList.end();
}

bool Channel::hasMode(char mode) const {
    switch (mode) {
        case 'i': return _inviteOnly;
        case 't': return _topicRestricted;
        case 'k': return !_key.empty();
        case 'l': return _userLimit > 0;
        default: return false;
    }
}

void Channel::setMode(char mode, bool enable) {
    switch (mode) {
        case 'i':
            _inviteOnly = enable;
            break;
        case 't':
            _topicRestricted = enable;
            break;
        case 'k':
            if (!enable) {
                _key.clear();
            }
            break;
        case 'l':
            if (!enable) {
                _userLimit = 0;
            }
            break;
    }
}

std::string Channel::getModeString() const {
    std::string modes = "+";

    if (_inviteOnly) modes += "i";
    if (_topicRestricted) modes += "t";
    if (!_key.empty()) modes += "k";
    if (_userLimit > 0) modes += "l";

    if (modes == "+") {
        return "";
    }

    std::string params;
    if (!_key.empty()) {
        params += " " + _key;
    }
    if (_userLimit > 0) {
        std::ostringstream oss;
        oss << " " << _userLimit;
        params += oss.str();
    }

    return modes + params;
}

---

2. KICKコマンド

2.1 仕様

構文: KICK <channel> <user> [<reason>]

目的:
- チャンネルからユーザーを強制退出させる
- オペレータ権限が必要

エラー:
403 ERR_NOSUCHCHANNEL - チャンネルが存在しない
441 ERR_USERNOTINCHANNEL - 対象がチャンネルにいない
442 ERR_NOTONCHANNEL - 自分がチャンネルにいない
482 ERR_CHANOPRIVSNEEDED - オペレータ権限がない

2.2 実装

void Server::cmdKick(Client* client, const Message& msg) {
    if (msg.params.size() < 2) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 461 " + client->getNickname() +
            " KICK :Not enough parameters\\r\\n");
        return;
    }

    std::string channelName = msg.params[0];
    std::string targetNick = msg.params[1];
    std::string reason = (msg.params.size() > 2) ?
                         msg.params[2] : client->getNickname();

    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;
    }

    // オペレータ権限の確認
    if (!channel->isOperator(client)) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 482 " + client->getNickname() + " " +
            channelName + " :You're not channel operator\\r\\n");
        return;
    }

    // 対象ユーザーの確認
    Client* target = getClientByNick(targetNick);

    if (!target || !channel->hasMember(target)) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 441 " + client->getNickname() + " " +
            targetNick + " " + channelName +
            " :They aren't on that channel\\r\\n");
        return;
    }

    // KICKメッセージを送信
    std::string kickMsg = ":" + client->getPrefix() + " KICK " +
                          channelName + " " + targetNick + " :" + reason + "\\r\\n";
    sendToChannel(channel, kickMsg, NULL);

    // チャンネルから退出
    channel->removeMember(target);
    target->leaveChannel(channel);

    // 空になったチャンネルを削除
    if (channel->isEmpty()) {
        removeChannel(channelName);
    }
}

---

3. INVITEコマンド

3.1 仕様

構文: INVITE <nickname> <channel>

目的:
- ユーザーをチャンネルに招待
- +i チャンネルの場合、オペレータ権限が必要

エラー:
401 ERR_NOSUCHNICK - 対象が存在しない
442 ERR_NOTONCHANNEL - 自分がチャンネルにいない
443 ERR_USERONCHANNEL - 対象が既にチャンネルにいる
482 ERR_CHANOPRIVSNEEDED - オペレータ権限がない(+i)

成功:
341 RPL_INVITING - 招待成功

3.2 実装

void Server::cmdInvite(Client* client, const Message& msg) {
    if (msg.params.size() < 2) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 461 " + client->getNickname() +
            " INVITE :Not enough parameters\\r\\n");
        return;
    }

    std::string targetNick = msg.params[0];
    std::string channelName = msg.params[1];

    // 対象ユーザーの確認
    Client* target = getClientByNick(targetNick);

    if (!target) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 401 " + client->getNickname() + " " +
            targetNick + " :No such nick/channel\\r\\n");
        return;
    }

    // チャンネルの確認
    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;
    }

    // +i の場合、オペレータ権限が必要
    if (channel->hasMode('i') && !channel->isOperator(client)) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 482 " + client->getNickname() + " " +
            channelName + " :You're not channel operator\\r\\n");
        return;
    }

    // 既にチャンネルにいる場合
    if (channel->hasMember(target)) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 443 " + client->getNickname() + " " +
            targetNick + " " + channelName +
            " :is already on channel\\r\\n");
        return;
    }

    // 招待リストに追加
    channel->addInvite(target);

    // 341 RPL_INVITING
    sendToClient(client->getFd(),
        ":" + _serverName + " 341 " + client->getNickname() + " " +
        targetNick + " " + channelName + "\\r\\n");

    // 対象に招待を通知
    sendToClient(target->getFd(),
        ":" + client->getPrefix() + " INVITE " + targetNick + " :" +
        channelName + "\\r\\n");
}

---

4. TOPICコマンド

4.1 仕様

構文: TOPIC <channel> [<topic>]

目的:
- トピックの取得または設定
- +t の場合、設定にはオペレータ権限が必要

エラー:
442 ERR_NOTONCHANNEL - チャンネルにいない
482 ERR_CHANOPRIVSNEEDED - オペレータ権限がない(+t)

成功:
331 RPL_NOTOPIC - トピックなし
332 RPL_TOPIC - トピックあり

4.2 実装

void Server::cmdTopic(Client* client, const Message& msg) {
    if (msg.params.empty()) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 461 " + client->getNickname() +
            " TOPIC :Not enough parameters\\r\\n");
        return;
    }

    std::string channelName = msg.params[0];
    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;
    }

    // トピックの取得
    if (msg.params.size() == 1) {
        if (channel->getTopic().empty()) {
            sendToClient(client->getFd(),
                ":" + _serverName + " 331 " + client->getNickname() + " " +
                channelName + " :No topic is set\\r\\n");
        } else {
            sendToClient(client->getFd(),
                ":" + _serverName + " 332 " + client->getNickname() + " " +
                channelName + " :" + channel->getTopic() + "\\r\\n");
        }
        return;
    }

    // トピックの設定
    // +t の場合、オペレータ権限が必要
    if (channel->hasMode('t') && !channel->isOperator(client)) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 482 " + client->getNickname() + " " +
            channelName + " :You're not channel operator\\r\\n");
        return;
    }

    std::string newTopic = msg.params[1];
    channel->setTopic(newTopic);

    // チャンネルに通知
    std::string topicMsg = ":" + client->getPrefix() + " TOPIC " +
                           channelName + " :" + newTopic + "\\r\\n";
    sendToChannel(channel, topicMsg, NULL);
}

---

5. MODEコマンド

5.1 仕様

構文: MODE <channel> [<modes> [<params>...]]

ft_ircで必要なモード:
i - invite-only(招待制)
t - topic restriction(トピック制限)
k - channel key(パスワード)
o - operator(オペレータ)
l - user limit(人数制限)

例:
MODE #test +i           → 招待制にする
MODE #test +k secret    → パスワード設定
MODE #test +o Alice     → オペレータ付与
MODE #test +l 50        → 人数制限
MODE #test +itk secret  → 複数モード

5.2 実装

void Server::cmdMode(Client* client, const Message& msg) {
    if (msg.params.empty()) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 461 " + client->getNickname() +
            " MODE :Not enough parameters\\r\\n");
        return;
    }

    std::string target = msg.params[0];

    // チャンネルモード
    if (target[0] == '#' || target[0] == '&') {
        handleChannelMode(client, msg);
    }
    // ユーザーモード(ft_ircでは通常不要)
    else {
        // 省略
    }
}

void Server::handleChannelMode(Client* client, const Message& msg) {
    std::string channelName = msg.params[0];
    Channel* channel = getChannel(channelName);

    if (!channel) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 403 " + client->getNickname() + " " +
            channelName + " :No such channel\\r\\n");
        return;
    }

    // モードなし → 現在のモードを表示
    if (msg.params.size() == 1) {
        std::string modes = channel->getModeString();
        sendToClient(client->getFd(),
            ":" + _serverName + " 324 " + client->getNickname() + " " +
            channelName + " " + modes + "\\r\\n");
        return;
    }

    // 自分がチャンネルにいるか確認
    if (!channel->hasMember(client)) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 442 " + client->getNickname() + " " +
            channelName + " :You're not on that channel\\r\\n");
        return;
    }

    // オペレータ権限の確認
    if (!channel->isOperator(client)) {
        sendToClient(client->getFd(),
            ":" + _serverName + " 482 " + client->getNickname() + " " +
            channelName + " :You're not channel operator\\r\\n");
        return;
    }

    // モード変更をパース
    std::string modeStr = msg.params[1];
    size_t paramIdx = 2;
    bool adding = true;
    std::string appliedModes;
    std::string appliedParams;

    for (size_t i = 0; i < modeStr.length(); i++) {
        char mode = modeStr[i];

        if (mode == '+') {
            adding = true;
            continue;
        }
        if (mode == '-') {
            adding = false;
            continue;
        }

        bool success = false;

        switch (mode) {
            case 'i':
                channel->setMode('i', adding);
                success = true;
                break;

            case 't':
                channel->setMode('t', adding);
                success = true;
                break;

            case 'k':
                if (adding) {
                    if (paramIdx < msg.params.size()) {
                        channel->setKey(msg.params[paramIdx]);
                        appliedParams += " " + msg.params[paramIdx];
                        paramIdx++;
                        success = true;
                    }
                } else {
                    channel->setKey("");
                    success = true;
                }
                break;

            case 'o':
                if (paramIdx < msg.params.size()) {
                    std::string targetNick = msg.params[paramIdx];
                    Client* targetClient = getClientByNick(targetNick);
                    paramIdx++;

                    if (targetClient && channel->hasMember(targetClient)) {
                        if (adding) {
                            channel->addOperator(targetClient);
                        } else {
                            channel->removeOperator(targetClient);
                        }
                        appliedParams += " " + targetNick;
                        success = true;
                    }
                }
                break;

            case 'l':
                if (adding) {
                    if (paramIdx < msg.params.size()) {
                        int limit = std::atoi(msg.params[paramIdx].c_str());
                        if (limit > 0) {
                            channel->setLimit(limit);
                            appliedParams += " " + msg.params[paramIdx];
                            success = true;
                        }
                        paramIdx++;
                    }
                } else {
                    channel->setLimit(0);
                    success = true;
                }
                break;

            default:
                // 未知のモード
                sendToClient(client->getFd(),
                    ":" + _serverName + " 472 " + client->getNickname() + " " +
                    std::string(1, mode) + " :is unknown mode char\\r\\n");
                continue;
        }

        if (success) {
            if (appliedModes.empty() || appliedModes.back() != (adding ? '+' : '-')) {
                appliedModes += adding ? "+" : "-";
            }
            appliedModes += mode;
        }
    }

    // モード変更を通知
    if (!appliedModes.empty()) {
        std::string modeMsg = ":" + client->getPrefix() + " MODE " +
                              channelName + " " + appliedModes +
                              appliedParams + "\\r\\n";
        sendToChannel(channel, modeMsg, NULL);
    }
}

---

6. チャンネル管理

6.1 サーバーでのチャンネル管理

Channel* Server::getChannel(const std::string& name) {
    std::string lowerName = toLower(name);
    std::map<std::string, Channel*>::iterator it = _channels.find(lowerName);
    if (it != _channels.end()) {
        return it->second;
    }
    return NULL;
}

Channel* Server::createChannel(const std::string& name) {
    std::string lowerName = toLower(name);
    Channel* channel = new Channel(name);
    _channels[lowerName] = channel;
    return channel;
}

void Server::removeChannel(const std::string& name) {
    std::string lowerName = toLower(name);
    std::map<std::string, Channel*>::iterator it = _channels.find(lowerName);
    if (it != _channels.end()) {
        delete it->second;
        _channels.erase(it);
    }
}

std::string Server::toLower(const std::string& str) {
    std::string result = str;
    for (size_t i = 0; i < result.length(); i++) {
        result[i] = std::tolower(result[i]);
    }
    return result;
}

---

まとめ

本章で学んだこと:

  • Channelクラス: メンバー、オペレータ、モードの管理
  • KICK: 強制退出
  • INVITE: 招待機能
  • TOPIC: トピック管理
  • MODE: チャンネルモード(i, t, k, o, l)
  • チャンネル管理: 作成、削除、検索

次章では、ボーナス機能を学びます。