Saving uploaded file from user with bot framework c# - c#

I am currently trying to allow a user to upload a file to the bot during a dialog flow. From there the bot will take the file and upload it to blob storage. When the file comes in the content property is null, however the content url, name, and type all have the correct values.
public virtual async Task StackTraceGathered(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
FileName = message.Attachments[0].Name;
HttpPostedFileBase file = (HttpPostedFileBase)message.Attachments[0].Content;
string filePath = HttpContext.Current.Server.MapPath("~/Files/" + file.FileName);
file.SaveAs(filePath);
if (message.Attachments != null && message.Attachments.Any())
{
var attachment = message.Attachments.First();
using (HttpClient httpClient = new HttpClient())
{
// Skype & MS Teams attachment URLs are secured by a JwtToken, so we need to pass the token from our bot.
if ((message.ChannelId.Equals("skype", StringComparison.InvariantCultureIgnoreCase) || message.ChannelId.Equals("msteams", StringComparison.InvariantCultureIgnoreCase))
&& new Uri(attachment.ContentUrl).Host.EndsWith("skype.com"))
{
var token = await new MicrosoftAppCredentials().GetTokenAsync();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
var responseMessage = await httpClient.GetAsync(attachment.ContentUrl);
var contentLenghtBytes = responseMessage.Content.Headers.ContentLength;
await context.PostAsync($"Attachment of {attachment.ContentType} type and size of {contentLenghtBytes} bytes received.");
}
}
else
{
await context.PostAsync("Hi there! I'm a bot created to show you how I can receive message attachments, but no attachment was sent to me. Please, try again sending a new message including an attachment.");
}
PromptDialog.Text(context, ProblemStartDuration, "How long has this been an issue? (Provide answer in days, if issue has been occurring for less than one day put 1).");
context.Wait(this.StackTraceGathered);
}

I don't see the issue, but I guess you are expecting the Content property to have something. It won't but you just need the Url. Two alternatives:
Download the attachment in the bot (as the code you are using in the question) and upload to blob storage
Try to upload the attachment directly from the Url using something like StartCopyFromBlob (check this)

Related

Send file from Android Kotlin to C# using POST

We have two applications: A C# REST-API, and a Kotlin Android application, we are using Google Platform Cloud Bucket to host the images.
A picture will be uploaded on the Android application, but the C# REST-API needs to upload it to the Google Cloud Platform.
This is the working C# code to upload a file to the Google Cloud Buckets:
[HttpPost]
[Route("upload")]
public IActionResult Upload()
{
var storageClient = StorageClient.Create(google_credentials);
string fileToUpload ="/Users/niel/Downloads/new_cat.jpg";
using (var fileStream = new FileStream(fileToUpload, FileMode.Open,
FileAccess.Read, FileShare.Read))
{
storageClient.UploadObject("test_storage_fotos", "new_cat", "image/jpeg", fileStream);
}
Console.WriteLine("uploaded the file successfully");
return Ok();
}
Now I need to replace fileToUpload with the content from a POST-request. Is there a way to do this? Picture from Android app > C# API > Google Buckets? The link from the C# API to Google Buckets is already working.
Is there a way in Kotlin to somehow get the byte-string of an image, post it to my C# API who takes the content and puts it in a FileStream? I than can upload the FileStream using storageClient.UploadObject? Is this a possibility?
Thanks!
Yes, you can definitely do this. Just send the file over to the server via http protocol with multipart/form-data content type.
In kotlin you can use ktor or any other http library to do that.
For ktor you'll need to add an implementation dependency
implementation "io.ktor:ktor-client-android:1.5.4"
And you might also need to add additional permission in AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
Then you can send a file with this snippet. Notice that imageUri is a content uri, for file uri the code would be a bit different
private fun getFileName(resolver: ContentResolver, uri: Uri): String {
val returnCursor: Cursor = resolver.query(uri, null, null, null, null)!!
val nameIndex: Int = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
returnCursor.moveToFirst()
val name: String = returnCursor.getString(nameIndex)
returnCursor.close()
return name
}
suspend fun postAndImage(imageUri: Uri, uploadEndPoint: String) {
val client = HttpClient(Android)
val cr = applicationContext.contentResolver
if(cr.getType(imageUri) == null) {
//process error
return
}
val stream = cr.openInputStream(imageUri)
if(stream == null) {
//process error
return
}
val response: HttpResponse = client.submitFormWithBinaryData(
url = uploadEndPoint,
formData = formData {
append("image", InputProvider { stream.asInput() }, Headers.build {
append(HttpHeaders.ContentType, cr.getType(imageUri)!!)
append(HttpHeaders.ContentDisposition, "filename=${getFileName(cr, imageUri)}")
})
}
)
stream.close()
//process response
}
And you'll need to modify you upload function slightly
[HttpPost]
[Route("upload")]
//the name of the argument must match the key that you pass in "append" function
public async Task<IActionResult> Post(IFormFile image)
{
var storageClient = StorageClient.Create(google_credentials);
using (var stream = image.OpenReadStream())
{
//it's also possible to get original file name from file name property
var fileName = Guid.NewGuid() + "." + Path.GetExtension(image.FileName);
//assuming bucket is already created
var storageObject = await storageClient
.UploadObjectAsync("test_storage_fotos", fileName, "image/jpeg", stream);
//save information about a storage object in database
}
return Ok();
}

Adding reference attachment to email responds missing SourceUrl parameter

I'm trying to send an email with an attachment bigger than 4MB through Microsoft Graph.
Searching around the web I came to the conclusion that for files bigger than 4MB, I would need to upload the file to onedrive through an upload session, and then add it to the email as a "reference attachment".
So far my code looks like this, first I upload the file to one drive
var graphClient = GetGraphClient();
var rootItem = graphClient.Users[userEmail].Drive.Root.Request().GetAsync().Result;
uploadSession = graphClient.Users[userEmail].Drive.Items[rootItem.Id].ItemWithPath(fullFileName).CreateUploadSession().Request().PostAsync().Result;
fullFileByteArray = Convert.FromBase64String(content);
stream = new MemoryStream(fullFileByteArray);
provider = new ChunkedUploadProvider(uploadSession, graphClient, stream);
driveItem = provider.UploadAsync(3).Result;
This works just fine, I upload the file to one drive and I get the id of said file. Next step is creating the email as a draft.
var email = await graphClient.Users[fromEmail].Messages.Request().AddAsync(message);
This also works fine, I create the email as a draft (I can see it on outlook), and I get the Id of said attachment.
Now onto the problematic part, when I try to add the attachments to the draft email.
foreach (var att in fileAttachments)
{
var attachment = new ReferenceAttachment();
attachment.Name = att.FullFileName;
attachment.ContentType = att.MIMEType;
attachment.Id = att.FileId;
attachment.ODataType = "#microsoft.graph.referenceAttachment";
attachment.Size = att.Size;
attachmentsParsed.Add(attachment);
await graphClient.Users[fromEmail].Messages[email.Id].Attachments.Request().AddAsync(attachment);
}
When I try to execute the AddAsync() it responds with:
Message: The property 'SourceUrl' is required when creating the entity.
Inner error:
AdditionalData:
request-id: {{someId}}
date: {{someDate}}
ClientRequestId: {{someId}}
) ---> Microsoft.Graph.ServiceException: Code: ErrorInvalidProperty
Message: The property 'SourceUrl' is required when creating the entity.
Inner error:
AdditionalData:
request-id: {{someId}}
date: {{someDate}}
ClientRequestId: {{someId}}
The thing is ReferenceAttachment does not have a SourceUrl property, neither do I find a sourceUrl parameter in the response from uploading the file. I tried adding it to the AdditionalData property of the attachment, which is a Dictionary, but it didn't work.
Also tried to send the request through Postman like this (tried to send it to the beta version of the API too):
{
"name":"{{someName}}",
"#odata.type": "#microsoft.graph.referenceAttachment",
"sourceUrl": "{{someUrl}}",
"contentType":"text/plain",
"id":"{{someId}}",
"size":"1553",
}
And the response was
{
"error": {
"code": "ErrorInvalidProperty",
"message": "The property 'SourceUrl' is required when creating the entity.",
"innerError": {
"request-id": "{{someId}}",
"date": "{{someDate}}"
}
}
}
Where do I add this SourceUrl property and where do I get it from?
I would suggest using the uploading of large file attachments directly to the Message without having to upload to onedrive first. This is described in the link below.
https://learn.microsoft.com/en-us/graph/outlook-large-attachments?tabs=http
There now exists support for this scenario in the latest version of the .Net SDK in the form of a LargeFileUploadTask.
Essentially, you will need to do the following steps
create an upload session for the attachment as follows. (Note that this available in the beta library for now https://www.nuget.org/packages/Microsoft.Graph.Beta/0.10.0-preview)
var fileStream; // file stream you wish to upload to the attachment
var attachmentItem = new AttachmentItem
{
AttachmentType = AttachmentType.File,
Name = "flower",
Size = fileStream.Length
};
await graphClient.Me.Messages[email.Id].Attachments
.CreateUploadSession(attachmentItem)
.Request()
.PostAsync();
Create the task using the stream you wish to upload.
// Create task
LargeFileUploadTask<FileAttachment> largeFileUploadTask = new LargeFileUploadTask<FileAttachment>(uploadSession, fileStream);
(Optional) You can setup monitoring of the upload progress as by creating an instance of IProgress
// Setup the progress monitoring
IProgress<long> progress = new Progress<long>(progress =>
{
Console.WriteLine($"Uploaded {progress} bytes of {stream.Length} bytes");
});
Upload the attachment to the message!
UploadResult<DriveItem> uploadResult = null;
try
{
uploadResult = await largeFileUploadTask.UploadAsync(progress);
if (uploadResult.UploadSucceeded)
{
//File attachement uplaod only returns the location URI on successful upload
Console.WriteLine($"File Uploaded {uploadResult.Location}");//Sucessful Upload
}
}
catch (ServiceException e)
{
Console.WriteLine(e.Message);
}
If you query the attachment of the message you should now view the attachement you added without any worries
var attachements = await graphClient.Me.Messages[email.Id].Attachments.Request().GetAsync();

Is there a way to accept file as an attachment in bot framework?

I have published my bot on Microsoft teams. Now I want to include a functionality in which, a user can upload a file as an attachment & bot will upload it on blob storage, How to handle this in bot framework?
The attachments sent by the user will end up in the Attachments collection of the IMessageActivity. There you will find the URL of the attachment the user sent.
Then, you will have to download the attachment and add your logic to upload it to Blob storage or any other storage you would like to use.
Here is a C# example showing how to access and download the attachments sent by the user. Added the code below for your reference:
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
if (message.Attachments != null && message.Attachments.Any())
{
var attachment = message.Attachments.First();
using (HttpClient httpClient = new HttpClient())
{
// Skype attachment URLs are secured by a JwtToken, so we need to pass the token from our bot.
if (message.ChannelId.Equals("skype", StringComparison.InvariantCultureIgnoreCase) && new Uri(attachment.ContentUrl).Host.EndsWith("skype.com"))
{
var token = await new MicrosoftAppCredentials().GetTokenAsync();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
var responseMessage = await httpClient.GetAsync(attachment.ContentUrl);
var contentLenghtBytes = responseMessage.Content.Headers.ContentLength;
await context.PostAsync($"Attachment of {attachment.ContentType} type and size of {contentLenghtBytes} bytes received.");
}
}
else
{
await context.PostAsync("Hi there! I'm a bot created to show you how I can receive message attachments, but no attachment was sent to me. Please, try again sending a new message including an attachment.");
}
context.Wait(this.MessageReceivedAsync);
}

Getting 403 (Forbidden), Microsoft Bot is trying to make a Web Client - download data call

I am trying to create a bot using Microsoft BotFramework
The issue I am facing is when the bot code is trying to download data(image which is uploaded by user to the bot) via any channel like Facebook messenger/Skype/Slack. The code to download is by creating a WebClient and making a DownloadData call by passing the attachment URL.
I am able to browse the uploaded image URL. Also, the code for download data via web client works fine if I write a console application rather than a Bot application.
c# Code snippet
WebClient wc = new WebClient();
byte[] bytes = wc.DownloadData(imageUrl); // This line gives 403 error
Kindly suggest what should be the solution for this issue.
Here is the complete fix for the issue:
private async Task<IEnumerable<byte[]>> GetAttachmentsAsByteArrayAsync(Activity activity)
{
var attachments = activity?.Attachments?
.Where(attachment => attachment.ContentUrl != null)
.Select(c => Tuple.Create(c.ContentType, c.ContentUrl));
if (attachments != null && attachments.Any())
{
var contentBytes = new List<byte[]>();
using (var connectorClient = new ConnectorClient(new Uri(activity.ServiceUrl)))
{
var token = await (connectorClient.Credentials as MicrosoftAppCredentials).GetTokenAsync();
foreach (var content in attachments)
{
var uri = new Uri(content.Item2);
using (var httpClient = new HttpClient())
{
if (uri.Host.EndsWith("skype.com") && uri.Scheme == "https")
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream"));
}
else
{
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(content.Item1));
}
contentBytes.Add(await httpClient.GetByteArrayAsync(uri));
}
}
}
return contentBytes;
}
return null;
}
https://github.com/Microsoft/BotBuilder/issues/662#issuecomment-232223965
you mean this fix? Did this work out for you?
I am using BotFramework with Node.js and received the same error.
Finally i found a workaround. I commented below lines in ChatConnector.js and its working fine for me now.
if (isEmulator && decoded_1.payload.appid != this.settings.appId) {
logger.error('ChatConnector: receive - invalid token. Requested by unexpected app ID.');
res.status(403);
res.end();
return;
}
if (isEmulator && decoded_1.payload.appid != this.settings.appId) {
logger.error('ChatConnector: receive - invalid token. Requested by unexpected app ID.');
res.status(403);
res.end();
return;
}

