1
1
# Copyright (c) Microsoft Corporation. All rights reserved.
2
2
# Licensed under the MIT License.
3
+
3
4
import os
4
5
import urllib .parse
5
6
import urllib .request
6
7
import base64
8
+ import json
7
9
8
10
from botbuilder .core import ActivityHandler , MessageFactory , TurnContext , CardFactory
9
- from botbuilder .schema import ChannelAccount , HeroCard , CardAction , ActivityTypes , Attachment , AttachmentData , Activity , \
11
+ from botbuilder .schema import (
12
+ ChannelAccount ,
13
+ HeroCard ,
14
+ CardAction ,
15
+ ActivityTypes ,
16
+ Attachment ,
17
+ AttachmentData ,
18
+ Activity ,
10
19
ActionTypes
11
- import json
20
+ )
21
+
22
+ """
23
+ Represents a bot that processes incoming activities.
24
+ For each user interaction, an instance of this class is created and the OnTurnAsync method is called.
25
+ This is a Transient lifetime service. Transient lifetime services are created
26
+ each time they're requested. For each Activity received, a new instance of this
27
+ class is created. Objects that are expensive to construct, or have a lifetime
28
+ beyond the single turn, should be carefully managed.
29
+ """
12
30
13
31
14
32
class AttachmentsBot (ActivityHandler ):
@@ -24,39 +42,62 @@ async def on_message_activity(self, turn_context: TurnContext):
24
42
await self ._display_options (turn_context )
25
43
26
44
async def _send_welcome_message (self , turn_context : TurnContext ):
45
+ """
46
+ Greet the user and give them instructions on how to interact with the bot.
47
+ :param turn_context:
48
+ :return:
49
+ """
27
50
for member in turn_context .activity .members_added :
28
51
if member .id != turn_context .activity .recipient .id :
29
52
await turn_context .send_activity (f"Welcome to AttachmentsBot { member .name } . This bot will introduce "
30
53
f"you to Attachments. Please select an option" )
31
54
await self ._display_options (turn_context )
32
55
33
56
async def _handle_incoming_attachment (self , turn_context : TurnContext ):
57
+ """
58
+ Handle attachments uploaded by users. The bot receives an Attachment in an Activity.
59
+ The activity has a List of attachments.
60
+ Not all channels allow users to upload files. Some channels have restrictions
61
+ on file type, size, and other attributes. Consult the documentation for the channel for
62
+ more information. For example Skype's limits are here
63
+ <see ref="https://support.skype.com/en/faq/FA34644/skype-file-sharing-file-types-size-and-time-limits"/>.
64
+ :param turn_context:
65
+ :return:
66
+ """
34
67
for attachment in turn_context .activity .attachments :
35
68
attachment_info = await self ._download_attachment_and_write (attachment )
36
- await turn_context .send_activity (
37
- f"Attachment { attachment_info ['filename' ]} has been received to { attachment_info ['local_path' ]} " )
69
+ if "filename" in attachment_info :
70
+ await turn_context .send_activity (
71
+ f"Attachment { attachment_info ['filename' ]} has been received to { attachment_info ['local_path' ]} " )
38
72
39
73
async def _download_attachment_and_write (self , attachment : Attachment ) -> dict :
40
- url = attachment .content_url
41
-
42
- local_filename = os .path .join (os .getcwd (), attachment .name )
43
-
74
+ """
75
+ Retrieve the attachment via the attachment's contentUrl.
76
+ :param attachment:
77
+ :return: Dict: keys "filename", "local_path"
78
+ """
44
79
try :
45
- response = urllib .request .urlopen ("http://www.python.org" )
80
+ response = urllib .request .urlopen (attachment . content_url )
46
81
headers = response .info ()
82
+
83
+ # If user uploads JSON file, this prevents it from being written as
84
+ # "{"type":"Buffer","data":[123,13,10,32,32,34,108..."
47
85
if headers ["content-type" ] == "application/json" :
48
- data = json .load (response .data )
49
- with open (local_filename , "w" ) as out_file :
50
- out_file .write (data )
51
-
52
- return {
53
- "filename" : attachment .name ,
54
- "local_path" : local_filename
55
- }
86
+ data = bytes (json .load (response )["data" ])
56
87
else :
57
- return None
58
- except :
59
- return None
88
+ data = response .read ()
89
+
90
+ local_filename = os .path .join (os .getcwd (), attachment .name )
91
+ with open (local_filename , "wb" ) as out_file :
92
+ out_file .write (data )
93
+
94
+ return {
95
+ "filename" : attachment .name ,
96
+ "local_path" : local_filename
97
+ }
98
+ except Exception as e :
99
+ print (e )
100
+ return {}
60
101
61
102
async def _handle_outgoing_attachment (self , turn_context : TurnContext ):
62
103
reply = Activity (
@@ -79,6 +120,15 @@ async def _handle_outgoing_attachment(self, turn_context: TurnContext):
79
120
await turn_context .send_activity (reply )
80
121
81
122
async def _display_options (self , turn_context : TurnContext ):
123
+ """
124
+ Create a HeroCard with options for the user to interact with the bot.
125
+ :param turn_context:
126
+ :return:
127
+ """
128
+
129
+ # Note that some channels require different values to be used in order to get buttons to display text.
130
+ # In this code the emulator is accounted for with the 'title' parameter, but in other channels you may
131
+ # need to provide a value for other parameters like 'text' or 'displayText'.
82
132
card = HeroCard (
83
133
text = "You can upload an image or select one of the following choices" ,
84
134
buttons = [
@@ -104,9 +154,17 @@ async def _display_options(self, turn_context: TurnContext):
104
154
await turn_context .send_activity (reply )
105
155
106
156
def _get_inline_attachment (self ) -> Attachment :
157
+ """
158
+ Creates an inline attachment sent from the bot to the user using a base64 string.
159
+ Using a base64 string to send an attachment will not work on all channels.
160
+ Additionally, some channels will only allow certain file types to be sent this way.
161
+ For example a .png file may work but a .pdf file may not on some channels.
162
+ Please consult the channel documentation for specifics.
163
+ :return: Attachment
164
+ """
107
165
file_path = os .path .join (os .getcwd (), "resources/architecture-resize.png" )
108
166
with open (file_path , "rb" ) as in_file :
109
- base64_image = base64 .b64encode (in_file .read ())
167
+ base64_image = base64 .b64encode (in_file .read ()). decode ()
110
168
111
169
return Attachment (
112
170
name = "architecture-resize.png" ,
@@ -115,6 +173,11 @@ def _get_inline_attachment(self) -> Attachment:
115
173
)
116
174
117
175
async def _get_upload_attachment (self , turn_context : TurnContext ) -> Attachment :
176
+ """
177
+ Creates an "Attachment" to be sent from the bot to the user from an uploaded file.
178
+ :param turn_context:
179
+ :return: Attachment
180
+ """
118
181
with open (os .path .join (os .getcwd (), "resources/architecture-resize.png" ), "rb" ) as in_file :
119
182
image_data = in_file .read ()
120
183
@@ -125,7 +188,6 @@ async def _get_upload_attachment(self, turn_context: TurnContext) -> Attachment:
125
188
AttachmentData (
126
189
name = "architecture-resize.png" ,
127
190
original_base64 = image_data ,
128
- thumbnail_base64 = image_data ,
129
191
type = "image/png"
130
192
)
131
193
)
@@ -134,7 +196,7 @@ async def _get_upload_attachment(self, turn_context: TurnContext) -> Attachment:
134
196
attachment_uri = \
135
197
base_uri \
136
198
+ ("" if base_uri .endswith ("/" ) else "/" ) \
137
- + f"v3/attachments/$ { urllib . parse . urlencode ( response .id ) } /views/original"
199
+ + f"v3/attachments/{ response .id } /views/original"
138
200
139
201
return Attachment (
140
202
name = "architecture-resize.png" ,
@@ -143,8 +205,12 @@ async def _get_upload_attachment(self, turn_context: TurnContext) -> Attachment:
143
205
)
144
206
145
207
def _get_internet_attachment (self ) -> Attachment :
208
+ """
209
+ Creates an Attachment to be sent from the bot to the user from a HTTP URL.
210
+ :return: Attachment
211
+ """
146
212
return Attachment (
147
- name = "Resources \ a rchitecture-resize.png" ,
213
+ name = "architecture-resize.png" ,
148
214
content_type = "image/png" ,
149
215
content_url = "https://docs.microsoft.com/en-us/bot-framework/media/how-it-works/architecture-resize.png"
150
216
)
0 commit comments