예제 #1
0
    def format(self, event):
        """
        Prepares an outbound line

        :param event: an inbound event
        :type event: Event or Message or Join or Leave

        :return: outbound line
        :rtype: str

        This function adapts inbound events to the appropriate
        format. It turns an object with multiple attributes
        to a single string that can be pushed to a Cisco Spark room.

        """
        if event.type == 'message':

            text = _(u"{}: {}")

            if event.content == event.text:
                return text.format(event.from_label, event.text)

            return Vibes(text=text.format(event.from_label, event.text),
                         content=text.format(event.from_label, event.content),
                         file=None)

        if event.type == 'join':
            return _(u"{} has joined").format(event.actor_label)

        if event.type == 'leave':
            return _(u"{} has left").format(event.actor_label)

        return _(u"an unknown event has been received")
예제 #2
0
 def on_init(self):
     """
     Localize strings for this command
     """
     self.keyword = _(u'sleep')
     self.information_message = _(u'Sleep for a while')
     self.usage_message = _(u'sleep <n>')
예제 #3
0
    def execute(self, bot, arguments=None, **kwargs):
        """
        Lists available commands and related usage information

        :param bot: The bot for this execution
        :type bot: Shellbot

        :param arguments: The arguments for this command
        :type arguments: str or ``None``

        """

        if self.engine.shell.commands == []:
            bot.say(_(u"No command has been found."))

        elif not arguments:
            lines = []

            for key in self.engine.shell.commands:
                command = self.engine.shell.command(key)
                if command.is_hidden:
                    pass

                elif self.allow(bot, command):
                    lines.append(u"{} - {}".format(
                        command.keyword, command.information_message))

            if not bot.channel.is_direct:  # cannot turn 1:1 to group

                for name in self.engine.list_factory.list_commands():
                    item = self.engine.list_factory.get_list(name)
                    first = next(iter(item))
                    lines.append(u"{} - {}".format(
                        name,
                        _(u"add participants ({}, ...)").format(first)))

            if lines:
                bot.say(_('Available commands:') + '\n' + '\n'.join(lines))

        else:
            command = self.engine.shell.command(arguments)

            if not command:
                bot.say(_(u"This command is unknown."))

            elif not self.allow(bot, command):
                bot.say(_(u"This command is unknown."))

            else:
                lines = []
                lines.append(u"{} - {}".format(command.keyword,
                                               command.information_message))
                if command.usage_message:
                    lines.append(
                        self.usage_template.format(command.usage_message))
                else:
                    lines.append(self.usage_template.format(command.keyword))

                if lines:
                    bot.say('\n'.join(lines))
예제 #4
0
    def __init__(self, attributes, index):
        """
        Represents a step in a linear process

        :param attributes: the dict to use
        :type attributes: dict

        :param index: index of this step
        :type index: int

        """
        self.label = attributes.get('label', _(u'Step {}').format(index))

        self.message = attributes.get('message',
                                      _(u'(no description is available)'))

        self.content = attributes.get('content', None)

        self.file = attributes.get('file', None)

        self.participants = attributes.get('participants', [])
        if isinstance(self.participants, string_types):
            self.participants = [self.participants]

        self.machine = attributes.get('machine', None)
예제 #5
0
 def on_init(self):
     """
     Localize strings for this command
     """
     self.keyword = _(u'*upload')
     self.information_message = _(u'Handle file upload')
     self.feedback_message = _(u"Thank you for the information shared!")
예제 #6
0
 def on_init(self):
     """
     Localize strings for this command
     """
     self.keyword = _(u'help')
     self.information_message = _(u'Show commands and usage')
     self.usage_message = _(u'help <command>')
     self.usage_template = _(u"usage: {}")
예제 #7
0
    def on_init(self):
        """
        Localize strings for this command
        """
        self.keyword = _(u'input')
        self.information_message = _(u'Display all input')

        self.no_input_message = _(u'There is nothing to display')
        self.input_header = _(u'Input:')
예제 #8
0
    def on_init(self):
        """
        Localize strings for this command
        """
        self.keyword = _(u'*default')
        self.information_message = _(u'Handle unmatched commands')

        self.participants_message = _(u"Adding participants from '{}'")
        self.default_message = _(u"Sorry, I do not know how to handle '{}'")
