From 51bfc341be1975b7a11e0b3a59cfbb4710e78446 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 4 Oct 2017 15:31:44 +0300 Subject: juick-xmpp-wip: router component --- .../main/java/com/juick/components/JuickBot.java | 546 +++++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 juick-xmpp/src/main/java/com/juick/components/JuickBot.java (limited to 'juick-xmpp/src/main/java/com/juick/components/JuickBot.java') diff --git a/juick-xmpp/src/main/java/com/juick/components/JuickBot.java b/juick-xmpp/src/main/java/com/juick/components/JuickBot.java new file mode 100644 index 00000000..4f838344 --- /dev/null +++ b/juick-xmpp/src/main/java/com/juick/components/JuickBot.java @@ -0,0 +1,546 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.components; + +import com.juick.User; +import com.juick.components.s2s.StanzaListener; +import com.juick.formatters.PlainTextFormatter; +import com.juick.service.*; +import org.apache.commons.lang3.StringUtils; +import org.ocpsoft.prettytime.PrettyTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import rocks.xmpp.addr.Jid; +import rocks.xmpp.core.stanza.model.*; +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.annotation.PostConstruct; +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 + */ +@Component +public class JuickBot implements StanzaListener, AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(JuickBot.class); + + @Inject + private XMPPServer xmpp; + @Inject + private XMPPConnection router; + @Value("${xmppbot_jid}") + private String xmppbotJidStr; + + private Jid jid; + + private PrettyTime pt; + + @Inject + public MessagesService messagesService; + @Inject + public UserService userService; + @Inject + public TagService tagService; + @Inject + public PMQueriesService pmQueriesService; + @Inject + public ShowQueriesService showQueriesService; + + @PostConstruct + public void init() { + xmpp.addStanzaListener(this); + jid = Jid.of(xmppbotJidStr); + broadcastPresence(null); + pt = new PrettyTime(new Locale("ru")); + } + + public Jid getJid() { + return jid; + } + + 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(jid.getLocal()); + + 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 = userService.getUIDbyName(username); + } + + if (toJuick || uid_to > 0) { + Presence reply = new Presence(); + reply.setFrom(p.getTo().withResource(jid.getResource())); + 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 = userService.getUIDbyName(username); + if (uid_to > 0) { + 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(jid.getResource())); + 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 = userService.getUIDbyName(username); + if (uid_to > 0) { + 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 = userService.getUserByJID(msg.getFrom().asBareJid().toEscapedString()); + if (user_from == null) { + signuphash = 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 (username.equals(jid.getLocal())) { + 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(jid.getLocal())) { + return incomingMessageJuick(user_from, msg); + } + + int uid_to = 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 (!userService.isInBLAny(uid_to, user_from.getUid())) { + success = pmQueriesService.createPM(user_from.getUid(), uid_to, msg.getBody()); + } + + if (success) { + Message m = new Message(); + m.setFrom(jid.asBareJid()); + 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); + router.sendStanza(m); + + m.setTo(Jid.of(Integer.toString(uid_to), "ws.juick.com", null)); + router.sendStanza(m); + + List jids; + boolean inroster = false; + jids = userService.getJIDsbyUID(uid_to); + for (String userJid : jids) { + Message mm = new Message(); + mm.setTo(Jid.of(userJid)); + mm.setType(Message.Type.CHAT); + inroster = pmQueriesService.havePMinRoster(user_from.getUid(), userJid); + if (inroster) { + mm.setFrom(Jid.of(jmsg.getUser().getName(), "juick.com", "Juick")); + mm.setBody(msg.getBody()); + } else { + mm.setFrom(jid); + 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()); + } + + return false; + } + + private void commandPing(Message m) { + Presence p = new Presence(m.getFrom()); + p.setFrom(jid); + 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?hash=" + 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 = userService.getUIDbyJID(user_to); + } else { + uid_to = userService.getUIDbyName(user_to); + } + + if (uid_to > 0) { + if (!userService.isInBLAny(uid_to, user_from.getUid())) { + if (pmQueriesService.createPM(user_from.getUid(), uid_to, body)) { + jids_to = 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.asBareJid()); + 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); + router.sendStanza(msg); + + msg.setTo(Jid.of(Integer.toString(uid_to), "ws.juick.com", null)); + router.sendStanza(msg); + + for (String userJid : jids_to) { + Message mm = new Message(); + mm.setTo(Jid.of(userJid)); + mm.setType(Message.Type.CHAT); + haveInRoster = pmQueriesService.havePMinRoster(user_from.getUid(), userJid); + if (haveInRoster) { + mm.setFrom(Jid.of(user_from.getName(), "juick.com", "Juick")); + mm.setBody(body); + } else { + mm.setFrom(jid); + 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 = userService.getUserBLUsers(user_from.getUid()); + List bltags = 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 = userService.getUserByJID(jidFrom.asBareJid().toEscapedString()); + sendReply(jidFrom, "Last messages:\n" + + printMessages(messagesService.getAll(user.getUid(), 0), true)); + return true; + } + + boolean commandUsers(Jid jidFrom) { + StringBuilder msg = new StringBuilder(); + msg.append("Recommended blogs"); + User currentUser = userService.getUserByJID(jidFrom.asBareJid().toEscapedString()); + List recommendedUsers = 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 = 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; + } + + void sendReply(Jid jidTo, String txt) { + Message reply = new Message(); + reply.setFrom(jid); + reply.setTo(jidTo); + reply.setType(Message.Type.CHAT); + reply.setBody(txt); + xmpp.sendOut(ClientMessage.from(reply)); + } + + void sendNotification(Stanza stanza) { + xmpp.sendOut(stanza); + } + + @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)) { + router.sendStanza(msg); + } + } else if (xmlValue instanceof IQ) { + IQ iq = (IQ) xmlValue; + router.sendStanza(iq); + } + } + + String printMessages(List mids, boolean crop) { + return 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(jid); + if (type != null) { + presence.setType(type); + } + userService.getActiveJIDs().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 { + broadcastPresence(Presence.Type.UNAVAILABLE); + } +} -- cgit v1.2.3