/*************************************************************************** * Copyright 2006-2016 by Christian Ihle * * contact@kouchat.net * * * * This file is part of KouChat. * * * * KouChat is free software; you can redistribute it and/or modify * * it under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation, either version 3 of * * the License, or (at your option) any later version. * * * * KouChat 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with KouChat. * * If not, see <http://www.gnu.org/licenses/>. * ***************************************************************************/ package net.usikkert.kouchat.net; import java.io.File; import java.util.Date; import net.usikkert.kouchat.message.CoreMessages; import net.usikkert.kouchat.misc.ChatState; import net.usikkert.kouchat.misc.CommandException; import net.usikkert.kouchat.misc.Controller; import net.usikkert.kouchat.misc.MessageController; import net.usikkert.kouchat.misc.Topic; import net.usikkert.kouchat.misc.User; import net.usikkert.kouchat.misc.WaitingList; import net.usikkert.kouchat.settings.Settings; import net.usikkert.kouchat.ui.UserInterface; import net.usikkert.kouchat.util.DateTools; import net.usikkert.kouchat.util.Logger; import net.usikkert.kouchat.util.Sleeper; import net.usikkert.kouchat.util.Tools; import net.usikkert.kouchat.util.Validate; /** * This class responds to events from the message parser. * * @author Christian Ihle */ public class DefaultMessageResponder implements MessageResponder { private static final Logger LOG = Logger.getLogger(DefaultMessageResponder.class); private final NetworkUtils networkUtils = new NetworkUtils(); private final Sleeper sleeper = new Sleeper(); private final DateTools dateTools = new DateTools(); private final Controller controller; private final User me; private final TransferList tList; private final WaitingList wList; private final UserInterface ui; private final MessageController msgController; private final ChatState chatState; private final CoreMessages coreMessages; /** * Constructor. * * @param controller The controller to use for communication. * @param ui The user interface to update. * @param settings The settings to use. * @param coreMessages The core messages to use. */ public DefaultMessageResponder(final Controller controller, final UserInterface ui, final Settings settings, final CoreMessages coreMessages) { Validate.notNull(controller, "Controller can not be null"); Validate.notNull(ui, "UserInterface can not be null"); Validate.notNull(settings, "Settings can not be null"); Validate.notNull(coreMessages, "Core messages can not be null"); this.controller = controller; this.ui = ui; this.coreMessages = coreMessages; msgController = ui.getMessageController(); me = settings.getMe(); tList = controller.getTransferList(); wList = controller.getWaitingList(); chatState = controller.getChatState(); } /** * Shows a message from a user in the user interface. * * @param userCode The unique code of the user who sent the message. * @param msg The message. * @param color The color the message has. */ @Override public void messageArrived(final int userCode, final String msg, final int color) { if (!controller.isNewUser(userCode)) { final User user = controller.getUser(userCode); if (!user.isAway()) { msgController.showUserMessage(user.getNick(), msg, color); // Visible but not in front if (ui.isVisible() && !ui.isFocused()) { me.setNewMsg(true); } ui.notifyMessageArrived(user, msg); } else { LOG.severe("User is away - ignoring message. user=%s, userCode=%s, message=%s", user, userCode, msg); } } else { LOG.severe("User is unknown - ignoring message. userCode=%s, message=%s", userCode, msg); } } /** * When a user logs off it is removed from the user list, and * any open private chat window is notified. * * @param userCode The unique code of the user who logged off. */ @Override public void userLogOff(final int userCode) { final User user = controller.getUser(userCode); if (user != null) { final String logOffMessage = coreMessages.getMessage("core.network.systemMessage.userLogOff", user.getNick()); controller.removeUser(user, logOffMessage); msgController.showSystemMessage(logOffMessage); } else { LOG.severe("User is unknown - ignoring logoff. userCode=%s", userCode); } } /** * When a user logs on, the user is added to the user list. * If the user's nick name is not valid, it is reset, and reported if * it is identical to the application user's nick. * * @param newUser The user logging on to the chat. */ @Override public void userLogOn(final User newUser) { if (me.getNick().trim().equalsIgnoreCase(newUser.getNick())) { LOG.severe("User logs on with your nick name - resetting nick and sending nick crash message. " + "user=%s, userCode=%s", newUser, newUser.getCode()); controller.sendNickCrashMessage(newUser.getNick()); newUser.setNick("" + newUser.getCode()); } else if (controller.isNickInUse(newUser.getNick())) { LOG.severe("User logs on with someone else's nick name - resetting nick. " + "user=%s, userCode=%s", newUser, newUser.getCode()); newUser.setNick("" + newUser.getCode()); } else if (!Tools.isValidNick(newUser.getNick())) { LOG.severe("User logs on with invalid nick name - resetting nick. " + "user=%s, userCode=%s", newUser, newUser.getCode()); newUser.setNick("" + newUser.getCode()); } controller.getUserList().add(newUser); msgController.showSystemMessage(newUser.getNick() + " logged on from " + newUser.getIpAddress()); } /** * If a user exposes itself without having sent a logon message first, * and the expose is not part of the application startup, the user * is checked and added to the user list. * * @param newUser The unknown user. */ private void userShowedUp(final User newUser) { if (me.getNick().trim().equalsIgnoreCase(newUser.getNick())) { controller.sendNickCrashMessage(newUser.getNick()); newUser.setNick("" + newUser.getCode()); } else if (controller.isNickInUse(newUser.getNick())) { newUser.setNick("" + newUser.getCode()); } else if (!Tools.isValidNick(newUser.getNick())) { newUser.setNick("" + newUser.getCode()); } controller.getUserList().add(newUser); msgController.showSystemMessage(newUser.getNick() + " showed up unexpectedly from " + newUser.getIpAddress()); } /** * Updates the topic, and shows it. * * @param userCode The unique code of the user who changed the topic. * @param newTopic The new topic. * @param nick The nick name of the user who changed the topic. * @param time The time when the topic was set. */ @Override public void topicChanged(final int userCode, final String newTopic, final String nick, final long time) { if (time > 0 && nick.length() > 0) { final Topic topic = controller.getTopic(); if (newTopic != null) { if (!newTopic.equals(topic.getTopic()) && time > topic.getTime()) { if (chatState.isLogonCompleted()) { msgController.showSystemMessage(nick + " changed the topic to: " + newTopic); } // Shown during startup. else { final String date = dateTools.dateToString(new Date(time), "HH:mm:ss, dd. MMM. yy"); msgController.showSystemMessage("Topic is: " + newTopic + " (set by " + nick + " at " + date + ")"); } topic.changeTopic(newTopic, nick, time); ui.showTopic(); } } else { if (!topic.getTopic().equals(newTopic) && time > topic.getTime() && chatState.isLogonCompleted()) { msgController.showSystemMessage(nick + " removed the topic"); topic.changeTopic("", "", time); ui.showTopic(); } } } } /** * Adds unknown users that are exposing themselves. * This happens mostly during startup, but can also happen after a timeout. * * @param user The unknown user who was exposed. */ @Override public void userExposing(final User user) { if (controller.isNewUser(user.getCode())) { // Usually this happens when someone returns from a timeout if (chatState.isLogonCompleted()) { if (wList.isWaitingUser(user.getCode())) { wList.removeWaitingUser(user.getCode()); } userShowedUp(user); } // This should ONLY happen during logon else { controller.getUserList().add(user); } } else { final User orgUser = controller.getUser(user.getCode()); // When users timeout, there can become sync issues if (!orgUser.getNick().equals(user.getNick())) { nickChanged(user.getCode(), user.getNick()); } if (!orgUser.getAwayMsg().equals(user.getAwayMsg())) { awayChanged(user.getCode(), user.isAway(), user.getAwayMsg()); } } } /** * When the user has logged on to the network, the application updates * the status. * * @param ipAddress The IP address of the application user. */ @Override public void meLogOn(final String ipAddress) { chatState.setLoggedOn(true); me.setIpAddress(ipAddress); me.setHostName(networkUtils.getLocalHostName()); msgController.showSystemMessage("You logged on as " + me.getNick() + " from " + createHostInfo(me)); ui.showTopic(); } /** * Returns a string containing both the host name and the ip address * if the host name is set, or just the ip address if not. * * @param user The user to get host info from. * @return A string with host info. */ private String createHostInfo(final User user) { if (user.getHostName() != null) { return user.getHostName() + " (" + user.getIpAddress() + ")"; } else { return user.getIpAddress(); } } /** * Updates the writing status of the user. * * @param userCode The unique code of the user who started or stopped writing. * @param writing If the user is writing or not. */ @Override public void writingChanged(final int userCode, final boolean writing) { controller.changeWriting(userCode, writing); } /** * Updates the away status for the user, both in the main window * and in the private chat window. * * @param userCode The unique code of the user who changed away status. * @param away If the user is away or not. * @param awayMsg The away message if the user is away, or an empty string. */ @Override public void awayChanged(final int userCode, final boolean away, final String awayMsg) { final User user = controller.getUser(userCode); if (user == null) { LOG.severe("Could not find user: %s", userCode); return; } try { controller.changeAwayStatus(userCode, away, awayMsg); if (away) { msgController.showSystemMessage(user.getNick() + " went away: " + user.getAwayMsg()); } else { msgController.showSystemMessage(user.getNick() + " came back"); } if (user.getPrivchat() != null) { user.getPrivchat().updateAwayState(); if (away) { msgController.showPrivateSystemMessage(user, user.getNick() + " went away: " + user.getAwayMsg()); } else { msgController.showPrivateSystemMessage(user, user.getNick() + " came back"); } } } catch (final CommandException e) { LOG.severe("Something very strange going on here: %s", e.getMessage()); } } /** * Updates the idle time of the application user, * and checks if the ip address has changed. * * @param ipAddress The IP address of the application user. */ @Override public void meIdle(final String ipAddress) { me.setLastIdle(System.currentTimeMillis()); if (!me.getIpAddress().equals(ipAddress) && chatState.isLoggedOn()) { msgController.showSystemMessage("You changed ip from " + me.getIpAddress() + " to " + ipAddress); me.setIpAddress(ipAddress); } } /** * Updates the idle time of the user, * and checks if the user's ip address has changed. * * @param userCode The unique code of the user who sent the idle message. * @param ipAddress The IP address of that user. */ @Override public void userIdle(final int userCode, final String ipAddress) { final User user = controller.getUser(userCode); if (user == null) { LOG.severe("Could not find user: %s", userCode); return; } user.setLastIdle(System.currentTimeMillis()); if (!user.getIpAddress().equals(ipAddress)) { msgController.showSystemMessage(user.getNick() + " changed ip from " + user.getIpAddress() + " to " + ipAddress); user.setIpAddress(ipAddress); } } /** * Sends the current topic. */ @Override public void topicRequested() { controller.sendTopicRequestedMessage(); } /** * Someone sent a message that the application user's nick is * in use, so the nick is reset. */ @Override public void nickCrash() { controller.changeNick(me.getCode(), "" + me.getCode()); msgController.showSystemMessage("Nick crash, resetting nick to " + me.getNick()); ui.showTopic(); } /** * Sends information about this client to the other clients. */ @Override public void exposeRequested() { controller.sendExposingMessage(); controller.sendClientInfo(); } /** * Changes the nick name of a user, if valid. * * @param userCode The unique code of the user who changed nick name. * @param newNick The new nick name. */ @Override public void nickChanged(final int userCode, final String newNick) { final User user = controller.getUser(userCode); if (user == null) { LOG.severe("Could not find user: %s", userCode); return; } if (!controller.isNickInUse(newNick) && Tools.isValidNick(newNick)) { final String oldNick = user.getNick(); controller.changeNick(userCode, newNick); msgController.showSystemMessage(oldNick + " changed nick to " + newNick); if (user.getPrivchat() != null) { msgController.showPrivateSystemMessage(user, oldNick + " changed nick to " + user.getNick()); user.getPrivchat().updateUserInformation(); } } else { LOG.severe("%s tried to change nick to '%s', which is invalid", user.getNick(), newNick); } } /** * Asks if the application user wants to receive a file from another user, * and if so, starts a server listening for a file transfer. * * @param userCode The unique code of the user who is asking to send a file. * @param byteSize The size of the file in bytes. * @param fileName The name of the file. * @param user The nick name of the user. * @param fileHash The hash code of the file. */ @Override public void fileSend(final int userCode, final long byteSize, final String fileName, final String user, final int fileHash) { if (!controller.isNewUser(userCode)) { final String size = Tools.byteToString(byteSize); final User tmpUser = controller.getUser(userCode); final File defaultFile = new File( System.getProperty("user.home") + System.getProperty("file.separator") + fileName); final FileReceiver fileRes = tList.addFileReceiver(tmpUser, defaultFile, byteSize); msgController.showSystemMessage( user + " is trying to send the file " + fileName + " (#" + fileRes.getId() + ") [" + size + "]"); if (ui.askFileSave(user, fileName, size)) { ui.showFileSave(fileRes); if (fileRes.isAccepted() && !fileRes.isCanceled()) { ui.showTransfer(fileRes); try { final int port = fileRes.startServer(); controller.sendFileAccept(tmpUser, port, fileHash, fileName); if (fileRes.transfer()) { msgController.showSystemMessage("Successfully received " + fileName + " from " + user + ", and saved as " + fileRes.getFile().getName()); } else { msgController.showSystemMessage("Failed to receive " + fileName + " from " + user); fileRes.cancel(); } } // Failed to start the server catch (final ServerException e) { LOG.severe(e, "Failed to start server: %s", e.getMessage()); msgController.showSystemMessage("Failed to receive " + fileName + " from " + user); controller.sendFileAbort(tmpUser, fileHash, fileName); fileRes.cancel(); } // Failed to send the accept message catch (final CommandException e) { msgController.showSystemMessage("Failed to receive " + fileName + " from " + user); fileRes.cancel(); } } else if (!fileRes.isCanceled()) { msgController.showSystemMessage("You declined to receive " + fileName + " from " + user); controller.sendFileAbort(tmpUser, fileHash, fileName); } } else if (!fileRes.isCanceled()) { msgController.showSystemMessage("You declined to receive " + fileName + " from " + user); controller.sendFileAbort(tmpUser, fileHash, fileName); } tList.removeFileReceiver(fileRes); } else { LOG.severe("Could not find user: %s", user); } } /** * The other user stopped a file transfer from the application user, * or the other way around. * Updates the status in the file sender. * * @param userCode The unique code of the other user. * @param fileName The name of the file. * @param fileHash The hash code of the file. */ @Override public void fileSendAborted(final int userCode, final String fileName, final int fileHash) { final User user = controller.getUser(userCode); final FileSender fileSender = tList.getFileSender(user, fileName, fileHash); if (fileSender != null) { fileSender.cancel(); msgController.showSystemMessage(user.getNick() + " aborted reception of " + fileName); tList.removeFileSender(fileSender); } final FileReceiver fileReceiver = tList.getFileReceiver(user, fileName); if (fileReceiver != null) { fileReceiver.cancel(); msgController.showSystemMessage(user.getNick() + " aborted sending of " + fileName); } } /** * The other user has accepted a file transfer. Will try to connect to the * user to send the file. * * @param userCode The unique code of the user who accepted a file transfer. * @param fileName The name of the file. * @param fileHash The hash code of the file. * @param port The port to use for connecting to the other user. */ @Override public void fileSendAccepted(final int userCode, final String fileName, final int fileHash, final int port) { final User user = controller.getUser(userCode); final FileSender fileSend = tList.getFileSender(user, fileName, fileHash); if (fileSend != null) { msgController.showSystemMessage(user.getNick() + " accepted sending of " + fileName); // Give the server some time to set up the connection first sleeper.sleep(200); if (fileSend.transfer(port)) { msgController.showSystemMessage(fileName + " successfully sent to " + user.getNick()); } else { msgController.showSystemMessage("Failed to send " + fileName + " to " + user.getNick()); } tList.removeFileSender(fileSend); } } /** * Updates the client information about the user. * * @param userCode The unique code of the user who sent client info. * @param client The client the user is using. * @param timeSinceLogon Number of milliseconds since the user logged on. * @param operatingSystem The user's operating system. * @param privateChatPort The port to use for sending private chat messages to this user. */ @Override public void clientInfo(final int userCode, final String client, final long timeSinceLogon, final String operatingSystem, final int privateChatPort) { final User user = controller.getUser(userCode); if (user != null) { user.setClient(client); user.setLogonTime(System.currentTimeMillis() - timeSinceLogon); user.setOperatingSystem(operatingSystem); user.setPrivateChatPort(privateChatPort); } else { LOG.severe("Could not find user: %s", userCode); } } }