Repository: microsoft/botbuilder-python

Issue:
[bug] ShowTypingMiddleware middleware in a python bot not functioning
## Version
`botbuilder-core        4.7.1`  
`botbuilder-schema      4.7.1`

## Describe the bug


``` python
#app.py 
ADAPTER = BotFrameworkAdapter(SETTINGS)
# show typing indicator on long activities
ADAPTER.use(ShowTypingMiddleware(delay=0.5, period=2.0))
```

``` python
#bot.py 
...

    async def on_message_activity(self, turn_context: TurnContext):
        if turn_context.activity.text == "middleware":
            await asyncio.sleep(10) # mock getting some data 
            await turn_context.send_activity("done")

...
```

## Expected behavior

I expect that calling the middleware 
- shows a TI for activities taking longer than .5 seconds 
- repeat sending a TI to the client every 2 seconds 

## Actual results :  

 - TI is sent one time only
 - no repeat TI are sent 
 - a runtime warning is shown:
```
 c:\develop\x\pybot1\.venv\lib\site-packages\botbuilder\core\show_typing_middleware.py:79: 
RuntimeWarning: coroutine 'ShowTypingMiddleware.on_turn.<locals>.start_interval' was never awaited
  start_interval(context, period, period)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
```

In the emulator log it is clear that only one TI indicator is sent , and no repeats are to be seen
```
[16:55:12]<- messageYou said 'middleware'
[16:55:12]POST200conversations.:conversationId.activities.:activityId
[16:55:12]POST201directline.conversations.:conversationId.activities
[16:55:43]-> messagemiddleware
[16:55:44]<- typing
[16:55:44]POST200conversations.:conversationId.activities.:activityId
[16:55:54]<- messagedone
[16:55:54]POST200conversations.:conversationId.activities.:activityId
[16:55:54]POST201directline.conversations.:conversationId.activities
```

