OAuth2 with Google Cloud Print - google-apps-script

I had written a Google Apps Script that connected to Google Cloud Print to automate some printing. The script would auto-run on a time interval, search for relevant files, and if found it would sent them to my printer. My code used OAuthConfig and was working fine, but now that class has been deprecated and after a weekend of trial & error and scouring the interwebs I can't get it to work with OAuth2.
Here's the OAuthConfig code that was working fine:
function printDoc(docId, docTitle, myPrinterId) {
var scope = 'https://www.googleapis.com/auth/cloudprint';
var url = 'https://www.google.com/cloudprint/submit';
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix"
};
var fetchArgs = googleOAuth_('google', scope, payloadOfSubmit);
fetchArgs.method = 'POST';
var responseOfSubmit = UrlFetchApp.fetch(url, fetchArgs);
var jsonOfSubmit = JSON.parse(responseOfSubmit.getContentText());
return jsonOfSubmit;
}
function googleOAuth_(name, scope, payloadData) {
var oAuthConfig = UrlFetchApp.addOAuthService(name);
oAuthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oAuthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?scope="+scope);
oAuthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oAuthConfig.setConsumerKey("anonymous");
oAuthConfig.setConsumerSecret("anonymous");
return {
oAuthServiceName:name,
oAuthUseToken:"always",
muteHttpExceptions:true,
payload:payloadData
};
}
I've successfully connected the github library for OAuth2. However, what's different about the instructions provided there, and on many other sites, is that they assume that the code will be deployed as a web service where a user is prompted to manually click to authorize the request. In my case the code will be saved on a Google Apps Script file, and the Cloud Printer is on the same Google account, so I never needed this manual intervention or back & forth with my original OAuthconfig.
My first attempt by adapting the instructions was:
function printDoc2(docId, docTitle, myPrinterId) {
var url = 'https://www.google.com/cloudprint/submit';
var scope = 'https://www.googleapis.com/auth/cloudprint';
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix",
};
var accessToken = googleOAuth_('google', scope).getAccessToken();
var params = {
method:"POST",
headers: {"Authorization": "Bearer " + accessToken},
muteHttpExceptions:true,
payload:payloadOfSubmit
};
var responseOfSubmit = UrlFetchApp.fetch(url, params);
//Logger.log(responseOfSubmit);
var jsonOfSubmit = JSON.parse(responseOfSubmit.getContentText());
return jsonOfSubmit;
}
function googleOAuth2_(name, scope) {
return OAuth2.createService(name)
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId("anonymous")
.setClientSecret("anonymous")
.setProjectKey(ScriptApp.getProjectKey())
.setPropertyStore(PropertiesService.getUserProperties())
.setScope(scope)
.setCallbackFunction('authCallback');
}
function authCallback(request) {
var driveService = getDriveService();
var isAuthorized = driveService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
But this gives me an error "Access not granted or expired" when it tries to run the line:
var accessToken = googleOAuth_('google', scope).getAccessToken();
So I found a apps ScriptApp Method getOAuthToken which seemed like it might give me the token I need. I replaced the above line with:
var accessToken = ScriptApp.getOAuthToken();
And the code executes but my response from the server is "Error 403 User credentials required".
Here's my third attempt based on #Mogsdad's suggestion:
function sendPrintJob(docId,myPrinterId,docTitle) {
var payloadOfSubmit = {
"printerid" : myPrinterId,
"title" : docTitle,
"content" : docId,
"contentType" : "google.kix" ,
};
var request = {
"method": "POST",
"headers":{"Authorization": "Bearer "+ScriptApp.getOAuthToken()},
"muteHttpExceptions": true
};
var responseOfSubmit = UrlFetchApp.fetch("https://www.google.com/cloudprint/submit", request);
Logger.log(responseOfSubmit);
}
I've tried a number of variations, including creating a Developer Console Project and using the Client ID provided there, but I keep getting stuck at these two issues (access not granted, or credentials required). If anyone can provide any help I'd really appreciate it.

Here are the steps that allowed me to connect Google Apps Script to Google Cloud Print, so I could then submit GCP jobs (these steps are all started from within Google Apps Script):
Add the OAuth2 library
(https://github.com/googlesamples/apps-script-oauth2) to your Google
Apps Script by going to: Resources > Libraries > Find Library
MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48 > Select
Create new web application in Developer Console Resources > Developer Console Project > Click the project link > APIs & Auth >
Credentials > Add Credentials > OAuth2.0 Client ID > Web
Application > Set Authorized redirect URIs to the format
https://script.google.com/macros/d/{PROJECT KEY}/usercallback
where project key is under File > Project Properties and copy
your client ID and client secret
Add the ID and Secret to "getCloudPrintService()" code below (replace client_id and client_secret)
Go to Run > ShowURL and authorize the script.
Open the Logger (Cmd + Enter), copy the URL and paste it in a new browser tab to complete the authorization.
Go to https://www.google.com/cloudprint/#printers , select your printer, click details, expand advanced details, and copy your Printer ID (it will be of the format 555aa555-5a55-5555-5555-55555a55a555)
Add the printer id to "printGoogleDocument()" code below (replace myPrinterId)
This resource was helpful in figuring the steps out: http://ctrlq.org/code/20061-google-cloud-print-with-apps-script, and you may also find these links helpful:
https://developers.google.com/cloud-print/docs/appInterfaces
https://mashe.hawksey.info/2015/10/setting-up-oauth2-access-with-google-apps-script-blogger-api-example/
function showURL() {
var cpService = getCloudPrintService();
if (!cpService.hasAccess()) {
Logger.log(cpService.getAuthorizationUrl());
} else {
Logger.log("You already have access to this service.");
}
}
function getCloudPrintService() {
return OAuth2.createService('print')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId(client_id)
.setClientSecret(client_secret)
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('https://www.googleapis.com/auth/cloudprint')
.setParam('login_hint', Session.getActiveUser().getEmail())
.setParam('access_type', 'offline')
.setParam('approval_prompt', 'force');
}
function authCallback(request) {
var isAuthorized = getCloudPrintService().handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('You can now use Google Cloud Print from Apps Script.');
} else {
return HtmlService.createHtmlOutput('Cloud Print Error: Access Denied');
}
}
function getPrinterList() {
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/search', {
headers: {
Authorization: 'Bearer ' + getCloudPrintService().getAccessToken()
},
muteHttpExceptions: true
}).getContentText();
var printers = JSON.parse(response).printers;
for (var p in printers) {
Logger.log("%s %s %s", printers[p].id, printers[p].name, printers[p].description);
}
}
function printGoogleDocument(docId, docTitle) {
// For notes on ticket options see https://developers.google.com/cloud-print/docs/cdd?hl=en
var ticket = {
version: "1.0",
print: {
color: {
type: "STANDARD_COLOR"
},
duplex: {
type: "NO_DUPLEX"
},
}
};
var payload = {
"printerid" : myPrinterId,
"content" : docId,
"title" : docTitle,
"contentType" : "google.kix", // allows you to print google docs
"ticket" : JSON.stringify(ticket),
};
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/submit', {
method: "POST",
payload: payload,
headers: {
Authorization: 'Bearer ' + getCloudPrintService().getAccessToken()
},
"muteHttpExceptions": true
});
// If successful, should show a job here: https://www.google.com/cloudprint/#jobs
response = JSON.parse(response);
if (response.success) {
Logger.log("%s", response.message);
} else {
Logger.log("Error Code: %s %s", response.errorCode, response.message);
}
return response;
}

