aboutsummaryrefslogtreecommitdiff
path: root/juick-xmpp/src/main/java/com/juick/components/JuickBot.java
diff options
context:
space:
mode:
Diffstat (limited to 'juick-xmpp/src/main/java/com/juick/components/JuickBot.java')
-rw-r--r--juick-xmpp/src/main/java/com/juick/components/JuickBot.java546
1 files changed, 546 insertions, 0 deletions
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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String> 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<String> 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<User> blusers = userService.getUserBLUsers(user_from.getUid());
+ List<String> 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<String> 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<String> 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<Integer> 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);
+ }
+}