## Additional context
also see Question on [SO](https://stackoverflow.com/posts/60467080/edit)



Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py b/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
--- a/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
+++ b/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
@@ -1,8 +1,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
-
-import time
-from functools import wraps
+import asyncio
 from typing import Awaitable, Callable
 
 from botbuilder.schema import Activity, ActivityTypes
@@ -11,38 +9,38 @@
 from .turn_context import TurnContext
 
 
-def delay(span=0.0):
-    def wrap(func):
-        @wraps(func)
-        async def delayed():
-            time.sleep(span)
-            await func()
-
-        return delayed
-
-    return wrap
-
-
 class Timer:
     clear_timer = False
 
-    async def set_timeout(self, func, time):
-        is_invocation_cancelled = False
-
-        @delay(time)
+    def set_timeout(self, func, span):
         async def some_fn():  # pylint: disable=function-redefined
+            await asyncio.sleep(span)
             if not self.clear_timer:
                 await func()
 
-        await some_fn()
-        return is_invocation_cancelled
+        asyncio.ensure_future(some_fn())
 
     def set_clear_timer(self):
         self.clear_timer = True
 
 
 class ShowTypingMiddleware(Middleware):
+    """
+    When added, this middleware will send typing activities back to the user when a Message activity
+    is received to let them know that the bot has received the message and is working on the response.
+    You can specify a delay before the first typing activity is sent and then a frequency, which
+    determines how often another typing activity is sent. Typing activities will continue to be sent
+    until your bot sends another message back to the user.
+    """
+
     def __init__(self, delay: float = 0.5, period: float = 2.0):
+        """
+        Initializes the middleware.
+
+        :param delay: Delay in seconds for the first typing indicator to be sent.
+        :param period: Delay in seconds for subsequent typing indicators.
+        """
+
         if delay < 0:
             raise ValueError("Delay must be greater than or equal to zero")
 
@@ -55,41 +53,43 @@ def __init__(self, delay: float = 0.5, period: float = 2.0):
     async def on_turn(
         self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
     ):
-        finished = False
         timer = Timer()
 
-        async def start_interval(context: TurnContext, delay: int, period: int):
+        def start_interval(context: TurnContext, delay, period):
             async def aux():
-                if not finished:
-                    typing_activity = Activity(
-                        type=ActivityTypes.typing,
-                        relates_to=context.activity.relates_to,
-                    )
+                typing_activity = Activity(
+                    type=ActivityTypes.typing, relates_to=context.activity.relates_to,
+                )
 
-                    conversation_reference = TurnContext.get_conversation_reference(
-                        context.activity
-                    )
+                conversation_reference = TurnContext.get_conversation_reference(
+                    context.activity
+                )
 
-                    typing_activity = TurnContext.apply_conversation_reference(
-                        typing_activity, conversation_reference
-                    )
+                typing_activity = TurnContext.apply_conversation_reference(
+                    typing_activity, conversation_reference
+                )
 
-                    await context.adapter.send_activities(context, [typing_activity])
+                asyncio.ensure_future(
+                    context.adapter.send_activities(context, [typing_activity])
+                )
 
-                    start_interval(context, period, period)
+                # restart the timer, with the 'period' value for the delay
+                timer.set_timeout(aux, period)
 
-            await timer.set_timeout(aux, delay)
+            # first time through we use the 'delay' value for the timer.
+            timer.set_timeout(aux, delay)
 
         def stop_interval():
-            nonlocal finished
-            finished = True
             timer.set_clear_timer()
 
+        # if it's a message, start sending typing activities until the
+        # bot logic is done.
         if context.activity.type == ActivityTypes.message:
-            finished = False
-            await start_interval(context, self._delay, self._period)
+            start_interval(context, self._delay, self._period)
 
+        # call the bot logic
         result = await logic()
+
         stop_interval()
 
         return result


Test patch:
diff --git a/libraries/botbuilder-core/tests/test_show_typing_middleware.py b/libraries/botbuilder-core/tests/test_show_typing_middleware.py
--- a/libraries/botbuilder-core/tests/test_show_typing_middleware.py
+++ b/libraries/botbuilder-core/tests/test_show_typing_middleware.py
@@ -1,68 +1,67 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import time
-import aiounittest
-
-from botbuilder.core import ShowTypingMiddleware
-from botbuilder.core.adapters import TestAdapter
-from botbuilder.schema import ActivityTypes
-
-
-class TestShowTypingMiddleware(aiounittest.AsyncTestCase):
-    async def test_should_automatically_send_a_typing_indicator(self):
-        async def aux(context):
-            time.sleep(0.600)
-            await context.send_activity(f"echo:{context.activity.text}")
-
-        def assert_is_typing(activity, description):  # pylint: disable=unused-argument
-            assert activity.type == ActivityTypes.typing
-
-        adapter = TestAdapter(aux)
-        adapter.use(ShowTypingMiddleware())
-
-        step1 = await adapter.send("foo")
-        step2 = await step1.assert_reply(assert_is_typing)
-        step3 = await step2.assert_reply("echo:foo")
-        step4 = await step3.send("bar")
-        step5 = await step4.assert_reply(assert_is_typing)
-        await step5.assert_reply("echo:bar")
-
-    async def test_should_not_automatically_send_a_typing_indicator_if_no_middleware(
-        self,
-    ):
-        async def aux(context):
-            await context.send_activity(f"echo:{context.activity.text}")
-
-        adapter = TestAdapter(aux)
-
-        step1 = await adapter.send("foo")
-        await step1.assert_reply("echo:foo")
-
-    async def test_should_not_immediately_respond_with_message(self):
-        async def aux(context):
-            time.sleep(0.600)
-            await context.send_activity(f"echo:{context.activity.text}")
-
-        def assert_is_not_message(
-            activity, description
-        ):  # pylint: disable=unused-argument
-            assert activity.type != ActivityTypes.message
-
-        adapter = TestAdapter(aux)
-        adapter.use(ShowTypingMiddleware())
-
-        step1 = await adapter.send("foo")
-        await step1.assert_reply(assert_is_not_message)
-
-    async def test_should_immediately_respond_with_message_if_no_middleware(self):
-        async def aux(context):
-            await context.send_activity(f"echo:{context.activity.text}")
-
-        def assert_is_message(activity, description):  # pylint: disable=unused-argument
-            assert activity.type == ActivityTypes.message
-
-        adapter = TestAdapter(aux)
-
-        step1 = await adapter.send("foo")
-        await step1.assert_reply(assert_is_message)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import asyncio
+import aiounittest
+
+from botbuilder.core import ShowTypingMiddleware
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import ActivityTypes
+
+
+class TestShowTypingMiddleware(aiounittest.AsyncTestCase):
+    async def test_should_automatically_send_a_typing_indicator(self):
+        async def aux(context):
+            await asyncio.sleep(0.600)
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        def assert_is_typing(activity, description):  # pylint: disable=unused-argument
+            assert activity.type == ActivityTypes.typing
+
+        adapter = TestAdapter(aux)
+        adapter.use(ShowTypingMiddleware())
+
+        step1 = await adapter.send("foo")
+        step2 = await step1.assert_reply(assert_is_typing)
+        step3 = await step2.assert_reply("echo:foo")
+        step4 = await step3.send("bar")
+        step5 = await step4.assert_reply(assert_is_typing)
+        await step5.assert_reply("echo:bar")
+
+    async def test_should_not_automatically_send_a_typing_indicator_if_no_middleware(
+        self,
+    ):
+        async def aux(context):
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        adapter = TestAdapter(aux)
+
+        step1 = await adapter.send("foo")
+        await step1.assert_reply("echo:foo")
+
+    async def test_should_not_immediately_respond_with_message(self):
+        async def aux(context):
+            await asyncio.sleep(0.600)
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        def assert_is_not_message(
+            activity, description
+        ):  # pylint: disable=unused-argument
+            assert activity.type != ActivityTypes.message
+
+        adapter = TestAdapter(aux)
+        adapter.use(ShowTypingMiddleware())
+
+        step1 = await adapter.send("foo")
+        await step1.assert_reply(assert_is_not_message)
+
+    async def test_should_immediately_respond_with_message_if_no_middleware(self):
+        async def aux(context):
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        def assert_is_message(activity, description):  # pylint: disable=unused-argument
+            assert activity.type == ActivityTypes.message
+
+        adapter = TestAdapter(aux)
+
+        step1 = await adapter.send("foo")
+        await step1.assert_reply(assert_is_message)