The scope "https://www.googleapis.com/auth/cloudprint" has to be included explicitly in the
manifest file
appscript.json (View > Show manifest file)
{
"timeZone": "Europe/Paris",
"dependencies": {
},
"oauthScopes": [
"https://www.googleapis.com/auth/documents",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/cloudprint"
],
"exceptionLogging": "STACKDRIVER"
}
Code.gs
function listPrinters() {
var options = {
headers: {
authorization: 'OAuth ' + ScriptApp.getOAuthToken()
}
}
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/search', options);
Logger.log(response);
}

Related

Having trouble to run the Google Drive API on Google Chrome

Here I am again still trying to run my platform with the Google Drive API. It works perfectly on Mozilla, but it does not work on Google Chrome. I keep getting this error:
Uncaught TypeError: Cannot read property 'init' of undefined
at initClient (google_drive:370)
at api.js:15
at fa (api.js:8)
at A (api.js:15)
at Array.r. (api.js:15)
at Object.x. [as loaded_0] (api.js:15)
at cb=gapi.loaded_0:1
Here is the code I am running:
var CLIENT_ID = '';
var API_KEY = '';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/drive.readonly';
var authorizeButton = document.getElementById('authorize_button');
var signoutButton = document.getElementById('signout_button');
var localdriveButton = document.getElementById('localdrive_button');
var FILES = {};
function handleClientLoad() {
setTimeout(function(){ gapi.load('client:auth2', initClient); }, 3000);
}
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick();
signoutButton.onclick = handleSignoutClick();
}, function(error) {
console.log(JSON.stringify(error, null, 2));
});
return gapi;
}
Does anyone have any idea of what problem I might have and how to fix it?