예제 #9
0
    def on_init(self):
        """
        Localize strings for this command
        """
        self.keyword = _(u'update')
        self.information_message = _(u'Update input content')

        self.no_arg = _(u'Thanks to provide the key and the data')
        self.no_input = _(u'There is nothing to update, input is empty')
        self.ok_msg = _(u'Update successfuly done')
예제 #10
0
    def trigger(self, bot):
        """
        Triggers a step

        :param bot: the bot to use
        :type bot: ShellBot

        This function does everything that is coming with a step:
        - send a message to the chat space,
        - maybe in MarkDown or HTML,
        - maybe with some attachment,
        - add participants to the channel,
        - reset and start a state machine

        Example::

            step.trigger(bot)

        """
        bot.say(_(u"New state: {} - {}").format(self.label, self.message))

        if self.content or self.file:
            bot.say(' ', content=self.content, file=self.file)

        bot.add_participants(self.participants)

        if self.machine:
            self.machine.stop()
            self.machine.reset()
            self._step_process = self.machine.start()
예제 #11
0
 def on_start(self):
     """
     Adds processing on engine start
     """
     sys.stdout.write(
         _(u"Type 'help' for guidance, or Ctl-C to exit.") + '\n')
     sys.stdout.flush()
예제 #12
0
    def execute(self, bot, arguments=None, **kwargs):
        """
        Checks and changes audit status

        :param bot: The bot for this execution
        :type bot: Shellbot

        :param arguments: either 'on' or 'off'
        :type arguments: str

        """
        if not self.has_been_enabled:
            bot.say(self.disabled_message)

        elif arguments == 'on':
            self.audit_on(bot)

        elif arguments == 'off':
            self.audit_off(bot)

        elif not arguments:
            self.audit_status(bot)

        else:
            bot.say(_(u"usage: {}").format(self.usage_message))
예제 #13
0
    def on_init(self, space=None, speaker=None, **kwargs):
        """
        Replicates messages to a secondary space

        :param space: the target space to use (optional)
        :type space: Space

        :param speaker: the speaker instance to use (optional)
        :type speaker: Speaker

        Parameters are provided mainly for test injection.
        """

        # create a secondary room
        #
        self.space = space if space else SparkSpace(
            context=self.engine.context)

        self.space.connect()

        title = u"{} - {}".format(self.space.configured_title(),
                                  _(u"Audited content"))

        self.space.bond(title=title)

        # speak incoming updates
        #
        self.context = Context()
        self.mouth = Queue()

        self.speaker = speaker if speaker else Speaker(engine=self.engine)
        self.speaker.start()
예제 #14
0
    def execute(self, bot, arguments=None, **kwargs):
        """
        Handles empty command

        :param bot: The bot for this execution
        :type bot: Shellbot

        :param arguments: The arguments for this command
        :type arguments: str or ``None``

        """
        if not hasattr(self, 'help_command'):
            self.help_command = self.engine.shell.command(_('help'))

        if self.help_command is None:
            bot.say(_(u"No help command has been found."))

        else:
            self.help_command.execute(bot)
예제 #15
0
    def test_global(self):

        logging.info('*** global ***')

        settings = {
            'localized': {
                'hello world': "What'up, Doc?",
                'another string': 'Bye!',
            },
            'space': {
                'title': 'space name',
                'participants': ['*****@*****.**'],
            },
            'server': {
                'url': 'http://to.no.where',
                'hook': '/hook',
                'binding': '0.0.0.0',
                'port': 8080,
            },
        }
        context = Context(settings)

        l10n.set_context(context)
        self.assertEqual(l10n.actual_strings, {})

        self.assertEqual(_('hello world'), "What'up, Doc?")
        self.assertEqual(l10n.actual_strings, {'hello world': "What'up, Doc?"})

        self.assertEqual(_('not localized'), 'not localized')
        self.assertEqual(l10n.actual_strings, {
            'hello world': "What'up, Doc?",
            'not localized': 'not localized'
        })

        self.assertEqual(_('another string'), 'Bye!')
        self.assertEqual(
            l10n.actual_strings, {
                'hello world': "What'up, Doc?",
                'another string': 'Bye!',
                'not localized': 'not localized'
            })
예제 #16
0
    def execute(self, bot, arguments=None, **kwargs):
        """
        Displays software version

        :param bot: The bot for this execution
        :type bot: Shellbot

        :param arguments: The arguments for this command
        :type arguments: str or ``None``

        """
        bot.say(_(u"{} version {}").format(self.engine.name,
                                           self.engine.version))
