diff --git a/.gitignore b/.gitignore index f2ecff4..cf41fd0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ *.class hs_err_pid* *.jar +*.war +*.ear # maven files @@ -13,6 +15,7 @@ pom.xml.versionsBackup pom.xml.next release.properties repo/.locks +dependency-reduced-pom.xml # eclipse files @@ -30,3 +33,30 @@ local.properties *.launch .classpath .target + +# NetBeans +nbproject/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +nb-configuration.xml + +# IntelliJ IDEA +*.iml +*.ipr +*.iws +.idea/ + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# Linux +.* +!.gitattributes +!.gitignore +*~ \ No newline at end of file diff --git a/pom.xml b/pom.xml index b8064bd..f435c7b 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 fr.delthas javaskype - 1.0.14 + 1.0.14.5-SNAPSHOT JavaSkype https://github.com/delthas/javaskype @@ -69,6 +69,12 @@ 4.12 test + + commons-io + commons-io + 2.4 + test + diff --git a/src/main/java/fr/delthas/skype/Chat.java b/src/main/java/fr/delthas/skype/Chat.java new file mode 100644 index 0000000..1123929 --- /dev/null +++ b/src/main/java/fr/delthas/skype/Chat.java @@ -0,0 +1,61 @@ +package fr.delthas.skype; + +import fr.delthas.skype.message.Message; + +/** + * Interface for chats + */ +public interface Chat { + + /** + * Get Skype client + * + * @return skype object + */ + Skype getSkype(); + + /** + * Get identity of a chat + * + * @return string + */ + String getIdentity(); + + /** + * Type of chat + * + * @return ChatType enum item + */ + ChatType getType(); + + /** + * Send message to chat + * + * @param message object of one of messages types + */ + void sendMessage(Message message); + + /** + * Edit message to chat. + * + * Work only with text message. + * + * @param message object of one of messages types + */ + void editMessage(Message message); + + /** + * Remove message to chat + * + * @param message object of one of messages types + */ + void removeMessage(Message message); + + /** + * ChatType enum + */ + public enum ChatType { + GROUP, + USER + } +} diff --git a/src/main/java/fr/delthas/skype/Constants.java b/src/main/java/fr/delthas/skype/Constants.java new file mode 100644 index 0000000..43c9b12 --- /dev/null +++ b/src/main/java/fr/delthas/skype/Constants.java @@ -0,0 +1,14 @@ +package fr.delthas.skype; + +/** + * Constants + */ +public class Constants { + public static String HEADER_TEXT = "Text"; + public static String HEADER_RICH_TEXT = "RichText"; + public static String HEADER_PICTURE = "RichText/UriObject"; + public static String HEADER_FILE = "RichText/Media_GenericFile"; + public static String HEADER_VIDEO = "RichText/Media_Video"; + public static String HEADER_CONTACT = "RichText/Contacts"; + public static String HEADER_MOJI = "RichText/Media_FlikMsg"; +} diff --git a/src/main/java/fr/delthas/skype/Group.java b/src/main/java/fr/delthas/skype/Group.java index b56b473..217683c 100644 --- a/src/main/java/fr/delthas/skype/Group.java +++ b/src/main/java/fr/delthas/skype/Group.java @@ -1,5 +1,7 @@ package fr.delthas.skype; +import fr.delthas.skype.message.Message; + import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -10,9 +12,8 @@ * A conversation between some Skype users. *