Get profile picture from Azure Active Directory

We have set the Azure AD as a identity provider in our application. We want to display profile picture that should come from Azure AD, in the application.
In order to test, I have added one Windows Live Id account (which has a profile picture) in the Azure AD. We then tried it using Graph Explorer, but no luck.
How do we get the profile picture from Azure AD?
You can use Azure Active Directory Graph Client to get user thumbnail photo
var servicePoint = new Uri("https://graph.windows.net");
var serviceRoot = new Uri(servicePoint, "<your tenant>"); //e.g. xxx.onmicrosoft.com
const string clientId = "<clientId>";
const string secretKey = "<secretKey>";// ClientID and SecretKey are defined when you register application with Azure AD
var authContext = new AuthenticationContext("https://login.windows.net/<tenant>/oauth2/token");
var credential = new ClientCredential(clientId, secretKey);
ActiveDirectoryClient directoryClient = new ActiveDirectoryClient(serviceRoot, async () =>
{
var result = await authContext.AcquireTokenAsync("https://graph.windows.net/", credential);
return result.AccessToken;
});
var user = await directoryClient.Users.Where(x => x.UserPrincipalName == "<username>").ExecuteSingleAsync();
DataServiceStreamResponse photo = await user.ThumbnailPhoto.DownloadAsync();
using (MemoryStream s = new MemoryStream())
{
photo.Stream.CopyTo(s);
var encodedImage = Convert.ToBase64String(s.ToArray());
}
Azure AD returns user's photo in binary format, you need to convert to Base64 string
Getting photos through Graph Explorer is not supported. Assuming that "signedInUser" already contains the signed in user entity, then this code snippet using the client library should work for you...
#region get signed in user's photo
if (signedInUser.ObjectId != null)
{
IUser sUser = (IUser)signedInUser;
IStreamFetcher photo = (IStreamFetcher)sUser.ThumbnailPhoto;
try
{
DataServiceStreamResponse response =
photo.DownloadAsync().Result;
Console.WriteLine("\nUser {0} GOT thumbnailphoto", signedInUser.DisplayName);
}
catch (Exception e)
{
Console.WriteLine("\nError getting the user's photo - may not exist {0} {1}", e.Message,
e.InnerException != null ? e.InnerException.Message : "");
}
}
#endregion
Alternatively you can do this through REST and it should look like this:
GET https://graph.windows.net/myorganization/users//thumbnailPhoto?api-version=1.5
Hope this helps,
According to the Azure AD Graph API Docs, Microsoft recommends transitioning to Microsoft Graph, as the Azure AD Graph API is being phased out.
However, to easily grab the photo via Azure AD, for now, use this URL template:
https://graph.windows.net/myorganization/users/{user_id}/thumbnailPhoto?api-version={version}
For example (this is a fake user id):
https://graph.windows.net/myorganization/users/abc1d234-01ab-1a23-12ab-abc0d123e456/thumbnailPhoto?api-version=1.6
The code below assumes you already have an authenticated user, with a token. This is a simplistic example; you'll want to change the return value to suit your needs, add error checking, etc.
const string ThumbUrl = "https://graph.windows.net/myorganization/users/{0}/thumbnailPhoto?api-version=1.6";
// Attempts to retrieve the thumbnail image for the specified user, with fallback.
// Returns: Fully formatted string for supplying as the src attribute value of an img tag.
private string GetUserThumbnail(string userId)
{
string thumbnail = "some base64 encoded fallback image";
string mediaType = "image/jpg"; // whatever your fallback image type is
string requestUrl = string.Format(ThumbUrl, userId);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", GetToken());
HttpResponseMessage response = client.GetAsync(requestUrl).Result;
if (response.IsSuccessStatusCode)
{
// Read the response as a byte array
var responseBody = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
// The headers will contain information on the image type returned
mediaType = response.Content.Headers.ContentType.MediaType;
// Encode the image string
thumbnail = Convert.ToBase64String(responseBody);
}
return $"data&colon;{mediaType};base64,{thumbnail}";
}
// Factored out for use with other calls which may need the token
private string GetToken()
{
return HttpContext.Current.Session["Token"] == null ? string.Empty : HttpContext.Current.Session["Token"].ToString();
}

Categories

Resources