예제 #17
0
    def execute(self, bot, arguments=None, **kwargs):
        """
        Restarts the underlying state machine

        :param bot: The bot for this execution
        :type bot: Shellbot

        :param arguments: The arguments for this command
        :type arguments: str or ``None``

        This function calls the ``restart()`` function of the underlying
        state machine. It also transmits text
        typed by the end user after the command verb, and any other
        parameters received from the shell, e.g., attachment, etc.

        Note: this command has no effect on a running machine.
        """
        if not bot.machine:
            bot.say(_(u"No state machine is available"))

        elif not bot.machine.restart(arguments=arguments, **kwargs):
            bot.say(_(u"Cannot restart the state machine"))
예제 #18
0
    def on_init(self):
        """
        Localize strings for this command and register event
        """
        self.keyword = _(u'audit')
        self.information_message = _(u'Check and change audit status')
        self.usage_message = _(u'audit [on|off]')

        self.disabled_message = _(u'Audit has not been enabled.')

        self.on_message = _(u'Chat interactions are currently audited.')
        self.off_message = _(u'Chat interactions are not audited.')
        self.already_on_message = _(u'Chat interactions are already audited.')
        self.already_off_message = _(u'Chat interactions are already private.')

        self.temporary_off_message = _(
            u"Please note that auditing will restart after {}")

        self.engine.register('bond', self)
예제 #19
0
    def on_inbound(self, **kwargs):
        """
        Updates the chat on inbound message

        """
        if kwargs.get('event') != 'inbound':
            return
        logging.debug(u"Receiving inbound message")

        message = kwargs('message')
        self.bot.say(
            _(u"Received {}: {} (from {})").format(message['input']['key'],
                                                   message['input']['value'],
                                                   message['from']))
예제 #20
0
    def say(self, bot):
        """
        Reports on this step

        :param bot: the bit to use
        :type bot: ShellBot

        This function posts to the chat space some information on this step.

        Example::

            step.say(bot)

        """
        bot.say(_(u"Current state: {} - {}").format(self.label, self.message))
예제 #21
0
    def execute(self, bot, arguments=None, **kwargs):
        """
        Closes the space

        :param bot: The bot for this execution
        :type bot: Shellbot

        :param arguments: The arguments for this command
        :type arguments: str or ``None``

        This function should report on progress by sending
        messages with one or multiple ``bot.say("Whatever response")``.

        """
        bot.say(_(u"Closing this channel"))
        bot.dispose()
예제 #22
0
    def execute(self, bot, arguments=None, **kwargs):
        """
        Moves underlying state machine to the next step

        :param bot: The bot for this execution
        :type bot: Shellbot

        :param arguments: The arguments for this command
        :type arguments: str or ``None``

        This function calls the ``step()`` function of the underlying
        state machine and provides a static event. It also transmits text
        typed by the end user after the command verb, and any other
        parameters received from the shell, e.g., attachment, etc.
        """
        if bot.machine:
            bot.machine.step(event=self.event, arguments=arguments, **kwargs)
        else:
            bot.say(_(u"No state machine is available"))
예제 #23
0
    def check(self):
        """
        Check settings

        This function reads key ``local`` and below, and update
        the context accordingly.

        This function also selects the right input for this local space.
        If some content has been provided during initialisation, it is used
        to simulate user input. Else stdin is read one line at a time.
        """
        self.context.check('space.title',
                           _(u'Collaboration space'),
                           filter=True)
        self.context.check('space.participants',
                           '$CHANNEL_DEFAULT_PARTICIPANTS',
                           filter=True)

        self.context.set('server.binding', None)  # no web server at all

        if self.input:

            def read_list():
                for line in self.input:
                    sys.stdout.write(line + '\n')
                    sys.stdout.flush()
                    yield line

            self._lines = read_list()  #  yield creates an iterator

        else:

            def read_stdin():
                readline = sys.stdin.readline()
                while readline:
                    yield readline.rstrip('\n')
                    readline = sys.stdin.readline()

            self._lines = read_stdin()  #  yield creates an iterator