* All information will be updated as updates are received (this object is NOT an immutable view/snapshot of a group). - * */ -public class Group { +public class Group implements Chat { private final Skype skype; private final String id; @@ -31,15 +32,28 @@ public class Group { * * @param message The message to send. */ + @Deprecated public void sendMessage(String message) { skype.sendGroupMessage(this, message); } + public void sendMessage(Message message) { + skype.doMessageAction(this, message, Skype.MessageAction.SEND); + } + + public void editMessage(Message message) { + skype.doMessageAction(this, message, Skype.MessageAction.EDIT); + } + + public void removeMessage(Message message) { + skype.doMessageAction(this, message, Skype.MessageAction.REMOVE); + } + /** * The id of a group is a special String used by Skype to uniquely identify groups. *

- * In case you know about network IDs and the like: if a group "adress" is "19:xxx@thread.skype", its id is "xxx". - * + * In case you know about network IDs and the like: if a group "address" is "19:xxx@thread.skype", its id is "19:xxx@thread.skype" or "19:xxx@p2p.thread.skype" . + * * @return The id of the group. */ public String getId() { @@ -89,7 +103,6 @@ void setUsers(List> users) { * * @param user The user to add to this group. * @param role The role of the newly added user. - * * @return true if the user wasn't in the group, and the Skype account has group admin rights if needed. */ public boolean addUser(User user, Role role) { @@ -111,9 +124,7 @@ public boolean addUser(User user, Role role) { * Removes a user from this group. Group admin rights are needed. * * @param user The user to remove from this group. - * * @return true if the Skype account has admin rights, and the user was in the group. - * * @see #isSelfAdmin() */ public boolean removeUser(User user) { @@ -138,9 +149,7 @@ public boolean removeUser(User user) { * * @param user The user whose role is to be changed * @param role The new role of the user. - * * @return true if the Skype account has admin rights, and the user was in the group and didn't have this role already. - * * @see #isSelfAdmin() */ public boolean changeUserRole(User user, Role role) { @@ -213,4 +222,18 @@ public String toString() { return "Group: " + getId(); } + @Override + public Skype getSkype() { + return skype; + } + + @Override + public String getIdentity() { + return id; + } + + @Override + public ChatType getType() { + return ChatType.GROUP; + } } diff --git a/src/main/java/fr/delthas/skype/GroupMessageListener.java b/src/main/java/fr/delthas/skype/GroupMessageListener.java index 64e0426..25d2242 100644 --- a/src/main/java/fr/delthas/skype/GroupMessageListener.java +++ b/src/main/java/fr/delthas/skype/GroupMessageListener.java @@ -1,19 +1,52 @@ package fr.delthas.skype; +import fr.delthas.skype.message.Message; /** * A listener for new messages sent to a Skype account. - * */ -public interface GroupMessageListener { +public interface GroupMessageListener extends MessageListener { /** * Called when a message is sent from a user to a group the Skype account is in while it is connected. * - * @param group The group in which the message has been sent. - * @param sender The sender of the message. - * @param message The message sent. + * @param group The group in which the message has been sent. + * @param sender The sender of the message. + * @param message The text of message sent. */ - public void messageReceived(Group group, User sender, String message); + @Deprecated + default void messageReceived(Group group, User sender, String message) { + } + + /** + * Called when a message is sent from a user to a group the Skype account is in while it is connected. + * + * @param group The group in which the message has been sent. + * @param sender The sender of the message. + * @param message The object of message sent. + */ + default void messageReceived(Group group, User sender, Message message) { + } + + /** + * Called when a message is edited by a user in a group the Skype account is in while it is connected. + * + * @param group The group in which the message has been edit. + * @param sender The sender of the message. + * @param message The object of message sent. + */ + default void messageEdited(Group group, User sender, Message message) { + } + + /** + * Called when a message is removed by a user in a group the Skype account is in while it is connected. + * + * @param group The group in which the message has been remove. + * @param sender The sender of the message. + * @param message The object of message sent. + */ + default void messageRemoved(Group group, User sender, Message message) { + } + } diff --git a/src/main/java/fr/delthas/skype/MessageListener.java b/src/main/java/fr/delthas/skype/MessageListener.java new file mode 100644 index 0000000..bd52c80 --- /dev/null +++ b/src/main/java/fr/delthas/skype/MessageListener.java @@ -0,0 +1,12 @@ +package fr.delthas.skype; + +/** + * A root listener for new messages sent to a Skype account. + */ +public interface MessageListener { + enum MessageEvent { + RECEIVED, + EDITED, + REMOVED + } +} diff --git a/src/main/java/fr/delthas/skype/NotifConnector.java b/src/main/java/fr/delthas/skype/NotifConnector.java index 2ceb61d..213bb6f 100644 --- a/src/main/java/fr/delthas/skype/NotifConnector.java +++ b/src/main/java/fr/delthas/skype/NotifConnector.java @@ -1,14 +1,24 @@ package fr.delthas.skype; -import java.io.BufferedInputStream; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.StringReader; +import fr.delthas.skype.MessageListener.MessageEvent; +import fr.delthas.skype.message.*; +import org.jsoup.Jsoup; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.net.ssl.SSLSocketFactory; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.CountDownLatch; @@ -18,17 +28,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.net.ssl.SSLSocketFactory; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.jsoup.Jsoup; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; +import static fr.delthas.skype.Constants.HEADER_RICH_TEXT; +import static fr.delthas.skype.Constants.HEADER_TEXT; class NotifConnector { @@ -102,7 +103,8 @@ private void processPacket(Packet packet) throws IOException { case "recentconversations-response": NodeList conversationNodes = doc.getElementsByTagName("conversation"); List threadIds = new ArrayList<>(); - outer: for (int i = 0; i < conversationNodes.getLength(); i++) { + outer: + for (int i = 0; i < conversationNodes.getLength(); i++) { Node conversation = conversationNodes.item(i); String id = null; boolean isThread = false; @@ -146,7 +148,7 @@ private void processPacket(Packet packet) throws IOException { sb.delete(0, sb.length()); sb.append(""); } - sb.append("19:").append(threadId).append("@thread.skype"); + sb.append("").append(threadId).append(""); } String body = sb.append("").toString(); sendPacket("GET", "MSGR\\THREADS", body); @@ -183,56 +185,110 @@ private void processPacket(Packet packet) throws IOException { } Object sender = parseEntity(formatted.sender); Object receiver = parseEntity(formatted.receiver); - switch (messageType) { - case "Text": - case "RichText": - if (!(sender instanceof User)) { - logger.fine("Received " + messageType + " message sent from " + sender + " which isn't a user"); - break; - } - if (receiver instanceof Group) { - skype.groupMessageReceived((Group) receiver, (User) sender, getPlaintext(formatted.body)); - } else { - skype.userMessageReceived((User) sender, getPlaintext(formatted.body)); - } - break; - case "ThreadActivity/AddMember": - List usernames = getXMLFields(formatted.body, "target"); - skype.usersAddedToGroup(usernames.stream().map(username -> (User) parseEntity(username)).collect(Collectors.toList()), (Group) sender); + if (messageType.startsWith(HEADER_RICH_TEXT) || messageType.startsWith(HEADER_TEXT)) { + // work with messages + if (!(sender instanceof User)) { + logger.fine("Received " + messageType + " message sent from " + sender + " which isn't a user"); break; - case "ThreadActivity/DeleteMember": - usernames = getXMLFields(formatted.body, "target"); - skype.usersAddedToGroup(usernames.stream().map(username -> (User) parseEntity(username)).collect(Collectors.toList()), (Group) sender); - break; - case "ThreadActivity/TopicUpdate": - skype.groupTopicChanged((Group) sender, getPlaintext(getXMLField(formatted.body, "value"))); + } + boolean isRemoved = false; + + String isMe = formatted.headers.get("Skype-EmoteOffset"); + String messageId = formatted.headers.get("Client-Message-ID"); + String editId = formatted.headers.get("Skype-EditedId"); + String html = formatted.body; + if (isMe != null) { + html = formatted.body.substring(Integer.parseInt(isMe)); + } + if (editId != null) { + String editOffset = formatted.headers.get("Skype-EditOffset"); + if (editOffset != null) { + Integer offset = Integer.parseInt(editOffset); + html = html.substring(offset); + } + html = html.replaceAll("", ""); + isRemoved = html.isEmpty(); + } + String id = messageId != null ? messageId : editId; + Message message = null; + MessageType type = MessageType.getTypeByHeaderType((messageType)); + if (type == null) { break; - case "ThreadActivity/RoleUpdate": - Document doc = getDocument(formatted.body); - NodeList targetNodes = doc.getElementsByTagName("target"); - List> roles = new ArrayList<>(targetNodes.getLength()); - for (int i = 0; i < targetNodes.getLength(); i++) { - Node targetNode = targetNodes.item(i); - User user = null; - Role role = null; - for (int j = 0; j < targetNode.getChildNodes().getLength(); j++) { - Node targetPropertyNode = targetNode.getChildNodes().item(j); - if (targetPropertyNode.getNodeName().equals("id")) { - user = (User) parseEntity(targetPropertyNode.getTextContent()); - skype.updateUser(user); - } else if (targetPropertyNode.getNodeName().equals("role")) { - role = Role.getRole(targetPropertyNode.getTextContent()); + } + switch (type) { + case TEXT: + String singleRow = html.replaceAll("\r", "").replaceAll("\n", ""); + Boolean isHasQuotes = Pattern.compile(".*.*<\\/quote>.*").matcher(singleRow).matches(); + message = new TextMessage(id, html, isMe != null, isHasQuotes); + break; + case PICTURE: + message = new PictureMessage(id, html); + break; + case FILE: + message = new FileMessage(id, html); + break; + case VIDEO: + message = new VideoMessage(id, html); + break; + case CONTACT: + message = new ContactMessage(id, html); + break; + case MOJI: + message = new MojiMessage(id, html); + break; + case UNKNOWN: + message = new UnknownMessage(id, html); + break; + default: + break; + } + MessageEvent eventType = editId == null ? MessageEvent.RECEIVED : isRemoved ? MessageEvent.REMOVED : MessageEvent.EDITED; + if (receiver instanceof Group) { + skype.doGroupMessageEvent(eventType, (Group) receiver, (User) sender, message); + } else { + skype.doUserMessageEvent(eventType, (User) sender, message); + } + } else { + switch (messageType) { + case "ThreadActivity/AddMember": + List usernames = getXMLFields(formatted.body, "target"); + skype.usersAddedToGroup(usernames.stream().map(username -> (User) parseEntity(username)).collect(Collectors.toList()), (Group) sender); + break; + case "ThreadActivity/DeleteMember": + usernames = getXMLFields(formatted.body, "target"); + skype.usersAddedToGroup(usernames.stream().map(username -> (User) parseEntity(username)).collect(Collectors.toList()), (Group) sender); + break; + case "ThreadActivity/TopicUpdate": + skype.groupTopicChanged((Group) sender, getPlaintext(getXMLField(formatted.body, "value"))); + break; + case "ThreadActivity/RoleUpdate": + Document doc = getDocument(formatted.body); + NodeList targetNodes = doc.getElementsByTagName("target"); + List> roles = new ArrayList<>(targetNodes.getLength()); + for (int i = 0; i < targetNodes.getLength(); i++) { + Node targetNode = targetNodes.item(i); + User user = null; + Role role = null; + for (int j = 0; j < targetNode.getChildNodes().getLength(); j++) { + Node targetPropertyNode = targetNode.getChildNodes().item(j); + if (targetPropertyNode.getNodeName().equals("id")) { + user = (User) parseEntity(targetPropertyNode.getTextContent()); + skype.updateUser(user); + } else if (targetPropertyNode.getNodeName().equals("role")) { + role = Role.getRole(targetPropertyNode.getTextContent()); + } + } + if (user != null && role != null) { + roles.add(new Pair<>(user, role)); } } - if (user != null && role != null) { - roles.add(new Pair<>(user, role)); - } - } - skype.usersRolesChanged((Group) sender, roles); - break; - default: - break; + skype.usersRolesChanged((Group) sender, roles); + break; + default: + break; + } } + } break; case "NFY": @@ -494,34 +550,49 @@ public void connect() throws IOException, InterruptedException { pingThread.start(); } + public void sendMessage(Chat chat, Message message) throws IOException { + String entity = ""; + switch (chat.getType()) { + case GROUP: + entity = chat.getIdentity(); + break; + case USER: + entity = "8:" + chat.getIdentity(); + break; + } + sendMessage(entity, message); + } + + @Deprecated public void sendUserMessage(User user, String message) throws IOException { sendMessage("8:" + user.getUsername(), getSanitized(message)); } + @Deprecated public void sendGroupMessage(Group group, String message) throws IOException { - sendMessage("19:" + group.getId() + "@thread.skype", getSanitized(message)); + sendMessage(group.getId(), getSanitized(message)); } public void addUserToGroup(User user, Role role, Group group) throws IOException { - String body = String.format("19:%s@thread.skype8:%s%s", + String body = String.format("%s8:%s%s", group.getId(), user.getUsername(), role.getRoleString()); sendPacket("PUT", "MSGR\\THREAD", body); } public void removeUserFromGroup(User user, Group group) throws IOException { - String body = String.format("19:%s@thread.skype8:%s", group.getId(), + String body = String.format("%s8:%s", group.getId(), user.getUsername()); sendPacket("DEL", "MSGR\\THREAD", body); } public void changeGroupTopic(Group group, String topic) throws IOException { String body = - String.format("19:%s@thread.skype%s", group.getId(), getSanitized(topic)); + String.format("%s%s", group.getId(), getSanitized(topic)); sendPacket("PUT", "MSGR\\THREAD", body); } public void changeUserRole(User user, Role role, Group group) throws IOException { - String body = String.format("19:%s@thread.skype8:%s%s", + String body = String.format("%s8:%s%s", group.getId(), user.getUsername(), role.getRoleString()); sendPacket("PUT", "MSGR\\THREAD", body); } @@ -533,12 +604,47 @@ public void changePresence(Presence presence) throws IOException { sendPacket("PUT", "MSGR\\PRESENCE", formattedPublicationMessage); } + @Deprecated private void sendMessage(String entity, String message) throws IOException { String body = FormattedMessage.format("8:" + username + ";epid={" + EPID + "}", entity, "Messaging: 2.0", message, "Content-Type: application/user+xml", "Message-Type: RichText"); sendPacket("SDG", "MSGR", body); } + private void sendMessage(String entity, Message message) throws IOException { + List headers = new ArrayList<>(Arrays.asList("Content-Type: application/user+xml", "Message-Type: " + MessageType.getTypeByClass(message).getHeaderType())); + if (message.getId() != null) { + headers.add("Skype-EditedId: " + message.getId()); + } + AbstractMessage abstractMessage = (AbstractMessage) message; + String body = ""; + switch (message.getType()) { + case TEXT: + TextMessage textMessage = (TextMessage) message; + body = FormattedMessage.format("8:" + username + ";epid={" + EPID + "}", entity, "Messaging: 2.0", getSanitized(textMessage.getHtml()), + headers.toArray(new String[headers.size()])); + sendPacket("SDG", "MSGR", body); + break; + case PICTURE: + case FILE: + case VIDEO: + case CONTACT: + case MOJI: + if (abstractMessage.isEmpty()) { + body = FormattedMessage.format("8:" + username + ";epid={" + EPID + "}", entity, "Messaging: 2.0", "", + headers.toArray(new String[headers.size()])); + sendPacket("SDG", "MSGR", body); + } else { + //Send this type of message not implemented yet; + } + break; + case UNKNOWN: + logger.warning("Strange UNKNOWN message has been tried to send."); + break; + } + + } + public synchronized void disconnect() { logger.finer("Stopping notification connector"); try { @@ -639,25 +745,11 @@ private Object parseEntity(String rawEntity) { logger.finest("Parsing entity " + rawEntity); int senderBegin = rawEntity.indexOf(':'); int network = Integer.parseInt(rawEntity.substring(0, senderBegin)); - int end0 = rawEntity.indexOf('@'); - int end1 = rawEntity.indexOf(';'); - if (end0 == -1) { - end0 = Integer.MAX_VALUE; - } - if (end1 == -1) { - end1 = Integer.MAX_VALUE; - } - int senderEnd = Math.min(end0, end1); - String name; - if (senderEnd == Integer.MAX_VALUE) { - name = rawEntity.substring(senderBegin + 1); - } else { - name = rawEntity.substring(senderBegin + 1, senderEnd); - } if (network == 8) { - return skype.getUser(name); + int i = rawEntity.indexOf(';'); + return skype.getUser(rawEntity.substring(2, i == -1 ? rawEntity.length() : i)); } else if (network == 19) { - return skype.getGroup(name); + return skype.getGroup(rawEntity); } else { logger.warning("Error while parsing entity " + rawEntity + ": unknown network:" + network); throw new IllegalArgumentException(); diff --git a/src/main/java/fr/delthas/skype/Skype.java b/src/main/java/fr/delthas/skype/Skype.java index 9bbc451..d39cabc 100644 --- a/src/main/java/fr/delthas/skype/Skype.java +++ b/src/main/java/fr/delthas/skype/Skype.java @@ -1,5 +1,9 @@ package fr.delthas.skype; +import fr.delthas.skype.MessageListener.MessageEvent; +import fr.delthas.skype.message.AbstractMessage; +import fr.delthas.skype.message.Message; + import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; @@ -10,11 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.logging.FileHandler; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; +import java.util.logging.*; import java.util.stream.Collectors; /** @@ -27,7 +27,6 @@ * LF which will be replaced with CRLF if needed. *

* If you want to report a bug, please enable debug/logs with {@link Skype#setDebug(Path)}, before using {@link #connect()}. - * */ public final class Skype { @@ -78,9 +77,8 @@ public Skype(String username, String password) { /** * Calls {@code connect(Presence.CONNECTED)}. * - * @throws IOException If an error is thrown while connecting. + * @throws IOException If an error is thrown while connecting. * @throws InterruptedException If the connection is interrupted. - * * @see #connect(Presence) */ public void connect() throws IOException, InterruptedException { @@ -92,8 +90,7 @@ public void connect() throws IOException, InterruptedException { * Connects the Skype interface. Will block until connected. * * @param presence The initial presence of the Skype account after connection. Cannot be {@link Presence#OFFLINE}. - * - * @throws IOException If an error is thrown while connecting. + * @throws IOException If an error is thrown while connecting. * @throws InterruptedException If the connection is interrupted. */ public void connect(Presence presence) throws IOException, InterruptedException { @@ -172,7 +169,6 @@ public User getSelf() { /** * @return The current list of contacts of the account (snapshot, won't be updated). - * */ public List getContacts() { ensureConnected(); @@ -211,9 +207,8 @@ public boolean isConnected() { * Enables or disables debug of the Skype library (globally). (By default logs are disabled.) *

* If enabled, debug information and logs will be written to a log file at the specified path. If the path is null, the debug will be disabled. - * + * * @param path The path at which to write debugging information, or null to disable logging. - * * @throws IOException may be thrown when adding a file handler to the logger */ public static void setDebug(Path path) throws IOException { @@ -376,6 +371,7 @@ void declineContactRequest(ContactRequest contactRequest) { // --- Package-private methods that simply call the notification connector --- // + @Deprecated void sendUserMessage(User user, String message) { ensureConnected(); try { @@ -386,6 +382,51 @@ void sendUserMessage(User user, String message) { } } + public enum MessageAction { + SEND, + EDIT, + REMOVE + } + + /** + * Execute action with message. + * Message can be send, edit and removed + * + * @param chat where will be send action + * @param message object of some message type + * @param action send, edit or removed + */ + void doMessageAction(Chat chat, Message message, MessageAction action) { + ensureConnected(); + try { + logger.finer("Do " + action.name() + "-action in chat: " + chat + " message: " + message); + switch (action) { + case SEND: + notifConnector.sendMessage(chat, message); + break; + case EDIT: + if (message.getId() != null) { + notifConnector.sendMessage(chat, message); + } else { + logger.warning("Message has not id. Can't edit message: " + message); + } + break; + case REMOVE: + if (message.getId() != null) { + AbstractMessage abstractMessage = (AbstractMessage) message; + abstractMessage.setHtml(""); + notifConnector.sendMessage(chat, message); + } else { + logger.warning("Message has not id. Can't remove message: " + message); + } + break; + } + } catch (IOException e) { + error(e); + } + } + + @Deprecated void sendGroupMessage(Group group, String message) { ensureConnected(); try { @@ -438,6 +479,7 @@ void changeGroupTopic(Group group, String topic) { // --- Listeners call methods --- // + @Deprecated void userMessageReceived(User sender, String message) { updateUser(sender); logger.finer("Received message: " + message + " from user: " + sender); @@ -446,6 +488,24 @@ void userMessageReceived(User sender, String message) { } } + void doUserMessageEvent(MessageEvent event, User sender, T message) { + logger.finer("Event: " + event + ", message('" + message.getType().getName() + "'): " + message + " from user: " + sender); + for (UserMessageListener listener : userMessageListeners) { + switch (event) { + case RECEIVED: + listener.messageReceived(sender, message); + break; + case EDITED: + listener.messageEdited(sender, message); + break; + case REMOVED: + listener.messageRemoved(sender, message); + break; + } + } + } + + @Deprecated void groupMessageReceived(Group group, User sender, String message) { logger.finer("Received group message: " + message + " from user: " + sender + " in group: " + group); for (GroupMessageListener listener : groupMessageListeners) { @@ -453,6 +513,23 @@ void groupMessageReceived(Group group, User sender, String message) { } } + void doGroupMessageEvent(MessageEvent event, Group group, User sender, T message) { + logger.finer("Event: " + event + ", message('" + message.getType().getName() + "'): " + message + " from user: " + sender + " in group: " + group); + for (GroupMessageListener listener : groupMessageListeners) { + switch (event) { + case RECEIVED: + listener.messageReceived(group, sender, message); + break; + case EDITED: + listener.messageEdited(group, sender, message); + break; + case REMOVED: + listener.messageRemoved(group, sender, message); + break; + } + } + } + void userPresenceChanged(User user, Presence oldPresence, Presence presence) { logger.finer("User: " + user + " changed presence from: " + oldPresence + " to: " + presence); for (UserPresenceListener listener : userPresenceListeners) { diff --git a/src/main/java/fr/delthas/skype/User.java b/src/main/java/fr/delthas/skype/User.java index da31e7a..9d651e3 100644 --- a/src/main/java/fr/delthas/skype/User.java +++ b/src/main/java/fr/delthas/skype/User.java @@ -1,13 +1,14 @@ package fr.delthas.skype; +import fr.delthas.skype.message.Message; + /** * A Skype account. *

* All information will be updated as updates are received (this object is NOT an immutable view/snapshot of a user account). - * */ -public class User { +public class User implements Chat { private final Skype skype; private final String username; @@ -26,7 +27,6 @@ public class User { /** * Blocks this user (without reporting the account). - * */ public void block() { skype.block(this); @@ -34,7 +34,6 @@ public void block() { /** * Unblocks this user. - * */ public void unblock() { skype.unblock(this); @@ -51,7 +50,6 @@ public void sendContactRequest(String greeting) { /** * Removes this user from the list of contacts of the Skype account. If the user isn't a contact, nothing happens. - * */ public void removeFromContacts() { skype.removeFromContacts(this); @@ -62,10 +60,26 @@ public void removeFromContacts() { * * @param message The message to send to this user. */ + @Deprecated public void sendMessage(String message) { skype.sendUserMessage(this, message); } + @Override + public void sendMessage(Message message) { + skype.doMessageAction(this, message, Skype.MessageAction.SEND); + } + + @Override + public void editMessage(Message message) { + skype.doMessageAction(this, message, Skype.MessageAction.EDIT); + } + + @Override + public void removeMessage(Message message) { + skype.doMessageAction(this, message, Skype.MessageAction.REMOVE); + } + /** * @return The username of this user. */ @@ -228,4 +242,18 @@ public String toString() { return "User: " + getUsername(); } + @Override + public Skype getSkype() { + return skype; + } + + @Override + public String getIdentity() { + return username; + } + + @Override + public ChatType getType() { + return ChatType.USER; + } } diff --git a/src/main/java/fr/delthas/skype/UserMessageListener.java b/src/main/java/fr/delthas/skype/UserMessageListener.java index 01c9ce5..732e8f2 100644 --- a/src/main/java/fr/delthas/skype/UserMessageListener.java +++ b/src/main/java/fr/delthas/skype/UserMessageListener.java @@ -1,18 +1,29 @@ package fr.delthas.skype; +import fr.delthas.skype.message.Message; + /** * A listener for new messages sent to a Skype account. - * */ -public interface UserMessageListener { +public interface UserMessageListener extends MessageListener { /** * Called when a message is sent from a user to the Skype account while it is connected. * - * @param sender The sender of the message. + * @param sender The sender of the message. * @param message The message sent. */ - public void messageReceived(User sender, String message); + @Deprecated + default void messageReceived(User sender, String message) { + } + + default void messageReceived(User sender, Message message) { + } + + default void messageEdited(User sender, Message message) { + } + default void messageRemoved(User sender, Message message) { + } } diff --git a/src/main/java/fr/delthas/skype/WebConnector.java b/src/main/java/fr/delthas/skype/WebConnector.java index c69b722..02e8036 100644 --- a/src/main/java/fr/delthas/skype/WebConnector.java +++ b/src/main/java/fr/delthas/skype/WebConnector.java @@ -115,9 +115,11 @@ private User updateUser(JSONObject userJSON, boolean newContactType) throws Pars } else { userUsername = userJSON.getString("id"); userDisplayName = userJSON.optString("display_name", null); - JSONObject nameJSON = userJSON.getJSONObject("name"); - userFirstName = nameJSON.optString("first", null); - userLastName = nameJSON.optString("surname", null); + if (userJSON.optString("type", "skype").equals("skype")) { + JSONObject nameJSON = userJSON.getJSONObject("name"); + userFirstName = nameJSON.optString("first", null); + userLastName = nameJSON.optString("surname", null); + } userMood = userJSON.optString("mood", null); if (userJSON.has("locations")) { JSONObject locationJSON = userJSON.optJSONArray("locations").optJSONObject(0); diff --git a/src/main/java/fr/delthas/skype/message/AbstractMessage.java b/src/main/java/fr/delthas/skype/message/AbstractMessage.java new file mode 100644 index 0000000..65c6b13 --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/AbstractMessage.java @@ -0,0 +1,67 @@ +package fr.delthas.skype.message; + +import org.jsoup.Jsoup; + +/** + * Abstract class for all type of message with all common field and methods + */ +public abstract class AbstractMessage implements Message { + + private String id; + private MessageType type; + private String html; + private String text; + + + public AbstractMessage(String id, String html) { + this.id = id; + this.html = html; + this.text = Jsoup.parseBodyFragment(html, "").text(); + this.type = MessageType.getTypeByClass(getClass().getSimpleName()); + } + + @Override + public String getId() { + return id; + } + + @Override + public MessageType getType() { + return type; + } + + protected void setType(MessageType type) { + this.type = type; + } + + public String getHtml() { + return html; + } + + public void setHtml(String html) { + this.html = html; + this.text = Jsoup.parseBodyFragment(html, "").text(); + } + + public String getText() { + return text; + } + + public void empty() { + setHtml(""); + } + + public boolean isEmpty() { + return html.isEmpty(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " {" + + "id='" + id + '\'' + + ", type=" + type + + ", html='" + html + '\'' + + ", text='" + text + '\'' + + '}'; + } +} diff --git a/src/main/java/fr/delthas/skype/message/ContactMessage.java b/src/main/java/fr/delthas/skype/message/ContactMessage.java new file mode 100644 index 0000000..a97abd6 --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/ContactMessage.java @@ -0,0 +1,12 @@ +package fr.delthas.skype.message; + +/** + * Message with contact + */ +public class ContactMessage extends AbstractMessage { + + public ContactMessage(String id, String html) { + super(id, html); + } + +} diff --git a/src/main/java/fr/delthas/skype/message/FileMessage.java b/src/main/java/fr/delthas/skype/message/FileMessage.java new file mode 100644 index 0000000..e07c5ae --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/FileMessage.java @@ -0,0 +1,10 @@ +package fr.delthas.skype.message; + +/** + * Message with file + */ +public class FileMessage extends AbstractMessage { + public FileMessage(String id, String html) { + super(id, html); + } +} diff --git a/src/main/java/fr/delthas/skype/message/Message.java b/src/main/java/fr/delthas/skype/message/Message.java new file mode 100644 index 0000000..2b339b1 --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/Message.java @@ -0,0 +1,16 @@ +package fr.delthas.skype.message; + +/** + * Interface for messages` + */ +public interface Message { + + public static String X_URL_THUMBNAIL = "/URIObject/@url_thumbnail"; + public static String X_URL = "/URIObject/@uri"; + public static String X_TYPE = "/URIObject/@type"; + public static String X_ORIGINAL_NAME = "/URIObject/OriginalName/@v"; + + String getId(); + + MessageType getType(); +} diff --git a/src/main/java/fr/delthas/skype/message/MessageType.java b/src/main/java/fr/delthas/skype/message/MessageType.java new file mode 100644 index 0000000..7480d83 --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/MessageType.java @@ -0,0 +1,65 @@ +package fr.delthas.skype.message; + +import static fr.delthas.skype.Constants.*; + +/** + * Skype message types + */ +public enum MessageType { + TEXT("Text", HEADER_RICH_TEXT, TextMessage.class), + PICTURE("Picture", HEADER_PICTURE, PictureMessage.class), + FILE("File", HEADER_FILE, FileMessage.class), + VIDEO("Video", HEADER_VIDEO, VideoMessage.class), + CONTACT("Contact", HEADER_CONTACT, ContactMessage.class), + MOJI("Moji", HEADER_MOJI, MojiMessage.class), + UNKNOWN("Unknown", "Unknown", UnknownMessage.class); + + private String name; + private String headerType; + private Class clazz; + + MessageType(String name, String headerType, Class clazz) { + this.name = name; + this.headerType = headerType; + this.clazz = clazz; + } + + public String getHeaderType() { + return headerType; + } + + public String getName() { + return name; + } + + public Class getClazz() { + return clazz; + } + + public static MessageType getTypeByClass(String className) { + for (MessageType type : MessageType.values()) { + if (type.getClazz().getSimpleName().equals(className)) { + return type; + } + } + return UNKNOWN; + } + + public static MessageType getTypeByClass(Message message) { + if (message != null) { + String className = message.getClass().getSimpleName(); + return getTypeByClass(className); + } + return UNKNOWN; + } + + public static MessageType getTypeByHeaderType(String headerType) { + if (HEADER_TEXT.equals(headerType)) return TEXT; + for (MessageType type : MessageType.values()) { + if (type.getHeaderType().equals(headerType)) { + return type; + } + } + return UNKNOWN; + } +} diff --git a/src/main/java/fr/delthas/skype/message/MojiMessage.java b/src/main/java/fr/delthas/skype/message/MojiMessage.java new file mode 100644 index 0000000..4893530 --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/MojiMessage.java @@ -0,0 +1,12 @@ +package fr.delthas.skype.message; + +/** + * Message with moji + */ +public class MojiMessage extends AbstractMessage { + + public MojiMessage(String id, String html) { + super(id, html); + } + +} diff --git a/src/main/java/fr/delthas/skype/message/PictureMessage.java b/src/main/java/fr/delthas/skype/message/PictureMessage.java new file mode 100644 index 0000000..44a08ff --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/PictureMessage.java @@ -0,0 +1,11 @@ +package fr.delthas.skype.message; + +/** + * Message with picture + */ +public class PictureMessage extends AbstractMessage { + + public PictureMessage(String id, String html) { + super(id, html); + } +} diff --git a/src/main/java/fr/delthas/skype/message/TextMessage.java b/src/main/java/fr/delthas/skype/message/TextMessage.java new file mode 100644 index 0000000..1da6db0 --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/TextMessage.java @@ -0,0 +1,43 @@ +package fr.delthas.skype.message; + +/** + * Simple text message + */ +public class TextMessage extends AbstractMessage { + private boolean isMe; + private boolean hasQuotes; + + public TextMessage(String id, String html) { + super(id, html); + } + + public TextMessage(String id, String html, boolean isMe) { + this(id, html); + this.isMe = isMe; + } + + public TextMessage(String id, String html, boolean isMe, boolean hasQuotes) { + this(id, html, isMe); + this.hasQuotes = hasQuotes; + } + + public boolean hasQuotes() { + return hasQuotes; + } + + public void setHasQuotes(boolean hasQuotes) { + this.hasQuotes = hasQuotes; + } + + public boolean isMe() { + return isMe; + } + + @Override + public String toString() { + return "TextMessage{" + + "isMe=" + isMe + + ", hasQuotes=" + hasQuotes + + "} " + super.toString(); + } +} diff --git a/src/main/java/fr/delthas/skype/message/UnknownMessage.java b/src/main/java/fr/delthas/skype/message/UnknownMessage.java new file mode 100644 index 0000000..89cc9cc --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/UnknownMessage.java @@ -0,0 +1,11 @@ +package fr.delthas.skype.message; + +/** + * Message with unknown type + */ +public class UnknownMessage extends AbstractMessage { + + public UnknownMessage(String id, String html) { + super(id, html); + } +} diff --git a/src/main/java/fr/delthas/skype/message/VideoMessage.java b/src/main/java/fr/delthas/skype/message/VideoMessage.java new file mode 100644 index 0000000..5b88d76 --- /dev/null +++ b/src/main/java/fr/delthas/skype/message/VideoMessage.java @@ -0,0 +1,11 @@ +package fr.delthas.skype.message; + +/** + * Message with video + */ +public class VideoMessage extends AbstractMessage { + + public VideoMessage(String id, String html) { + super(id, html); + } +} diff --git a/src/test/java/fr/delthas/skype/TestListeners.java b/src/test/java/fr/delthas/skype/TestListeners.java new file mode 100644 index 0000000..7617b45 --- /dev/null +++ b/src/test/java/fr/delthas/skype/TestListeners.java @@ -0,0 +1,68 @@ +package fr.delthas.skype; + +import fr.delthas.skype.message.Message; +import fr.delthas.skype.message.TextMessage; +import org.junit.Assert; + +import java.util.logging.Logger; + +@SuppressWarnings({"javadoc", "static-method"}) +public class TestListeners { + + public static Logger logger = Logger.getLogger("TestConnect"); + + public void testConnect() { + Skype skype = new Skype("username", "password"); + try { + logger.info("Starting..."); + skype.connect(); + logger.info("Started"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + skype.setErrorListener(e -> { + e.printStackTrace(); + Assert.fail(e.getMessage()); + }); + skype.addGroupMessageListener(new GroupMessageListener() { + @Override + public void messageReceived(Group group, User sender, Message message) { + logger.info("Received " + message); + } + + @Override + public void messageEdited(Group group, User sender, Message message) { + logger.info("Edited " + message); + } + + @Override + public void messageRemoved(Group group, User sender, Message message) { + logger.info("Removed " + message); + } + }); + + skype.addUserMessageListener(new UserMessageListener() { + @Override + public void messageReceived(User sender, Message message) { + logger.info("Received " + message); + } + + @Override + public void messageEdited(User sender, Message message) { + logger.info("Edited " + message); + } + + @Override + public void messageRemoved(User sender, Message message) { + logger.info("Removed " + message); + } + }); + + } + + public static void main(String... strings) { + new TestListeners().testConnect(); + } + +} diff --git a/src/test/java/fr/delthas/skype/message/TestMessage.java b/src/test/java/fr/delthas/skype/message/TestMessage.java new file mode 100644 index 0000000..c135de5 --- /dev/null +++ b/src/test/java/fr/delthas/skype/message/TestMessage.java @@ -0,0 +1,74 @@ +package fr.delthas.skype.message; + +import fr.delthas.skype.ParseException; +import org.apache.commons.io.IOUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.Test; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Class of fr.delthas.skype + * + * @author aivanov + */ +public class TestMessage { + + private DocumentBuilder documentBuilder; + + + @Test + public void test() throws Exception { + documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + + String xml = getXml("fr/delthas/skype/message/picture.xml"); + System.out.println(getXMLField(xml, "URIObject")); + } + + private static Document getPlaintext(String string) { + return Jsoup.parseBodyFragment(string); + } + + public static String getXml(String path) throws IOException { + return IOUtils.toString(ClassLoader.getSystemResourceAsStream(path), "UTF-8"); + } + + + private org.w3c.dom.Document getDocument(String XML) throws ParseException { + try { + return documentBuilder.parse(new InputSource(new StringReader(XML))); + } catch (IOException | SAXException e) { + throw new ParseException(e); + } + } + + private List getXMLFields(String XML, String fieldName) throws ParseException { + NodeList nodes = getDocument(XML).getElementsByTagName(fieldName); + List fields = new ArrayList<>(nodes.getLength()); + for (int i = 0; i < nodes.getLength(); i++) { + fields.add(nodes.item(i).getTextContent()); + } + return fields; + } + + private String getXMLField(String XML, String fieldName) throws ParseException { + List fields = getXMLFields(XML, fieldName); + if (fields.size() > 1) { + throw new ParseException(); + } + if (fields.size() == 0) { + return null; + } + return fields.get(0); + } + +} diff --git a/src/test/resources/fr/delthas/skype/message/contact.xml b/src/test/resources/fr/delthas/skype/message/contact.xml new file mode 100644 index 0000000..85d61bf --- /dev/null +++ b/src/test/resources/fr/delthas/skype/message/contact.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/test/resources/fr/delthas/skype/message/file.xml b/src/test/resources/fr/delthas/skype/message/file.xml new file mode 100644 index 0000000..3c574b2 --- /dev/null +++ b/src/test/resources/fr/delthas/skype/message/file.xml @@ -0,0 +1,12 @@ + + + To view this shared file, go to: + hosts + hosts + + https://login.skype.com/login/sso?go=webclient.xmm&docid=0-weu-d1-39307d971c65d7d697876272123304e4 + + + + diff --git a/src/test/resources/fr/delthas/skype/message/moji.xml b/src/test/resources/fr/delthas/skype/message/moji.xml new file mode 100644 index 0000000..65a2ec6 --- /dev/null +++ b/src/test/resources/fr/delthas/skype/message/moji.xml @@ -0,0 +1,11 @@ + + + + Moji + + https://login.skype.com/login/sso?go=xmmfallback&2b3bd2ed622e443c8ff98c5df55b4e31 + + + + diff --git a/src/test/resources/fr/delthas/skype/message/picture.xml b/src/test/resources/fr/delthas/skype/message/picture.xml new file mode 100644 index 0000000..ee1559f --- /dev/null +++ b/src/test/resources/fr/delthas/skype/message/picture.xml @@ -0,0 +1,10 @@ + +To view this shared + photo, go to: + + https://login.skype.com/login/sso?go=xmmfallback?pic=0-weu-d1-347eba640ba729af0d46f4acb3deffce + + + + diff --git a/src/test/resources/fr/delthas/skype/message/ss.xml b/src/test/resources/fr/delthas/skype/message/ss.xml new file mode 100644 index 0000000..e86667d --- /dev/null +++ b/src/test/resources/fr/delthas/skype/message/ss.xml @@ -0,0 +1 @@ +(goldmedal) \ No newline at end of file diff --git a/src/test/resources/fr/delthas/skype/message/video.xml b/src/test/resources/fr/delthas/skype/message/video.xml new file mode 100644 index 0000000..b01f189 --- /dev/null +++ b/src/test/resources/fr/delthas/skype/message/video.xml @@ -0,0 +1,10 @@ +To view this video +message, go to: +Video Message +Video Message + + https://login.skype.com/login/sso?go=xmmfallback&vim=0-neu-d1-d22932af94c973a48a944f983da278cd + + + \ No newline at end of file