Error in Twilio .net SDK making TaskRouterCapability for TaskQueue - twilio

I am trying to pull some statistics for a TaskQueue using the TaskRouter.js SDK. For this I need to generate a capability router token, which allows access to the TaskQueue. According to the sample at twilio docs, I am supposed to pass null as the channel parameter(look in C# .Net sample), to generate the TaskRouterCapability token. But when I do this, I get an exception (object is null in get_Claims).
Looking at the source code, I should pass in the TaskQueue Sid as the channel id instead of null. When I did this a token was correctly generated.
To start off, am using the basic token generation example code at twilio docs :
class Program {
static void Main(string[] args)
{
// Find your Account Sid and Auth Token at twilio.com/console
const string accountSid = "ACbe0c12d747XXXXXXXXXXXXXXXXXb";
const string authToken = "your_auth_token";
const string workspaceSid = "WSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const string taskQueueSid = "WQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
var urls = new PolicyUrlUtils(workspaceSid, taskQueueSid);
var allowFetchSubresources = new Policy($"{urls.TaskQueue}/**",
HttpMethod.Get);
var allowUpdates = new Policy(urls.TaskQueue, HttpMethod.Post);
var policies = new List<Policy>
{
allowFetchSubresources,
allowUpdates
};
// By default, tokens are good for one hour.
// Override this default timeout by specifiying a new value (in seconds).
// For example, to generate a token good for 8 hours:
var capability = new TaskRouterCapability(
accountSid,
authToken,
workspaceSid,
null,
policies: policies,
expiration: DateTime.UtcNow.AddSeconds(28800) // 60 * 60 * 8
);
Console.WriteLine(capability.ToJwt());
} }
class PolicyUrlUtils {
const string taskRouterBaseUrl = "https://taskrouter.twilio.com";
const string taskRouterVersion = "v1";
readonly string _workspaceSid;
readonly string _taskQueueSid;
public PolicyUrlUtils(string workspaceSid, string taskQueueSid)
{
_workspaceSid = workspaceSid;
_taskQueueSid = taskQueueSid;
}
public string TaskQueue => $"{Workspace}/TaskQueue/{_taskQueueSid}";
string Workspace =>
$"{taskRouterBaseUrl}/{taskRouterVersion}/Workspaces/{_workspaceSid}";
}
This gives me an exception on the last line(capability.ToJwt()). Exception is:
System.NullReferenceException occurred
HResult=0x80004003
Message=Object reference not set to an instance of an object.
Source=<Cannot evaluate the exception source>
StackTrace:
at Twilio.Jwt.Taskrouter.TaskRouterCapability.get_Claims()
at Twilio.Jwt.BaseJwt.ToJwt()
at DeleteMe.Program.Main(String[] args) in D:\Projects\DeleteMe\DeleteMe\Program.cs:line 46
Now, I looked at the source code of TaskRouterCapability at twilio-csharp github, and seems to be the queue sid should be passed as the channel parameter. When I do this, the token is created. So I took the token generated here, and put it into this HTML file:
<html>
<head>
<script type="text/javascript" src="https://media.twiliocdn.com/taskrouter/js/v1.11/taskrouter.min.js"></script>
<script type="text/javascript">
var taskQueue = new Twilio.TaskRouter.TaskQueue("token generated by console application");
taskQueue.on("ready", function (taskQueue) {
console.log(taskQueue.sid) // 'WQxxx'
console.log(taskQueue.friendlyName) // 'Simple FIFO Queue'
console.log(taskQueue.targetWorkers) // '1==1'
console.log(taskQueue.maxReservedWorkers) // 20
});
</script>
</head>
<body>
</body>
This then gives me some messages in the console:
taskrouter.min.js:1 Websocket opened: wss://event-bridge.twilio.com/v1/wschannels/ACxxxxxxxxxxxxxx/WQxxxxxxxx?token=eyJh.....&closeExistingSessions=false
taskrouter.min.js:1 Received a message of type [connected]
taskrouter.min.js:1 POST https://event-bridge.twilio.com/v1/wschannels/ACxxxxxxxxx/WQxxxxxxxxxxx 403 (Policies defined such that we cannot access the given resource)
So the connected event is called, but the ready event never happens.

Turns out there are 2 errors in the sample:
The channel parameter of TaskRouterCapability constructor should be passed the TaskQueue Sid rather than null. A value of null causes an Exception
System.NullReferenceException occurred HResult=0x80004003 Message=Object reference not set to an instance of an object. Source= StackTrace: at Twilio.Jwt.Taskrouter.TaskRouterCapability.get_Claims() at Twilio.Jwt.BaseJwt.ToJwt()
There is a typo in the TaskQueue property of PolicyUrlUtils. The URL should have TaskQueues instead of TaskQueue
Have submitted a pull request for the same https://github.com/TwilioDevEd/api-snippets/pull/539

Related

Unable to get Twilio sms status callbacks when sending proactive message

I'm trying to track the sms delivery status of the messages I send using the bot framework. I'm using Twilio, and sending proactive messages. Right now I'm trying to do so with twilio status callbacks
This is similar to this question, I tried that approach but I couldn't get it to work. I've added my url on the TwiML app and it is not firing. I have double and triple checked, and I suspect this url is somehow ignored or not going through with my current set up. I don't get any callbacks on the proactive message nor on the replies the bot sends to the user. However the flow works fine and I can reply and get proper responses from the bot. Edit: calling this "approach 1"
approach 2: I've also tried this doing some light modifications on Twilio adapter, to be able to add my callback just before create message. (I changed it so it uses a customized client wrapper that adds my callback url when creating the twilio resource) This does work, partially: when I reply a message from my bot, I get the status callbacks. But as the proactive message is sent using the default adapter, I don't get a callback on the initial message.
approach 3: Finally, I also tried using the TwilioAdapter when sending the proactive message but for some reason as soon as I send an activity, the TurnContext is disposed, so I can't save the state or do any subsequent actions. This leads me to believe twilio adapter is not intended to be used this way (can't be used on proactive messages), but I'm willing to explore this path if necessary.
Here is the modified Twilio Adapter:
public class TwilioAdapterWithErrorHandler : TwilioAdapter
{
private const string TwilioNumberKey = "TwilioNumber";
private const string TwilioAccountSidKey = "TwilioAccountSid";
private const string TwilioAuthTokenKey = "TwilioAuthToken";
private const string TwilioValidationUrlKey = "TwilioValidationUrl";
public TwilioAdapterWithErrorHandler(IConfiguration configuration, ILogger<TwilioAdapter> logger, TwilioAdapterOptions adapterOptions = null)
: base(
new TwilioClientWrapperWithCallback(new TwilioClientWrapperOptions(configuration[TwilioNumberKey], configuration[TwilioAccountSidKey], configuration[TwilioAuthTokenKey], new Uri(configuration[TwilioValidationUrlKey]))), adapterOptions, logger)
{
OnTurnError = async (turnContext, exception) =>
{
// Log any leaked exception from the application.
logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");
Task[] tasks = {
// Send a message to the user
turnContext.SendActivityAsync("We're sorry but this bot encountered an error when processing your answer."),
// Send a trace activity, which will be displayed in the Bot Framework Emulator
turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError")
};
Task all = Task.WhenAll(tasks); //task with the long running tasks
await Task.WhenAny(all, Task.Delay(5000)); //wait with a timeout
};
}
}
Modified client Wrapper:
public class TwilioClientWrapperWithCallback : TwilioClientWrapper
{
public TwilioClientWrapperWithCallback(TwilioClientWrapperOptions options) : base(options) { }
public async override Task<string> SendMessageAsync(TwilioMessageOptions messageOptions, CancellationToken cancellationToken)
{
var createMessageOptions = new CreateMessageOptions(messageOptions.To)
{
ApplicationSid = messageOptions.ApplicationSid,
MediaUrl = messageOptions.MediaUrl,
Body = messageOptions.Body,
From = messageOptions.From,
};
createMessageOptions.StatusCallback = new System.Uri("https://myApp.ngrok.io/api/TwilioSms/SmsStatusUpdated");
var messageResource = await MessageResource.CreateAsync(createMessageOptions).ConfigureAwait(false);
return messageResource.Sid;
}
}
Finally, here's my summarized code that sends the proactive message:
[HttpPost("StartConversationWithSuperBill/{superBillId:long}")]
[HttpPost("StartConversationWithSuperBill/{superBillId:long}/Campaign/{campaignId:int}")]
public async Task<IActionResult> StartConversation(long superBillId, int? campaignId)
{
ConversationReference conversationReference = this.GetConversationReference("+17545517768");
//Start a new conversation.
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, async (turnContext, token) =>
{
await turnContext.SendActivityAsync("proactive message 1");
//this code was edited for brevity. Here I would start a new dialog that would cascade into another, but the end result is the same, as soon as a message is sent, the turn context is disposed.
await turnContext.SendActivityAsync("proactive message 2"); //throws ObjectDisposedException
}, default(CancellationToken));
var result = new { status = "Initialized fine!" };
return new JsonResult(result);
}
private ConversationReference GetConversationReference(string targetNumber)
{
string fromNumber = "+18632704234";
return new ConversationReference
{
User = new ChannelAccount { Id = targetNumber, Role = "user" },
Bot = new ChannelAccount { Id = fromNumber, Role = "bot" },
Conversation = new ConversationAccount { Id = targetNumber },
//ChannelId = "sms",
ChannelId = "twilio-sms", //appparently when using twilio adapter we need to set this. if using TwiML app and not using Twilio Adapter, use the above. Otherwise the frameworks interprets answers from SMS as new conversations instead.
ServiceUrl = "https://sms.botframework.com/",
};
}
(I can see that I could just call create conversation reference and do two callbacks, one for each message, but in my actual code I'm creating a dialog that sends one message and then invokes another dialog that starts another message)
Edit 2:
Some clarifications:
On approach 2, I'm using two adapters, as suggested by code sample and documentation on using twilio adapter. The controller that starts the proactive message uses an instance of a default adapter (similar to this one), and TwilioController (the one that gets the twilio incoming messages) uses TwilioAdapterWithErrorHandler.
On approach 3, I excluded the default adapter, and both controllers use TwilioAdapterWithErrorHandler.
Edit 3:
Here's a small repo with the issue.
I found a fix for this problem, around approach 3, by changing the overload I use for ContinueConversation. Replace this :
//Start a new conversation.
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, async (turnContext, token) =>
With this:
//Start a new conversation.
var twilioAdapter = (TwilioAdapterWithErrorHandler)_adapter;
await twilioAdapter.ContinueConversationAsync(_appId, conversationReference, async (context, token) =>
This way, the context is not disposed, an I can use the twilio adapter for the proactive message and have status callbacks on all messages.

Updating Data on a OneNote Page Error, "Timeouts are not supported on this stream"

I have the goal of being able to programmatically update OneNote page data using C#. The Microsoft Graph API reference documentation suggests this can only be done by page element, not by page, and gives the following C# Graph SDK example:
GraphServiceClient graphClient = new GraphServiceClient( authProvider );
var stream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(#"[
{
'target':'#para-id',
'action':'insert',
'position':'before',
'content':'<img src=""image-url-or-part-name"" alt=""image-alt-text"" />'
},
{
'target':'#list-id',
'action':'append',
'content':'<li>new-page-content</li>'
}
]
"));
var pages = new OnenotePage();
pages.Content = stream;
await graphClient.Me.Onenote.Pages["{onenotePage-id}"]
.Request()
.UpdateAsync(pages);
Below is the relevant snippet of my code:
GraphServiceClient client; // authenticated service client
CancellationToken cancellationToken; // a cancellation token
string userId; // identifier of user whose page contains the paragraph to be updated
string pageId; // identifier of page containing paragraph to be updated
string paragraphId; // identifier of paragraph to be updated
string filePath; // location of text file containing updated paragraph data
await client.Users[userId].Onenote.Pages[pageId]
.Request()
.UpdateAsync(new OnenotePage
{
Content = new MemoryStream(Encoding.UTF8.GetBytes(
// [
// {
// 'target':'{paragraphId}',
// 'action':'replace',
// 'content':'<p>{File.ReadAllText(filePath)}</p>'
// }
// ]
$"[{{'target':'{paragraphId}','action':'replace','content':'<p>{File.ReadAllText(filePath)}</p>'}}]"))
}, cancellationToken);
Microsoft's REST documentation includes PATCH /users/{id | userPrincipalName}/onenote/pages/{id}/content as a valid HTTP request, so my above code seems like it should work, even though it doesn't use the .Me option as in their example. For some reason, however, my code keeps throwing an InvalidOperationException, declaring that, "Timeouts are not supported on this stream," whenever it tries to execute the await command. Below are the details of the exception:
System.InvalidOperationException
HResult=0x80131509
Message=Timeouts are not supported on this stream.
Source=System.Private.CoreLib
StackTrace:
at System.IO.Stream.get_ReadTimeout()
When I try to run the raw REST command on the official Graph Explorer, I get a No Content - 204 message, confirming that the PATCH worked. Please note again, however, that I am instead simply using the C# MS Graph SDK.
Where am I going wrong? How can I accomplish my goal?
EDIT: I still don't have a solution to the SDK throwing InvalidOperationExceptions at me, and thus do not consider this matter resolved, but since the API seems to be working just fine, I went ahead and found a workaround to accomplish my goal. Posted here, in case anyone else encounters my same issue and needs something that works.
GraphServiceClient client; // authenticated service client
CancellationToken cancellationToken; // a cancellation token
string userId; // identifier of user whose page contains the paragraph to be updated
string pageId; // identifier of page containing paragraph to be updated
string paragraphId; // identifier of paragraph to be updated
string filePath; // location of text file containing updated paragraph data
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Patch,
client.Users[userId].Onenote.Pages[pageId].Content
.Request()
.RequestUrl)
{
Content = new StringContent(
// [
// {
// 'target':'{paragraphId}',
// 'action':'replace',
// 'content':'<p>{File.ReadAllText(filePath)}</p>'
// }
// ]
$"[{{'target':'{paragraphId}','action':'replace','content':'<p>{File.ReadAllText(filePath)}</p>'}}]",
Encoding.UTF8,
"application/json")
};
await client.AuthenticationProvider.AuthenticateRequestAsync(request);
await client.HttpProvider.SendAsync(request);

Azure function: send notification to specific users

i've written an Azure function and connected the output to a notification-hub to send push notifications using APNS. It works fine as long as i send the notification to all registered devices, but i don't know how to use tags in order to address a specific user. If i try to use a tag, i get an error message saying "Exception while executing function: Functions.SendSinglePushNotification. Microsoft.Azure.WebJobs.Host: Error while handling parameter notification after function returned:. Microsoft.Azure.NotificationHubs: notification.Tag property should be null."
Here's my code so far:
#r "Microsoft.Azure.NotificationHubs"
#r "Newtonsoft.Json"
using System;
using Microsoft.Azure.NotificationHubs;
using Newtonsoft.Json;using
Microsoft.Azure.WebJobs.Host.Bindings.Runtime;
public static void Run(HttpRequestMessage req, TraceWriter log,Binder
binder, out AppleNotification notification)
{
string user = "Test";
string tagExpression = "Test";
string userTag = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "userid", true) == 0)
.Value;
string apnsNotificationPayload = "{\"aps\": {\"alert\": \"Test: (" + user + ")\" }}";
notification = new AppleNotification(apnsNotificationPayload);
}
I was trying to use notification = new
AppleNotification(apnsNotificationPayload,tagExpression);
but that does not work. How can i achieve that?
Thanks a lot and best regards
I had similar issue. Eventually, what worked for me was constructing Notification client manually. I am developing functions in Visual Studio, so my code is slightly different from yours.
[FunctionName("MyFunction")]
public static async Task Run([ServiceBusTrigger("queuename", AccessRights.Listen, Connection =
"<connection-settings-name>")] string myQueueItem, TraceWriter log)
{
log.Info($"C# Queue trigger function processed: {myQueueItem}");
var notificationHubSas = "<DefaultFullSharedAccessSignature from Azure portal>";
var notificationHubName = "myhub";
var nhClient = NotificationHubClient.CreateClientFromConnectionString(notificationHubSas, notificationHubName);
var tags = "";
await nhClient.SendTemplateNotificationAsync(<notification payload>, tags);
}

