Related
fine people of Stack Overflow.
I'm trying to solve a problem I'm having involving twilio functions, messaging services, and databases.
What I'm attempting to do is send a message to all members of a database at once.
My code is a mess, as Javascript isn't my native language and I'm rather new to twilio.
The problem I believe I'm having is with the async/await feature of javascript.
Here is my code so far:
// Boiler Plate Deta Code
const { Deta } = require("deta");
// Function to access database and get object of conta
async function FetchDB(db) {
let res = await db.fetch();
allItems = res.items;
// continue fetching until last is not seen
while (res.last){
res = await db.fetch({}, {last: res.last});
allItems = allItems.concat(res.items);
}
}
// Function to get total number of contacts.
async function ReturnNumberOfContacts(allItems) {
number_of_contacts = allItems.length;
}
// Function to send message to contact in database.
async function SendMessages(allItems, message) {
allItems.forEach(contact => {
let users_name = contact.name
client.messages
.create({
body: `Hey ${users_name}! ${message}`,
messagingServiceSid: messaging_service,
to: contact.key
})
});
}
// Function to submit response to broadcaster.
async function SuccessResponse(user_name, number_of_contacts) {
responseObject = {
"actions": [
{
"say": `${user_name}, your broadcast has successfully sent to ${number_of_contacts} contacts.`
},
{
"listen": true
}
]
}
}
// Main Function
exports.handler = async function(context, event, callback) {
// Placeholder for number of contacts
let number_of_contacts;
// Place holder for object from database of all contacts
let allItems;
// Placeholder for users message
let message;
// Placeholder for response to user
let responseObject;
//Twilio and Deta, Etc Const
const client = require('twilio')(context.ACCOUNT_SID, context.AUTH_TOKEN);
const deta = Deta(context.DETA_PROJECT_KEY);
const db = deta.Base("users2");
const messaging_service = context.MESSAGING_SERVICE;
// From Phone Number
const from = event.UserIdentifier;
// Parse memory
const memory = JSON.parse(event.Memory);
// Fetch all items from database and return total number of contacts.
// Update relavent variables
await FetchDB(db, allItems).then(ReturnNumberOfContacts(allItems));
// Figure out if message came from short circuit broadcast or normal
if (memory.triggered) {
message = memory.message;
} else {
message = memory.twilio.collected_data.broadcast_message.answers.message_input.answer;
}
// Check if verified and set name.
const current_user = await db.get(from);
// Get the current users name or set a default value
let user_name = current_user.name || "friend";
// Determine if user is an authorized broadcaster
if (from === context.BROADCAST_NUMBER) {
// Decide if the sending of a message should be cancelled.
if (message.toLowerCase() === "c" || message.toLowerCase() === "cancel") {
responseObject = {
"actions": [
{
"say": `${user_name}, you have canceled your request and no messages have been sent.`
},
{
"listen": false
}
]
}
// Return Callback and end task
callback(null, responseObject);
}
// Move forward with sending a message.
else {
// Send message to users in database and send success message to broadcaster.
await SendMessages(message, client, messaging_service)
.then(SuccessResponse(user_name, number_of_contacts))
return callback(null, responseObject);
}
// The user is not authorized so return this.
}
return callback(null, {
"actions": [
{
"say": "You are not authorized to broadcast."
},
{
"listen": false
}
]
})
};
So when the Fetch() function is triggered, I want the database to load a list of everyone and have twilio send them the desired message saved in the message variable. I have the code working so that I can read from the database and get the proper values, and send a single text message with the desired message, but the problem I'm having now is integrating it all together.
Thanks if anyone can point me in the right direction here.
Again, I'm new to javascript and more specifically asynchronous programming.
Twilio developer evangelist here.
The issue from the error says that allItems is undefined when you call ReturnNumberOfContacts.
I think the issue comes from trying to use allItems as a sort of global variable, same for number_of_contacts. It would be better for FetchDB to resolve with the list of items and ReturnNumberOfContacts to resolve with the number of the items.
You also have some arguments missing when you call SendMessages in your function. I've updated it to the point that I think it will work:
// Boiler Plate Deta Code
const { Deta } = require("deta");
// Function to access database and get object of conta
async function FetchDB(db) {
let res = await db.fetch();
let allItems = res.items;
// continue fetching until last is not seen
while (res.last) {
res = await db.fetch({}, { last: res.last });
allItems = allItems.concat(res.items);
}
return allItems;
}
// Function to send message to contact in database.
async function SendMessages(allItems, message, client, messagingService) {
return Promise.all(
allItems.map((contact) => {
let usersName = contact.name;
return client.messages.create({
body: `Hey ${usersName}! ${message}`,
messagingServiceSid: messagingService,
to: contact.key,
});
})
);
}
// Main Function
exports.handler = async function (context, event, callback) {
// Placeholder for users message
let message;
//Twilio and Deta, Etc Const
const client = require("twilio")(context.ACCOUNT_SID, context.AUTH_TOKEN);
const deta = Deta(context.DETA_PROJECT_KEY);
const db = deta.Base("users2");
const messagingService = context.MESSAGING_SERVICE;
// From Phone Number
const from = event.UserIdentifier;
// Parse memory
const memory = JSON.parse(event.Memory);
// Fetch all items from database and return total number of contacts.
// Update relavent variables
const allItems = await FetchDB(db);
const numberOfContacts = allItems.length;
// Figure out if message came from short circuit broadcast or normal
if (memory.triggered) {
message = memory.message;
} else {
message =
memory.twilio.collected_data.broadcast_message.answers.message_input
.answer;
}
// Check if verified and set name.
const currentUser = await db.get(from);
// Get the current users name or set a default value
let userName = currentUser.name || "friend";
// Determine if user is an authorized broadcaster
if (from === context.BROADCAST_NUMBER) {
// Decide if the sending of a message should be cancelled.
if (message.toLowerCase() === "c" || message.toLowerCase() === "cancel") {
// Return Callback and end task
callback(null, {
actions: [
{
say: `${userName}, you have canceled your request and no messages have been sent.`,
},
{
listen: false,
},
],
});
}
// Move forward with sending a message.
else {
// Send message to users in database and send success message to broadcaster.
await SendMessages(allItems, message, client, messagingService);
return callback(null, {
actions: [
{
say: `${userName}, your broadcast has successfully sent to ${numberOfContacts} contacts.`,
},
{
listen: true,
},
],
});
}
// The user is not authorized so return this.
}
return callback(null, {
actions: [
{
say: "You are not authorized to broadcast.",
},
{
listen: false,
},
],
});
};
What I did here was change FetchDB to only take the db as an argument, then to create a local allItems variable that collects all the contacts and then returns them.
async function FetchDB(db) {
let res = await db.fetch();
let allItems = res.items;
// continue fetching until last is not seen
while (res.last) {
res = await db.fetch({}, { last: res.last });
allItems = allItems.concat(res.items);
}
return allItems;
}
This is then called in the main body of the function to assign a local variable. I also replaced the ReturnNumberOfContacts function with a simple assignment.
const allItems = await FetchDB(db);
const numberOfContacts = allItems.length;
One thing you may want to consider is how many contacts you are trying to send messages to during this function. There are a few limits you need to be aware of.
Firstly, Function execution time is limited to 10 seconds so you need to make sure you can load and send all your messages within that amount of time if you want to use a Twilio Function for this.
Also, there are limits for the number of concurrent connections you can make to the Twilio API. That limit used to be 100 connections per account, but it may vary these days. When sending asynchronous API requests as you do in JavaScript, the platform will attempt to create as many connections to the API that it can in order to trigger all the requests asynchronously. If you have more than 100 contacts you are trying to send messages to here, that will quickly exhaust your available concurrent connections and you will receive 429 errors. You may choose to use a queue, like p-queue, to ensure your concurrent connections never get too high. The issue in this case is that it will then take longer to process the queue which brings me back to the original limit of 10 seconds of function execution.
So, I think the above code may work in theory now, but using it in practice may have other issues that you will need to consider.
fine people of Stack Overflow.
I'm trying to solve a problem I'm having involving twilio functions, messaging services, and databases.
What I'm attempting to do is send a message to all members of a database at once.
My code is a mess, as Javascript isn't my native language and I'm rather new to twilio.
The problem I believe I'm having is with the async/await feature of javascript.
Here is my code so far:
// Boiler Plate Deta Code
const { Deta } = require("deta");
// Function to access database and get object of conta
async function FetchDB(db) {
let res = await db.fetch();
allItems = res.items;
// continue fetching until last is not seen
while (res.last){
res = await db.fetch({}, {last: res.last});
allItems = allItems.concat(res.items);
}
}
// Function to get total number of contacts.
async function ReturnNumberOfContacts(allItems) {
number_of_contacts = allItems.length;
}
// Function to send message to contact in database.
async function SendMessages(allItems, message) {
allItems.forEach(contact => {
let users_name = contact.name
client.messages
.create({
body: `Hey ${users_name}! ${message}`,
messagingServiceSid: messaging_service,
to: contact.key
})
});
}
// Function to submit response to broadcaster.
async function SuccessResponse(user_name, number_of_contacts) {
responseObject = {
"actions": [
{
"say": `${user_name}, your broadcast has successfully sent to ${number_of_contacts} contacts.`
},
{
"listen": true
}
]
}
}
// Main Function
exports.handler = async function(context, event, callback) {
// Placeholder for number of contacts
let number_of_contacts;
// Place holder for object from database of all contacts
let allItems;
// Placeholder for users message
let message;
// Placeholder for response to user
let responseObject;
//Twilio and Deta, Etc Const
const client = require('twilio')(context.ACCOUNT_SID, context.AUTH_TOKEN);
const deta = Deta(context.DETA_PROJECT_KEY);
const db = deta.Base("users2");
const messaging_service = context.MESSAGING_SERVICE;
// From Phone Number
const from = event.UserIdentifier;
// Parse memory
const memory = JSON.parse(event.Memory);
// Fetch all items from database and return total number of contacts.
// Update relavent variables
await FetchDB(db, allItems).then(ReturnNumberOfContacts(allItems));
// Figure out if message came from short circuit broadcast or normal
if (memory.triggered) {
message = memory.message;
} else {
message = memory.twilio.collected_data.broadcast_message.answers.message_input.answer;
}
// Check if verified and set name.
const current_user = await db.get(from);
// Get the current users name or set a default value
let user_name = current_user.name || "friend";
// Determine if user is an authorized broadcaster
if (from === context.BROADCAST_NUMBER) {
// Decide if the sending of a message should be cancelled.
if (message.toLowerCase() === "c" || message.toLowerCase() === "cancel") {
responseObject = {
"actions": [
{
"say": `${user_name}, you have canceled your request and no messages have been sent.`
},
{
"listen": false
}
]
}
// Return Callback and end task
callback(null, responseObject);
}
// Move forward with sending a message.
else {
// Send message to users in database and send success message to broadcaster.
await SendMessages(message, client, messaging_service)
.then(SuccessResponse(user_name, number_of_contacts))
return callback(null, responseObject);
}
// The user is not authorized so return this.
}
return callback(null, {
"actions": [
{
"say": "You are not authorized to broadcast."
},
{
"listen": false
}
]
})
};
So when the Fetch() function is triggered, I want the database to load a list of everyone and have twilio send them the desired message saved in the message variable. I have the code working so that I can read from the database and get the proper values, and send a single text message with the desired message, but the problem I'm having now is integrating it all together.
Thanks if anyone can point me in the right direction here.
Again, I'm new to javascript and more specifically asynchronous programming.
Twilio developer evangelist here.
The issue from the error says that allItems is undefined when you call ReturnNumberOfContacts.
I think the issue comes from trying to use allItems as a sort of global variable, same for number_of_contacts. It would be better for FetchDB to resolve with the list of items and ReturnNumberOfContacts to resolve with the number of the items.
You also have some arguments missing when you call SendMessages in your function. I've updated it to the point that I think it will work:
// Boiler Plate Deta Code
const { Deta } = require("deta");
// Function to access database and get object of conta
async function FetchDB(db) {
let res = await db.fetch();
let allItems = res.items;
// continue fetching until last is not seen
while (res.last) {
res = await db.fetch({}, { last: res.last });
allItems = allItems.concat(res.items);
}
return allItems;
}
// Function to send message to contact in database.
async function SendMessages(allItems, message, client, messagingService) {
return Promise.all(
allItems.map((contact) => {
let usersName = contact.name;
return client.messages.create({
body: `Hey ${usersName}! ${message}`,
messagingServiceSid: messagingService,
to: contact.key,
});
})
);
}
// Main Function
exports.handler = async function (context, event, callback) {
// Placeholder for users message
let message;
//Twilio and Deta, Etc Const
const client = require("twilio")(context.ACCOUNT_SID, context.AUTH_TOKEN);
const deta = Deta(context.DETA_PROJECT_KEY);
const db = deta.Base("users2");
const messagingService = context.MESSAGING_SERVICE;
// From Phone Number
const from = event.UserIdentifier;
// Parse memory
const memory = JSON.parse(event.Memory);
// Fetch all items from database and return total number of contacts.
// Update relavent variables
const allItems = await FetchDB(db);
const numberOfContacts = allItems.length;
// Figure out if message came from short circuit broadcast or normal
if (memory.triggered) {
message = memory.message;
} else {
message =
memory.twilio.collected_data.broadcast_message.answers.message_input
.answer;
}
// Check if verified and set name.
const currentUser = await db.get(from);
// Get the current users name or set a default value
let userName = currentUser.name || "friend";
// Determine if user is an authorized broadcaster
if (from === context.BROADCAST_NUMBER) {
// Decide if the sending of a message should be cancelled.
if (message.toLowerCase() === "c" || message.toLowerCase() === "cancel") {
// Return Callback and end task
callback(null, {
actions: [
{
say: `${userName}, you have canceled your request and no messages have been sent.`,
},
{
listen: false,
},
],
});
}
// Move forward with sending a message.
else {
// Send message to users in database and send success message to broadcaster.
await SendMessages(allItems, message, client, messagingService);
return callback(null, {
actions: [
{
say: `${userName}, your broadcast has successfully sent to ${numberOfContacts} contacts.`,
},
{
listen: true,
},
],
});
}
// The user is not authorized so return this.
}
return callback(null, {
actions: [
{
say: "You are not authorized to broadcast.",
},
{
listen: false,
},
],
});
};
What I did here was change FetchDB to only take the db as an argument, then to create a local allItems variable that collects all the contacts and then returns them.
async function FetchDB(db) {
let res = await db.fetch();
let allItems = res.items;
// continue fetching until last is not seen
while (res.last) {
res = await db.fetch({}, { last: res.last });
allItems = allItems.concat(res.items);
}
return allItems;
}
This is then called in the main body of the function to assign a local variable. I also replaced the ReturnNumberOfContacts function with a simple assignment.
const allItems = await FetchDB(db);
const numberOfContacts = allItems.length;
One thing you may want to consider is how many contacts you are trying to send messages to during this function. There are a few limits you need to be aware of.
Firstly, Function execution time is limited to 10 seconds so you need to make sure you can load and send all your messages within that amount of time if you want to use a Twilio Function for this.
Also, there are limits for the number of concurrent connections you can make to the Twilio API. That limit used to be 100 connections per account, but it may vary these days. When sending asynchronous API requests as you do in JavaScript, the platform will attempt to create as many connections to the API that it can in order to trigger all the requests asynchronously. If you have more than 100 contacts you are trying to send messages to here, that will quickly exhaust your available concurrent connections and you will receive 429 errors. You may choose to use a queue, like p-queue, to ensure your concurrent connections never get too high. The issue in this case is that it will then take longer to process the queue which brings me back to the original limit of 10 seconds of function execution.
So, I think the above code may work in theory now, but using it in practice may have other issues that you will need to consider.
I'm trying to implement a simple call whisper that lets our agent know which phone number/product has been dialed. When a call comes in, it goes through a studio flow where the caller chooses a language. The call is then routed to a taskrouter queue via the "Enqueue" widget. From what I've read in the documentation, I need to pass the callback function a instruction: 'call' parameter in order to be able to specify a URL to be "played" to the agent.
I'm currently limited to implementing everything inside of twilio itself via Studio and Functions.
Why isn't this working, and what specifically do I need to do?
Thanks!
Assignment Callback Function
exports.handler = function(context, event, callback) {
const client = context.getTwilioClient();
let eventInfo = JSON.parse(event.TaskAttributes);
// const response = new Twilio.twiml.VoiceResponse();
console.log(Object.keys(event));
console.log(JSON.parse(event.TaskAttributes));
console.log(Object.keys(JSON.parse(event.TaskAttributes)));
// console.log(JSON.parse(event.WorkerAttributes));
const worker = JSON.parse(event.WorkerAttributes);
// console.log("ReservationSid: " + event.ReservationSid);
// console.log(typeof eventInfo);
// // ATTEMPT 1
// // This works to dequeue a call to a worker, but there is no whisper functionality
// callback(null, {
// 'instruction':'dequeue',
// 'post_work_activity_sid' : 'WORKSPACEACTIVITYSID',
// 'from' : eventInfo.from
// });
// // ATTEMPT 2
// // This works to play the whisper to the agent, but the caller is never connected to the agent.
let callbackURL = 'TWILIOFUNCTIONURL';
callback(null, {
'instruction':'call',
'post_work_activity_sid' : 'WORKSPACEACTIVITYSID',
'from' : eventInfo.from,
'url' : callbackURL
});
// // ATTEMPT 3 - Building a twiml version of attempt #2
// // This doesn't work.
//
// let callbackURL = 'TWILIOFUNCTIONURL';
// let twiml = new Twilio.twiml.VoiceResponse();
// const dial = twiml.dial();
// dial.queue({
// 'instruction':'call',
// // 'post_work_activity_sid' : 'WORKSPACEACTIVITYSID',
// 'from' : eventInfo.from,
// 'accept' : true,
// 'url' : callbackURL,
// // 'reservationSid' : event.ReservationSid
// }, event.selected_language);
// console.log(dial.toString());
// console.log(twiml.toString());
// callback(null, twiml);
// // ATTEMPT 4 - Build a twiml version of attempt #1
// // This doesn't work.
//
// let twiml = new Twilio.twiml.VoiceResponse();
// twiml.dial({
// 'instruction':'dequeue',
// 'post_work_activity_sid' : 'WORKSPACEACTIVITYSID',
// 'from' : eventInfo.from
// // 'to' : 'WORKSPACEQUEUESID'
// });
// console.log(twiml.toString());
// callback(null, twiml);
};
Callback URL containing queue announcement stuff.
exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse();
// let client = context.getTwilioClient();
// console.log("BEGIN Queue Announcer - context");
// console.log(Object.keys(context));
// console.log(context);
// console.log("BEGIN Queue Announcer - event");
// console.log(Object.keys(event));
// console.log(event);
// console.log("BEGIN Queue Announcer - client");
// console.log(Object.keys(client));
// console.log(client);
// console.log("END Queue Announcer");
// Random attempt to get the call info
// console.log("BEGIN Queue Announcer - CallSid");
// console.log(event.CallSid);
// client.calls(event.CallSid)
// .fetch()
// .then(call => console.log("Call: " + call));
// console.log("END Queue Announcer - CallSid");
let client = context.getTwilioClient();
twiml.say("QUEUE NAME GOES HERE");
twiml.dial()
.queue({
// 'reservationSid' : event.ReservationSid
}, 'english');
callback(null, twiml);
};
Here's what I did that worked. This is contained within a function that is effectively the "assignment callback URL".
Steps:
Use the 'echo' twimlet to generate the twiml. Modify the supplied URL to include the 'event.RegistrationSid' where appropriate.
Accept the registration via a POST request.
Do the callback, including the 'instruction : call' parameter.
exports.handler = function(context, event, callback) {
const client = context.getTwilioClient();
let eventInfo = JSON.parse(event.TaskAttributes);
// Generate the callback url
let callbackURL = `http://twimlets.com/echo?Twiml=%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3CResponse%3E%0A%3CSay%3ECALLWHISPERGOESHERE%3C%2FSay%3E%0A%3CDial%3E%0A%3CQueue%20reservationSid%3D%22${event.ReservationSid}%22%3E%3C%2FQueue%3E%0A%3C%2FDial%3E%0A%3C%2FResponse%3E&`;
// Generate POST request to accept the reservation
const rp = require("request-promise");
const postURI = `https://taskrouter.twilio.com/v1/Workspaces/${event.WorkspaceSid}/Tasks/${event.TaskSid}/Reservations/${event.ReservationSid}`;
const postForm = {
ReservationStatus: "accepted"
};
const postAuth = {
user: context.ACCOUNT_SID,
pass: context.AUTH_TOKEN
};
const options = {
method: "POST",
uri: postURI,
auth: postAuth,
form: postForm
};
rp(options)
.then(body => {
console.log(body);
callback(null, null);
})
.catch(err => {
console.log(err);
callback(null, err);
});
// Call the agent
callback(null, {
instruction: "call",
post_work_activity_sid: "POSTWORKACTIVITYSID",
from: eventInfo.from,
url: callbackURL
});
};
I've followed this API reference :
https://developers.google.com/youtube/analytics/v1/sample-application
And created "index.html","index.css","index.js" files. I've inserted a proper CLIENT_ID but when I run "index.html" file it shows the following o/p :
I am new to web development and trying to understand API's. Can anyone please tell me how to make this code work?
Code for index.js is like this :
(function() {
// Retrieve your client ID from the Google APIs Console at
// https://code.google.com/apis/console.
var OAUTH2_CLIENT_ID = 'YOUR_CLIENT_ID';
var OAUTH2_SCOPES = [
'https://www.googleapis.com/auth/yt-analytics.readonly',
'https://www.googleapis.com/auth/youtube.readonly'
];
var ONE_MONTH_IN_MILLISECONDS = 1000 * 60 * 60 * 24 * 30;
// Keeps track of the currently authenticated user's YouTube user ID.
var channelId;
// For information about the Google Chart Tools API, see
// https://developers.google.com/chart/interactive/docs/quick_start
google.load('visualization', '1.0', {'packages': ['corechart']});
// The Google APIs JS client invokes this callback automatically after loading.
// See http://code.google.com/p/google-api-javascript-client/wiki/Authentication
window.onJSClientLoad = function() {
gapi.auth.init(function() {
window.setTimeout(checkAuth, 1);
});
};
// Attempt the immediate OAuth 2 client flow as soon as the page loads.
// If the currently logged-in Google Account has previously authorized
// OAUTH2_CLIENT_ID, then it will succeed with no user intervention.
// Otherwise, it will fail and the user interface that prompts for
// authorization will need to be displayed.
function checkAuth() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: true
}, handleAuthResult);
}
// Handle the result of a gapi.auth.authorize() call.
function handleAuthResult(authResult) {
if (authResult) {
// Auth was successful. Hide auth prompts and show things
// that should be visible after auth succeeds.
$('.pre-auth').hide();
$('.post-auth').show();
loadAPIClientInterfaces();
} else {
// Auth was unsuccessful. Show things related to prompting for auth
// and hide the things that should be visible after auth succeeds.
$('.post-auth').hide();
$('.pre-auth').show();
// Make the #login-link clickable. Attempt a non-immediate OAuth 2 client
// flow. The current function will be called when that flow completes.
$('#login-link').click(function() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
});
}
}
// Load the client interface for the YouTube Analytics and Data APIs, which
// is required to use the Google APIs JS client. More info is available at
// http://code.google.com/p/google-api-javascript-client/wiki/GettingStarted#Loading_the_Client
function loadAPIClientInterfaces() {
gapi.client.load('youtube', 'v3', function() {
gapi.client.load('youtubeAnalytics', 'v1', function() {
// After both client interfaces load, use the Data API to request
// information about the authenticated user's channel.
getUserChannel();
});
});
}
// Calls the Data API to retrieve info about the currently authenticated
// user's YouTube channel.
function getUserChannel() {
// https://developers.google.com/youtube/v3/docs/channels/list
var request = gapi.client.youtube.channels.list({
// "mine: true" indicates that you want to retrieve the authenticated user's channel.
mine: true,
part: 'id,contentDetails'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
// We will need the channel's channel ID to make calls to the
// Analytics API. The channel ID looks like "UCdLFeWKpkLhkguiMZUp8lWA".
channelId = response.items[0].id;
// This string, of the form "UUdLFeWKpkLhkguiMZUp8lWA", is a unique ID
// for a playlist of videos uploaded to the authenticated user's channel.
var uploadsListId = response.items[0].contentDetails.relatedPlaylists.uploads;
// Use the uploads playlist ID to retrieve the list of uploaded videos.
getPlaylistItems(uploadsListId);
}
});
}
// Calls the Data API to retrieve the items in a particular playlist. In this
// example, we are retrieving a playlist of the currently authenticated user's
// uploaded videos. By default, the list returns the most recent videos first.
function getPlaylistItems(listId) {
// https://developers.google.com/youtube/v3/docs/playlistItems/list
var request = gapi.client.youtube.playlistItems.list({
playlistId: listId,
part: 'snippet'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
if ('items' in response) {
// jQuery.map() iterates through all of the items in the response and
// creates a new array that only contains the specific property we're
// looking for: videoId.
var videoIds = $.map(response.items, function(item) {
return item.snippet.resourceId.videoId;
});
// Now that we know the IDs of all the videos in the uploads list,
// we can retrieve info about each video.
getVideoMetadata(videoIds);
} else {
displayMessage('There are no videos in your channel.');
}
}
});
}
// Given an array of video ids, obtains metadata about each video and then
// uses that metadata to display a list of videos to the user.
function getVideoMetadata(videoIds) {
// https://developers.google.com/youtube/v3/docs/videos/list
var request = gapi.client.youtube.videos.list({
// The 'id' property value is a comma-separated string of video IDs.
id: videoIds.join(','),
part: 'id,snippet,statistics'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
// Get the jQuery wrapper for #video-list once outside the loop.
var videoList = $('#video-list');
$.each(response.items, function() {
// Exclude videos that don't have any views, since those videos
// will not have any interesting viewcount analytics data.
if (this.statistics.viewCount == 0) {
return;
}
var title = this.snippet.title;
var videoId = this.id;
// Create a new <li> element that contains an <a> element.
// Set the <a> element's text content to the video's title, and
// add a click handler that will display Analytics data when invoked.
var liElement = $('<li>');
var aElement = $('<a>');
// The dummy href value of '#' ensures that the browser renders the
// <a> element as a clickable link.
aElement.attr('href', '#');
aElement.text(title);
aElement.click(function() {
displayVideoAnalytics(videoId);
});
// Call the jQuery.append() method to add the new <a> element to
// the <li> element, and the <li> element to the parent
// list, which is identified by the 'videoList' variable.
liElement.append(aElement);
videoList.append(liElement);
});
if (videoList.children().length == 0) {
displayMessage('Your channel does not have any videos that have been viewed.');
}
}
});
}
// Requests YouTube Analytics for a video, and displays results in a chart.
function displayVideoAnalytics(videoId) {
if (channelId) {
// To use a different date range, modify the ONE_MONTH_IN_MILLISECONDS
// variable to a different millisecond delta as desired.
var today = new Date();
var lastMonth = new Date(today.getTime() - ONE_MONTH_IN_MILLISECONDS);
var request = gapi.client.youtubeAnalytics.reports.query({
// The start-date and end-date parameters need to be YYYY-MM-DD strings.
'start-date': formatDateString(lastMonth),
'end-date': formatDateString(today),
// A future YouTube Analytics API release should support channel==default.
// In the meantime, you need to explicitly specify channel==channelId.
// See https://devsite.googleplex.com/youtube/analytics/v1/#ids
ids: 'channel==' + channelId,
dimensions: 'day',
// See https://developers.google.com/youtube/analytics/v1/available_reports for details
// on different filters and metrics you can request when dimensions=day.
metrics: 'views',
filters: 'video==' + videoId
});
request.execute(function(response) {
// This function is called regardless of whether the request succeeds.
// The response either has valid analytics data or an error message.
if ('error' in response) {
displayMessage(response.error.message);
} else {
displayChart(videoId, response);
}
});
} else {
displayMessage('The YouTube user id for the current user is not available.');
}
}
// Boilerplate code to take a Date object and return a YYYY-MM-DD string.
function formatDateString(date) {
var yyyy = date.getFullYear().toString();
var mm = padToTwoCharacters(date.getMonth() + 1);
var dd = padToTwoCharacters(date.getDate());
return yyyy + '-' + mm + '-' + dd;
}
// If number is a single digit, prepend a '0'. Otherwise, return it as a string.
function padToTwoCharacters(number) {
if (number < 10) {
return '0' + number;
} else {
return number.toString();
}
}
// Calls the Google Chart Tools API to generate a chart of analytics data.
function displayChart(videoId, response) {
if ('rows' in response) {
hideMessage();
// The columnHeaders property contains an array of objects representing
// each column's title – e.g.: [{name:"day"},{name:"views"}]
// We need these column titles as a simple array, so we call jQuery.map()
// to get each element's "name" property and create a new array that only
// contains those values.
var columns = $.map(response.columnHeaders, function(item) {
return item.name;
});
// The google.visualization.arrayToDataTable() wants an array of arrays.
// The first element is an array of column titles, calculated above as
// "columns". The remaining elements are arrays that each represent
// a row of data. Fortunately, response.rows is already in this format,
// so it can just be concatenated.
// See https://developers.google.com/chart/interactive/docs/datatables_dataviews#arraytodatatable
var chartDataArray = [columns].concat(response.rows);
var chartDataTable = google.visualization.arrayToDataTable(chartDataArray);
var chart = new google.visualization.LineChart(document.getElementById('chart'));
chart.draw(chartDataTable, {
// Additional options can be set if desired.
// See https://developers.google.com/chart/interactive/docs/reference#visdraw
title: 'Views per Day of Video ' + videoId
});
} else {
displayMessage('No data available for video ' + videoId);
}
}
// Helper method to display a message on the page.
function displayMessage(message) {
$('#message').text(message).show();
}
// Helper method to hide a previously displayed message on the page.
function hideMessage() {
$('#message').hide();
}
})();
I've got the same issue and found a possible solution at stackoverflow:
before: gapi.auth.authorize
put: gapi.client.setApiKey(API_KEY);
For me the error message dissappears then but now I get a blank website. Does anyone know how to solve this issue?
Thanks,
Martin
EDIT: this solved my problem: Youtube Analytics API 403 error
I have already modified some metrics but basically this should work:
(function() {
// Retrieve your client ID from the Google APIs Console at
// https://code.google.com/apis/console.
var OAUTH2_CLIENT_ID = 'CLIENT_ID';
var OAUTH2_SCOPES = [
'https://www.googleapis.com/auth/yt-analytics.readonly',
'https://www.googleapis.com/auth/youtube.readonly'
];
var ONE_MONTH_IN_MILLISECONDS = 1000 * 60 * 60 * 24 * 30;
// Keeps track of the currently authenticated user's YouTube user ID.
var channelId;
// For information about the Google Chart Tools API, see
// https://developers.google.com/chart/interactive/docs/quick_start
google.load('visualization', '1.0', {'packages': ['corechart']});
// The Google APIs JS client invokes this callback automatically after loading.
// See http://code.google.com/p/google-api-javascript-client/wiki/Authentication
window.onJSClientLoad = function() {
gapi.auth.init(function() {
window.setTimeout(checkAuth, 1);
});
};
// Attempt the immediate OAuth 2 client flow as soon as the page loads.
// If the currently logged-in Google Account has previously authorized
// OAUTH2_CLIENT_ID, then it will succeed with no user intervention.
// Otherwise, it will fail and the user interface that prompts for
// authorization will need to be displayed.
function checkAuth() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
}
// Handle the result of a gapi.auth.authorize() call.
function handleAuthResult(authResult) {
if (authResult) {
// Auth was successful. Hide auth prompts and show things
// that should be visible after auth succeeds.
$('.pre-auth').hide();
$('.post-auth').show();
loadAPIClientInterfaces();
} else {
// Auth was unsuccessful. Show things related to prompting for auth
// and hide the things that should be visible after auth succeeds.
$('.post-auth').hide();
$('.pre-auth').show();
// Make the #login-link clickable. Attempt a non-immediate OAuth 2 client
// flow. The current function will be called when that flow completes.
$('#login-link').click(function() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
});
}
}
// Load the client interface for the YouTube Analytics and Data APIs, which
// is required to use the Google APIs JS client. More info is available at
// http://code.google.com/p/google-api-javascript-client/wiki/GettingStarted#Loading_the_Client
function loadAPIClientInterfaces() {
gapi.client.load('youtube', 'v3', function() {
gapi.client.load('youtubeAnalytics', 'v1', function() {
// After both client interfaces load, use the Data API to request
// information about the authenticated user's channel.
getUserChannel();
});
});
}
// Calls the Data API to retrieve info about the currently authenticated
// user's YouTube channel.
function getUserChannel() {
// https://developers.google.com/youtube/v3/docs/channels/list
var request = gapi.client.youtube.channels.list({
// "mine: true" indicates that you want to retrieve the authenticated user's channel.
mine: true,
part: 'id,contentDetails'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
// We will need the channel's channel ID to make calls to the
// Analytics API. The channel ID looks like "UCdLFeWKpkLhkguiMZUp8lWA".
channelId = response.items[0].id;
// This string, of the form "UUdLFeWKpkLhkguiMZUp8lWA", is a unique ID
// for a playlist of videos uploaded to the authenticated user's channel.
var uploadsListId = response.items[0].contentDetails.relatedPlaylists.uploads;
// Use the uploads playlist ID to retrieve the list of uploaded videos.
getPlaylistItems(uploadsListId);
}
});
}
// Calls the Data API to retrieve the items in a particular playlist. In this
// example, we are retrieving a playlist of the currently authenticated user's
// uploaded videos. By default, the list returns the most recent videos first.
function getPlaylistItems(listId) {
// https://developers.google.com/youtube/v3/docs/playlistItems/list
var request = gapi.client.youtube.playlistItems.list({
playlistId: listId,
part: 'snippet',
maxResults: 30
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
if ('items' in response) {
// jQuery.map() iterates through all of the items in the response and
// creates a new array that only contains the specific property we're
// looking for: videoId.
var videoIds = $.map(response.items, function(item) {
return item.snippet.resourceId.videoId;
});
// Now that we know the IDs of all the videos in the uploads list,
// we can retrieve info about each video.
getVideoMetadata(videoIds);
} else {
displayMessage('There are no videos in your channel.');
}
}
});
}
// Given an array of video ids, obtains metadata about each video and then
// uses that metadata to display a list of videos to the user.
function getVideoMetadata(videoIds) {
// https://developers.google.com/youtube/v3/docs/videos/list
var request = gapi.client.youtube.videos.list({
// The 'id' property value is a comma-separated string of video IDs.
id: videoIds.join(','),
part: 'id,snippet,statistics'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
// Get the jQuery wrapper for #video-list once outside the loop.
var videoList = $('#video-list');
$.each(response.items, function() {
// Exclude videos that don't have any views, since those videos
// will not have any interesting viewcount analytics data.
if (this.statistics.viewCount == 0) {
return;
}
var title = this.snippet.title;
var videoId = this.id;
// Create a new <li> element that contains an <a> element.
// Set the <a> element's text content to the video's title, and
// add a click handler that will display Analytics data when invoked.
var liElement = $('<li>');
var aElement = $('<a>');
// The dummy href value of '#' ensures that the browser renders the
// <a> element as a clickable link.
aElement.attr('href', '#');
aElement.text(title);
aElement.click(function() {
displayVideoAnalytics(videoId);
});
// Call the jQuery.append() method to add the new <a> element to
// the <li> element, and the <li> element to the parent
// list, which is identified by the 'videoList' variable.
liElement.append(aElement);
videoList.append(liElement);
});
if (videoList.children().length == 0) {
displayMessage('Your channel does not have any videos that have been viewed.');
}
}
});
}
// Requests YouTube Analytics for a video, and displays results in a chart.
function displayVideoAnalytics(videoId) {
if (channelId) {
// To use a different date range, modify the ONE_MONTH_IN_MILLISECONDS
// variable to a different millisecond delta as desired.
var today = new Date();
var lastMonth = new Date(today.getTime() - ONE_MONTH_IN_MILLISECONDS);
var request = gapi.client.youtubeAnalytics.reports.query({
// The start-date and end-date parameters need to be YYYY-MM-DD strings.
'start-date': '2014-11-01',
'end-date': formatDateString(today),
// A future YouTube Analytics API release should support channel==default.
// In the meantime, you need to explicitly specify channel==channelId.
// See https://devsite.googleplex.com/youtube/analytics/v1/#ids
ids: 'channel==' + channelId,
dimensions: 'elapsedVideoTimeRatio',
// See https://developers.google.com/youtube/analytics/v1/available_reports for details
// on different filters and metrics you can request when dimensions=day.
metrics: 'audienceWatchRatio',
filters: 'video==' + videoId
});
request.execute(function(response) {
// This function is called regardless of whether the request succeeds.
// The response either has valid analytics data or an error message.
if ('error' in response) {
displayMessage(response.error.message);
} else {
displayChart(videoId, response);
}
});
} else {
displayMessage('The YouTube user id for the current user is not available.');
}
}
// Boilerplate code to take a Date object and return a YYYY-MM-DD string.
function formatDateString(date) {
var yyyy = date.getFullYear().toString();
var mm = padToTwoCharacters(date.getMonth() + 1);
var dd = padToTwoCharacters(date.getDate());
return yyyy + '-' + mm + '-' + dd;
}
// If number is a single digit, prepend a '0'. Otherwise, return it as a string.
function padToTwoCharacters(number) {
if (number < 10) {
return '0' + number;
} else {
return number.toString();
}
}
// Calls the Google Chart Tools API to generate a chart of analytics data.
function displayChart(videoId, response) {
if ('rows' in response) {
hideMessage();
// The columnHeaders property contains an array of objects representing
// each column's title – e.g.: [{name:"day"},{name:"views"}]
// We need these column titles as a simple array, so we call jQuery.map()
// to get each element's "name" property and create a new array that only
// contains those values.
var columns = $.map(response.columnHeaders, function(item) {
return item.name;
});
// The google.visualization.arrayToDataTable() wants an array of arrays.
// The first element is an array of column titles, calculated above as
// "columns". The remaining elements are arrays that each represent
// a row of data. Fortunately, response.rows is already in this format,
// so it can just be concatenated.
// See https://developers.google.com/chart/interactive/docs/datatables_dataviews#arraytodatatable
var chartDataArray = [columns].concat(response.rows);
var chartDataTable = google.visualization.arrayToDataTable(chartDataArray);
var chart = new google.visualization.LineChart(document.getElementById('chart'));
chart.draw(chartDataTable, {
// Additional options can be set if desired.
// See https://developers.google.com/chart/interactive/docs/reference#visdraw
width: 800, height: 300, is3D: false,
title: 'Audience Retention per Video ' + videoId
});
} else {
displayMessage('No data available for video ' + videoId);
}
}
// Helper method to display a message on the page.
function displayMessage(message) {
$('#message').text(message).show();
}
// Helper method to hide a previously displayed message on the page.
function hideMessage() {
$('#message').hide();
}
})();
I hope this helps.
Martin
Put your Api key in url like
"https://www.googleapis.com/qpxExpress/v1/trips/search?key=Your_Api_Key"
instead in header
try and run, this work in my case.....
I am creating a call center application ,and when a call come to twilio client on chrome browser it ask for permission to allow use of speaker and mic .When second call comes I have to allow it for two times and if third call come I have to allow three times and so on.
and in firfox browser ,when I acccept call second time it automatically disconnect and give error of mediastream.
Please suggest me where I am wrong and how to solve this problem.
Below is the javascript of twilio client,If need some more information please let me know
<script
type="text/javascript">
//var
token
=callaction();
var
connection=null;
var
content;
$(document).ready(function(){
$('#hangupcallbutton').hide();
$.post('cleanify?action=createcall',
function(data){
content=
data;
Twilio.Device.setup(content,
{
debug:
true}
);
}
);
Twilio.Device.ready(function
(device)
{
$("#log").text("Ready");
}
);
Twilio.Device.error(function
(error)
{
$("#log").text("Error:
"
+
error.message);
}
);
Twilio.Device.connect(function
(conn)
{
//alert("connect
handler");
$("#log").text("Successfully
established
call");
connection
=
conn;
}
);
Twilio.Device.disconnect(function
(conn)
{
//alert("in
disconnect");
$("#callbutton").html('Call');
$("#callbutton").removeClass("btn-danger
").addClass("btn-success");
//
$("#callbutton").toggleClass('btn-danger
btn-success');
$("#log").text("Call
ended");
$('#RejectCallButton').show();
$('#acceptcallbutton').show();
$('#hangupcallbutton').hide();
$('#myModal').modal('hide');
}
);
Twilio.Device.incoming(function
(conn)
{
//
alert("in
incomming");
connection
=
conn;
$("#log").text("aa
rahi
h
call");
$('#myModal').modal('show');
$('#incomingnumber').html(conn.parameters.From);
$('#RejectCallButton').click(function()
{
connection.reject();
}
);
$('#acceptcallbutton').click(function()
{
connection.accept();
$('#RejectCallButton').hide();
$('#acceptcallbutton').hide();
$('#hangupcallbutton').show();
}
);
$('#hangupcallbutton').click(function()
{
$('#RejectCallButton').show();
$('#acceptcallbutton').show();
$('#hangupcallbutton').hide();
connection.disconnect();
}
);
//
accept
the
incoming
connection
and
start
two-way
audio
}
);
Twilio.Device.cancel(
function
(conn)
{
//alert("in
cancel");
connection
=
conn;
$('#myModal').modal('hide');
}
);
$.each(['0','1','2','3','4','5','6','7','8','9','star','pound'],
function(index,
value)
{
$('#button'
+
value).click(function(){
//alert("hello");
if(connection)
{
if
(value=='star')
connection.sendDigits('*')
else
if
(value=='pound')
connection.sendDigits('#')
else
connection.sendDigits(value)
return
false;
}
}
);
}
);
//
Do
something
with
c
alert(content);
}
);
function
callhandle(){
if($("#callbutton").html().trim()
==
'Call'){
$("#callbutton").html('HangUp');
$("#callbutton").removeClass("btn-success").addClass("btn-danger");
//
$("#callbutton").toggleClass('btn-success
btn-danger');
call();
}
else
if($("#callbutton").html().trim()
==
'HangUp'){
hangup();
$("#callbutton").html('Call');
$("#callbutton").removeClass("btn-danger").addClass("btn-success");
//
$("#callbutton").toggleClass('btn-danger
btn-success');
}
}
function
call()
{
//
get
the
phone
number
to
connect
the
call
to
//
get
the
phone
number
to
connect
the
call
to
//
alert(
$("#selectOutgoing").val());
params
=
{"PhoneNumber":
$('.dialnumber').val(),"From":
$("#selectOutgoing").val(),"isclient":
"true"}
;
Twilio.Device.connect(params);
}
function
hangup()
{
Twilio.Device.disconnectAll();
}
</script>
From my experience, Chrome will only store those settings over HTTPS. Over HTPP Chrome will not store the settings thus it will prompt for the permissions every time
It might happen after a call before initialize twilio when you get another call . So , you must initialize first .