package com.juick.components.s2s; import com.juick.User; import com.juick.components.XMPPServer; import com.juick.formatters.PlainTextFormatter; import org.apache.commons.lang3.StringUtils; import org.ocpsoft.prettytime.PrettyTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rocks.xmpp.addr.Jid; import rocks.xmpp.core.stanza.model.Message; import rocks.xmpp.core.stanza.model.Presence; import rocks.xmpp.core.stanza.model.Stanza; import rocks.xmpp.core.stanza.model.StanzaError; import rocks.xmpp.core.stanza.model.client.ClientMessage; import rocks.xmpp.core.stanza.model.client.ClientPresence; import rocks.xmpp.core.stanza.model.errors.Condition; import javax.inject.Inject; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * * @author ugnich */ public class JuickBot implements StanzaListener, AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(JuickBot.class); XMPPServer xmpp; PrettyTime pt; @Inject public JuickBot(XMPPServer xmpp) { this.xmpp = xmpp; xmpp.addStanzaListener(this); if (!xmpp.messagesService.isReadonly()) { broadcastPresence(null); } pt = new PrettyTime(new Locale("ru")); } private static final String HELPTEXT = "@username text - Send private message\n" + "*tagname Blah-blah-blah - Post a message with tag 'tagname'\n" + "#1234 Blah-blah-blah - Answer to message #1234\n" + "#1234/5 Blah - Answer to reply #1234/5\n" + "! #1234 - Recommend post\n" + "\n" + "# - Show last messages from your feed (## - second page, ...)\n" + "@ - Show recomendations and popular personal blogs\n" + "* - Show your tags\n" + "#1234 - Show message\n" + "#1234+ - Show message with replies\n" + "@username - Show user's info\n" + "@username+ - Show user's info and last 10 messages\n" + "@username *tag - User's messages with this tag\n" + "*tag - Show last 10 messages with this tag\n" + "? blah - Search posts for 'blah'\n" + "? @username blah - Searching among user\'s posts for 'blah'\n" + "D #123 - Delete message\n" + "D #123/45 - Delete reply\n" + "DL - Delete last message/reply\n" + "S - Show your subscriptions\n" + "S #123 - Subscribe to message replies\n" + "S @username - Subscribe to user's blog\n" + "U #123 - Unsubscribe from comments\n" + "U @username - Unsubscribe from user's blog\n" + "BL - Show your blacklist\n" + "BL @username - Add/delete user to/from your blacklist\n" + "BL *tag - Add/delete tag to/from your blacklist\n" + "ON / OFF - Enable/disable subscriptions delivery\n" + "PING - Pong\n" + "\n" + "Read more: http://juick.com/help/"; public boolean incomingPresence(Presence p) { final String username = p.getTo().getLocal(); final boolean toJuick = username.equals("juick"); if (p.getType() == null) { Presence reply = new Presence(); reply.setFrom(p.getTo().asBareJid()); reply.setTo(p.getFrom().asBareJid()); reply.setType(Presence.Type.UNSUBSCRIBE); xmpp.sendOut(ClientPresence.from(reply)); return true; } else if (p.getType().equals(Presence.Type.PROBE)) { int uid_to = 0; if (!toJuick) { uid_to = xmpp.userService.getUIDbyName(username); } if (toJuick || uid_to > 0) { Presence reply = new Presence(); reply.setFrom(p.getTo().withResource("Juick")); reply.setTo(p.getFrom()); reply.setPriority((byte)10); xmpp.sendOut(ClientPresence.from(reply)); } else { Presence reply = new Presence(); reply.setFrom(p.getTo()); reply.setTo(p.getFrom()); reply.setType(Presence.Type.ERROR); reply.setId(p.getId()); reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); xmpp.sendOut(ClientPresence.from(reply)); return true; } return true; } else if (p.getType().equals(Presence.Type.SUBSCRIBE)) { boolean canSubscribe = false; if (toJuick) { canSubscribe = true; } else { int uid_to = xmpp.userService.getUIDbyName(username); if (uid_to > 0) { xmpp.pmQueriesService.addPMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); canSubscribe = true; } } if (canSubscribe) { Presence reply = new Presence(); reply.setFrom(p.getTo()); reply.setTo(p.getFrom()); reply.setType(Presence.Type.SUBSCRIBED); xmpp.sendOut(ClientPresence.from(reply)); reply.setFrom(reply.getFrom().withResource("Juick")); reply.setPriority((byte) 10); reply.setType(null); xmpp.sendOut(ClientPresence.from(reply)); return true; } else { Presence reply = new Presence(); reply.setFrom(p.getTo()); reply.setTo(p.getFrom()); reply.setType(Presence.Type.ERROR); reply.setId(p.getId()); reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); xmpp.sendOut(ClientPresence.from(reply)); return true; } } else if (p.getType().equals(Presence.Type.UNSUBSCRIBE)) { if (!toJuick) { int uid_to = xmpp.userService.getUIDbyName(username); if (uid_to > 0) { xmpp.pmQueriesService.removePMinRoster(uid_to, p.getFrom().asBareJid().toEscapedString()); } } Presence reply = new Presence(); reply.setFrom(p.getTo()); reply.setTo(p.getFrom()); reply.setType(Presence.Type.UNSUBSCRIBED); xmpp.sendOut(ClientPresence.from(reply)); } return false; } public boolean incomingMessage(Message msg) { if (StringUtils.isBlank(msg.getBody()) || (msg.getType() != null && msg.getType().equals(Message.Type.ERROR))) { return false; } String username = msg.getTo().getLocal(); User user_from; String signuphash = StringUtils.EMPTY; user_from = xmpp.userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); if (user_from == null) { signuphash = xmpp.userService.getSignUpHashByJID(msg.getFrom().asBareJid().toEscapedString()); } if (user_from == null) { Message reply = new Message(); reply.setFrom(msg.getTo()); reply.setTo(msg.getFrom()); reply.setType(Message.Type.CHAT); if (xmpp.messagesService.isReadonly()) { reply.setBody("Регистрация временно недоступна\n\nRegistration temporarily unavailable"); } else { if (username.equals("juick")) { reply.setBody("Для того, чтобы начать пользоваться сервисом, пожалуйста пройдите быструю регистрацию: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nTo start using Juick, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); } else { reply.setBody("Внимание, системное сообщение!\nВаш JabberID не обнаружен в списке доверенных. Для того, чтобы отправить сообщение пользователю " + username + "@juick.com, пожалуйста зарегистрируйте свой JabberID в системе: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nЕсли у вас уже есть учетная запись на Juick, вы сможете присоединить этот JabberID к ней.\n\nWarning, system message!\nYour JabberID is not found in our server's white list. To send a message to " + username + "@juick.com, please sign up: http://juick.com/signup?type=xmpp&hash=" + signuphash + "\nIf you already have an account on Juick, you will be proposed to attach this JabberID to your existing account."); } } xmpp.sendOut(ClientMessage.from(reply)); return true; } if (username.equals("juick")) { return incomingMessageJuick(user_from, msg); } int uid_to = xmpp.userService.getUIDbyName(username); if (uid_to == 0) { Message reply = new Message(); reply.setFrom(msg.getTo()); reply.setTo(msg.getFrom()); reply.setType(Message.Type.ERROR); reply.setId(msg.getId()); reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.ITEM_NOT_FOUND)); xmpp.sendOut(ClientMessage.from(reply)); return true; } boolean success = false; if (!xmpp.userService.isInBLAny(uid_to, user_from.getUid()) && !xmpp.messagesService.isReadonly()) { success = xmpp.pmQueriesService.createPM(user_from.getUid(), uid_to, msg.getBody()); } if (success) { Message m = new Message(); m.setFrom(Jid.of("juick@juick.com")); m.setTo(Jid.of(Integer.toString(uid_to), "push.juick.com", null)); com.juick.Message jmsg = new com.juick.Message(); jmsg.setUser(user_from); jmsg.setText(msg.getBody()); m.addExtension(jmsg); xmpp.getRouter().sendStanza(m); m.setTo(Jid.of(Integer.toString(uid_to), "ws.juick.com", null)); xmpp.getRouter().sendStanza(m); List jids; boolean inroster = false; jids = xmpp.userService.getJIDsbyUID(uid_to); for (String jid : jids) { Message mm = new Message(); mm.setTo(Jid.of(jid)); mm.setType(Message.Type.CHAT); inroster = xmpp.pmQueriesService.havePMinRoster(user_from.getUid(), jid); if (inroster) { mm.setFrom(Jid.of(jmsg.getUser().getName(), "juick.com", "Juick")); mm.setBody(msg.getBody()); } else { mm.setFrom(Jid.of("juick", "juick.com", "Juick")); mm.setBody("Private message from @" + jmsg.getUser().getName() + ":\n" + msg.getBody()); } xmpp.sendOut(ClientMessage.from(mm)); } } else { Message reply = new Message(); reply.setFrom(msg.getTo()); reply.setTo(msg.getFrom()); reply.setType(Message.Type.ERROR); reply.setId(msg.getId()); reply.setError(new StanzaError(StanzaError.Type.CANCEL, Condition.NOT_ALLOWED)); xmpp.sendOut(ClientMessage.from(reply)); } return false; } private static Pattern regexPM = Pattern.compile("^\\@(\\S+)\\s+([\\s\\S]+)$"); public boolean incomingMessageJuick(User user_from, Message msg) { String command = msg.getBody().trim(); int commandlen = command.length(); // COMPATIBILITY if (commandlen > 7 && command.substring(0, 3).equalsIgnoreCase("PM ")) { command = command.substring(3).trim(); commandlen = command.length(); } if (commandlen == 4) { if (command.equalsIgnoreCase("PING")) { commandPing(msg); return true; } else if (command.equalsIgnoreCase("HELP")) { commandHelp(msg); return true; } } else if (commandlen == 5 && command.equalsIgnoreCase("LOGIN")) { commandLogin(msg, user_from); return true; } else if (command.charAt(0) == '@') { Matcher matchPM = regexPM.matcher(command); if (matchPM.find()) { String user_to = matchPM.group(1); String msgtxt = matchPM.group(2); commandPM(msg, user_from, user_to, msgtxt); return true; } } else if (commandlen == 2 && command.equalsIgnoreCase("BL")) { commandBLShow(msg, user_from); return true; } else if (commandlen == 2 && command.equalsIgnoreCase("#+")) { return commandLast(msg.getFrom()); } else if (command.equalsIgnoreCase("@")) { return commandUsers(msg.getFrom()); } else if (xmpp.messagesService.isReadonly()) { return commandMaintenance(msg.getFrom()); } return false; } private void commandPing(Message m) { Presence p = new Presence(m.getFrom()); p.setFrom(xmpp.getJid()); p.setPriority((byte) 10); xmpp.sendOut(ClientPresence.from(p)); sendReply(m.getFrom(), "PONG"); } private void commandHelp(Message m) { sendReply(m.getFrom(), HELPTEXT); } private void commandLogin(Message m, User user_from) { sendReply(m.getFrom(), "http://juick.com/login?" + xmpp.userService.getHashByUID(user_from.getUid())); } private void commandPM(Message m, User user_from, String user_to, String body) { int ret = 0; int uid_to = 0; List jids_to = null; boolean haveInRoster = false; if (user_to.indexOf('@') > 0) { uid_to = xmpp.userService.getUIDbyJID(user_to); } else { uid_to = xmpp.userService.getUIDbyName(user_to); } if (uid_to > 0) { if (!xmpp.userService.isInBLAny(uid_to, user_from.getUid())) { if (xmpp.pmQueriesService.createPM(user_from.getUid(), uid_to, body)) { jids_to = xmpp.userService.getJIDsbyUID(uid_to); ret = 200; } else { ret = 500; } } else { ret = 403; } } else { ret = 404; } if (ret == 200) { Message msg = new Message(); msg.setFrom(Jid.of("juick", "juick.com", null)); msg.setTo(Jid.of(Integer.toString(uid_to), "push.juick.com", null)); com.juick.Message jmsg = new com.juick.Message(); jmsg.setUser(user_from); jmsg.setText(body); msg.addExtension(jmsg); xmpp.getRouter().sendStanza(msg); msg.setTo(Jid.of(Integer.toString(uid_to), "ws.juick.com", null)); xmpp.getRouter().sendStanza(msg); for (String jid : jids_to) { Message mm = new Message(); mm.setTo(Jid.of(jid)); mm.setType(Message.Type.CHAT); haveInRoster = xmpp.pmQueriesService.havePMinRoster(user_from.getUid(), jid); if (haveInRoster) { mm.setFrom(Jid.of(user_from.getName(), "juick.com", "Juick")); mm.setBody(body); } else { mm.setFrom(Jid.of("juick", "juick.com", "Juick")); mm.setBody("Private message from @" + user_from.getName() + ":\n" + body); } xmpp.sendOut(ClientMessage.from(mm)); } } Message reply = new Message(); reply.setFrom(m.getTo()); reply.setTo(m.getFrom()); if (ret == 200) { reply.setType(m.getType()); reply.setBody("Private message sent"); } else { reply.setType(Message.Type.ERROR); reply.setBody("Error " + ret); } xmpp.sendOut(ClientMessage.from(reply)); } private void commandBLShow(Message m, User user_from) { List blusers = xmpp.userService.getUserBLUsers(user_from.getUid()); List bltags = xmpp.tagService.getUserBLTags(user_from.getUid()); String txt = StringUtils.EMPTY; if (bltags.size() > 0) { for (String bltag : bltags) { txt += "*" + bltag + "\n"; } if (blusers.size() > 0) { txt += "\n"; } } if (blusers.size() > 0) { for (User bluser : blusers) { txt += "@" + bluser.getName() + "\n"; } } if (txt.isEmpty()) { txt = "You don't have any users or tags in your blacklist."; } sendReply(m.getFrom(), txt); } boolean commandLast(Jid jidFrom) { User user = xmpp.userService.getUserByJID(jidFrom.asBareJid().toEscapedString()); sendReply(jidFrom, "Last messages:\n" + printMessages(xmpp.messagesService.getAll(user.getUid(), 0), true)); return true; } boolean commandUsers(Jid jidFrom) { StringBuilder msg = new StringBuilder(); msg.append("Recommended blogs"); User currentUser = xmpp.userService.getUserByJID(jidFrom.asBareJid().toEscapedString()); List recommendedUsers = xmpp.showQueriesService.getRecommendedUsers(currentUser); if (recommendedUsers.size() > 0) { for (String user : recommendedUsers) { msg.append("\n@").append(user); } } else { msg.append("\nNo recommendations now. Subscribe to more blogs. ;)"); } msg.append("\n\nTop 10 personal blogs:"); List topUsers = xmpp.showQueriesService.getTopUsers(); if (topUsers.size() > 0) { for (String user : topUsers) { msg.append("\n@").append(user); } } else { msg.append("\nNo top users. Empty DB? ;)"); } sendReply(jidFrom, msg.toString()); return true; } private boolean commandMaintenance(Jid jidFrom) { sendReply(jidFrom, "Комментирование временно недоступно"); return true; } void sendReply(Jid jidTo, String txt) { Message reply = new Message(); reply.setFrom(xmpp.getJid()); reply.setTo(jidTo); reply.setType(Message.Type.CHAT); reply.setBody(txt); xmpp.sendOut(ClientMessage.from(reply)); } @Override public void stanzaReceived(Stanza xmlValue) { if (xmlValue instanceof Presence) { Presence p = (Presence) xmlValue; if (p.getType() == null || !p.getType().equals(Presence.Type.ERROR)) { incomingPresence(p); } } else if (xmlValue instanceof Message) { Message msg = (Message) xmlValue; if (!incomingMessage(msg)) { xmpp.getRouter().sendStanza(msg); } } } String printMessages(List mids, boolean crop) { return xmpp.messagesService.getMessages(mids).stream() .sorted(Collections.reverseOrder()) .map(PlainTextFormatter::formatPostSummary).collect(Collectors.joining("\n\n")); } void broadcastPresence(Presence.Type type) { Presence presence = new Presence(); presence.setFrom(xmpp.getJid()); if (type != null) { presence.setType(type); } xmpp.userService.getAllJIDs().forEach(j -> { try { presence.setTo(Jid.of(j)); xmpp.sendOut(ClientPresence.from(presence)); } catch (IllegalArgumentException ex) { logger.warn("Invalid jid: {}", j, ex); } }); } @Override public void close() throws Exception { if (!xmpp.messagesService.isReadonly()) { broadcastPresence(Presence.Type.UNAVAILABLE); } } }