예제 #24
0
    def put(self, event):
        """
        Processes one event

        :param event: an inbound event
        :type event: Event or Message or Join or Leave

        With this class a string representation of the received event
        is forwarded to the speaker queue of a chat space.
        """
        logging.debug(u"- updating with a {} event".format(event.type))
        logging.debug(event.attributes)

        if event.get('url'):
            attachment = self.space.download_attachment(event.url)
            message = _(u"{}: {}").format(event.from_label,
                                          os.path.basename(attachment))

            self.mouth.put(Vibes(text=message, file=attachment))

        else:
            self.mouth.put(self.format(event))
예제 #25
0
class Input(Machine):
    """
    Asks for some input

    This implements a state machine that can get one piece of input
    from chat participants. It can ask a question, wait for some input,
    check provided data and provide guidance when needed.

    Example::

        machine = Input(bot=bot, question="PO Number?", key="order.id")
        machine.start()
        ...

    In normal operation mode, the machine asks a question in the chat space,
    then listen for an answer, captures it, and stops.

    When no adequate answer is provided, the machine will provide guidance
    in the chat space after some delay, and ask for a retry. Multiple retries
    can take place, until correct input is provided, or the machine is
    timed out.

    The machine can also time out after a (possibly) long duration, and send
    a message in the chat space when giving up.

    If correct input is mandatory, no time out will take place and the machine
    will really need a correct answer to stop.

    Data that has been captured can be read from the machine itself.
    For example::

        value = machine.get('answer')

    If the machine is given a key, this is used for feeding the bot store.
    For example::

        machine.build(key='my_field', ...)
        ...

        value = bot.recall('input')['my_field']

    The most straightforward way to process captured data in real-time
    is to subclass ``Input``, like in the following example::

        class MyInput(Input):

            def on_input(self, value):
                mail.send_message(value)

        machine = MyInput(...)
        machine.start()

    """
    ANSWER_MESSAGE = _(u"Ok, this has been noted")
    RETRY_MESSAGE = _(u"Invalid input, please retry")
    CANCEL_MESSAGE = _(u"Ok, forget about it")

    RETRY_DELAY = 20.0  # amount of seconds between retries
    CANCEL_DELAY = 40.0  # amount of seconds before time out

    def on_init(self,
                question=None,
                question_content=None,
                mask=None,
                regex=None,
                on_answer=None,
                on_answer_content=None,
                on_answer_file=None,
                on_retry=None,
                on_retry_content=None,
                on_retry_file=None,
                retry_delay=None,
                on_cancel=None,
                on_cancel_content=None,
                on_cancel_file=None,
                cancel_delay=None,
                is_mandatory=False,
                key=None,
                **kwargs):
        """
        Asks for some input

        :param question: Message to ask for some input
        :type question: str

        :param question_content: Rich message to ask for some input
        :type question_content: str

        :param mask: A mask to filter the input
        :type mask: str

        :param regex: A regular expression to filter the input
        :type regex: str

        :param on_answer: Message on successful data capture
        :type on_answer: str

        :param on_answer_content: Rich message on successful data capture
        :type on_answer_content: str in Markdown or HTML format

        :param on_answer_file: File to be uploaded on successful data capture
        :type on_answer_file: str

        :param on_retry: Message to provide guidance and ask for retry
        :type on_retry: str

        :param on_retry_content: Rich message on retry
        :type on_retry_content: str in Markdown or HTML format

        :param on_retry_file: File to be uploaded on retry
        :type on_retry_file: str

        :param retry_delay: Repeat the on_retry message after this delay in seconds
        :type retry_delay: int

        :param on_cancel: Message on time out
        :type on_cancel: str

        :param on_cancel_content: Rich message on time out
        :type on_cancel_content: str in Markdown or HTML format

        :param on_cancel_file: File to be uploaded on time out
        :type on_cancel_file: str

        :param is_mandatory: If the bot will insist and never give up
        :type is_mandatory: boolean

        :param cancel_delay: Give up on this input after this delay in seconds
        :type cancel_delay: int

        :param key: The label associated with data captured in bot store
        :type key: str


        If a mask is provided, it is used to filter provided input.
        Use following conventions to build the mask:

        * ``A`` - Any kind of unicode symbol such as ``g`` or ``ç``
        * ``9`` - A digit such as ``0`` or ``2``
        * ``+`` - When following ``#`` or ``9``, indicates optional extensions
           of the same type
        * Any other symbol, including punctuation or white space, has to match
           exactly.

        For example:

        * ``9999A``  will match 4 digits and 1 additional character
        * ``#9-A+`` will match ``#3-June 2017``

        Alternatively, you can provide a regular expression (regex) to extract
        useful information from the input.

        You can use almost every regular expression that is supported
        by python. If parenthesis are used, the function returns the first
        matching group.

        For example, you can capture an identifier with a given prefix::

            machine.build(question="What is the identifier?",
                          regex=r'ID-\d\w\d+', ...)
            ...

            id = machine.filter('The id is ID-1W27 I believe')
            assert id == 'ID-1W27'

        As a grouping example, you can capture a domain name by asking for
        some e-mail address like this::

            machine.build(question="please enter your e-mail address",
                          regex=r'@([\w.]+)', ...)
            ...

            domain_name = machine.filter('my address is [email protected]')
            assert domain_name == 'acme.com'

        """
        assert question or question_content
        self.question = question
        self.question_content = question_content

        assert regex is None or mask is None  # use only one of them
        self.regex = regex
        self.mask = mask

        self.on_answer = on_answer
        self.on_answer_content = on_answer_content
        self.on_answer_file = on_answer_file

        self.on_retry = on_retry
        self.on_retry_content = on_retry_content
        self.on_retry_file = on_retry_file

        self.on_cancel = on_cancel
        self.on_cancel_content = on_cancel_content
        self.on_cancel_file = on_cancel_file

        assert is_mandatory in (True, False)
        self.is_mandatory = is_mandatory

        if retry_delay is not None:
            assert float(retry_delay) > 0
            self.RETRY_DELAY = float(retry_delay)

        if cancel_delay is not None:
            assert float(cancel_delay) > 0
            self.CANCEL_DELAY = float(cancel_delay)
            assert self.CANCEL_DELAY > self.RETRY_DELAY

        self.key = key

        states = ['begin', 'waiting', 'delayed', 'end']

        transitions = [
            {
                'source': 'begin',
                'target': 'waiting',
                'action': self.ask
            },
            {
                'source': 'waiting',
                'target': 'end',
                'condition': lambda **z: self.get('answer') is not None,
                'action': self.stop
            },
            {
                'source': 'waiting',
                'target': 'delayed',
                'condition': lambda **z: self.elapsed > self.RETRY_DELAY,
                'action': self.say_retry,
            },
            {
                'source': 'delayed',
                'target': 'end',
                'condition': lambda **z: self.get('answer') is not None,
                'action': self.stop
            },
            {
                'source':
                'delayed',
                'target':
                'end',
                'condition':
                lambda **z: self.elapsed > self.CANCEL_DELAY and not self.
                is_mandatory,
                'action':
                self.cancel
            },
        ]

        during = {
            'begin': self.on_inbound,
            'waiting': self.on_inbound,
            'delayed': self.on_inbound,
            'end': self.on_inbound,
        }
        self.build(states=states, transitions=transitions, initial='begin')

        self.start_time = time.time()

    @property
    def elapsed(self):
        """
        Measures time since the question has been asked

        Used in the state machine for repeating the question and on time out.
        """
        return time.time() - self.start_time

    def say_answer(self, input):
        """
        Responds on correct capture

        :param input: the text that has been noted
        :type input: str

        """
        text = self.on_answer.format(input) if self.on_answer else None
        content = self.on_answer_content.format(
            input) if self.on_answer_content else None
        file = self.on_answer_file if self.on_answer_file else None

        if not text and not content:
            text = self.ANSWER_MESSAGE.format(input)

        self.bot.say(text)

        if content or file:
            self.bot.say(' ', content=content, file=file)

    def say_retry(self):
        """
        Provides guidance on retry

        """
        text = self.on_retry if self.on_retry else None
        content = self.on_retry_content if self.on_retry_content else None
        file = self.on_retry_file if self.on_retry_file else None

        if not text and not content:
            text = self.RETRY_MESSAGE

        self.bot.say(text)

        if content or file:
            self.bot.say(' ', content=content, file=file)

    def say_cancel(self):
        """
        Says that input has been timed out

        """
        text = self.on_cancel if self.on_cancel else None
        content = self.on_cancel_content if self.on_cancel_content else None
        file = self.on_cancel_file if self.on_cancel_file else None

        if not text and not content:
            text = self.CANCEL_MESSAGE

        self.bot.say(text)

        if content or file:
            self.bot.say(' ', content=content, file=file)

    def ask(self):
        """
        Asks the question in the chat space

        """

        text = self.question if self.question else None
        content = self.question_content if self.question_content else None

        self.bot.say(text)

        if content:
            self.bot.say(' ', content=content)

        self.start_time = time.time()
        self.listen()

    def listen(self):
        """
        Listens for data received from the chat space

        This function starts a separate process to scan the
        ``bot.fan`` queue until time out.
        """
        p = Process(target=self.receive)
        p.daemon = True
        p.start()
        return p

    def receive(self):
        """
        Receives data from the chat space

        This function implements a loop until some data has been
        actually captured, or until the state machine stops for some reason.

        The loop is also stopped when the parameter ``general.switch``
        is changed in the context. For example::

            engine.set('general.switch', 'off')

        """
        logging.info(u"Receiving input")

        self.set('answer', None)
        try:
            while self.bot.engine.get('general.switch', 'on') == 'on':

                if self.get('answer'):
                    break  # on good answer

                if not self.is_running:
                    break  # on machine stop

                try:
                    if self.bot.fan.empty():
                        label = 'fan.' + self.bot.id
                        self.bot.engine.set(label, time.time())
                        time.sleep(self.TICK_DURATION)
                        continue

                    item = self.bot.fan.get(True, self.TICK_DURATION)
                    if item is None:
                        break

                    logging.debug(u"Input has been received")
                    self.execute(arguments=item)

                except Exception as feedback:
                    logging.exception(feedback)
                    break

        except KeyboardInterrupt:
            pass

        logging.info(u"Input receiver has been stopped")

    def execute(self, arguments=None, **kwargs):
        """
        Receives data from the chat

        :param arguments: data captured from the chat space
        :type arguments: str

        This function checks data that is provided, and provides guidance
        if needed. It can extract information from the provided mask
        or regular expression, and save it for later use.
        """
        if not arguments:
            self.say_retry()
            return

        arguments = self.filter(text=arguments)

        if not arguments:
            self.say_retry()
            return

        # store at machine level
        self.set('answer', arguments)

        # store at bot level
        if self.key:
            self.bot.update('input', self.key, arguments)

        # use the input in this instance as well
        self.on_input(value=arguments, **kwargs)

        # advertise subscribers
        if self.key:
            self.bot.publisher.put(
                self.bot.id, {
                    'from': self.bot.id,
                    'input': {
                        'key': self.key,
                        'value': arguments
                    }
                })

        self.say_answer(arguments)

        self.step(event='tick')

    def filter(self, text):
        """
        Filters data from user input

        :param text: Text coming from the chat space
        :type text: str

        :return: Data to be captured, or None

        If a mask is provided, or a regular expression, they are used
        to extract useful information from provided data.

        Example to read a PO mumber::

            machine.build(mask='9999A', ...)
            ...

            po = machine.filter('PO Number is 2413v')
            assert po == '2413v'

        """

        if self.mask:
            return self.search_mask(self.mask, text)

        if self.regex:
            return self.search_expression(self.regex, text)

        return text

    def search_mask(self, mask, text):
        """
        Searches for structured data in text

        :param mask: A simple expression to be searched
        :type mask: str

        :param text: The string from the chat space
        :type text: str

        :return: either the matching expression, or None

        Use following conventions to build the mask:

        * ``A`` - Any kind of unicode symbol, such as ``g`` or ``ç``
        * ``9`` - A digit, such as ``0`` or ``2``
        * ``+`` - When following ``#`` or ``9``, indicates optional extensions
           of the same type
        * Any other symbol, including punctuation or white space, has to match
           exactly.

        Some mask examples:

        * ``9999A``  will match 4 digits and 1 additional character
        * ``#9-A+`` will match ``#3-June 2017``

        Example to read a PO mumber::

            machine.build(question="What is the PO number?",
                          mask='9999A', ...)
            ...

            po = machine.filter('PO Number is 2413v')
            assert po == '2413v'

        """
        assert mask
        assert text

        logging.debug(u"Searching for mask '{}'".format(mask))
        mask = mask.replace('+', 'pLuS')
        mask = re.escape(mask)
        mask = mask.replace('pLuS',
                            '+').replace('A', '\S').replace('9', '\d').replace(
                                'Z', '[^0-9]')
        logging.debug(u"- translated to expression '{}'".format(mask))

        pattern = re.compile(mask, re.U)
        searched = pattern.search(text)
        if not searched:
            logging.debug(u"- no match")
            return None

        matched = searched.group()
        logging.debug(u"- found '{}'".format(matched))
        return matched

    def search_expression(self, regex, text):
        """
        Searches for a regular expression in text

        :param regex: A regular expression to be matched
        :type regex: str

        :param text: The string from the chat space
        :type text: str

        :return: either the matching expression, or None

        You can use almost every regular expression that is supported
        by python. If parenthesis are used, the function returns the first
        matching group.

        For example, you can capture an identifier with a given prefix::

            machine.build(question="What is the identifier?",
                          regex=r'ID-\d\w\d+', ...)
            ...

            id = machine.filter('The id is ID-1W27 I believe')
            assert id == 'ID-1W27'

        As a grouping example, you can capture a domain name by asking for
        some e-mail address like this::

            machine.build(question="please enter your e-mail address",
                          regex=r'@([\w.]+)', ...)
            ...

            domain_name = machine.filter('my address is [email protected]')
            assert domain_name == 'acme.com'

        """
        assert regex
        assert text

        logging.debug(u"Searching for expression '{}'".format(regex))
        pattern = re.compile(regex, re.I)
        searched = pattern.search(text)
        if not searched:
            logging.debug(u"- no match")
            return None

        matched = searched.group()
        if len(searched.groups()) > 0:  # return the first matching group
            matched = searched.groups()[0]
        logging.debug(u"- found '{}'".format(matched))
        return matched

    def on_input(self, value, **kwargs):
        """
        Processes input data

        :param value: data that has been captured
        :type value: str

        This function is called as soon as some input has been captured.
        It can be overlaid in subclass, as in the following example::

            class MyInput(Input):

                def on_input(self, value):
                    mail.send_message(value)

            machine = MyInput(...)
            machine.start()

        The extra parameters wil be used in case of attachment with
        the value.
        """
        pass

    def on_inbound(self, **kwargs):
        """
        Updates the chat on inbound message

        """
        if kwargs.get('event') != 'inbound':
            return
        logging.debug(u"Receiving inbound message")

        message = kwargs('message')
        self.bot.say(
            _(u"Received {}: {} (from {})").format(message['input']['key'],
                                                   message['input']['value'],
                                                   message['from']))

    def cancel(self):
        """
        Cancels the question

        Used by the state machine on time out
        """
        self.say_cancel()
        self.stop()
