diff --git a/.gitignore b/.gitignore index ac80349..e900c00 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,7 @@ bot-keys.jks owner-trusted-certs.jks /target/ /.idea/ -*.iml \ No newline at end of file +*.iml + +# validation files +conversationData-*.txt \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4665f85..1e4430d 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ at.researchstudio.sat won-bot - 0.9 + 0.10-SNAPSHOT diff --git a/src/main/java/won/bot/debugbot/impl/DebugBot.java b/src/main/java/won/bot/debugbot/impl/DebugBot.java index accd554..be2b408 100644 --- a/src/main/java/won/bot/debugbot/impl/DebugBot.java +++ b/src/main/java/won/bot/debugbot/impl/DebugBot.java @@ -10,13 +10,16 @@ */ package won.bot.debugbot.impl; +import java.io.StringWriter; import java.lang.invoke.MethodHandles; import java.net.URI; import java.text.DecimalFormat; import java.time.Duration; import java.util.ArrayList; +import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.Set; @@ -26,6 +29,7 @@ import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.Model; +import org.apache.jena.riot.RDFDataMgr; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StopWatch; @@ -99,12 +103,15 @@ import won.protocol.util.WonConversationUtils; import won.protocol.util.WonRdfUtils; import won.protocol.util.linkeddata.WonLinkedDataUtils; +import won.protocol.util.pretty.Lang_WON; import won.protocol.validation.WonConnectionValidator; /** - * Bot that reacts to each new atom that is created in the system by creating two atoms, it sends a connect message from - * one of these atoms, and a hint message for original atom offering match to another of these atoms. Additionally, it - * reacts to certain commands send via text messages on the connections with the created by the bot atoms. + * Bot that reacts to each new atom that is created in the system by creating + * two atoms, it sends a connect message from one of these atoms, and a hint + * message for original atom offering match to another of these atoms. + * Additionally, it reacts to certain commands send via text messages on the + * connections with the created by the bot atoms. */ public class DebugBot extends EventBot implements MatcherExtension, TextMessageCommandExtension, ServiceAtomExtension { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -135,292 +142,420 @@ public ServiceAtomBehaviour getServiceAtomBehaviour() { @Override protected void initializeEventListeners() { String welcomeMessage = "Greetings! I am the DebugBot. I " - + "can simulate multiple other users so you can test things. I understand a few commands. To see which ones, " - + "type 'usage'."; + + "can simulate multiple other users so you can test things. I understand a few commands. To see which ones, " + + "type 'usage'."; String welcomeHelpMessage = "When connecting with me, you can say 'ignore', or 'deny' to make me ignore or deny requests, and 'wait N' to make me wait N seconds (max 99) before reacting."; final EventListenerContext ctx = getEventListenerContext(); final EventBus bus = getEventBus(); // define BotCommands for TextMessageCommandBehaviour ArrayList botCommands = new ArrayList<>(); botCommands.add(new PatternMatcherTextMessageCommand("hint ((random|incompatible) socket)", - "create a new atom and send me an atom or socket hint (between random or incompatible sockets)", - Pattern.compile("^hint(\\s+((random|incompatible)\\s+)?socket)?$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> { - matcher.matches(); - boolean socketHint = matcher.group(1) != null; - boolean incompatible = "incompatible".equals(matcher.group(3)); - boolean random = "random".equals(matcher.group(3)); - String hintType = socketHint ? incompatible ? "incompatible SocketHintMessage" - : random ? "random SocketHintMessage" : "SocketHintMessage" : "AtomHintMessage"; - bus.publish(new ConnectionMessageCommandEvent(connection, - "Ok, I'll create a new atom and send a " + hintType + " to you.")); - bus.publish(new HintDebugCommandEvent(connection, - socketHint - ? incompatible ? HintType.INCOMPATIBLE_SOCKET_HINT - : random ? HintType.RANDOM_SOCKET_HINT : HintType.SOCKET_HINT - : HintType.ATOM_HINT)); - })); + "create a new atom and send me an atom or socket hint (between random or incompatible sockets)", + Pattern.compile("^hint(\\s+((random|incompatible)\\s+)?socket)?$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + matcher.matches(); + boolean socketHint = matcher.group(1) != null; + boolean incompatible = "incompatible".equals(matcher.group(3)); + boolean random = "random".equals(matcher.group(3)); + String hintType = socketHint + ? incompatible ? "incompatible SocketHintMessage" + : random ? "random SocketHintMessage" : "SocketHintMessage" + : "AtomHintMessage"; + bus.publish(new ConnectionMessageCommandEvent(connection, + "Ok, I'll create a new atom and send a " + hintType + " to you.")); + bus.publish(new HintDebugCommandEvent(connection, + socketHint + ? incompatible ? HintType.INCOMPATIBLE_SOCKET_HINT + : random ? HintType.RANDOM_SOCKET_HINT + : HintType.SOCKET_HINT + : HintType.ATOM_HINT)); + })); botCommands.add(new EqualsTextMessageCommand("close", "close the current connection", "close", - (Connection connection) -> { - bus.publish(new ConnectionMessageCommandEvent(connection, "Ok, I'll close this connection")); - bus.publish(new CloseCommandEvent(connection)); - })); + (Connection connection) -> { + bus.publish(new ConnectionMessageCommandEvent(connection, + "Ok, I'll close this connection")); + bus.publish(new CloseCommandEvent(connection)); + })); botCommands.add(new EqualsTextMessageCommand("modify", "modify the atom's description", "modify", - (Connection connection) -> { - bus.publish(new ConnectionMessageCommandEvent(connection, "Ok, I'll change my atom description.")); - bus.publish(new ReplaceDebugAtomContentCommandEvent(connection)); - })); + (Connection connection) -> { + bus.publish(new ConnectionMessageCommandEvent(connection, + "Ok, I'll change my atom description.")); + bus.publish(new ReplaceDebugAtomContentCommandEvent(connection)); + })); botCommands.add(new PatternMatcherTextMessageCommand("connect", - "create a new atom and send connection request to it", - Pattern.compile("^connect$", Pattern.CASE_INSENSITIVE), (Connection connection, Matcher matcher) -> { - bus.publish(new ConnectionMessageCommandEvent(connection, - "Ok, I'll create a new atom and make it send a connect to you.")); - bus.publish(new ConnectDebugCommandEvent(connection)); - })); - botCommands.add(new PatternMatcherTextMessageCommand("deactivate", - "deactivate remote atom of the current connection", - Pattern.compile("^deactivate$", Pattern.CASE_INSENSITIVE), (Connection connection, Matcher matcher) -> { - bus.publish(new ConnectionMessageCommandEvent(connection, - "Ok, I'll deactivate this atom. This will close the connection we are currently talking on.")); - bus.publish(new DeactivateAtomCommandEvent(connection.getAtomURI())); - })); - botCommands.add(new PatternMatcherTextMessageCommand("chatty (on|off)", - "send chat messages spontaneously every now and then? (default: on)", - Pattern.compile("^chatty(\\s+(on|off))?$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> { - if (matcher.matches()) { - String param = matcher.group(2); - if ("on".equals(param)) { + "create a new atom and send connection request to it", + Pattern.compile("^connect$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { bus.publish(new ConnectionMessageCommandEvent(connection, - "Ok, I'll send you messages spontaneously from time to time.")); - bus.publish(new SetChattinessDebugCommandEvent(connection, true)); - } else if ("off".equals(param)) { + "Ok, I'll create a new atom and make it send a connect to you.")); + bus.publish(new ConnectDebugCommandEvent(connection)); + })); + botCommands.add(new PatternMatcherTextMessageCommand("deactivate", + "deactivate remote atom of the current connection", + Pattern.compile("^deactivate$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { bus.publish(new ConnectionMessageCommandEvent(connection, - "Ok, from now on I will be quiet and only respond to your messages.")); - bus.publish(new SetChattinessDebugCommandEvent(connection, false)); - } - } - })); + "Ok, I'll deactivate this atom. This will close the connection we are currently talking on.")); + bus.publish(new DeactivateAtomCommandEvent(connection.getAtomURI())); + })); + botCommands.add(new PatternMatcherTextMessageCommand("chatty (on|off)", + "send chat messages spontaneously every now and then? (default: on)", + Pattern.compile("^chatty(\\s+(on|off))?$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + if (matcher.matches()) { + String param = matcher.group(2); + if ("on".equals(param)) { + bus.publish(new ConnectionMessageCommandEvent(connection, + "Ok, I'll send you messages spontaneously from time to time.")); + bus.publish(new SetChattinessDebugCommandEvent(connection, true)); + } else if ("off".equals(param)) { + bus.publish(new ConnectionMessageCommandEvent(connection, + "Ok, from now on I will be quiet and only respond to your messages.")); + bus.publish(new SetChattinessDebugCommandEvent(connection, false)); + } + } + })); botCommands.add(new PatternMatcherTextMessageCommand("cache (eager|lazy)", "use lazy or eager RDF cache", - Pattern.compile("^cache(\\s+(eager|lazy))?$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> { - if (matcher.matches()) { - String param = matcher.group(2); - if ("eager".equals(param)) { - bus.publish(new ConnectionMessageCommandEvent(connection, - "Ok, I'll put any message I receive or send into the RDF cache. This slows down message processing in general, but operations that require crawling connection data will be faster.")); - bus.publish(new SetCacheEagernessCommandEvent(true)); - } else if ("lazy".equals(param)) { - bus.publish(new ConnectionMessageCommandEvent(connection, - "Ok, I won't put messages I receive or send into the RDF cache. This speeds up message processing in general, but operations that require crawling connection data will be slowed down.")); - bus.publish(new SetCacheEagernessCommandEvent(false)); - } - } - })); - botCommands.add(new PatternMatcherTextMessageCommand("send N", - "send N messages, one per second. N must be an integer between 1 and 9", - Pattern.compile("^send ([1-9])$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> { - matcher.find(); - String nStr = matcher.group(1); - int n = Integer.parseInt(nStr); - bus.publish(new SendNDebugCommandEvent(connection, n)); - })); - botCommands.add(new PatternMatcherTextMessageCommand("validate", "download the connection data and validate it", - Pattern.compile("^validate$", Pattern.CASE_INSENSITIVE), (Connection connection, Matcher matcher) -> { - bus.publish(new ConnectionMessageCommandEvent(connection, - "ok, I'll validate the connection - but I'll need to crawl the connection data first, please be patient.")); - // initiate crawl behaviour - CrawlConnectionCommandEvent command = new CrawlConnectionCommandEvent(connection.getAtomURI(), - connection.getConnectionURI()); - CrawlConnectionDataBehaviour crawlConnectionDataBehaviour = new CrawlConnectionDataBehaviour(ctx, - command, Duration.ofSeconds(60)); - final StopWatch crawlStopWatch = new StopWatch(); - crawlStopWatch.start("crawl"); - crawlConnectionDataBehaviour - .onResult(new SendMessageReportingCrawlResultAction(ctx, connection, crawlStopWatch)); - crawlConnectionDataBehaviour.onResult(new SendMessageOnCrawlResultAction(ctx, connection) { - @Override - protected Model makeSuccessMessage(CrawlConnectionCommandSuccessEvent successEvent) { - try { - logger.debug("validating data of connection {}", command.getConnectionURI()); - // TODO: use one validator for all invocations - WonConnectionValidator validator = new WonConnectionValidator(); - StringBuilder message = new StringBuilder(); - boolean valid = validator.validate(successEvent.getCrawledData(), message); - String successMessage = "Connection " + command.getConnectionURI() + " is valid: " - + valid + " " + message.toString(); - return WonRdfUtils.MessageUtils.textMessage(successMessage); - } catch (Exception e) { - return WonRdfUtils.MessageUtils.textMessage("Caught exception during validation: " + e); + Pattern.compile("^cache(\\s+(eager|lazy))?$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + if (matcher.matches()) { + String param = matcher.group(2); + if ("eager".equals(param)) { + bus.publish(new ConnectionMessageCommandEvent(connection, + "Ok, I'll put any message I receive or send into the RDF cache. This slows down message processing in general, but operations that require crawling connection data will be faster.")); + bus.publish(new SetCacheEagernessCommandEvent(true)); + } else if ("lazy".equals(param)) { + bus.publish(new ConnectionMessageCommandEvent(connection, + "Ok, I won't put messages I receive or send into the RDF cache. This speeds up message processing in general, but operations that require crawling connection data will be slowed down.")); + bus.publish(new SetCacheEagernessCommandEvent(false)); + } } - } - }); - crawlConnectionDataBehaviour.activate(); - })); - botCommands.add(new PatternMatcherTextMessageCommand("retract (mine|proposal)", - "retract the last (proposal) message you sent, or the last message I sent", - Pattern.compile("^retract(\\s+((mine)|(proposal)))?$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> { - matcher.matches(); - boolean useWrongSender = matcher.group(3) != null; - boolean retractProposes = matcher.group(4) != null; - String whose = useWrongSender ? "your" : "my"; - String which = retractProposes ? "proposal " : ""; - referToEarlierMessages(ctx, bus, connection, - "ok, I'll retract " + whose + " latest " + which - + "message - but 'll need to crawl the connection data first, please be patient.", - state -> { - URI uri = state.getNthLatestMessage(m -> retractProposes - ? (m.isProposesMessage() || m.isProposesToCancelMessage()) - && m.getEffects().stream().anyMatch(MessageEffect::isProposes) - : useWrongSender ? m.getSenderAtomURI().equals(connection.getTargetAtomURI()) - : m.getSenderAtomURI().equals(connection.getAtomURI()), - 0); - return uri == null ? Collections.EMPTY_LIST : Collections.singletonList(uri); - }, WonRdfUtils.MessageUtils::addRetracts, - (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { - if (uris == null || uris.length == 0 || uris[0] == null) { - return "Sorry, I cannot retract any messages - I did not find any."; + })); + botCommands.add(new PatternMatcherTextMessageCommand("send N", + "send N messages, one per second. N must be an integer between 1 and 9", + Pattern.compile("^send ([1-9])$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + matcher.find(); + String nStr = matcher.group(1); + int n = Integer.parseInt(nStr); + bus.publish(new SendNDebugCommandEvent(connection, n)); + })); + botCommands.add(new PatternMatcherTextMessageCommand("validate (attach)", + "download the connection data and validate it", + Pattern.compile("^validate(\\s+(attach))?$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + bus.publish(new ConnectionMessageCommandEvent(connection, + "ok, I'll validate the connection - but I'll need to crawl the connection data first, please be patient.")); + // initiate crawl behaviour + CrawlConnectionCommandEvent command = new CrawlConnectionCommandEvent( + connection.getAtomURI(), + connection.getConnectionURI()); + CrawlConnectionDataBehaviour crawlConnectionDataBehaviour = new CrawlConnectionDataBehaviour( + ctx, + command, Duration.ofSeconds(60)); + final StopWatch crawlStopWatch = new StopWatch(); + crawlStopWatch.start("crawl"); + crawlConnectionDataBehaviour + .onResult(new SendMessageReportingCrawlResultAction(ctx, connection, + crawlStopWatch)); + crawlConnectionDataBehaviour.onResult(new SendMessageOnCrawlResultAction(ctx, connection) { + @Override + protected Model makeSuccessMessage(CrawlConnectionCommandSuccessEvent successEvent) { + try { + logger.debug("validating data of connection {}", command.getConnectionURI()); + // TODO: use one validator for all + // invocations + WonConnectionValidator validator = new WonConnectionValidator(); + StringBuilder message = new StringBuilder(); + boolean valid = validator.validate(successEvent.getCrawledData(), message); + String successMessage = "Connection " + command.getConnectionURI() + + " is valid: " + + valid + " " + message.toString(); + if (matcher.matches()) { + String param = matcher.group(2); + if ("attach".equals(param)) { + // add data as file + // attachment to message + StringWriter writer = new StringWriter(); + Lang_WON.init(); + RDFDataMgr.write(writer, successEvent.getCrawledData(), + Lang_WON.TRIG_WON_CONVERSATION); + String dataSetInput = writer.toString(); + Date date = new Date(); + String fileName = "conversationData-" + date.getTime() + ".trig"; + byte[] fileContent = dataSetInput.getBytes("UTF-8"); + String encodedString = Base64.getEncoder().encodeToString(fileContent); + return WonRdfUtils.MessageUtils.fileMessage(encodedString, fileName, + "application/trig", successMessage); + } + } + return WonRdfUtils.MessageUtils.textMessage(successMessage); + } catch (Exception e) { + return WonRdfUtils.MessageUtils + .textMessage("Caught exception during validation: " + e); + } } - Optional retractedString = state.getTextMessage(uris[0]); - String finalRetractedString = retractedString.map(s -> ", which read, '" + s + "'") - .orElse(", which had no text message"); - return "Ok, I am hereby retracting " + whose + " message" + finalRetractedString - + " (uri: " + uris[0] + ")." + "\n The query for finding that message took " - + getDurationString(queryDuration) + " seconds."; }); - })); - botCommands.add(new PatternMatcherTextMessageCommand("reject (yours)", - "reject the last rejectable message I (you) sent", - Pattern.compile("^reject(\\s+(yours))?$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> { - matcher.matches(); - boolean useWrongSender = matcher.group(2) != null; - String whose = useWrongSender ? "my" : "your"; - referToEarlierMessages(ctx, bus, connection, "ok, I'll reject " + whose - + " latest rejectable message - but I'll need to crawl the connection data first, please be patient.", - state -> { - URI uri = state.getLatestProposesOrClaimsMessageSentByAtom( - useWrongSender ? connection.getAtomURI() : connection.getTargetAtomURI()); - return uri == null ? Collections.EMPTY_LIST : Collections.singletonList(uri); - }, WonRdfUtils.MessageUtils::addRejects, - (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { - if (uris == null || uris.length == 0 || uris[0] == null) { - return "Sorry, I cannot reject any of " + whose - + " messages - I did not find any suitable message."; + crawlConnectionDataBehaviour.activate(); + })); + botCommands.add(new PatternMatcherTextMessageCommand("send dataset (agreements|claims|proposals)", + "download the connection data and returns dataset for (agreements|claims|proposals)", + Pattern.compile("^send dataset(\\s+((agreements)|(claims)|(proposals)))?$", + Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + bus.publish(new ConnectionMessageCommandEvent(connection, + "ok, I'll return the dataset - but I'll need to crawl the connection data first, please be patient.")); + // initiate crawl behaviour + CrawlConnectionCommandEvent command = new CrawlConnectionCommandEvent( + connection.getAtomURI(), + connection.getConnectionURI()); + CrawlConnectionDataBehaviour crawlConnectionDataBehaviour = new CrawlConnectionDataBehaviour( + ctx, + command, Duration.ofSeconds(60)); + final StopWatch crawlStopWatch = new StopWatch(); + crawlStopWatch.start("crawl"); + crawlConnectionDataBehaviour + .onResult(new SendMessageReportingCrawlResultAction(ctx, connection, + crawlStopWatch)); + crawlConnectionDataBehaviour.onResult(new SendMessageOnCrawlResultAction(ctx, connection) { + @Override + protected Model makeSuccessMessage(CrawlConnectionCommandSuccessEvent successEvent) { + try { + if (matcher.matches()) { + String successMessage = "Retrieved datased for connection: "; + Dataset dataSet = successEvent.getCrawledData(); + AgreementProtocolState agreementState = AgreementProtocolState.of(dataSet); + String datasetString = new String(); + String filePrefix = new String(); + StringWriter writer = new StringWriter(); + Lang_WON.init(); + Dataset dataset = null; + String param = matcher.group(2); + if ("agreements".equals(param)) { + filePrefix = "agreementData"; + dataset = agreementState.getAgreements(); + } else if ("proposals".equals(param)) { + filePrefix = "proposalData"; + dataset = agreementState.getPendingProposals(); + } else if ("claims".equals(param)) { + filePrefix = "claimsData"; + dataset = agreementState.getClaims(); + } else { + throw new Exception("Second command param not known"); + } + if (dataset == null) { + return WonRdfUtils.MessageUtils.textMessage( + "No " + param + " data found for this conversation"); + } + RDFDataMgr.write(writer, dataset, + Lang_WON.TRIG_WON_CONVERSATION); + datasetString = writer.toString(); + Date date = new Date(); + String fileName = filePrefix + "-" + date.getTime() + ".trig"; + byte[] fileContent = datasetString.getBytes("UTF-8"); + String encodedString = Base64.getEncoder().encodeToString(fileContent); + return WonRdfUtils.MessageUtils.fileMessage(encodedString, fileName, + "application/trig", successMessage); + } + throw new Exception("Command param not known"); + } catch (Exception e) { + return WonRdfUtils.MessageUtils + .textMessage("Caught exception during dataset retrieval: " + e); + } } - Optional retractedString = state.getTextMessage(uris[0]); - String finalRetractedString = retractedString.map(s -> ", which read, '" + s + "'") - .orElse(", which had no text message"); - return "Ok, I am hereby rejecting " + whose + " message" + finalRetractedString - + " (uri: " + uris[0] + ")." + "\n The query for finding that message took " - + getDurationString(queryDuration) + " seconds."; }); - })); + crawlConnectionDataBehaviour.activate(); + })); + botCommands.add(new PatternMatcherTextMessageCommand("retract (mine|proposal)", + "retract the last (proposal) message you sent, or the last message I sent", + Pattern.compile("^retract(\\s+((mine)|(proposal)))?$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + matcher.matches(); + boolean useWrongSender = matcher.group(3) != null; + boolean retractProposes = matcher.group(4) != null; + String whose = useWrongSender ? "your" : "my"; + String which = retractProposes ? "proposal " : ""; + referToEarlierMessages(ctx, bus, connection, + "ok, I'll retract " + whose + " latest " + which + + "message - but 'll need to crawl the connection data first, please be patient.", + state -> { + URI uri = state.getNthLatestMessage(m -> retractProposes + ? (m.isProposesMessage() + || m.isProposesToCancelMessage()) + && m.getEffects().stream().anyMatch( + MessageEffect::isProposes) + : useWrongSender ? m.getSenderAtomURI() + .equals(connection.getTargetAtomURI()) + : m.getSenderAtomURI().equals(connection + .getAtomURI()), + 0); + return uri == null ? Collections.EMPTY_LIST + : Collections.singletonList(uri); + }, WonRdfUtils.MessageUtils::addRetracts, + (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { + if (uris == null || uris.length == 0 || uris[0] == null) { + return "Sorry, I cannot retract any messages - I did not find any."; + } + Optional retractedString = state.getTextMessage(uris[0]); + String finalRetractedString = retractedString + .map(s -> ", which read, '" + s + "'") + .orElse(", which had no text message"); + return "Ok, I am hereby retracting " + whose + " message" + + finalRetractedString + + " (uri: " + uris[0] + ")." + + "\n The query for finding that message took " + + getDurationString(queryDuration) + " seconds."; + }); + })); + botCommands.add(new PatternMatcherTextMessageCommand("reject (yours)", + "reject the last rejectable message I (you) sent", + Pattern.compile("^reject(\\s+(yours))?$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + matcher.matches(); + boolean useWrongSender = matcher.group(2) != null; + String whose = useWrongSender ? "my" : "your"; + referToEarlierMessages(ctx, bus, connection, "ok, I'll reject " + whose + + " latest rejectable message - but I'll need to crawl the connection data first, please be patient.", + state -> { + URI uri = state.getLatestProposesOrClaimsMessageSentByAtom( + useWrongSender ? connection.getAtomURI() + : connection.getTargetAtomURI()); + return uri == null ? Collections.EMPTY_LIST + : Collections.singletonList(uri); + }, WonRdfUtils.MessageUtils::addRejects, + (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { + if (uris == null || uris.length == 0 || uris[0] == null) { + return "Sorry, I cannot reject any of " + whose + + " messages - I did not find any suitable message."; + } + Optional retractedString = state.getTextMessage(uris[0]); + String finalRetractedString = retractedString + .map(s -> ", which read, '" + s + "'") + .orElse(", which had no text message"); + return "Ok, I am hereby rejecting " + whose + " message" + + finalRetractedString + + " (uri: " + uris[0] + ")." + + "\n The query for finding that message took " + + getDurationString(queryDuration) + " seconds."; + }); + })); botCommands.add(new PatternMatcherTextMessageCommand("propose (my|any) (N)", - "propose one (N, max 9) of my(/your/any) messages for an agreement", - Pattern.compile("^propose(\\s+((my)|(any))?\\s*([1-9])?)?$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> { - matcher.matches(); - boolean my = matcher.group(3) != null; - boolean any = matcher.group(4) != null; - int count = matcher.group(5) == null ? 1 : Integer.parseInt(matcher.group(5)); - boolean allowOwnClauses = any || !my; - boolean allowCounterpartClauses = any || my; - String whose = allowOwnClauses ? allowCounterpartClauses ? "our" : "my" : allowCounterpartClauses - ? "your" : " - sorry, don't know which ones to choose, actually - "; - referToEarlierMessages(ctx, bus, connection, "ok, I'll make a proposal containing " + count + " of " - + whose - + " latest messages as clauses - but I'll need to crawl the connection data first, please be patient.", - state -> state.getNLatestMessageUris(m -> { - URI ownedAtomUri = connection.getAtomURI(); - URI targetAtomUri = connection.getTargetAtomURI(); - return ownedAtomUri != null && ownedAtomUri.equals(m.getSenderAtomURI()) - && allowOwnClauses - || targetAtomUri != null && targetAtomUri.equals(m.getSenderAtomURI()) - && allowCounterpartClauses; - }, count + 1).subList(1, count + 1), WonRdfUtils.MessageUtils::addProposes, - (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { - if (uris == null || uris.length == 0 || uris[0] == null) { - return "Sorry, I cannot propose the messages - I did not find any."; - } - // Optional proposedString = - // state.getTextMessage(uris[0]); - return "Ok, I am hereby making the proposal, containing " + uris.length + " clauses." - + "\n The query for finding the clauses took " - + getDurationString(queryDuration) + " seconds."; - }); - })); + "propose one (N, max 9) of my(/your/any) messages for an agreement", + Pattern.compile("^propose(\\s+((my)|(any))?\\s*([1-9])?)?$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + matcher.matches(); + boolean my = matcher.group(3) != null; + boolean any = matcher.group(4) != null; + int count = matcher.group(5) == null ? 1 : Integer.parseInt(matcher.group(5)); + boolean allowOwnClauses = any || !my; + boolean allowCounterpartClauses = any || my; + String whose = allowOwnClauses ? allowCounterpartClauses ? "our" : "my" + : allowCounterpartClauses ? "your" + : " - sorry, don't know which ones to choose, actually - "; + referToEarlierMessages(ctx, bus, connection, "ok, I'll make a proposal containing " + count + + " of " + + whose + + " latest messages as clauses - but I'll need to crawl the connection data first, please be patient.", + state -> state.getNLatestMessageUris(m -> { + URI ownedAtomUri = connection.getAtomURI(); + URI targetAtomUri = connection.getTargetAtomURI(); + return ownedAtomUri != null && ownedAtomUri.equals(m.getSenderAtomURI()) + && allowOwnClauses + || targetAtomUri != null + && targetAtomUri.equals( + m.getSenderAtomURI()) + && allowCounterpartClauses; + }, count + 1).subList(1, count + 1), WonRdfUtils.MessageUtils::addProposes, + (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { + if (uris == null || uris.length == 0 || uris[0] == null) { + return "Sorry, I cannot propose the messages - I did not find any."; + } + // Optional proposedString = + // state.getTextMessage(uris[0]); + return "Ok, I am hereby making the proposal, containing " + uris.length + + " clauses." + + "\n The query for finding the clauses took " + + getDurationString(queryDuration) + " seconds."; + }); + })); botCommands.add(new PatternMatcherTextMessageCommand("accept", - "accept the last proposal/claim made (including cancellation proposals)", - Pattern.compile("^accept$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> referToEarlierMessages(ctx, bus, connection, - "ok, I'll accept your latest proposal - but I'll need to crawl the connection data first, please be patient.", - state -> { - URI uri = state.getLatestPendingProposalOrClaim(Optional.empty(), - Optional.of(connection.getTargetAtomURI())); - return uri == null ? Collections.EMPTY_LIST : Collections.singletonList(uri); - }, WonRdfUtils.MessageUtils::addAccepts, - (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { - if (uris == null || uris.length == 0 || uris[0] == null) { - return "Sorry, I cannot accept any proposal - I did not find pending proposals"; - } - return "Ok, I am hereby accepting your latest proposal (uri: " + uris[0] + ")." - + "\n The query for finding it took " + getDurationString(queryDuration) - + " seconds."; - }))); + "accept the last proposal/claim made (including cancellation proposals)", + Pattern.compile("^accept$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> referToEarlierMessages(ctx, bus, connection, + "ok, I'll accept your latest proposal - but I'll need to crawl the connection data first, please be patient.", + state -> { + URI uri = state.getLatestPendingProposalOrClaim(Optional.empty(), + Optional.of(connection.getTargetAtomURI())); + return uri == null ? Collections.EMPTY_LIST + : Collections.singletonList(uri); + }, WonRdfUtils.MessageUtils::addAccepts, + (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { + if (uris == null || uris.length == 0 || uris[0] == null) { + return "Sorry, I cannot accept any proposal - I did not find pending proposals"; + } + return "Ok, I am hereby accepting your latest proposal (uri: " + uris[0] + + ")." + + "\n The query for finding it took " + + getDurationString(queryDuration) + + " seconds."; + }))); botCommands.add(new PatternMatcherTextMessageCommand("cancel", - "propose to cancel the newest agreement (that wasn't only a cancellation)", - Pattern.compile("^cancel$", Pattern.CASE_INSENSITIVE), - (Connection connection, Matcher matcher) -> referToEarlierMessages(ctx, bus, connection, - "ok, I'll propose to cancel our latest agreement - but I'll need to crawl the connection data first, please be patient.", - state -> { - URI uri = state.getLatestAgreement(); - return uri == null ? Collections.EMPTY_LIST : Collections.singletonList(uri); - }, WonRdfUtils.MessageUtils::addProposesToCancel, - (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { - if (uris == null || uris.length == 0 || uris[0] == null || state == null) { - return "Sorry, I cannot propose to cancel any agreement - I did not find any"; - } - return "Ok, I am hereby proposing to cancel our latest agreement (uri: " + uris[0] + ")." - + "\n The query for finding it took " + getDurationString(queryDuration) - + " seconds."; - }))); + "propose to cancel the newest agreement (that wasn't only a cancellation)", + Pattern.compile("^cancel$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> referToEarlierMessages(ctx, bus, connection, + "ok, I'll propose to cancel our latest agreement - but I'll need to crawl the connection data first, please be patient.", + state -> { + URI uri = state.getLatestAgreement(); + return uri == null ? Collections.EMPTY_LIST + : Collections.singletonList(uri); + }, WonRdfUtils.MessageUtils::addProposesToCancel, + (Duration queryDuration, AgreementProtocolState state, URI... uris) -> { + if (uris == null || uris.length == 0 || uris[0] == null || state == null) { + return "Sorry, I cannot propose to cancel any agreement - I did not find any"; + } + return "Ok, I am hereby proposing to cancel our latest agreement (uri: " + + uris[0] + ")." + + "\n The query for finding it took " + + getDurationString(queryDuration) + + " seconds."; + }))); botCommands.add(new PatternMatcherTextMessageCommand("inject", - "send a message in this connection that will be forwarded to all other connections we have", - Pattern.compile("^inject$", Pattern.CASE_INSENSITIVE), (Connection connection, Matcher matcher) -> { - bus.publish(new ConnectionMessageCommandEvent(connection, - "Ok, I'll send you one message that will be injected into our other connections by your WoN node if the inject permission is granted")); - // build a message to be injected into all connections of the receiver atom - // (not - // controlled by us) - Model messageModel = WonRdfUtils.MessageUtils.textMessage("This is the injected message."); - // the atom whose connections we want to inject into - URI targetAtom = connection.getTargetAtomURI(); - // we iterate over our atoms and see which of them are connected to the - // remote - // atom - Set myatoms = ctx.getBotContextWrapper().retrieveAllAtomUris(); - Set targetConnections = myatoms.stream() - // don't inject into the current connection - .filter(uri -> !connection.getAtomURI().equals(uri)).map(uri -> { - // for each of my (the bot's) atoms, check if they are - // connected to the remote - // atom of the current conversation - Dataset atomNetwork = WonLinkedDataUtils.getConnectionNetwork(uri, - ctx.getLinkedDataSource()); - return WonRdfUtils.AtomUtils.getTargetConnectionURIsForTargetAtoms(atomNetwork, - Collections.singletonList(targetAtom), Optional.of(ConnectionState.CONNECTED)); - }).flatMap(Collection::stream).collect(Collectors.toSet()); - bus.publish(new ConnectionMessageCommandEvent(connection, messageModel, targetConnections)); - })); + "send a message in this connection that will be forwarded to all other connections we have", + Pattern.compile("^inject$", Pattern.CASE_INSENSITIVE), + (Connection connection, Matcher matcher) -> { + bus.publish(new ConnectionMessageCommandEvent(connection, + "Ok, I'll send you one message that will be injected into our other connections by your WoN node if the inject permission is granted")); + // build a message to be injected into all connections of the receiver atom + // (not + // controlled by us) + Model messageModel = WonRdfUtils.MessageUtils.textMessage("This is the injected message."); + // the atom whose connections we want to inject into + URI targetAtom = connection.getTargetAtomURI(); + // we iterate over our atoms and see which of them are connected to the + // remote + // atom + Set myatoms = ctx.getBotContextWrapper().retrieveAllAtomUris(); + Set targetConnections = myatoms.stream() + // don't inject into the current connection + .filter(uri -> !connection.getAtomURI().equals(uri)).map(uri -> { + // for each of my (the bot's) atoms, check if they are + // connected to the remote + // atom of the current conversation + Dataset atomNetwork = WonLinkedDataUtils.getConnectionNetwork(uri, + ctx.getLinkedDataSource()); + return WonRdfUtils.AtomUtils.getTargetConnectionURIsForTargetAtoms( + atomNetwork, + Collections.singletonList(targetAtom), + Optional.of(ConnectionState.CONNECTED)); + }).flatMap(Collection::stream).collect(Collectors.toSet()); + bus.publish(new ConnectionMessageCommandEvent(connection, messageModel, targetConnections)); + })); // activate ServiceAtomBehaviour serviceAtomBehaviour = new ServiceAtomBehaviour(ctx); serviceAtomBehaviour.activate(); // activate TextMessageCommandBehaviour textMessageCommandBehaviour = new TextMessageCommandBehaviour(ctx, - botCommands.toArray(new TextMessageCommand[0])); + botCommands.toArray(new TextMessageCommand[0])); textMessageCommandBehaviour.activate(); // eagerly cache RDF data BotBehaviour eagerlyCacheBehaviour = new EagerlyPopulateCacheBehaviour(ctx); @@ -447,53 +582,54 @@ protected Model makeSuccessMessage(CrawlConnectionCommandSuccessEvent successEve // as soon as the echo atom triggered by debug connect created, connect to // original bus.subscribe(AtomCreatedEventForDebugConnect.class, - new RandomDelayedAction(ctx, CONNECT_DELAY_MILLIS, CONNECT_DELAY_MILLIS, 1, - new ConnectWithAssociatedAtomAction(ctx, SocketType.ChatSocket.getURI(), - SocketType.ChatSocket.getURI(), welcomeMessage + " " + welcomeHelpMessage))); + new RandomDelayedAction(ctx, CONNECT_DELAY_MILLIS, CONNECT_DELAY_MILLIS, 1, + new ConnectWithAssociatedAtomAction(ctx, SocketType.ChatSocket.getURI(), + SocketType.ChatSocket.getURI(), + welcomeMessage + " " + welcomeHelpMessage))); // as soon as the echo atom triggered by debug hint command created, hint to // original bus.subscribe(AtomCreatedEventForDebugHint.class, - new RandomDelayedAction(ctx, CONNECT_DELAY_MILLIS, CONNECT_DELAY_MILLIS, 1, - new HintAssociatedAtomAction(ctx, SocketType.ChatSocket.getURI(), - SocketType.ChatSocket.getURI(), matcherUri))); + new RandomDelayedAction(ctx, CONNECT_DELAY_MILLIS, CONNECT_DELAY_MILLIS, 1, + new HintAssociatedAtomAction(ctx, SocketType.ChatSocket.getURI(), + SocketType.ChatSocket.getURI(), matcherUri))); // if the original atom wants to connect - always open bus.subscribe(ConnectFromOtherAtomEvent.class, noInternalServiceAtomEventFilter, - new OpenConnectionDebugAction(ctx, welcomeMessage, welcomeHelpMessage), - new PublishSetChattinessEventAction(ctx, true)); + new OpenConnectionDebugAction(ctx, welcomeMessage, welcomeHelpMessage), + new PublishSetChattinessEventAction(ctx, true)); // if the remote side opens, send a greeting and set to chatty. bus.subscribe(ConnectFromOtherAtomEvent.class, noInternalServiceAtomEventFilter, - new PublishSetChattinessEventAction(ctx, true)); + new PublishSetChattinessEventAction(ctx, true)); // filter to prevent reacting to message Commands NotFilter noTextMessageCommandsFilter = getNoTextMessageCommandFilter(); bus.subscribe(ConnectFromOtherAtomEvent.class, - new AndFilter(noTextMessageCommandsFilter, noInternalServiceAtomEventFilter), - new DebugBotIncomingGenericMessageAction(ctx)); + new AndFilter(noTextMessageCommandsFilter, noInternalServiceAtomEventFilter), + new DebugBotIncomingGenericMessageAction(ctx)); // if the bot receives a text message - try to map the command of the text // message to a DebugEvent bus.subscribe(MessageFromOtherAtomEvent.class, noTextMessageCommandsFilter, - new DebugBotIncomingGenericMessageAction(ctx)); + new DebugBotIncomingGenericMessageAction(ctx)); bus.subscribe(CloseCommandSuccessEvent.class, new PublishSetChattinessEventAction(ctx, false)); // react to close event: set connection to not chatty bus.subscribe(CloseFromOtherAtomEvent.class, new PublishSetChattinessEventAction(ctx, false)); MessageTimingManager timingManager = new MessageTimingManager(ctx); // on every actEvent there is a chance we send a chatty message bus.subscribe(ActEvent.class, - new SendChattyMessageAction(ctx, CHATTY_MESSAGE_PROBABILITY, timingManager, - DebugBotIncomingGenericMessageAction.RANDOM_MESSAGES, - DebugBotIncomingGenericMessageAction.LAST_MESSAGES)); + new SendChattyMessageAction(ctx, CHATTY_MESSAGE_PROBABILITY, timingManager, + DebugBotIncomingGenericMessageAction.RANDOM_MESSAGES, + DebugBotIncomingGenericMessageAction.LAST_MESSAGES)); // process eliza messages with eliza bus.subscribe(MessageToElizaEvent.class, new AnswerWithElizaAction(ctx)); // remember when we sent the last message bus.subscribe(WonMessageSentOnConnectionEvent.class, new RecordMessageSentTimeAction(ctx, timingManager)); // remember when we got the last message bus.subscribe(WonMessageReceivedOnConnectionEvent.class, - new RecordMessageReceivedTimeAction(ctx, timingManager)); + new RecordMessageReceivedTimeAction(ctx, timingManager)); // initialize the sent timestamp when the connect message is received bus.subscribe(ConnectFromOtherAtomEvent.class, new RecordMessageSentTimeAction(ctx, timingManager)); // Usage Command Event Subscriptions: bus.subscribe(ReplaceDebugAtomContentCommandEvent.class, new ReplaceDebugAtomContentAction(ctx)); bus.subscribe(SendNDebugCommandEvent.class, new SendNDebugMessagesAction(ctx, DELAY_BETWEEN_N_MESSAGES, - DebugBotIncomingGenericMessageAction.N_MESSAGES)); + DebugBotIncomingGenericMessageAction.N_MESSAGES)); // react to the hint and connect commands by creating an atom (it will fire // correct atom created for connect/hint // events) @@ -518,7 +654,8 @@ protected void doRun(Event event, EventListener executingListener) throws Except } /*********************************************************************************** - * Mini framework for allowing the bot to refer to earlier messages while trying to avoid code duplication + * Mini framework for allowing the bot to refer to earlier messages while trying + * to avoid code duplication ***********************************************************************************/ private interface MessageFinder { List findMessages(AgreementProtocolState state); @@ -533,30 +670,32 @@ private interface TextMessageMaker { } private void referToEarlierMessages(EventListenerContext ctx, EventBus bus, Connection con, - String crawlAnnouncement, MessageFinder messageFinder, MessageReferrer messageReferrer, - TextMessageMaker textMessageMaker) { + String crawlAnnouncement, MessageFinder messageFinder, MessageReferrer messageReferrer, + TextMessageMaker textMessageMaker) { bus.publish(new ConnectionMessageCommandEvent(con, crawlAnnouncement)); // initiate crawl behaviour CrawlConnectionCommandEvent command = new CrawlConnectionCommandEvent(con.getAtomURI(), con.getConnectionURI()); CrawlConnectionDataBehaviour crawlConnectionDataBehaviour = new CrawlConnectionDataBehaviour(ctx, command, - Duration.ofSeconds(60)); + Duration.ofSeconds(60)); final StopWatch crawlStopWatch = new StopWatch(); crawlStopWatch.start("crawl"); AgreementProtocolState state = WonConversationUtils.getAgreementProtocolState(con.getConnectionURI(), - ctx.getLinkedDataSource()); + ctx.getLinkedDataSource()); crawlStopWatch.stop(); Duration crawlDuration = Duration.ofMillis(crawlStopWatch.getLastTaskTimeMillis()); getEventListenerContext().getEventBus() - .publish(new ConnectionMessageCommandEvent(con, - "Finished crawl in " + getDurationString(crawlDuration) + " seconds. The dataset has " - + state.getConversationDataset().asDatasetGraph().size() + " rdf graphs.")); + .publish(new ConnectionMessageCommandEvent(con, + "Finished crawl in " + getDurationString(crawlDuration) + + " seconds. The dataset has " + + state.getConversationDataset().asDatasetGraph().size() + + " rdf graphs.")); Model messageModel = makeReferringMessage(state, messageFinder, messageReferrer, textMessageMaker); getEventListenerContext().getEventBus().publish(new ConnectionMessageCommandEvent(con, messageModel)); crawlConnectionDataBehaviour.activate(); } private Model makeReferringMessage(AgreementProtocolState state, MessageFinder messageFinder, - MessageReferrer messageReferrer, TextMessageMaker textMessageMaker) { + MessageReferrer messageReferrer, TextMessageMaker textMessageMaker) { int origPrio = Thread.currentThread().getPriority(); Thread.currentThread().setPriority(Thread.MAX_PRIORITY); StopWatch queryStopWatch = new StopWatch(); @@ -567,7 +706,7 @@ private Model makeReferringMessage(AgreementProtocolState state, MessageFinder m Thread.currentThread().setPriority(origPrio); Duration queryDuration = Duration.ofMillis(queryStopWatch.getLastTaskTimeMillis()); Model messageModel = WonRdfUtils.MessageUtils - .textMessage(textMessageMaker.makeTextMessage(queryDuration, state, targetUriArray)); + .textMessage(textMessageMaker.makeTextMessage(queryDuration, state, targetUriArray)); return messageReferrer.referToMessages(messageModel, targetUriArray); }