2
2
# Licensed under the MIT License.
3
3
4
4
import asyncio
5
- import sys
6
- from copy import deepcopy , copy
5
+ from copy import copy
7
6
from uuid import uuid4
8
- from typing import List , Callable , Iterable , Tuple
9
- from botbuilder .schema import Activity , ActivityTypes , ConversationReference , ResourceResponse
10
-
11
- # from .bot_adapter import BotAdapter
7
+ from typing import List , Callable , Union
8
+ from botbuilder .schema import Activity , ConversationReference , ResourceResponse
12
9
13
10
14
11
class BotContext (object ):
15
- def __init__ (self , adapter , request : Activity ):
16
- self .adapter = adapter
17
- self .request : Activity = request
18
- self .responses : List [Activity ] = []
19
- self ._services : dict = {}
20
- self ._responded : bool = False
21
- self ._on_send_activity : Callable [[]] = []
22
- self ._on_update_activity : Callable [[]] = []
23
- self ._on_delete_activity : Callable [[]] = []
24
-
25
- if not self .request :
12
+ def __init__ (self , adapter_or_context , request : Activity = None ):
13
+ """
14
+ Creates a new BotContext instance.
15
+ :param adapter_or_context:
16
+ :param request:
17
+ """
18
+ if isinstance (adapter_or_context , BotContext ):
19
+ adapter_or_context .copy_to (self )
20
+ else :
21
+ self .adapter = adapter_or_context
22
+ self ._activity = request
23
+ self .responses : List [Activity ] = []
24
+ self ._services : dict = {}
25
+ self ._on_send_activities : Callable [[]] = []
26
+ self ._on_update_activity : Callable [[]] = []
27
+ self ._on_delete_activity : Callable [[]] = []
28
+ self ._responded = {'responded' : False }
29
+
30
+ if self .adapter is None :
31
+ raise TypeError ('BotContext must be instantiated with an adapter.' )
32
+ if self .activity is None :
26
33
raise TypeError ('BotContext must be instantiated with a request parameter of type Activity.' )
27
34
35
+ def copy_to (self , context : 'BotContext' ) -> None :
36
+ """
37
+ Called when this TurnContext instance is passed into the constructor of a new TurnContext
38
+ instance. Can be overridden in derived classes.
39
+ :param context:
40
+ :return:
41
+ """
42
+ for attribute in ['adapter' , 'activity' , '_responded' , '_services' ,
43
+ '_on_send_activities' , '_on_update_activity' , '_on_delete_activity' ]:
44
+ setattr (context , attribute , getattr (self , attribute ))
45
+
46
+ @property
47
+ def activity (self ):
48
+ """
49
+ The received activity.
50
+ :return:
51
+ """
52
+ return self ._activity
53
+
54
+ @activity .setter
55
+ def activity (self , value ):
56
+ """
57
+ Used to set BotContext._activity when a context object is created. Only takes instances of Activities.
58
+ :param value:
59
+ :return:
60
+ """
61
+ if not isinstance (value , Activity ):
62
+ raise TypeError ('BotContext: cannot set `activity` to a type other than Activity.' )
63
+ else :
64
+ self ._activity = value
65
+
66
+ @property
67
+ def responded (self ):
68
+ """
69
+ If `true` at least one response has been sent for the current turn of conversation.
70
+ :return:
71
+ """
72
+ return self ._responded ['responded' ]
73
+
74
+ @responded .setter
75
+ def responded (self , value ):
76
+ if not value :
77
+ raise ValueError ('BotContext: cannot set BotContext.responded to False.' )
78
+ else :
79
+ self ._responded ['responded' ] = True
80
+
81
+ @property
82
+ def services (self ):
83
+ """
84
+ Map of services and other values cached for the lifetime of the turn.
85
+ :return:
86
+ """
87
+ return self ._services
88
+
28
89
def get (self , key : str ) -> object :
29
90
if not key or not isinstance (key , str ):
30
91
raise TypeError ('"key" must be a valid string.' )
@@ -55,44 +116,96 @@ def set(self, key: str, value: object) -> None:
55
116
56
117
self ._services [key ] = value
57
118
58
- async def send_activity (self , * activity_or_text : Tuple [Activity , str ]):
59
- reference = BotContext .get_conversation_reference (self .request )
119
+ async def send_activity (self , * activity_or_text : Union [Activity , str ]) -> ResourceResponse :
120
+ """
121
+ Sends a single activity or message to the user.
122
+ :param activity_or_text:
123
+ :return:
124
+ """
125
+ reference = BotContext .get_conversation_reference (self .activity )
60
126
output = [BotContext .apply_conversation_reference (
61
127
Activity (text = a , type = 'message' ) if isinstance (a , str ) else a , reference )
62
128
for a in activity_or_text ]
63
129
for activity in output :
64
130
activity .input_hint = 'acceptingInput'
65
131
66
132
async def callback (context : 'BotContext' , output ):
67
- responses = await context .adapter .send_activity ( output )
133
+ responses = await context .adapter .send_activities ( context , output )
68
134
context ._responded = True
69
135
return responses
70
136
71
- await self ._emit (self ._on_send_activity , output , callback (self , output ))
137
+ await self ._emit (self ._on_send_activities , output , callback (self , output ))
72
138
73
139
async def update_activity (self , activity : Activity ):
74
- return asyncio .ensure_future (self ._emit (self ._on_update_activity ,
75
- activity ,
76
- self .adapter .update_activity (activity )))
140
+ """
141
+ Replaces an existing activity.
142
+ :param activity:
143
+ :return:
144
+ """
145
+ return await self ._emit (self ._on_update_activity , activity , self .adapter .update_activity (self , activity ))
77
146
78
- @staticmethod
79
- async def _emit (plugins , arg , logic ):
147
+ async def delete_activity (self , id_or_reference : Union [str , ConversationReference ]):
148
+ """
149
+ Deletes an existing activity.
150
+ :param id_or_reference:
151
+ :return:
152
+ """
153
+ if type (id_or_reference ) == str :
154
+ reference = BotContext .get_conversation_reference (self .activity )
155
+ reference .activity_id = id_or_reference
156
+ else :
157
+ reference = id_or_reference
158
+ return await self ._emit (self ._on_delete_activity , reference , self .adapter .delete_activity (self , reference ))
159
+
160
+ def on_send_activities (self , handler ) -> 'BotContext' :
161
+ """
162
+ Registers a handler to be notified of and potentially intercept the sending of activities.
163
+ :param handler:
164
+ :return:
165
+ """
166
+ self ._on_send_activities .append (handler )
167
+ return self
168
+
169
+ def on_update_activity (self , handler ) -> 'BotContext' :
170
+ """
171
+ Registers a handler to be notified of and potentially intercept an activity being updated.
172
+ :param handler:
173
+ :return:
174
+ """
175
+ self ._on_update_activity .append (handler )
176
+ return self
177
+
178
+ def on_delete_activity (self , handler ) -> 'BotContext' :
179
+ """
180
+ Registers a handler to be notified of and potentially intercept an activity being deleted.
181
+ :param handler:
182
+ :return:
183
+ """
184
+ self ._on_delete_activity .append (handler )
185
+ return self
186
+
187
+ async def _emit (self , plugins , arg , logic ):
80
188
handlers = copy (plugins )
81
189
82
190
async def emit_next (i : int ):
191
+ context = self
83
192
try :
84
193
if i < len (handlers ):
85
- await handlers [i ](arg , emit_next (i + 1 ))
86
- asyncio .ensure_future (logic )
194
+ async def next_handler ():
195
+ await emit_next (i + 1 )
196
+ await handlers [i ](context , arg , next_handler )
197
+
87
198
except Exception as e :
88
199
raise e
89
200
await emit_next (0 )
201
+ # This should be changed to `return await logic()`
202
+ return await logic
90
203
91
204
@staticmethod
92
205
def get_conversation_reference (activity : Activity ) -> ConversationReference :
93
206
"""
94
207
Returns the conversation reference for an activity. This can be saved as a plain old JSON
95
- bject and then later used to message the user proactively.
208
+ object and then later used to message the user proactively.
96
209
97
210
Usage Example:
98
211
reference = BotContext.get_conversation_reference(context.request)
@@ -119,9 +232,9 @@ def apply_conversation_reference(activity: Activity,
119
232
:param is_incoming:
120
233
:return:
121
234
"""
122
- activity .channel_id = reference .channel_id
123
- activity .service_url = reference .service_url
124
- activity .conversation = reference .conversation
235
+ activity .channel_id = reference .channel_id
236
+ activity .service_url = reference .service_url
237
+ activity .conversation = reference .conversation
125
238
if is_incoming :
126
239
activity .from_property = reference .user
127
240
activity .recipient = reference .bot
@@ -134,4 +247,3 @@ def apply_conversation_reference(activity: Activity,
134
247
activity .reply_to_id = reference .activity_id
135
248
136
249
return activity
137
-
0 commit comments