HttpResponseMessage ReasonPhrase max length?

I have this code:
public void Put(int id, DistributionRuleModelListItem model)
{
CommonResultModel pre = new BLL.DistributionRules().Save(id, model, true);
if(!pre.success){
DAL.DBManager.DestroyContext();
var resp = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(string.Format("Internal server error for distruleId: {0}", id)),
ReasonPhrase = pre.message.Replace(Environment.NewLine, " ")//.Substring(0,400)
};
throw new HttpResponseException(resp);
}
}
There is logic that can set the value of pre.message to be an exception.ToString() and if it is too long i receive the following application exception:
Specified argument was out of the range of valid values. Parameter
name: value
But if I uncomment .Substring(0,400) everything works fine and on client side I receive the correct response and it is possible to show it to the user.
What is the max length of ReasonPhrase? I can't find any documentation that specifies this value.
I couldn't find the max value documented anywhere, however through trial and error, I found it to have a maximum length of 512 bytes.

Why is my DotNetOpenAuth consumer not respecting the version 1.0a?

I am building an OAuth service provider using DotNetOpenAuth, and to test it I have modified the sample wcf consumer to simply call a plain http endpoint. The token request works fine, but when I request access to a protected resource, I get the following protocol execption:
The following required parameters were missing from the DotNetOpenAuth.OAuth.Messages.AuthorizedTokenRequest message: oauth_verifier
When I look at the log output on my service provider I see this:
Error while performing basic validation of AuthorizedTokenRequest with these message parts:
oauth_token: pgzjBIs0pKCeDIcaIinyrV5Jhi0=
oauth_consumer_key: sampleconsumer
oauth_nonce: TM0Rc8kg
oauth_signature_method: HMAC-SHA1
oauth_signature: zmpxK5c69n1VzTEEcrnnd4e+qYI=
oauth_version: 1.0
oauth_timestamp: 1305067751
Notice the oauth_version: 1.0, even though I have specified ProtocolVersion.V10a when I create the consumer.
If I specify ProtocolVersion.V10 on both sides I get this exception:
Expected message DotNetOpenAuth.OAuth.Messages.AccessProtectedResourceRequest but received DotNetOpenAuth.OAuth.Messages.AuthorizedTokenRequest instead.
Here is the consumer code to get the token (this is straight from the sample code):
WebConsumer consumer = this.CreateConsumer();
UriBuilder callback = new UriBuilder(Request.Url);
callback.Query = null;
string[] scopes = (from item in this.scopeList.Items.OfType<ListItem>()
where item.Selected
select item.Value).ToArray();
string scope = string.Join("|", scopes);
var requestParams = new Dictionary<string, string> { { "scope", scope } };
var response = consumer.PrepareRequestUserAuthorization(callback.Uri, requestParams, null);
consumer.Channel.Send(response);
Here is my consumer code that is failing:
var accessToken = Session["WcfAccessToken"] as string;
var consumer = CreateConsumer();
var serviceEndpoint = new MessageReceivingEndpoint("https://mymachine/test/getUserName", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.PostRequest);
var httpRequest = consumer.PrepareAuthorizedRequest(serviceEndpoint, accessToken);
var httpResponse = httpRequest.GetResponse();
In my service provider I call serviceProvider.ReadProtectedResourceAuthorization(); and it fails with the exception I mentioned above.
Any ideas what I am doing wrong?
This was a silly mistake on my part, I was returning the wrong TokenType, from my IServiceProviderTokenManager. The correct logic is shown in the service provider sample, and looks something like this:
if (tokenObject.State == TokenAuthorizationState.AccessToken)
return TokenType.AccessToken;
return TokenType.RequestToken;

Resources