"Failed to issue Dequeue" when using Twilio Task Router for non-phone related tasks - twilio

NOTE: Code snippets below are functional. The "dequeue" error mentioned in this post was based on an existing Assignment Callback external to these scripts. Once the URL was removed and the reservation.dequeue moved to this code, the error was resolved.
We are in the process of developing a chat application using Conversations between two people. I currently have it "wired" up with the following steps when a user initiates the chat:
Conversation is created.
User is created.
User is added to the conversation.
Task is created with conversation meta-data in attributes.
(followed by steps on the other user's session to accept the reservation, etc.)
These steps work as expected, but a "40140 - Failed to issue Dequeue instruction due to missing 'call_sid' property" is generated since the task isn't an incoming phone call. I tried putting the task into the "SMS" Task Channel, but that didn't stop the error.
I couldn't find any specific documentation on creating non-phone call-based tasks so I might be setting up the task routing incorrectly.
Here are code snippets showing how I create (in .NET) the conversation, user, and task, and how I accept (in TaskRouter.js) the reservation.
/***********************************************************************************************************
This code is server-side in .NET
***********************************************************************************************************/
public ConversationCredentials CreateConversation( string program, string name )
{
var memberId = DateTime.Now.ToString( "yyyyMMdd" ); // Temporary
TwilioClient.Init( _twilioAccountSid,_twilioAuthToken );
// If we decide to keep conversations on Twilio, we should replace the memberid with phiid, since member id might change
var conversation = ConversationResource.Create(
friendlyName: memberId + "_" + DateTime.Now.ToString( "HHmmss" )
);
var conversationCredentials = JoinConversation( conversation.Sid, name );
var taskSid = CreateTask( program, conversation.Sid, memberId );
conversationCredentials.taskSid = taskSid;
return conversationCredentials;
}
public ConversationCredentials JoinConversation( string conversationSid, string name )
{
var identity = name + "_" + DateTime.Now.ToString( "HHmmss" ); // Makes sure the user is unique, in case it's an employee joining more than one chat session)
TwilioClient.Init( _twilioAccountSid,_twilioAuthToken );
var participant = ParticipantResource.Create(
pathConversationSid: conversationSid,
identity: identity
);
var user = UserResource.Update(
pathSid: identity,
friendlyName: name
);
var token = GetJWT( _twilioConversationServiceSid, name ); // Conversation Service Sid
var conversationCredentials = new ConversationCredentials();
conversationCredentials.token = token;
conversationCredentials.conversationSid = conversationSid;
conversationCredentials.participantSid = participant.Sid;
conversationCredentials.participantName = name;
conversationCredentials.participantIdentity = participant.Identity;
return conversationCredentials;
}
public string CreateTask( string program, string conversationSid, string memberId )
{
TwilioClient.Init( _twilioAccountSid, _twilioAuthToken );
var attributes = JsonConvert.SerializeObject( new Dictionary<string,Object>()
{
{"conversationSid", conversationSid },
{"memberId", memberId },
{"program", program },
{"call_sid", "CHAT" }
}, Formatting.Indented);
var task = TaskResource.Create(
attributes: attributes,
workflowSid: _twilioWorkflowSid,
pathWorkspaceSid: _twilioWorkspaceSid_Nurses,
taskChannel: "Default"
);
return task.Sid;
}
/***********************************************************************************************************
This code is browser-side using TaskRouter.js
NOTE: This handles both voice (works fine) and conversations (the part in question)
***********************************************************************************************************/
registerTaskRouterCallbacks( _this ) : void {
this.worker.on('ready', function(worker) {
_this.updateButton( worker.activityName, "" );
});
this.worker.on("reservation.created", function(reservation) {
if ( reservation.task.attributes.type != "CHAT" )
{
_this.updateButton( "Call", reservation.task.attributes.from.replace( "+1", "" ) );
reservation.dequeue();
} else {
_this.updateButton( "Chat", reservation.task.attributes.memberId );
confirm("You have an incoming chat!");
reservation.accept();
// This is where the chat window would pop-up
}
});
this.worker.on("reservation.accepted", function(reservation) {
_this.worker.update({"ActivitySid": _this.activitySids["Busy"][0].sid});
_this.updateButton( "Busy", "" );
});

The "dequeue" error mentioned in this post was based on an existing Assignment Callback external to these scripts. Once the URL was removed and the reservation.dequeue moved to this code, the error was resolved.

Related

How can I access a Teams user's email address?

I'm trying to build a Microsoft Teams integration for an app, but I'm having some trouble getting a user's email address.
I used the Microsoft Teams extension for VS Code to scaffold a basic app. I'm using the BotFramework v4 (NodeJS) on my server. I'm able to receive requests from Teams and respond to them as well.
To get an user's email address, I am using the TeamsInfo.getMember(context, id) method, where the id is obtained from context.activity.from object. Unfortunately, calling this method results in a RestError: Unknown with a status code of 400.
I'm not sure what I'm missing here. My app is registered with the Azure Active Directory and has the User.Read.All permission. Am I missing something here?
Any help would be appreciated!
For some context, I'm trying to build a Messaging Extension Action Command.
Code:
import {
TurnContext,
TeamsActivityHandler,
CardFactory,
MessagingExtensionAction,
TeamsInfo,
} from 'botbuilder';
export default class TeamsMessagingExtensionsActionBot extends TeamsActivityHandler {
constructor() {
super();
}
// #ts-ignore
handleTeamsMessagingExtensionSubmitAction(
context: TurnContext,
action: MessagingExtensionAction,
) {
switch (action.commandId) {
case 'someCommand':
return handleCommand(context, action);
default:
throw new Error('NotImplemented');
}
}
}
async function handleCommand(
context: TurnContext,
action: MessagingExtensionAction,
) {
const card = CardFactory.heroCard(
'Some Command',
'We have received your command!',
);
const user = await TeamsInfo.getMember(context, context.activity.from.id);
console.log('User:', user);
const attachment = {
contentType: card.contentType,
content: card.content,
preview: card,
};
return {
composeExtension: {
type: 'result',
attachmentLayout: 'list',
attachments: [attachment],
},
};
}
This is the error I get when calling TeamsInfo.getMember(): JSON

How do I set custom parameters for Twilio TVOCallInvite on iOS?

I'm trying to pass custom parameters in call invite structure but don't receive them on the recipient side.
let connectOptions: TVOConnectOptions = TVOConnectOptions(accessToken: token) { (builder) in
builder.params = ["To": "Recipeint Id",
"From": "Caller name"]
builder.uuid = uuid
}
self.call = TwilioVoice.connect(with: connectOptions, delegate: self)
Any ideas?
You need to add logic in backend or you can say server code for the same.
Link for server code in node
https://github.com/twilio/voice-quickstart-server-node
need to modify below function
function makeCall(request, response) {
// The recipient of the call, a phone number or a client
var to = null;
if (request.method == 'POST') {
to = request.body.to;
callerId = request.body.from; //--> new added line for caller name
} else {
to = request.query.to;
callerId = request.body.from; //--> new added line for caller name
}
const voiceResponse = new VoiceResponse();
if (!to) {
voiceResponse.say("Congratulations! You have made your first call! Good bye.");
} else if (isNumber(to)) {
const dial = voiceResponse.dial({callerId : callerNumber});
dial.number(to);
} else {
const dial = voiceResponse.dial({callerId : callerId});
dial.client(to);
}
console.log('Response:' + voiceResponse.toString());
return response.send(voiceResponse.toString());
}
also need make var instead of const for callerId variable , and If you are passing caller name then node code format should save value in this format
'client:callername'
i.e. client: and after that value which is being passed from iOS app
Gave up actually with this idea... Followed this instruction so that the app reports a call to CallKit and then updates the caller name after that.

YouTube Analytics API End Screen Report, how can I determine End Screen Element?

For an End Screens report, how can I determine which target is represented by the end_screen_element_id on each record?
For example: on my channel, let's say I have "Video 1" set up with two video end screen elements "Video 2" and Video 3". I want to know how many clicks at the end of "Video 1" went to "Video 2" and how many went to "Video 3".
The data returned for this report gives me a video_id field indicating which video was watched and an end_screen_element_clicks field indicating how many times a viewer clicked through to the end screen video...
...HOWEVER, the only identifier for which video the viewer clicked through to is an end_screen_element_id field, which looks like a GUID and apparently somehow refers to the full end screen element definition, and therefore presumably what video is represented by that definition.
I'm unable to find any reports or other API calls for getting detail information on that end_screen_element_id field, particularly which video it represents.
How can I use that field or otherwise figure out what end screen video the viewer clicked through to?
More Information
The data returned looks like this:
Data returned in the End Screens report
Here's a screenshot that may help explain what I'm trying to do with the data: YouTube Analytics screen shot
The following C# code demonstrates how the report is requested:
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = CLIENT_ID,
ClientSecret = CLIENT_SECRET
},
new[] { YouTubeReportingService.Scope.YtAnalyticsReadonly },
"user",
CancellationToken.None,
new FileDataStore("Drive.Auth.Store")).Result;
YouTubeReportingService reportingService = new YouTubeReportingService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = APPLICATION_NAME
});
// Submit a report job to obtain the latest End Screen statistics.
Job channelEndScreenJob = new Job();
channelEndScreenJob.ReportTypeId = "channel_end_screens_a1";
channelEndScreenJob.Name = JOB_NAME;
Job createdJob =
reportingService.Jobs.Create(channelEndScreenJob).Execute();
A separate service retrieves the report like this:
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = CLIENT_ID,
ClientSecret = CLIENT_SECRET
},
new[] { YouTubeReportingService.Scope.YtAnalyticsReadonly },
"user",
CancellationToken.None,
new FileDataStore("Drive.Auth.Store")).Result;
YouTubeReportingService reportingService = new YouTubeReportingService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = APPLICATION_NAME
});
// Retrieve data from jobs that were previously submitted.
ListJobsResponse jobList = reportingService.Jobs.List().Execute();
if (jobList.Jobs != null)
{
foreach (Job job in jobList.Jobs)
{
ListReportsResponse reportList = reportingService.Jobs.Reports.List(job.Id).Execute();
if (reportList.Reports != null)
{
foreach (Report report in reportList.Reports)
{
MediaResource.DownloadRequest getRequest = reportingService.Media.Download("");
// Download the report data.
using (MemoryStream stream = new MemoryStream())
{
getRequest.MediaDownloader.Download(report.DownloadUrl, stream);
stream.Flush();
stream.Position = 0;
using (StreamReader reader = new StreamReader(stream))
{
// Parse report...
DataTable parsedReport = ReportToDataTable(reader.ReadToEnd());
// ...and get data on videos watched and videos clicked to.
foreach (DataRow row in parsedReport.Rows)
{
string videoWatched = row["video_id"].ToString();
string videoClickedToFromEndScreen = **WHAT???**
}
}
}
}
}
}
}