how to bind the version of the apps script in a variable?

I would like to know if it is possible to bind the version of the apps script, the version we have in 'manage version' menu, in a variable. Then I will be able to display on UI.
Thanks a lot,
You'll need to use the Apps Script REST API to get the project's version number.
Projects Versions List
function getProjectVersionNumber(scriptId,theAccessTkn) {
try{
var allVersions,errMsg,highestVersion,options,payload,response,url;
if (!scriptId) {
//Logger.log('There was an error - No scriptID')
//Error handling function
throw new Error('There is no script ID for fnk getProjectVersionNumber');
}
if (!theAccessTkn) {
theAccessTkn = ScriptApp.getOAuthToken();
}
url = "https://script.googleapis.com/v1/projects/" + scriptId + "/versions";
options = {
"method" : "GET",
"muteHttpExceptions": true,
"headers": {
'Authorization': 'Bearer ' + theAccessTkn
}
};
response = UrlFetchApp.fetch(url,options);
//Logger.log('response : ' + JSON.stringify(response).slice(0,500));
response = JSON.parse(response);//The response must be parsed into JSON even though it is an object
if (typeof response === 'object') {
errMsg = response.error;
if (errMsg) {
errMsg = errMsg.message;
return 'err' + errMsg;
}
}
allVersions = response.versions;//Get versions
highestVersion = allVersions[0].versionNumber;
//Logger.log("highestVersion: " + highestVersion)
return highestVersion;
}catch(e) {
myErrorHandlingFunction_(e);
}
}
This is been a featured request for a long time, as now it is possible to retrieve the version history and the current version using the API, on the following reference you'll find the API calls:
https://developers.google.com/apps-script/api/reference/rest
Although it's not possible to enable this API as an advanced service, so you'll have to recreate the oauth2 authentication process using apps script.
As the documentation says:
If you want to use a Google API that isn't available as an advanced service, just connect to it like any other external API.

Public Google Script doesn't run from a chrome extension

