package org.myrobotlab.service; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException; import org.myrobotlab.codec.CodecUtils; // import org.myrobotlab.framework.Message; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.ServiceType; import org.myrobotlab.io.FileIO; import org.myrobotlab.io.FindFile; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.ProgramAB.Response; import org.myrobotlab.service.Xmpp.XmppMsg; import org.slf4j.Logger; // FIXME - use Peers ! public class Shoutbox extends Service { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(Shoutbox.class); public static class DefaultNameProvider implements NameProvider { @Override public String getName(String token) { return token; } } public interface NameProvider { String getName(String token); } /** * POJO Shout is the most common message structure being sent from client to * WSServer and from WSServer broadcasted to clients - therefore instead of a * seperate system message we will have system data components of the shout - * these are to display server data on the clients */ public static class Shout implements Serializable { private static final long serialVersionUID = 1L; public String from; public String type; public String msg; public String color; public String ip; // TODO change to key public String clientId; public String time; } static final public String TYPE_SYSTEM = "TYPE_SYSTEM"; static final public String TYPE_USER = "TYPE_USER"; static final public String ORGIN_XMPP = "ORGIN_XMPP"; static final public String ORGIN_WEB = "ORGIN_WEB"; transient static NameProvider nameProvider = new DefaultNameProvider(); /** * Core to managing the connections are the keys The keys for websockets are * defined as remoteIp:remotePort - unfortunately these are null on disconnect * so a seperate lookup needs to be utilized The keys for xmpp "buddies" are * simply their jabber ids * * A Connection's UserId is a "user friendly" identification of the user using * that connection * * @param ws * @return */ static public String makeKey(String ws) { return String.format("%s:%s", ws, ws); } transient ProgramAB chatbot; transient Xmpp xmpp; transient ArrayList<String> xmppRelays = new ArrayList<String>(); transient ArrayList<String> chatbotNames = new ArrayList<String>(); transient HashMap<String, String> aliases = new HashMap<String, String>(); int imageDefaultHeight = 200; int imageDefaultWidth = 200; // FIXME - the amount of methods you DONT want exposed will be dwarfed by // the number you do - So, Security // will need to wildcard or list a filter of excludes // FIXME - standard interfaces for all GATEWAY SERVICES - onMsg() // addListener() // FIXME - Ma. Vo. name link on shoutbox // FIXME - decoding or encoing on specific GATEWAY Interface - e.g. ws or // xmpp // FIXME - login, Security, Authentication & Authorization done through the // Security service - restrictions only at Gateway // FIXME - impersonate mr.turing ? // FIXME make userShout userShoutAll systemShout systemShoutAll // TODO - system commands - refresh / clear / reload / history / // resize-format / stats / show times / set my color // FIXME - define client & server - system and user commands // TODO - number of sessions / authenticated / guests - query deeper on each // user - stats - geo-location // FIXME - permissions - erase my chat - moderate others // scrollable - non scrollable - set wrap - menu display - Angular.js / // jquery // levels of authorization / admin // hover over - display - time other (user) info // TODO - auto resize images // TODO - add modify or delete own shout // TODO - days alive ! - stats (poll thread - only pushes on changes) // TODO - force logout command // FIXME - color options // transient Connections conns = new Connections(); int maxShoutsInMemory = 200; ArrayList<Shout> shouts = new ArrayList<Shout>(); HashMap<String, Object> clients = new HashMap<String, Object>(); int msgCount; transient FileWriter fw = null; transient BufferedWriter bw = null; int maxArchiveRecordCount = 50; public Shoutbox(String n) { super(n); chatbotNames.add("@mrt"); chatbotNames.add("@mr.turing"); chatbotNames.add("@mrturing"); } public String addXMPPRelay(String user) { xmppRelays.add(user); // xmpp.sendMessage("now shoutbox relay", user); FIXME return user; } public void archive(Shout shout) { try { File dir = new File(getName()); // archive chats if (!dir.exists()) { dir.mkdir(); } if (fw == null) { String filename = String.format("%s/shouts.%s.js", getName(), TSFormatter.format(new Date())); File archive = new File(filename); fw = new FileWriter(archive.getAbsoluteFile()); bw = new BufferedWriter(fw); String d = String.format("%s", CodecUtils.toJson(shout)); bw.write(d); return; } String d = String.format(",%s", CodecUtils.toJson(shout)); bw.write(d); bw.flush(); if (msgCount % maxArchiveRecordCount == 0) { close(bw); fw = null; bw = null; } } catch (Exception e) { Logging.logError(e); } } private void chatWithChatbot(String foundName, Shout shout) { // clean found name - we don't want to send @mrt etc to Alice 2.0 String msg = shout.msg.replace(foundName, ""); chatbot.getResponse(shout.from, msg); } // WTFU - publishShout does not use this .. why?? public Shout createShout(String type, String msg) { Shout shout = new Shout(); shout.type = type; shout.msg = msg; return shout; } public String findChatBotName(String msg) { for (String name : chatbotNames) { if (msg.contains(name)) { return name; } } return null; } public void getXMPPRelays() { Shout shout = createShout(TYPE_USER, Arrays.toString(xmppRelays.toArray())); shout.from = "mr.turing"; invoke("publishShout", shout); } /** * archiving restores last json file back into newly started shoutbox */ public void loadShouts() { try { File latest = null; // restore the last file back into memory List<File> files = FindFile.find(getName(), "shouts.*.js", false, false); for (int i = 0; i < files.size(); ++i) { File f = files.get(i); if (latest == null) { latest = f; } if (f.lastModified() > latest.lastModified()) { latest = f; } } if (latest == null) { log.info("no files found to restore"); return; } info("loading latest file %s", latest); String json = String.format("[%s]", FileIO.toString(latest.getAbsoluteFile())); Shout[] saved = CodecUtils.fromJson(json, Shout[].class); for (int i = 0; i < saved.length; ++i) { shouts.add(saved[i]); } } catch (Exception e) { Logging.logError(e); } } /** * * @param myName */ public void setNickName(String nickname) { // WebGui web log.info("setNickName {}", nickname); } public void mimicTuring(String msg) { Shout shout = createShout(TYPE_USER, msg); shout.from = "mr.turing"; invoke("publishShout", shout); } // FIXME - refactor ---(all msgs from non websockets e.g. chatbot | xmpp | // other --to--> websockets // FIXME - onChatBotResponse // onProgramAB response - onChatBotResponse ??? public Response onResponse(Response response) { log.info("chatbot shouting"); // String r = resizeImage(response.msg); String r = response.msg; // conns.addConnection("mr.turing", "mr.turing"); Shout shout = createShout(TYPE_USER, r); shout.from = "mr.turing"; invoke("publishShout", shout); return response; } // FIXME FIXME FIXME - not normalized with publishShout(WebSocket) :PPPP // FIXME - must fill in your name - "Greg Perry" somewhere.. public void onXMPPMsg(XmppMsg xmppMsg) { log.info(String.format("XMPP - %s %s", xmppMsg.from, xmppMsg.msg)); // not exactly the same model as onConnect - so we try to add each time String user = "me";// FIXME // xmpp.getEntry(xmppMsg.msg.getFrom()).getName(); // conns.addConnection(xmppMsg.msg.getFrom(), user); Shout shout = createShout(TYPE_USER, xmppMsg.msg); shout.from = user; invoke("publishShout", shout); } /** * shout of minimal complexity * * @param msg */ public void shout(String msg) { // an optimized shout - there is client id & auth stuff which should be // supplied at the service level // a client should simply shout('my text') and all the other parts be // filled in on overloaded methods shout("test", msg); } /** * max complexity shout * * @param msg * @param clientId */ public void shout(String clientId, String msg) { Shout shout = createShout(TYPE_USER, msg); shout.clientId = clientId; shout.from = clientId; // ???? invoke("publishShout", shout); } // EXCHANGE need "session-key" to do a - connection/session-key for user // FIXME NOT NORMALIZED with onXMPPMsg() !!!! // public void publishShout(WSMsg wsmsg) { is Message necessary here? public Shout publishShout(Shout shout) throws NotConnectedException, XMPPException { log.info(String.format("publishShout %s %s", shout.from, shout.msg)); String foundName = findChatBotName(shout.msg); if (foundName != null) { chatWithChatbot(foundName, shout); } shouts.add(shout); // Message out = createMessage("shoutclient", "publishShout", // CodecUtils.toJson(shout)); // TODO: what do we do with the result of this method? createMessage("shoutclient", "publishShout", CodecUtils.toJson(shout)); // webgui.sendToAll(out); if (xmpp != null && !TYPE_SYSTEM.equals(shout.type)) { for (int i = 0; i < xmppRelays.size(); ++i) { String relayName = xmppRelays.get(i); String jabberID = null;// FIXME xmpp.getJabberID(relayName); // don't echo to self // if (!key.startsWith(jabberID)) { filter took out mrt and // other activity ! log.info(String.format("sending from %s %s -> to xmpp client - relayName [%s] jabberID [%s] shout.msg [%s]", Thread.currentThread().getId(), shout.from, relayName, jabberID, shout.msg)); xmpp.sendMessage(String.format("%s: %s", shout.from, shout.msg), jabberID); // } } } archive(shout); return shout; } public void quickStart(String xmpp, String password) throws Exception { startXMPP(xmpp, password); startChatBot(); addXMPPRelay("Keith McGerald"); aliases.put("Keith McGerald", "kmcgerald"); // addXMPPRelay("Orbous Mundus"); // addXMPPRelay("Alessandro Didonna"); // addXMPPRelay("Dwayne Williams"); // addXMPPRelay("Aatur Mehta"); addXMPPRelay("Greg Perry"); aliases.put("Greg Perry", "GroG"); addTask(30 * 60 * 1000, "savePredicates"); } public String removeXMPPRelay(String user) { xmppRelays.remove(user); // conns.remove(xmpp.getJabberID(user)); return user; } // TODO: when this gets used we could add it back in // private String resizeImage(String shout) { // int x = shout.indexOf("<img"); // if (x > 0) { // int space = shout.indexOf(" ", x); // int endTag = shout.indexOf(">", x); // int insert = (space < endTag) ? space : endTag; // String r = String.format("%s width=%d height=%d %s", shout.substring(0, // insert), imageDefaultWidth, imageDefaultHeight, shout.substring(insert)); // log.info(String.format("=========== RESIZE ============ %s", r)); // } // // return shout; // } public void savePredicates() { try { log.info("saving Predicates"); if (chatbot != null) { chatbot.savePredicates(); } } catch (Exception e) { Logging.logError(e); } } // --------- XMPP END ------------ // String lastShoutMsg = null; public void sendTo(String type, String key, Object data) { Shout shout = createShout(TYPE_SYSTEM, CodecUtils.toJson(data)); String msgString = CodecUtils.toJson(shout); // TODO: do something with the "sendTo" message? // Message sendTo = createMessage("shoutclient", "publishShout", msgString); createMessage("shoutclient", "publishShout", msgString); } public void setNameProvider(NameProvider nameProvider2) { nameProvider = nameProvider2; } public NameProvider setNameProvider(String classname) throws ClassNotFoundException, InstantiationException, IllegalAccessException { Class<?> theClass = Class.forName(classname); nameProvider = (NameProvider) theClass.newInstance(); return nameProvider; } // TO PEER OR NOT TO PEER THAT IS THE QUESTION... public void startChatBot() { if (chatbot != null) { error("chatbot already started"); return; } chatbot = (ProgramAB) Runtime.start("chatbot", "ProgramAB"); chatbot.startSession("ProgramAB", "alice2"); chatbot.addResponseListener(this); } @Override public void startService() { super.startService(); try { // TODO FIGURE THIS OUT :P OATH ? String provider = "org.myrobotlab.client.DrupalNameProvider"; log.info(String.format("attempting to set name provider - %s", provider)); setNameProvider(provider); } catch (Exception e) { error(e); } loadShouts(); } // ---- outbound ----> /* * * * CONCEPTS systemBroadcast - system needs to send to all system message list * - system sends to a list of users system message channel - * * channel - a group of recievers & senders * * Authenticaiton & Authorization - OATH query to Drupal? * * DATA timezone - set time zode - use UTC for all server data * * // system related public int connectionCount; public int userCount; public * int guestCount; public int msgCount; * * getVersion */ // --------- XMPP BEGIN ------------ public void startXMPP(String user, String password) throws Exception { if (xmpp == null) { xmpp = (Xmpp) Runtime.start("xmpp", "XMPP"); } xmpp.connect("myrobotlab.org", 5222, user, password); xmpp.addXmppMsgListener(this); } public static void main(String args[]) { LoggingFactory.init(Level.INFO); try { Shoutbox shoutbox = (Shoutbox) Runtime.start("shoutbox", "Shoutbox"); Shout shout = new Shout(); shout.from = "Fred"; shout.msg = "Hello I'm Fred"; shoutbox.shouts.add(shout); shout = new Shout(); shout.from = "George"; shout.msg = "Hi I'm George"; shoutbox.shouts.add(shout); shoutbox.createShout(TYPE_SYSTEM, "this is a test shout"); shoutbox.createShout(TYPE_SYSTEM, "this is another test shout"); shoutbox.createShout(TYPE_SYSTEM, "more test shouting"); Runtime.start("cli", "Cli"); Runtime.start("webgui", "WebGui"); } catch (Exception e) { Logging.logError(e); } } /** * This static method returns all the details of the class without it having * to be constructed. It has description, categories, dependencies, and peer * definitions. * * @return ServiceType - returns all the data * */ static public ServiceType getMetaData() { ServiceType meta = new ServiceType(Shoutbox.class.getCanonicalName()); meta.addDescription("shoutbox server"); meta.addCategory("connectivity"); return meta; } }