Embeded Watson Virtual Agent chatbot missing response

I've created an html file with embedded Watson Virtual Agent chat bot, code similar below, with WVA strictly using the building core capabilities:
IBMChat.init({
el: 'ibm_chat_root',
baseURL: 'https://api.ibm.com/virtualagent/run/api/v1',
botID: '',
XIBMClientID: '',
XIBMClientSecret: ''
});
What I noticed is if I run the WVA in Preview mode, and have input "pay bill", the WVA can come back with two piece response, with first:
Accessing your account information...
and second the make payment:
Your account balance is $42.01 due on 5/17/2017. What would you like to do? (More options coming soon!)
However, if I enter the same in my HTML chatbot, the response only comes back with the first part:
Accessing your account information...
and second part never comes out.
Does anyone else experience the same problem?
The version in the "Preview" mode has some mock "action" handlers setup. Obviously, not every one of you users would owe $42! In the sample code on the github, the mock action handlers are not setup. There are examples on how to subscribe to those action events with handlers here: https://github.com/watson-virtual-agents/chat-widget/tree/master/examples/basic-actions-example
As of 5/31/17 you can cover all the built in actions using the code snippet below...
const config = { instance: null };
const getUserProfileVariablesMap = {
'bill_amount': '42.01',
'payment_due_date': (() => {
const currentDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
return `${currentDate.getMonth() + 1}/${currentDate.getDate()}/${currentDate.getFullYear()}`;
})(),
'authorized_users': 'Bob Everyman and Jane Doe'
};
const getUserProfileVariables = (data) => {
const variables = data.message.action.args.variables;
variables.forEach(v => {
const value = getUserProfileVariablesMap[v];
(value) ? config.instance.profile.set(v, value) : config.instance.profile.set(v, '[sample data]');
});
config.instance.sendSilently('success');
};
const success = () => config.instance.sendSilently('success');
const agent = () => config.instance.receive('On your own site you would run code to connect to an agent now.');
const accountSettings = () => config.instance.receive('On your own site you would run code to open the Account Settings page now.');
function registerActions(instance) {
config.instance = instance;
instance.subscribe('action:getUserProfileVariables', getUserProfileVariables);
instance.subscribe('action:updateAddress', success);
instance.subscribe('action:updateUserName', success);
instance.subscribe('action:updatePhoneNumber', success);
instance.subscribe('action:updateEmail', success);
instance.subscribe('action:payBill', success);
instance.subscribe('action:sendPaymentReceipt', success);
instance.subscribe('action:agent', agent);
instance.subscribe('action:openAccountSettingsPage', accountSettings);
};
window.IBMChatActions = {
registerActions: registerActions
};
// window.IBMChatActions.registerActions(window.IBMChat);
On the Administrative Preview, you are getting fake code stubs that handle action requests from the agent.
When one of these actions are invoked, the widget will print the "Processing..." message and then invoke all registered subscribers for that action. It is up to these registered subscribers to continue the conversation flow by silently sending "success", "failure", or "cancel" back to the server.
For example, the agent might pass down the "payBill" action. You would want to call your payment gateway, determine if it was successful, and then notify the agent of the result:
IBMChat.init(/* Settings */);
IBMChat.subscribe('action:payBill', function() {
var data = {
amount: IBMChat.profile.get('amount'),
card: {
number: IBMChat.profile.get('cc_number'),
// ... other private card data
}
};
$.post('https://www.myserver.com/payment-gateway', data)
.done( function() {
IBMChat.sendSilently('success');
})
.fail( function() {
IBMChat.sendSilently('failure');
});
});
Actions Documentation
https://github.com/watson-virtual-agents/chat-widget/blob/master/docs/DOCS.md#actions