I have a Google script which modifies the content of a google spreadsheet (it's not the final script of course), but I have a problem to run this simple script from a google chrome extension.
Here is the script attached to my spreadsheet :
function insertData(parameters) {
var spreadsheet = SpreadsheetApp.openByUrl(THE_URL_OF_THE_SPREADSHEET)
spreadsheet.getRange('A5').activate();
spreadsheet.getCurrentCell().setValue(parameters.data1);
spreadsheet.getRange('B5').activate();
spreadsheet.getCurrentCell().setValue(parameters.data2);
}
I deployed this script both as a web app (execute as me + access to everyone, even anonymous) and as an executable API (access to anyone).
Then I tried this JS script to run my google script, from a google chrome extension, using this code that I got from an google chrome extension example:
sendDataToExecutionAPICallback: function() {
post({ 'url': 'https://script.googleapis.com/v1/scripts/' + SCRIPT_ID + ':run',
'callback': obj.executionAPIResponse,
'token': 'MY_GENERATED_TOKEN'
'request': {
'function': 'insertData',
'parameters': {
'data1': 'ok1 from script',
'data2': 'ok2 from script'
},
'devMode': true
}
});
},
executionAPIResponse: function(response){
var obj = this;
var info;
if (response.response.result.status == 'ok'){
info = 'Data has been entered';
} else {
info = 'Error...';
}
obj.displayMessage(info);
}
And I have this post function :
function post(options) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
// JSON response assumed. Other APIs may have different responses.
options.callback(JSON.parse(xhr.responseText));
} else if(xhr.readyState === 4 && xhr.status !== 200) {
console.log('post', xhr.readyState, xhr.status, xhr.responseText);
}
};
xhr.open('POST', options.url, true);
// Set standard Google APIs authentication header.
xhr.setRequestHeader('Authorization', 'Bearer ' + options.token);
xhr.send(JSON.stringify(options.request));
}
And when I call the sendDataToExecutionAPICallback function, I got this auth error:
"error": {
"code": 401,
"message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
EDIT1:
After generating a token, and added it to my code, I have this error:
"error": {
"code": 403,
"message": "The caller does not have permission",
"status": "PERMISSION_DENIED"
}
I would like to propose two methods. Please choose one of them.
Method 1: Execution API (Method: scripts.run of Apps Script API)
In order to run the functions using Execution API, please carry out the following flow.
Create a project (standalone or bound script).
Copy and paste your GAS script to the project. In this case, your GAS script was not modified.
Deploy API executable. As a sample, choose "Only myself" as "Who has access to the script"
Enable Apps Script API at API console.
Using the save button, save the project on the script editor. This is an important point. By this, the scripts are reflected to the deployed execution API.
Retrieve access token from client ID and client secret of this project.
Please include https://www.googleapis.com/auth/drive, https://www.googleapis.com/auth/drive.scripts, https://www.googleapis.com/auth/spreadsheets in the scopes. https://www.googleapis.com/auth/drive might not be used for this situation.
Please use the retrieved access token to the following modified script.
Javascript :
sendDataToExecutionAPICallback: function() {
post({
'url': 'https://script.googleapis.com/v1/scripts/' + SCRIPT_ID + ':run',
'callback': obj.executionAPIResponse,
'token': 'MY_GENERATED_TOKEN',
'request': {
'function': 'insertData',
'parameters': [{
'data1': 'ok1 from script',
'data2': 'ok2 from script',
}],
'devMode': true,
}
});
},
executionAPIResponse: function(response) {
var info;
if (!response.error) {
info = 'Data has been entered';
} else {
info = 'Error...';
}
obj.displayMessage(info);
},
function post(options) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
// JSON response assumed. Other APIs may have different responses.
options.callback(JSON.parse(xhr.responseText));
} else if(xhr.readyState === 4 && xhr.status !== 200) {
console.log('post', xhr.readyState, xhr.status, xhr.responseText);
}
};
xhr.open('POST', options.url, true);
// Set standard Google APIs authentication header.
xhr.setRequestHeader('Authorization', 'Bearer ' + options.token);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(options.request));
}
Note :
In my environment, this modified script works. But if in your environment, this didn't work, could you please try the method 2?
Method 2: Web Apps
From your question and comments, I'm worried that the access token might not be able to be used. So I would like to also propose the method using Web Apps. In this case, you can run the function without the access token. But the password key to run is used at the payload.
When you use this, please deploy Web Apps as "Execute the app as:" : Me and "Who has access to the app:": Anyone, even anonymous. If you want to know the detail information of Web Apps, please check here.
In this sample script, it doesn't use xhr.setRequestHeader('Authorization', 'Bearer ' + options.token); of post().
The modified script is as follows. Please put GAS to the script editor which deploys Web Apps.
GAS :
function doPost(e) {
var p = JSON.parse(e.postData.contents);
if (p.password == 'samplePassword') {
insertData(p.parameters);
return ContentService.createTextOutput("ok");
} else {
return ContentService.createTextOutput("error");
}
}
function insertData(parameters) {
var spreadsheet = SpreadsheetApp.openByUrl(THE_URL_OF_THE_SPREADSHEET);
spreadsheet.getRange('A5').activate();
spreadsheet.getCurrentCell().setValue(parameters.data1);
spreadsheet.getRange('B5').activate();
spreadsheet.getCurrentCell().setValue(parameters.data2);
}
Javascript :
sendDataToExecutionAPICallback: function() {
post({
'url': 'https://script.google.com/macros/s/#####/exec', // URL of Web Apps
'callback': obj.executionAPIResponse,
'request': {
'parameters': {
'data1': 'ok1 from script',
'data2': 'ok2 from script'
},
'password': 'samplePassword',
}
});
},
executionAPIResponse: function(response) {
var info;
if (response == 'ok') {
info = 'Data has been entered';
} else {
info = 'Error...';
}
obj.displayMessage(info);
},
function post(options) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
// JSON response assumed. Other APIs may have different responses.
options.callback(xhr.responseText);
} else if(xhr.readyState === 4 && xhr.status !== 200) {
console.log('post', xhr.readyState, xhr.status, xhr.responseText);
}
};
xhr.open('POST', options.url, true);
// Set standard Google APIs authentication header.
xhr.send(JSON.stringify(options.request));
}
Note :
If you use Web Apps, after copied and pasted the GAS script to the script editor, please redeploy Web Apps as a new version. By this, the latest script is reflected.
In my environment, I could confirm that both methods worked.

