From a6b07685219dee3b48a816e562fdbe93f2419779 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 29 Jun 2016 15:15:15 +0300 Subject: add juick bot protocol WIP --- .../com/juick/server/protocol/JuickProtocol.java | 375 +++++++++++++++++++++ .../com/juick/server/protocol/ProtocolReply.java | 23 ++ .../server/protocol/annotation/UserCommand.java | 31 ++ 3 files changed, 429 insertions(+) create mode 100644 src/main/java/com/juick/server/protocol/JuickProtocol.java create mode 100644 src/main/java/com/juick/server/protocol/ProtocolReply.java create mode 100644 src/main/java/com/juick/server/protocol/annotation/UserCommand.java (limited to 'src') diff --git a/src/main/java/com/juick/server/protocol/JuickProtocol.java b/src/main/java/com/juick/server/protocol/JuickProtocol.java new file mode 100644 index 00000000..923c404e --- /dev/null +++ b/src/main/java/com/juick/server/protocol/JuickProtocol.java @@ -0,0 +1,375 @@ +package com.juick.server.protocol; + +import com.juick.*; +import com.juick.json.MessageSerializer; +import com.juick.server.*; +import com.juick.server.protocol.annotation.UserCommand; +import com.juick.xmpp.extensions.JuickMessage; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Created by oxpa on 22.03.16. + */ + +public class JuickProtocol { + MessageSerializer json = new MessageSerializer(); + JdbcTemplate sql; + String baseUri; + + public JuickProtocol(JdbcTemplate sql, String baseUri) { + this.sql = sql; + this.baseUri = baseUri; + } + + /** + * find command by pattern and invoke + * @param user who send command + * @param userInput given by user + * @return command result + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws NoSuchMethodException + */ + public ProtocolReply getReply(User user, String userInput) throws InvocationTargetException, + IllegalAccessException, NoSuchMethodException { + Optional cmd = Arrays.asList(getClass().getDeclaredMethods()).stream() + .filter(m -> m.isAnnotationPresent(UserCommand.class)) + .filter(m -> Pattern.compile(m.getAnnotation(UserCommand.class).pattern(), + m.getAnnotation(UserCommand.class).patternFlags()).matcher(userInput).matches()) + .findFirst(); + if (!cmd.isPresent()) { + // default command - post as new message + return postMessage(user, userInput); + } else { + Matcher matcher = Pattern.compile(cmd.get().getAnnotation(UserCommand.class).pattern(), + cmd.get().getAnnotation(UserCommand.class).patternFlags()).matcher(userInput); + List groups = new ArrayList<>(); + while (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + groups.add(matcher.group(i)); + } + } + return (ProtocolReply) getClass().getMethod(cmd.get().getName(), User.class, String[].class) + .invoke(this, user, groups.toArray(new String[groups.size()])); + } + } + + public ProtocolReply postMessage(User user, String input) { + List tags = TagQueries.fromString(sql, input, false); + String body = input.substring(TagQueries.toString(tags).length()); + int mid = MessagesQueries.createMessage(sql, user.getUID(), body, null, tags); + //app.events().publishEvent(new JuickMessageEvent(app.messages().getMessage(mid))); + return new ProtocolReply("New message posted.\n#" + mid + " " + baseUri + mid, + Optional.of(json.serializeList(Collections.singletonList(MessagesQueries.getMessage(sql, mid))))); + } + + @UserCommand(pattern = "^#(\\++)$", help = "#+ - Show last Juick messages (#++ - second page, ...)") + public ProtocolReply commandLast(User user, String... arguments) { + // number of + is the page count + int page = arguments[0].length(); + List mids = MessagesQueries.getAll(sql, user.getUID(), page); + List messages = MessagesQueries.getMessages(sql, mids); + // TODO: message toString + return new ProtocolReply("Last messages: \n" + String.join("\n", messages.stream().map(Object::toString) + .collect(Collectors.toList())), Optional.of(json.serializeList(messages))); + } + + @UserCommand(pattern = "^\\s*bl\\s*$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "BL - Show your blacklist") + public ProtocolReply commandBL(User user_from, String... arguments) { + List blusers; + List bltags; + + blusers = UserQueries.getUserBLUsers(sql, user_from.getUID()); + bltags = TagQueries.getUserBLTags(sql, user_from.getUID()); + + + String txt = ""; + 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.getUName() + "\n"; + } + } + if (txt.isEmpty()) { + txt = "You don't have any users or tags in your blacklist."; + } + return new ProtocolReply(txt, Optional.empty()); + } + + @UserCommand(pattern = "^\\@([^\\s\\n\\+]+)(\\+?)$", + help = "@username+ - Show user's info and last 10 messages (@username++ - second page, ..)") + public ProtocolReply commandUser(User user, String... arguments) { + User blogUser = UserQueries.getUserByName(sql, arguments[0]); + int page = arguments[1].length(); + if (blogUser.getUID() > 0) { + List mids = MessagesQueries.getUserBlog(sql, blogUser.getUID(), 0, page); + List messages = MessagesQueries.getMessages(sql, mids); + return new ProtocolReply(String.format("Last messages from @%s:\n%s", arguments[0], + String.join("\n", messages.stream() + .map(Object::toString).collect(Collectors.toList()))), + Optional.of(json.serializeList(messages))); + } + return new ProtocolReply("User not found", Optional.empty()); + } + + @UserCommand(pattern = "^\\s*d\\s*\\#([0-9]+)\\s*$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "D #12345 - delete the message") + public ProtocolReply commandDel(User user, String... args) { + try { + int mid = Integer.parseInt(args[0]); + if (MessagesQueries.deleteMessage(sql, user.getUID(), mid)) { + return new ProtocolReply(String.format("Message %s deleted", mid), Optional.empty()); + } + } catch (NumberFormatException e) { + return new ProtocolReply("Error", Optional.empty()); + } + return new ProtocolReply("Error", Optional.empty()); + } + + @UserCommand(pattern = "^\\s*login\\s*$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "LOGIN - log in to Juick website") + public ProtocolReply commandLogin(User user, String... arguments) { + return new ProtocolReply(baseUri + "?" + UserQueries.getHashByUID(sql, user.getUID()), + Optional.empty()); + } + + @UserCommand(pattern = "^(#+)$", help = "# - Show last messages from your feed (## - second page, ...)") + public ProtocolReply commandMyFeed(User user, String... arguments) { + // number of # is the page count + int page = arguments[0].length(); + List mids = MessagesQueries.getMyFeed(sql, user.getUID(), page); + List messages = MessagesQueries.getMessages(sql, mids); + // TODO: add instructions for empty feed + return new ProtocolReply("Your feed: \n" + String.join("\n", + messages.stream().map(Object::toString).collect(Collectors.toList())), + Optional.of(json.serializeList(messages))); + } + + @UserCommand(pattern = "^\\s*(on|off)\\s*$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "ON/OFF - Enable/disable subscriptions delivery") + public ProtocolReply commandOnOff(User user, String[] input) { + UserQueries.ActiveStatus newStatus; + String retValUpdated; + if (input[0].toLowerCase().equals("on")) { + newStatus = UserQueries.ActiveStatus.Active; + retValUpdated = "Notifications are activated for " + user.getJID(); + } else { + newStatus = UserQueries.ActiveStatus.Inactive; + retValUpdated = "Notifications are disabled for " + user.getJID(); + } + + if (UserQueries.setActiveStatusForJID(sql, user.getJID(), newStatus)) { + return new ProtocolReply(retValUpdated, Optional.empty()); + } else { + return new ProtocolReply(String.format("Subscriptions status for %s was not changed", user.getJID()), + Optional.empty()); + } + } + + @UserCommand(pattern = "^\\s*ping\\s*$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "PING - returns you a PONG") + public ProtocolReply commandPing(User user, String[] input) { + return new ProtocolReply("PONG", Optional.empty()); + } + + @UserCommand(pattern = "^\\@(\\S+)\\s+([\\s\\S]+)$", help = "@username message - send PM to username") + public ProtocolReply commandPM(User user_from, String... arguments) { + String user_to = arguments[0]; + String body = arguments[1]; + int ret = 0; + + int uid_to = 0; + String jid_to = null; + boolean haveInRoster = false; + + if (user_to.indexOf('@') > 0) { + uid_to = UserQueries.getUIDbyJID(sql, user_to); + } else { + uid_to = UserQueries.getUIDbyName(sql, user_to); + } + + if (uid_to > 0) { + if (!UserQueries.isInBLAny(sql, uid_to, user_from.getUID())) { + if (PMQueries.createPM(sql, user_from.getUID(), uid_to, body)) { + jid_to = UserQueries.getJIDsbyUID(sql, uid_to); + if (jid_to != null) { + haveInRoster = PMQueries.havePMinRoster(sql, user_from.getUID(), jid_to); + } + ret = 200; + } else { + ret = 500; + } + } else { + ret = 403; + } + } else { + ret = 404; + } + + + if (ret == 200) { + JuickMessage jmsg = new JuickMessage(); + jmsg.setUser(user_from); + jmsg.setText(body); + // TODO: add PM payload + app.events().publishEvent(new JuickMessageEvent(jmsg)); + /* TODO: move to XMPP component + if (jid_to != null) { + Message mm = new Message(); + mm.to = new JID(jid_to); + mm.type = Message.Type.chat; + if (haveInRoster) { + mm.from = new JID(user_from.getUName(), getDomain(), "Juick"); + mm.body = body; + } else { + mm.from = new JID("juick", getDomain(), "Juick"); + mm.body = "Private message from @" + user_from.getUName() + ":\n" + body; + } + return Collections.singletonList(mm); + } + */ + } + if (ret == 200) { + return new ProtocolReply("Private message sent", Optional.empty()); + } else { + return new ProtocolReply("Error " + ret, Optional.empty()); + } + } + + @UserCommand(pattern = "^#(\\d+)(\\+?)$", help = "#1234 - Show message (#1234+ - message with replies)") + public ProtocolReply commandShow(User user, String... arguments) { + boolean showReplies = arguments[1].length() > 0; + int mid; + try { + mid = Integer.parseInt(arguments[0]); + } catch (NumberFormatException e) { + return new ProtocolReply("Error", Optional.empty()); + } + Message msg = MessagesQueries.getMessage(sql, mid); + if (showReplies) { + List replies = MessagesQueries.getReplies(sql, mid); + replies.add(0, msg); + return new ProtocolReply(String.join("\n", + replies.stream().map(Object::toString).collect(Collectors.toList())), + Optional.of(json.serializeList(replies))); + } + return new ProtocolReply(msg.toString(), Optional.of(json.serializeList(Collections.singletonList(msg)))); + } + @UserCommand(pattern = "^(#|\\.)(\\d+)((\\.|\\-|\\/)(\\d+))?\\s([\\s\\S]+)", + help = "#1234 *tag *tag2 - edit tags\n#1234 text - reply to message") + public ProtocolReply EditOrReply(User user, String... args) { + int mid; + try { + mid = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + return new ProtocolReply("Error", Optional.empty()); + } + int rid; + try { + rid = Integer.parseInt(args[4]); + } catch (NumberFormatException e) { + rid = 0; + } + String txt = args[5]; + List messageTags = TagQueries.fromString(sql, txt, true); + if (messageTags.size() > 0) { + if (user.getUID() != MessagesQueries.getMessageAuthor(sql, mid).getUID()) { + return new ProtocolReply("It is not your message", Optional.empty()); + } + TagQueries.updateTags(sql, mid, messageTags); + return new ProtocolReply("Tags are updated", Optional.empty()); + } else { + int newrid = MessagesQueries.createReply(sql, mid, rid, user.getUID(), txt, null); + return new ProtocolReply("Reply posted.\n#" + mid + "/" + newrid + " " + + baseUri + mid + "/" + newrid, + Optional.of(json.serializeList(Collections.singletonList(MessagesQueries.getReply(sql, mid, newrid))))); + } + } + + @UserCommand(pattern = "^(s|u)\\s+#(\\d+)$", help = "S #1234 - subscribe to comments", + patternFlags = Pattern.CASE_INSENSITIVE) + public ProtocolReply commandSubscribeMessage(User user, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + int mid; + try { + mid = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + return new ProtocolReply("Error", Optional.empty()); + } + if (subscribe) { + if (SubscriptionsQueries.subscribeMessage(sql, mid, user.getUID())) { + return new ProtocolReply("Subscribed", Optional.empty()); + } + } else { + if (SubscriptionsQueries.unSubscribeMessage(sql, mid, user.getUID())) { + return new ProtocolReply("Unsubscribed from #" + mid, Optional.empty()); + } + return new ProtocolReply("You was not subscribed to #" + mid, Optional.empty()); + } + return new ProtocolReply("Error", Optional.empty()); + } + @UserCommand(pattern = "^(s|u)\\s+\\@(\\S+)$", help = "S @user - subscribe to user's posts", + patternFlags = Pattern.CASE_INSENSITIVE) + public ProtocolReply commandSubscribeUser(User user, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + User toUser = UserQueries.getUserByName(sql, args[1]); + if (toUser.getUID() > 0) { + if (subscribe) { + if (SubscriptionsQueries.subscribeUser(sql, user, toUser)) { + return new ProtocolReply("Subscribed", Optional.empty()); + // TODO: notification + // TODO: already subscribed case + } + } else { + if (SubscriptionsQueries.unSubscribeUser(sql, user, toUser)) { + return new ProtocolReply("Unsubscribed from @" + toUser.getUName(), Optional.empty()); + } + return new ProtocolReply("You was not subscribed to @" + toUser.getUName(), Optional.empty()); + } + } + return new ProtocolReply("Error", Optional.empty()); + } + @UserCommand(pattern = "^(s|u)\\s+\\*(\\S+)$", help = "S *tag - subscribe to tag" + + "\nU *tag - unsubscribe from tag", patternFlags = Pattern.CASE_INSENSITIVE) + public ProtocolReply commandSubscribeTag(User user, String... args) { + boolean subscribe = args[0].equalsIgnoreCase("s"); + Tag tag = TagQueries.getTag(sql, args[1], true); + if (subscribe) { + if (SubscriptionsQueries.subscribeTag(sql, user, tag)) { + return new ProtocolReply("Subscribed", Optional.empty()); + } + } else { + if (SubscriptionsQueries.unSubscribeTag(sql, user, tag)) { + return new ProtocolReply("Unsubscribed from " + tag.Name, Optional.empty()); + } + return new ProtocolReply("You was not subscribed to " + tag.Name, Optional.empty()); + } + return new ProtocolReply("Error", Optional.empty()); + } + + @UserCommand(pattern = "^\\s*help\\s*$", patternFlags = Pattern.CASE_INSENSITIVE, + help = "HELP - returns this help message") + public ProtocolReply commandHelp(User user, String[] input) { + List commandsHelp = Arrays.asList(getClass().getDeclaredMethods()).stream() + .filter(m -> m.isAnnotationPresent(UserCommand.class)) + .map(m -> m.getAnnotation(UserCommand.class).help()) + .collect(Collectors.toList()); + return new ProtocolReply(String.join("\n", commandsHelp), Optional.empty()); + } +} diff --git a/src/main/java/com/juick/server/protocol/ProtocolReply.java b/src/main/java/com/juick/server/protocol/ProtocolReply.java new file mode 100644 index 00000000..d9d36a5d --- /dev/null +++ b/src/main/java/com/juick/server/protocol/ProtocolReply.java @@ -0,0 +1,23 @@ +package com.juick.server.protocol; + +import java.util.Optional; + +/** + * Created by vitalyster on 08.04.2016. + */ +public class ProtocolReply { + + private Optional json; + private String description; + + public ProtocolReply(String text, Optional json) { + this.description = text; + this.json = json; + } + public String getDescription() { + return description; + } + public Optional getJson() { + return json; + } +} diff --git a/src/main/java/com/juick/server/protocol/annotation/UserCommand.java b/src/main/java/com/juick/server/protocol/annotation/UserCommand.java new file mode 100644 index 00000000..af7c4924 --- /dev/null +++ b/src/main/java/com/juick/server/protocol/annotation/UserCommand.java @@ -0,0 +1,31 @@ +package com.juick.server.protocol.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by oxpa on 22.03.16. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserCommand { + /** + * + * @return a command pattern + */ + String pattern() default ""; + + /** + * + * @return pattern flags + */ + int patternFlags() default 0; + + /** + * + * @return a string used in HELP command output. Basically, only 1 string + */ + String help() default ""; +} -- cgit v1.2.3