logging in with different user / resource owner

i am trying to write a tool that creates entries in the google calendar.
after following the google docs and creating an client-identifier/secret in the api console, i managed to put together a client that authenticates correctly and shows my registered google calendars. right now for me it looks like my google-account is somehow tied to my client-identifier/secret. what i want to know is: how can i change the auth process so that it is possible for an other user of this tool to enter his google-id and get access to his calendars?
EDIT: in other words (used in the RFC): I want make the resource owner-part editable while leaving the client-part unchanged. but my example, although working, ties together client and resource owner.
here is my app that works fine so far:
public void Connect()
{
var provider = new NativeApplicationClient(GoogleAuthenticationServer.Description);
provider.ClientIdentifier = "123456123456.apps.googleusercontent.com";
provider.ClientSecret = "nASdjKlhnaxEkasDhhdfLklr";
var auth = new OAuth2Authenticator<NativeApplicationClient>(provider, GetAuthorization);
var service = new CalendarService(auth);
//Events instances = service.Events.Instances("primary", "recurringEventId").Fetch();
var list = service.CalendarList.List().Fetch();
foreach (var itm in list.Items)
Console.WriteLine(itm.Summary);
}
private static readonly byte[] AditionalEntropy = { 1, 2, 3, 4, 5 };
private static IAuthorizationState GetAuthorization(NativeApplicationClient arg)
{
var state = new AuthorizationState(new[] { CalendarService.Scopes.Calendar.GetStringValue() });
state.Callback = new Uri(NativeApplicationClient.OutOfBandCallbackUrl);
var refreshToken = LoadRefreshToken();
if (!String.IsNullOrWhiteSpace(refreshToken))
{
state.RefreshToken = refreshToken;
if (arg.RefreshToken(state))
return state;
}
var authUri = arg.RequestUserAuthorization(state);
// Request authorization from the user (by opening a browser window):
Process.Start(authUri.ToString());
var frm = new FormAuthCodeInput();
frm.ShowDialog();
// Retrieve the access token by using the authorization code:
var auth = arg.ProcessUserAuthorization(frm.txtAuthCode.Text, state);
StoreRefreshToken(state);
return auth;
}
private static string LoadRefreshToken()
{
try
{
return Encoding.Unicode.GetString(ProtectedData.Unprotect(Convert.FromBase64String(Properties.Settings.Default.RefreshToken), AditionalEntropy, DataProtectionScope.CurrentUser));
}
catch
{
return null;
}
}
private static void StoreRefreshToken(IAuthorizationState state)
{
Properties.Settings.Default.RefreshToken = Convert.ToBase64String(ProtectedData.Protect(Encoding.Unicode.GetBytes(state.RefreshToken), AditionalEntropy, DataProtectionScope.CurrentUser));
Properties.Settings.Default.Save();
}
Prompt the user to enter their ClientIdentifier and ClientSecret, then pass these values to your Connect method.
i solved the problem myself.
the problem was, that i'm usually always connected to google and because i did't log out from google before my app redirected to google to get the access-token, google automatically generated the access-token for my account - skipping the part where an input-form appears where anyone could enter his/her user-credentials to let google generate an access-token for his/her account.

Resources