How upload a video to Youtube Using Google Apps Script?

I want to use Google Apps Script to upload a video YouTube using the YouTube Data API v3. This is my code:
function YouTubeAPI()
{
var url = {URL VIDEO};
var file = UrlFetchApp.fetch(url).getBlob();
Logger.log(file.getName());
var snippet = {
"snippet": {
"title": "Summer vacation in California",
"description": "Had a great time surfing in Santa Cruz",
"tags": ["surfing", "Santa Cruz"],
"categoryId": "22"},"status": {"privacyStatus": "private"}};
YouTube.Videos.insert(snippet, 'snippet,status', file)
}
The response is "Unauthorized", I check the Google Console is enable, and in the Script also enable the Youtube Data API.
If you have an MP4 file in your Google Drive that is less than 50MB, then you can get the file from your Google Drive and upload it to YouTube using the YouTube Data API. I created a new Google Cloud Platform (GCP) project, and enabled the YouTube Data API and the Google Drive API, and associated the new GCP project with my Apps Script project. Also, I manually added the needed scopes to the appsscript.json file.
{
"timeZone": "Your time zone will be here",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "YouTube",
"version": "v3",
"serviceId": "youtube"
}
]
},
"oauthScopes": ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/script.external_request","https://www.googleapis.com/auth/youtube"],
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
The code:
function uploadToYouTube(mp4_fileId) {
var blob,mp4_fileId,part,requestResource,response;
var options = {},snippet = {};
/*
You will need to create a GCP standard project and associate it with this Apps Script project-
In the new code editor click the settings cog wheel and scroll down to:
Google Cloud Platform (GCP) Project -
You may get an error:
In order to change your project, you will need to configure the OAuth consent screen. Configure your OAuth Consent details.
And if you do not have a Google Workspace account then you wont be able to set up the GCP project as "INTERNAL"
You will need to enable the Google Drive API and the YouTube API in the associated GCP project -
*/
/*
This code needs the file ID of the MP4 file in your Google Drive -
To get the file ID of an MP4 video file in Google Drive, right click the MP4 in your Google Drive
and choose, "Get link"
The link will look like this:
https://drive.google.com/file/d/FILE_ID_IS_HERE/view?usp=sharing
In the URL is the file ID
*/
options = {
"method" : "get",
"headers" : {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
"muteHttpExceptions":true
}
mp4_fileId = mp4_fileId ? mp4_fileId : 'PUT_YOUR_MP4_FILE_ID_HERE';
const url = `https://www.googleapis.com/drive/v3/files/` + mp4_fileId + `?alt=media`;
response = UrlFetchApp.fetch(url, options);
//Logger.log('response.getResponseCode(): ' + response.getResponseCode())
if (response.getResponseCode() !== 200) {
return;
}
blob = response.getBlob();
//Logger.log('blob.getName(): ' + blob.getName())
/*
{"snippet":{
"playlistId":"YOUR_PLAYLIST_ID",
"position":0,
"resourceId":{
"kind":"youtube#video",
"videoId":"abcdefg"
}
}
}
*/
/*
{
"snippet": {
"title": "Summer vacation in California",
"description": "Had fun surfing in Santa Cruz",
"tags": ["surfing", "Santa Cruz"],
"categoryId": "22"
},
"status": {
"privacyStatus": "private"
}
}
*/
requestResource = {};
snippet.title = "AAA_Put_Title_Here";
snippet.description = "Description of video goes here";
snippet.categoryId = "22";
options.snippet = snippet;
options.status = {
"privacyStatus": "private"
}
part = "snippet,status";//This correlates to the options
//YouTube.Videos.insert(resource: Youtube_v3.Youtube.V3.Schema.Video, part: string[], mediaData: Blob, optionalArgs: Object)
var response = YouTube.Videos.insert(requestResource, part, blob, options);
if (!response || !response.kind) {//There was an error
console.log("Error!")
}
//Logger.log('response: ' + response);
}
Try to use code below. This sample code finds the user's uploads then updates the most recent upload's description by appending a string.
/**
* This sample finds the active user's uploads, then updates the most recent
* upload's description by appending a string.
*/
function updateVideo() {
// 1. Fetch all the channels owned by active user
var myChannels = YouTube.Channels.list('contentDetails', {mine: true});
// 2. Iterate through the channels and get the uploads playlist ID
for (var i = 0; i < myChannels.items.length; i++) {
var item = myChannels.items[i];
var uploadsPlaylistId = item.contentDetails.relatedPlaylists.uploads;
var playlistResponse = YouTube.PlaylistItems.list('snippet', {
playlistId: uploadsPlaylistId,
maxResults: 1
});
// Get the videoID of the first video in the list
var video = playlistResponse.items[0];
var originalDescription = video.snippet.description;
var updatedDescription = originalDescription + ' Description updated via Google Apps Script';
video.snippet.description = updatedDescription;
var resource = {
snippet: {
title: video.snippet.title,
description: updatedDescription,
categoryId: '22'
},
id: video.snippet.resourceId.videoId
};
YouTube.Videos.update(resource, 'id,snippet');
}
}
For more information, download the demo app here: https://github.com/youtube/api-samples

Google Drive API push notifications can't be stopped / cancelled

I am watching a Drive resource. Setting up the watch (using the googleapis 0.2.13-alpha client with node.js and drive.file.watch):
exports.subscribeDriveCallbacksCmd = function( user, fileId ){
var userId = user.id;
var baseUrl = exports.BASE_URL;
var subscribeUrl = baseUrl+"/incoming/file";
var watchId = 'id-'+fileId+'-'+(new Date()).getTime();
var subscription = {
id: watchId,
token: userId+':'+fileId,
type: 'web_hook',
address: subscribeUrl,
params:{
ttl: 600
}
};
var params = {
fileId: fileId
};
//var cmd = client.drive.files.watch( params, subscription );
// FIXME - Hack around bug in RPC implememntation
var hack = {channel:subscription};
for( var key in params ){
hack[key] = params[key];
}
var cmd = client.drive.files.watch( hack );
return cmd;
};
var cmd = exports.subscribeDriveCallbacksCmd( user, '0ZZuoVaqdWGhpUk9PZZ' );
var batch = client.newBatchRequest();
batch.add(cmd);
batch.withAuthClient(user.auth).execute(cb);
After this, I'm getting a response of
{ kind: 'api#channel',
id: 'id-0ZZuoVaqdWGhpUk9PZZ-1374536746592',
resourceId: 'WT6g4bx-4or2kPWsL53z7YxZZZZ',
resourceUri: 'https://www.googleapis.com/drive/v2/files/0AHuoVaqdWGhpUkZZZZ?updateViewedDate=false&alt=json',
token: '101852559274654726533:0ZZuoVaqdWGhpUk9PZZ',
expiration: '1374537347934' }
and a sync callback with the following headers
'x-goog-channel-id': 'id-0ZZuoVaqdWGhpUk9PZZ-1374536746592',
'x-goog-channel-expiration': 'Mon, 22 Jul 2013 23:55:47 GMT',
'x-goog-resource-state': 'sync',
'x-goog-message-number': '1',
'x-goog-resource-id': 'WT6g4bx-4or2kPWsL53z7YxZZZZ',
'x-goog-resource-uri': 'https://www.googleapis.com/drive/v2/files/0AHuoVaqdWGhpUkZZZZ?updateViewedDate=false&alt=json',
'x-goog-channel-token': '101852559274654726533:0ZZuoVaqdWGhpUk9PZZ',
'user-agent': 'APIs-Google; (+http://code.google.com/apis)
There are several problems with this, however:
The resource-id returned by both of these do not match the fileId passed when I subscribed to the watch. It does match the ID given in the resource-uri
Trying to use either the resourceID returned here, or the fileId passed when I subscribed, returns an error when I try to stop the channel.
The error given for drive.channel.stop varies depending on how I do the call. If I use the API Explorer at the bottom of the Channel: Stop page, providing either the resourceId or the fileId for the resourceId parameter, I get
404 Not Found
{
"error": {
"errors": [
{
"domain": "global",
"reason": "notFound",
"message": "Channel not found"
}
],
"code": 404,
"message": "Channel not found"
}
}
If I use the node.js library with this code:
exports.cancelDriveCallbacksCmd = function( watchId, fileId, resourceId ){
var body = {
id: watchId,
resourceId: resourceId
};
var cmd = client.drive.channels.stop( body );
return cmd;
};
var cmd = exports.cancelDriveCallbacksCmd( 'id-0ZZuoVaqdWGhpUk9PZZ-1374536746592', '0ZZuoVaqdWGhpUk9PZZ', 'WT6g4bx-4or2kPWsL53z7YxZZZZ' );
var batch = client.newBatchRequest();
batch.add(cmd);
batch.withAuthClient(user.auth).execute(cb);
I get the error
{ code: 500,
message: 'Internal Error',
data:
[ { domain: 'global',
reason: 'internalError',
message: 'Internal Error' } ] }
which I suspected was related to Bug 59 which has a workaround (which was the hack code I was using above) but should have the fix in place sometime this week, I understand.
So I changed it to this code, which worked around the bug for files.watch:
exports.cancelDriveCallbacksCmd = function( watchId, fileId, resourceId ){
var params = {};
var body = {
id: watchId,
resourceId: resourceId,
fileId: fileId
};
//var cmd = client.drive.channels.stop( params, body );
// FIXME - hack around bug in RPC implementation
var hack = {channel:body};
for( var key in params ){
hack[key] = params[key];
}
var cmd = client.drive.channels.stop( hack );
console.log( 'cancelDriveCallbacksCmd', hack );
return cmd;
};
But I get the same 500 error.
Any thoughts about what I might be doing wrong or how to even go about debugging where I might be going wrong with it?
Push notification is designed to watch any api resource, although it only supports Changes and Files for now. Thus, it needs unique resourceId for all resource type. That is the reason why they have resourceId that is not equal to fileId.
Confirmations do come back with info about which file it is watching. Check header of your response. Also, you can make use of token to save channel specific information if you want.
If you are using API explorer, you cannot unsubscribe from the channel because as you know, push notification requires additional verification of url through apis console and apis explorer is not authenticated to access your notification. It is working as intended by security reason. I will report about this issue to stop people from getting confused with this.
fileId doesn't go to request body. It should be one of the parameters. Also, you should make request to Channels.stop() to unsubscribe. Something like this:
Code to subscribe:
var channel= {
'id': 'yourchannelid',
'type': 'web_hook',
'address': 'https://notification.endpoint'
};
var request = client.drive.files.watch({'fileId': fileId, 'channel':channel});
Code to unsubscribe
var request = client.drive.channels.stop({'id': channelId, 'resourceId':resourceId});