/*
* The Mana World Server
* Copyright 2004 The Mana World Development Team
*
* This file is part of The Mana World.
*
* The Mana World is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* any later version.
*
* The Mana World is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with The Mana World; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Id$
*/
#include <list>
#include "defines.h"
#include "account-server/characterdata.hpp"
#include "account-server/guild.hpp"
#include "account-server/guildmanager.hpp"
#include "account-server/serverhandler.hpp"
#include "chat-server/chatchannelmanager.hpp"
#include "chat-server/chatclient.hpp"
#include "chat-server/chathandler.hpp"
#include "net/connectionhandler.hpp"
#include "net/messagein.hpp"
#include "net/messageout.hpp"
#include "net/netcomputer.hpp"
#include "utils/logger.h"
#include "utils/stringfilter.h"
#include "utils/tokendispenser.hpp"
struct ChatPendingLogin
{
std::string character;
AccountLevel level;
int timeout;
};
typedef std::map< std::string, ChatPendingLogin > ChatPendingLogins;
static ChatPendingLogins pendingLogins;
typedef std::map< std::string, ChatClient * > ChatPendingClients;
static ChatPendingClients pendingClients;
void registerChatClient(const std::string &token,
const std::string &name,
int level)
{
ChatPendingClients::iterator i = pendingClients.find(token);
if (i != pendingClients.end())
{
ChatClient *computer = i->second;
computer->characterName = name;
computer->accountLevel = (AccountLevel) level;
pendingClients.erase(i);
MessageOut result;
result.writeShort(CPMSG_CONNECT_RESPONSE);
result.writeByte(ERRMSG_OK);
computer->send(result);
}
else
{
ChatPendingLogin p;
p.character = name;
p.level = (AccountLevel) level;
p.timeout = 300; // world ticks
pendingLogins.insert(std::make_pair(token, p));
}
}
bool ChatHandler::startListen(enet_uint16 port)
{
LOG_INFO("Chat handler started:");
return ConnectionHandler::startListen(port);
}
void ChatHandler::removeOutdatedPending()
{
ChatPendingLogins::iterator i = pendingLogins.begin(), next;
while (i != pendingLogins.end())
{
next = i; ++next;
if (--i->second.timeout <= 0) pendingLogins.erase(i);
i = next;
}
}
NetComputer *ChatHandler::computerConnected(ENetPeer *peer)
{
return new ChatClient(peer);
}
void ChatHandler::computerDisconnected(NetComputer *computer)
{
// Remove user from all channels
chatChannelManager->
removeUserFromAllChannels(static_cast<ChatClient*>(computer));
ChatPendingClients::iterator i_end = pendingClients.end();
for (ChatPendingClients::iterator i = pendingClients.begin();
i != i_end; ++i)
{
if (i->second == computer)
{
pendingClients.erase(i);
break;
}
}
delete computer;
}
void ChatHandler::process(enet_uint32 timeout)
{
ConnectionHandler::process(timeout);
removeOutdatedPending();
}
void ChatHandler::processMessage(NetComputer *comp, MessageIn &message)
{
ChatClient &computer = *static_cast< ChatClient * >(comp);
MessageOut result;
if (computer.characterName.empty())
{
if (message.getId() != PCMSG_CONNECT) return;
std::string magic_token = message.readString(MAGIC_TOKEN_LENGTH);
ChatPendingLogins::iterator i = pendingLogins.find(magic_token);
if (i == pendingLogins.end())
{
ChatPendingClients::iterator i_end = pendingClients.end();
for (ChatPendingClients::iterator i = pendingClients.begin();
i != i_end; ++i)
{
if (i->second == &computer) return;
}
pendingClients.insert(std::make_pair(magic_token, &computer));
return;
}
computer.characterName = i->second.character;
computer.accountLevel = i->second.level;
pendingLogins.erase(i);
result.writeShort(CPMSG_CONNECT_RESPONSE);
result.writeByte(ERRMSG_OK);
computer.send(result);
// sendGuildRejoin(computer);
return;
}
switch (message.getId())
{
case PCMSG_CHAT:
handleChatMessage(computer, message);
break;
case PCMSG_ANNOUNCE:
handleAnnounceMessage(computer, message);
break;
case PCMSG_PRIVMSG:
handlePrivMsgMessage(computer, message);
break;
case PCMSG_REGISTER_CHANNEL:
handleRegisterChannelMessage(computer, message);
break;
case PCMSG_UNREGISTER_CHANNEL:
handleUnregisterChannelMessage(computer, message);
break;
case PCMSG_ENTER_CHANNEL:
handleEnterChannelMessage(computer, message);
break;
case PCMSG_QUIT_CHANNEL:
handleQuitChannelMessage(computer, message);
break;
case PCMSG_LIST_CHANNELS:
handleListChannelsMessage(computer, message);
break;
case PCMSG_LIST_CHANNELUSERS:
handleListChannelUsersMessage(computer, message);
break;
case PCMSG_DISCONNECT:
handleDisconnectMessage(computer, message);
break;
default:
LOG_WARN("ChatHandler::processMessage, Invalid message type"
<< message.getId());
result.writeShort(XXMSG_INVALID);
break;
}
if (result.getLength() > 0)
computer.send(result);
}
void
ChatHandler::handleCommand(ChatClient &computer, const std::string &command)
{
LOG_INFO("Chat: Received unhandled command: " << command);
MessageOut result;
result.writeShort(CPMSG_ERROR);
result.writeByte(CHAT_UNHANDLED_COMMAND);
computer.send(result);
}
void
ChatHandler::warnPlayerAboutBadWords(ChatClient &computer)
{
// We could later count if the player is really often unpolite.
MessageOut result;
result.writeShort(CPMSG_ERROR);
result.writeByte(CHAT_USING_BAD_WORDS); // The Channel
computer.send(result);
LOG_INFO(computer.characterName << " says bad words.");
}
void
ChatHandler::handleChatMessage(ChatClient &client, MessageIn &msg)
{
std::string text = msg.readString();
// Pass it through the slang filter (false when it contains bad words)
if (!stringFilter->filterContent(text))
{
warnPlayerAboutBadWords(client);
return;
}
short channelId = msg.readShort();
ChatChannel *channel = chatChannelManager->getChannel(channelId);
if (channel)
{
LOG_DEBUG(client.characterName << " says in channel " << channelId
<< ": " << text);
MessageOut result(CPMSG_PUBMSG);
result.writeShort(channelId);
result.writeString(client.characterName);
result.writeString(text);
sendInChannel(channel, result);
}
}
void
ChatHandler::handleAnnounceMessage(ChatClient &client, MessageIn &msg)
{
std::string text = msg.readString();
if (!stringFilter->filterContent(text))
{
warnPlayerAboutBadWords(client);
return;
}
if (client.accountLevel == AL_ADMIN || client.accountLevel == AL_GM)
{
// TODO: b_lindeijer: Shouldn't announcements also have a sender?
LOG_INFO("ANNOUNCE: " << text);
MessageOut result(CPMSG_ANNOUNCEMENT);
result.writeString(text);
// We send the message to all players in the default channel as it is
// an announcement.
sendToEveryone(result);
}
else
{
MessageOut result(CPMSG_ERROR);
result.writeByte(ERRMSG_INSUFFICIENT_RIGHTS);
client.send(result);
LOG_INFO(client.characterName <<
" couldn't make an announcement due to insufficient rights.");
}
}
void
ChatHandler::handlePrivMsgMessage(ChatClient &client, MessageIn &msg)
{
std::string user = msg.readString();
std::string text = msg.readString();
if (!stringFilter->filterContent(text))
{
warnPlayerAboutBadWords(client);
return;
}
// We seek the player to whom the message is told and send it to her/him.
sayToPlayer(client, user, text);
}
void
ChatHandler::handleRegisterChannelMessage(ChatClient &client, MessageIn &msg)
{
MessageOut reply(CPMSG_REGISTER_CHANNEL_RESPONSE);
char channelType = msg.readByte();
if (!channelType) // 0 public, 1 private
{
if (client.accountLevel != AL_ADMIN &&
client.accountLevel != AL_GM)
{
// Removed the need for admin/gm rights to create public channels
//reply.writeByte(ERRMSG_INSUFFICIENT_RIGHTS);
//send message
//return;
}
}
std::string channelName = msg.readString();
std::string channelAnnouncement = msg.readString();
std::string channelPassword = msg.readString();
if (!stringFilter->filterContent(channelName) ||
!stringFilter->filterContent(channelAnnouncement))
{
warnPlayerAboutBadWords(client);
return;
}
// Checking strings for length and double quotes
if (channelName.empty() ||
channelName.length() > MAX_CHANNEL_NAME ||
stringFilter->findDoubleQuotes(channelName) ||
channelAnnouncement.length() > MAX_CHANNEL_ANNOUNCEMENT ||
stringFilter->findDoubleQuotes(channelAnnouncement) ||
channelPassword.length() > MAX_CHANNEL_PASSWORD ||
stringFilter->findDoubleQuotes(channelPassword))
{
reply.writeByte(ERRMSG_INVALID_ARGUMENT);
}
else if (guildManager->doesExist(channelName))
{
// Channel already exists
reply.writeByte(ERRMSG_INVALID_ARGUMENT);
}
else
{
// We attempt to create a new channel
short channelId;
// TODO: b_lindeijer: These methods should really be combined.
if (channelType)
{
channelId = chatChannelManager->registerPrivateChannel(
channelName,
channelAnnouncement,
channelPassword);
}
else
{
channelId = chatChannelManager->registerPublicChannel(
channelName,
channelAnnouncement,
channelPassword);
}
if (channelId)
{
// We add the player as admin of this channel as he created it. The
// user registering a private channel is the only one to be able to
// update the password and the announcement in it and also to
// remove it.
ChatChannel *channel = chatChannelManager->getChannel(channelId);
channel->addUser(&client);
reply.writeByte(ERRMSG_OK);
reply.writeShort(channelId);
reply.writeString(channelName);
}
else
{
reply.writeByte(ERRMSG_FAILURE);
}
}
client.send(reply);
}
void
ChatHandler::handleUnregisterChannelMessage(ChatClient &client, MessageIn &msg)
{
MessageOut reply(CPMSG_UNREGISTER_CHANNEL_RESPONSE);
short channelId = msg.readShort();
ChatChannel *channel = chatChannelManager->getChannel(channelId);
if (!channel)
{
reply.writeByte(ERRMSG_INVALID_ARGUMENT);
}
else if (channelId < (signed) MAX_PUBLIC_CHANNELS_RANGE)
{
// Public channel
// Get character based on name
CharacterPtr character =
serverHandler->getCharacter(client.characterName);
const std::string &channelName = channel->getName();
if (client.accountLevel == AL_ADMIN || client.accountLevel == AL_GM)
{
warnUsersAboutPlayerEventInChat(
channel, "", CHAT_EVENT_LEAVING_PLAYER);
if (chatChannelManager->removeChannel(channelId))
reply.writeByte(ERRMSG_OK);
else
reply.writeByte(ERRMSG_FAILURE);
}
else if (guildManager->doesExist(channelName))
{
Guild *guild = guildManager->findByName(channelName);
if (guild->checkLeader(character.get()))
{
// TODO: b_lindeijer: I think it would be better if guild
// channels were removed in response to a guild being
// removed, as opposed to removing a guild because its
// channel disappears.
chatChannelManager->removeChannel(channelId);
guildManager->removeGuild(guild->getId());
reply.writeByte(ERRMSG_OK);
}
else
{
reply.writeByte(ERRMSG_INSUFFICIENT_RIGHTS);
}
}
else
{
reply.writeByte(ERRMSG_INSUFFICIENT_RIGHTS);
}
}
else
{
// Private channel
// We first see if the user is the admin (first user) of the channel
const ChatChannel::ChannelUsers &userList = channel->getUserList();
ChatChannel::ChannelUsers::const_iterator i = userList.begin();
if (*i != &client)
{
reply.writeByte(ERRMSG_INSUFFICIENT_RIGHTS);
}
else
{
// Make every user quit the channel
warnUsersAboutPlayerEventInChat(
channel, "", CHAT_EVENT_LEAVING_PLAYER);
if (chatChannelManager->removeChannel(channelId))
{
reply.writeByte(ERRMSG_OK);
}
else
{
reply.writeByte(ERRMSG_FAILURE);
}
}
}
client.send(reply);
}
void
ChatHandler::handleEnterChannelMessage(ChatClient &client, MessageIn &msg)
{
MessageOut reply(CPMSG_ENTER_CHANNEL_RESPONSE);
std::string channelName = msg.readString();
std::string givenPassword = msg.readString();
short channelId = chatChannelManager->getChannelId(channelName);
ChatChannel *channel = chatChannelManager->getChannel(channelId);
// TODO: b_lindeijer: Currently, the client has to join its guild channels
// explicitly by sending 'enter channel' messages. This should be
// changed to implicitly joining relevant guild channels right after
// login.
Guild *guild = guildManager->findByName(channelName);
if (!channelId || !channel)
{
reply.writeByte(ERRMSG_INVALID_ARGUMENT);
}
else if (!channel->getPassword().empty() &&
channel->getPassword() != givenPassword)
{
// Incorrect password (should probably have its own return value)
reply.writeByte(ERRMSG_INVALID_ARGUMENT);
}
else if (guild && !guild->checkInGuild(client.characterName))
{
// Player tried to join a guild channel of a guild he's not a member of
reply.writeByte(ERRMSG_INVALID_ARGUMENT);
}
else
{
if (channel->addUser(&client))
{
// In the case of a guild, send user joined message.
if (guild)
{
sendUserJoined(channel, client.characterName);
}
reply.writeByte(ERRMSG_OK);
// The user entered the channel, now give him the channel
// id, the announcement string and the user list.
reply.writeShort(channelId);
reply.writeString(channelName);
reply.writeString(channel->getAnnouncement());
const ChatChannel::ChannelUsers &users = channel->getUserList();
for (ChatChannel::ChannelUsers::const_iterator i = users.begin(),
i_end = users.end();
i != i_end; ++i)
{
reply.writeString((*i)->characterName);
}
// Send an CPMSG_UPDATE_CHANNEL to warn other clients a user went
// in the channel.
warnUsersAboutPlayerEventInChat(channel,
client.characterName,
CHAT_EVENT_NEW_PLAYER);
}
else
{
reply.writeByte(ERRMSG_FAILURE);
}
}
client.send(reply);
}
void
ChatHandler::handleQuitChannelMessage(ChatClient &client, MessageIn &msg)
{
MessageOut reply(CPMSG_QUIT_CHANNEL_RESPONSE);
short channelId = msg.readShort();
ChatChannel *channel = chatChannelManager->getChannel(channelId);
if (channelId == 0 || !channel)
{
reply.writeByte(ERRMSG_INVALID_ARGUMENT);
}
else if (!channel->removeUser(&client))
{
reply.writeByte(ERRMSG_FAILURE);
}
else
{
reply.writeByte(ERRMSG_OK);
reply.writeShort(channelId);
// Send an CPMSG_UPDATE_CHANNEL to warn other clients a user left
// the channel.
warnUsersAboutPlayerEventInChat(channel,
client.characterName,
CHAT_EVENT_LEAVING_PLAYER);
// TODO: b_lindeijer: Clients aren't supposed to quit guild
// channels explicitly, this should rather happen
// implicitly. See similar note at handling 'enter channel'
// messages.
const std::string &channelName = channel->getName();
if (guildManager->doesExist(channelName))
{
// Send a user left message
sendUserLeft(channel, client.characterName);
}
}
client.send(reply);
}
void
ChatHandler::handleListChannelsMessage(ChatClient &client, MessageIn &msg)
{
MessageOut reply(CPMSG_LIST_CHANNELS_RESPONSE);
std::list<const ChatChannel*> channels =
chatChannelManager->getPublicChannels();
for (std::list<const ChatChannel*>::iterator i = channels.begin(),
i_end = channels.end();
i != i_end; ++i)
{
const std::string &name = (*i)->getName();
short users = (*i)->getUserList().size();
reply.writeString(name);
reply.writeShort(users);
}
client.send(reply);
}
void
ChatHandler::handleListChannelUsersMessage(ChatClient &client, MessageIn &msg)
{
MessageOut reply(CPMSG_LIST_CHANNELUSERS_RESPONSE);
// TODO: b_lindeijer: Since it only makes sense to ask for the list of
// users in a channel you're in, this message should really take
// a channel id instead.
std::string channelName = msg.readString();
int channelId = chatChannelManager->getChannelId(channelName);
ChatChannel *channel = chatChannelManager->getChannel(channelId);
reply.writeString(channelName);
if (channel)
{
const ChatChannel::ChannelUsers &users = channel->getUserList();
for (ChatChannel::ChannelUsers::const_iterator
i = users.begin(), i_end = users.end(); i != i_end; ++i)
{
reply.writeString((*i)->characterName);
}
}
client.send(reply);
}
void
ChatHandler::handleDisconnectMessage(ChatClient &client, MessageIn &msg)
{
MessageOut reply(CPMSG_DISCONNECT_RESPONSE);
reply.writeByte(ERRMSG_OK);
chatChannelManager->removeUserFromAllChannels(&client);
client.send(reply);
}
void
ChatHandler::sayToPlayer(ChatClient &computer, const std::string &playerName,
const std::string &text)
{
MessageOut result;
LOG_DEBUG(computer.characterName << " says to " << playerName << ": "
<< text);
// Send it to the being if the being exists
result.writeShort(CPMSG_PRIVMSG);
result.writeString(computer.characterName);
result.writeString(text);
for (NetComputers::iterator i = clients.begin(), i_end = clients.end();
i != i_end; ++i) {
if (static_cast< ChatClient * >(*i)->characterName == playerName)
{
(*i)->send(result);
break;
}
}
}
void ChatHandler::warnUsersAboutPlayerEventInChat(ChatChannel *channel,
const std::string &userName,
char eventId)
{
MessageOut msg(CPMSG_CHANNEL_EVENT);
msg.writeShort(channel->getId());
msg.writeByte(eventId);
msg.writeString(userName);
sendInChannel(channel, msg);
}
void ChatHandler::sendInChannel(ChatChannel *channel, MessageOut &msg)
{
const ChatChannel::ChannelUsers &users = channel->getUserList();
for (ChatChannel::ChannelUsers::const_iterator
i = users.begin(), i_end = users.end(); i != i_end; ++i)
{
(*i)->send(msg);
}
}
void ChatHandler::sendGuildEnterChannel(const MessageOut &msg,
const std::string &name)
{
// TODO: b_lindeijer: This method is just an inefficient way to send a
// message to a player with a certain name. Would be good to get
// rid of it.
for (NetComputers::iterator i = clients.begin(), i_end = clients.end();
i != i_end; ++i)
{
if (static_cast< ChatClient * >(*i)->characterName == name)
{
(*i)->send(msg);
break;
}
}
}
void ChatHandler::sendGuildInvite(const std::string &invitedName,
const std::string &inviterName,
const std::string &guildName)
{
MessageOut msg(CPMSG_GUILD_INVITED);
msg.writeString(inviterName);
msg.writeString(guildName);
// TODO: b_lindeijer: This is just an inefficient way to send a message to
// a player with a certain name. Would be good if the invitedName
// could be replaced with a ChatClient.
for (NetComputers::iterator i = clients.begin(), i_end = clients.end();
i != i_end; ++i)
{
if (static_cast< ChatClient * >(*i)->characterName == invitedName)
{
(*i)->send(msg);
break;
}
}
}
#if 0
void ChatHandler::sendGuildRejoin(ChatClient &client)
{
// Get character based on name.
CharacterPtr character = serverHandler->getCharacter(client.characterName);
// Get list of guilds and check what rights they have.
std::vector<std::string> guilds = character->getGuilds();
for (unsigned int i = 0; i != guilds.size(); ++i)
{
Guild *guild = guildManager->findByName(guilds[i]);
short leader = 0;
if (!guild)
{
return;
}
if (guild->checkLeader(character.get()))
{
leader = 1;
}
MessageOut msg(CPMSG_GUILD_REJOIN);
msg.writeString(guild->getName());
msg.writeShort(guild->getId());
msg.writeShort(leader);
client.send(msg);
serverHandler->enterChannel(guild->getName(), character.get());
}
}
#endif
void ChatHandler::sendUserJoined(ChatChannel *channel, const std::string &name)
{
MessageOut msg(CPMSG_USERJOINED);
msg.writeShort(channel->getId());
msg.writeString(name);
sendInChannel(channel, msg);
}
void ChatHandler::sendUserLeft(ChatChannel *channel, const std::string &name)
{
MessageOut msg(CPMSG_USERLEFT);
msg.writeShort(channel->getId());
msg.writeString(name);
sendInChannel(channel, msg);
}