예제 #26
0
    def do(self, line, received=None):
        """
        Handles one line of text

        :param line: a line of text to parse and to handle
        :type line: str

        :param received: the message that contains the command
        :type received: Message

        This function uses the first token as a verb, and looks for a command
        of the same name in the shell.

        If the command does not exist, the command ``*default`` is used
        instead. Default behavior is implemented in
        ``shellbot.commands.default`` yet you can load a different command
        for customization.

        If an empty line is provided, the command ``*empty`` is triggered.
        Default implementation is provided in ``shellbot.commands.empty``.

        When a file has been uploaded, the information is given to the
        command that is executed. If no message is provided with the file, the
        command ``*upload`` is triggered instad of ``*empty``.
        Default implementation is provided in ``shellbot.commands.upload``.

        Following parameters are used for the execution of a command:

        - ``bot`` - A bot instance is retrieved from the channel id mentioned
          in ``received``, and provided to the command.

        - ``arguments`` - This is a string that contains everything after the
          command verb. When ``hello How are you doing?`` is submitted to the
          shell, ``hello`` is the verb, and ``How are you doing?`` are the
          arguments. This is the regular case. If there is no command ``hello``
          then the command ``*default`` is used instead, and arguments provided
          are the full line ``hello How are you doing?``.

        - ``attachment`` - When a file has been uploaded, this attribute
          provides its external name, e.g., ``picture024.png``. This can be used
          in the executed command, if you keep in mind that the same name can be
          used multiple times in a conversation.

        - ``url`` - When a file has been uploaded, this is the handle by which
          actual content can be retrieved. Usually, ask the underlying space
          to get a local copy of the document.

        """
        line = str(line) if line else ''  # sanity check

        logging.info(u"Handling: {}".format(line))
        self.line = line
        self.count += 1

        tokens = line.split(' ')
        verb = tokens.pop(0)
        if len(verb) < 1:
            if received and received.url:
                verb = _(u'*upload')
            else:
                verb = _(u'*empty')
        verb = verb.lower()  # align across devices

        kwargs = {}

        if len(tokens) > 0:
            kwargs['arguments'] = ' '.join(tokens)
        else:
            kwargs['arguments'] = ''

        if received and received.attachment:
            kwargs['attachment'] = received.attachment

        if received and received.url:
            kwargs['url'] = received.url

        channel_id = received.channel_id if received else None
        bot = self.engine.get_bot(channel_id)

        if bot.channel is None:
            return

        sorry_message = _(u"Sorry, I do not know how to handle '{}'")

        try:
            if verb in self._commands.keys():
                command = self._commands[verb]

                if ( (command.in_direct and bot.channel.is_direct)
                    or (command.in_group and not bot.channel.is_direct) ):

                    self.verb = verb
                    command.execute(bot, **kwargs)

                else:

                    logging.debug(u"- command cannot be used in this channel")
                    bot.say(sorry_message.format(verb))

            elif _(u'*default') in self._commands.keys():
                kwargs['arguments'] = line  # provide full input line
                command = self._commands[_(u'*default')]
                command.execute(bot, **kwargs)

            else:
                bot.say(sorry_message.format(verb))

        except Exception:
            bot.say(sorry_message.format(verb))
            raise
