第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)
- チャンネル管理: 作成、削除、検索
次章では、ボーナス機能を学びます。