예제 #27
0
 def on_init(self):
     """
     Localize strings for this command
     """
     self.keyword = _(u'step')
     self.information_message = _(u'Move process to next step')
예제 #28
0
    def test_configure(self):

        logging.info('*** configure ***')

        self.engine.configure({})
        self.assertEqual(self.engine.space.ears, self.engine.ears)
        self.assertTrue(self.engine.list_factory is not None)

        self.engine.context.clear()
        settings = {
            'bot': {
                'on_enter': 'Hello!',
                'on_exit': 'Bye!',
            },
            'localized': {
                'hello world': "What'up, Doc?",
                'another string': 'Bye!',
            },
            'space': {
                'title': 'space name',
                'participants': ['*****@*****.**'],
            },
            'server': {
                'url': 'http://to.no.where',
                'hook': '/hook',
                'binding': '0.0.0.0',
                'port': 8080,
            },
        }
        self.engine.configure(settings)
        self.assertEqual(self.engine.get('bot.on_enter'), 'Hello!')
        self.assertEqual(self.engine.get('bot.on_exit'), 'Bye!')
        self.assertEqual(self.engine.get('space.title'), 'space name')
        self.assertEqual(self.engine.get('space.participants'),
                         ['*****@*****.**'])
        self.assertEqual(self.engine.get('server.url'), 'http://to.no.where')
        self.assertEqual(self.engine.get('server.hook'), '/hook')

        self.assertEqual(_('hello world'), "What'up, Doc?")
        self.assertEqual(_('not localized'), 'not localized')

        self.engine.context.clear()
        self.engine.configure_from_path(
            os.path.join(os.path.dirname(os.path.abspath(__file__)),
                         'test_settings', 'regular.yaml'))
        self.assertEqual(self.engine.get('bot.on_enter'),
                         'How can I help you?')
        self.assertEqual(self.engine.get('bot.on_exit'), 'Bye for now')
        self.assertEqual(self.engine.get('space.title'), 'Support channel')
        self.assertEqual(self.engine.get('space.participants'),
                         ['*****@*****.**', '*****@*****.**'])
        self.assertEqual(self.engine.get('server.url'), None)
        self.assertEqual(self.engine.get('server.hook'), None)
        self.assertEqual(self.engine.get('server.binding'), None)
        self.assertEqual(self.engine.get('server.port'), None)

        items = [x for x in self.engine.list_factory.get_list('SupportTeam')]
        self.assertEqual(items,
                         ['*****@*****.**', '*****@*****.**'])

        items = [x for x in self.engine.list_factory.get_list('*unknown*list')]
        self.assertEqual(items, [])

        names = self.engine.list_factory.list_commands()
        self.assertEqual(sorted(names), ['SupportTeam'])
예제 #29
0
 def on_init(self):
     """
     Localize strings for this command
     """
     self.keyword = _(u'pass')
     self.information_message = _(u'Do absolutely nothing')
예제 #30
0
 def on_init(self):
     """
     Localize strings for this command
     """
     self.keyword = _(u'echo')
     self.information_message = _(u'Echo input string')