Insert YouTube top level comment using Google Apps Script - google-apps-script

I am trying to create a program using Google Apps Script that inserts a comment when a certain YouTube channel uploads. I have been able to get the latest YouTube video ID from the channel but when I try to insert a comment, it throws an error, "Parse Error (line 19, file 'Code')".
Line 19: YouTube.CommentThreads.insert("snippet", {
Here's my code:
function getVideo() {
// MrBeast Channel ID: UCX6OQ3DkcsbYNE6H8uQQuVA
var channel = "UCX6OQ3DkcsbYNE6H8uQQuVA";
var fttx = "FIRST!";
var results = YouTube.Channels.list("contentDetails", {"id": channel});
for (var i in results.items) {
var item = results.items[i];
var playlistId = item.contentDetails.relatedPlaylists.uploads;
// Uploads Playlist ID: UUX6OQ3DkcsbYNE6H8uQQuVA
var playlistResponse = YouTube.PlaylistItems.list("snippet", {"playlistId": playlistId, "maxResults": 1});
for (var j = 0; j < playlistResponse.items.length; j++) {
var playlistItem = playlistResponse.items[j];
var latvid = playlistItem.snippet.resourceId.videoId;
comment(latvid, channel, fttx);
}
}
}
function comment(vid, ytch, fc) {
YouTube.CommentThreads.insert("snippet", {
"snippet.channelId": ytch,
"snippet.videoId": vid,
"snippet.topLevelComment.snippet.textOriginal": fc
});
}

Per Apps Script advanced services documentation, when specifying resources (such as a CommentThread) they are the first parameter to a method. If you use the Apps Script editor's autocomplete, it is very clear about the required order:
Also note that you have incorrectly created your resource body - you have various sub-properties. For example, the snippet property is a required member of the CommentThread resource. Three "snippet.___" properties are not equivalent to one snippet property with 3 sub-properties.
Thus the solution to resolve the parse error in YouTube.CommentThreads.insert is to use the required method signature, with the required resource format:
function startCommentThread(vid, ytch, fc) {
const resource = {
snippet: {
channelId: ytch,
videoId: vid,
topLevelComment: {
snippet: {
textOriginal: fc
}
}
}
};
YouTube.CommentThreads.insert(resource, "snippet");
}

According to docs the {}, is missing and single quotes are used. I'm unable to test this right now but hopefully it will solve your problem.
commentThreadsInsert('snippet',
{},
{'snippet.channelId': '',
'snippet.videoId': '',
'snippet.topLevelComment.snippet.textOriginal': ''
});

Related

Google Docs API - complete documentation (hyperlink issue)

I hope everyone is in good health. This post is my continue of my previous post
My main goal
So main goal was to get the hyperlink and change it the text linked with it. I initially used code from this post and modified it to change the text of first hyperlink. Here is my modified code to change the text of first hyperlink.
function onOpen() {
const ui = DocumentApp.getUi();
ui.createMenu('What to do?')
.addItem('HyperLink Modifier', 'findAndReplacetext')
.addToUi();
}
/**
* Get an array of all LinkUrls in the document. The function is
* recursive, and if no element is provided, it will default to
* the active document's Body element.
*
* #param element The document element to operate on.
* .
* #returns {Array} Array of objects, vis
* {element,
* startOffset,
* endOffsetInclusive,
* url}
*/
function getAllLinks(element) {
var links = [];
element = element || DocumentApp.getActiveDocument().getBody();
if (element.getType() === DocumentApp.ElementType.TEXT) {
var textObj = element.editAsText();
var text = element.getText();
var inUrl = false;
for (var ch=0; ch < text.length; ch++) {
var url = textObj.getLinkUrl(ch);
if (url != null) {
if (!inUrl) {
// We are now!
inUrl = true;
var curUrl = {};
curUrl.element = element;
curUrl.url = String( url ); // grab a copy
curUrl.startOffset = ch;
}
else {
curUrl.endOffsetInclusive = ch;
}
}
else {
if (inUrl) {
// Not any more, we're not.
inUrl = false;
links.push(curUrl); // add to links
curUrl = {};
}
}
}
if (inUrl) {
// in case the link ends on the same char that the element does
links.push(curUrl);
}
}
else {
var numChildren = element.getNumChildren();
for (var i=0; i<numChildren; i++) {
links = links.concat(getAllLinks(element.getChild(i)));
}
}
return links;
}
/**
* Replace all or part of UrlLinks in the document.
*
* #param {String} searchPattern the regex pattern to search for
* #param {String} replacement the text to use as replacement
*
* #returns {Number} number of Urls changed
*/
function findAndReplacetext() {
var links = getAllLinks();
while(links.length > 0){
var link = links[0];
var paragraph = link.element.getText();
var linkText = paragraph.substring(link.startOffset, link.endOffsetInclusive+1);
var newlinkText = `(${linkText})[${link.url}]`
link.element.deleteText(link.startOffset, link.endOffsetInclusive);
link.element.insertText(link.startOffset, newlinkText);
links = getAllLinks();
}
}
String.prototype.betterReplace = function(search, replace, position) {
if (this.length > position) {
return this.slice(0, position) + this.slice(position).replace(search, replace);
}
return this;
}
Note: I used insertText and deleteText functions to update the text value of hyperlink.
My problem with above code
Now the problem was that this code was running too slow. I thought may be it was because I was running the script every-time I needed to search for next hyperlink, So maybe I can break the loop and only get the first hyperlink each time. Then from my previous post the guy gave me a solution to break loop and only get the first hyperlink but when I tried the new code unfortunately it was still slow. In that post he also proposed me a new method by using Google Docs API, I tried using that it was was super fast. Here is the code using Google Docs API
function myFunction() {
const doc = DocumentApp.getActiveDocument();
const res = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
if (paragraph && paragraph.elements) {
paragraph.elements.forEach(({textRun}) => {
if (textRun && textRun.textStyle && textRun.textStyle.link) {
ar.push({text: textRun.content, url: textRun.textStyle.link.url});
}
});
}
return ar;
}, []);
console.log(res) // You can retrieve 1st link and test by console.log(res[0]).
}
My new problem
I liked the new code but I am stuck again at this point as I am unable to find how can I change the text associated with the hyperlink. I tried using the functions setContent and setUrl but they don't seem to work. Also I am unable to find the documentation for these functions on main documentation of this API. I did find I reference for previously mentioned functions here but they are not available for appscript. Here is the sample document I am working on
https://docs.google.com/document/d/1eRvnR2NCdsO94C5nqly4nRXCttNziGhwgR99jElcJ_I/edit?usp=sharing
End note:
I hope I was able to completly convey my message and all the details assosiated with it. If not kindly don't be mad at me, I am still in learning process and my English skills are pretty weak. Anyway if you want any other data let me know in the comments and Thanks for giving your time I really appreciate that.
In order to remove all the hyperlink from your document, you can do the following:
First, retrieve the start and end indexes of these hyperlinks. This can be done by calling documents.get, iterate through all elements in the body content, checking which ones are paragraphs, iterating through the corresponding TextRun, and checking which TextRuns contain a TextStyle with a link property. All this is already done in the code you provided in your question.
Next, for all TextRuns that include a link, retrieve their startIndex and endIndex.
Using these retrieved indexes, call batchUpdate to make an UpdateTextStyleRequest. You want to remove the link property between each pair of indexes, and for that you would just need to set fields to link (in order to specify which properties you want to update) and don't set a link property in the textStyle property you provide in the request since, as the docs for TextStyle say:
link: If unset, there is no link.
Code sample:
function removeHyperlinks() {
const doc = DocumentApp.getActiveDocument();
const hyperlinkIndexes = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
if (paragraph && paragraph.elements) {
paragraph.elements.forEach(element => {
const textRun = element.textRun;
if (textRun && textRun.textStyle && textRun.textStyle.link) {
ar.push({startIndex: element.startIndex, endIndex: element.endIndex });
}
});
}
return ar;
}, []);
hyperlinkIndexes.forEach(hyperlinkIndex => {
const resourceUpdateStyle = {
requests: [
{
updateTextStyle: {
textStyle: {},
fields: "link",
range: {
startIndex: hyperlinkIndex.startIndex,
endIndex: hyperlinkIndex.endIndex
}
}
}
]
}
Docs.Documents.batchUpdate(resourceUpdateStyle, doc.getId());
});
}

Create new tweet with Google Script

(I have read already the solution given in Tweet using Google Script but it doesn't work for me. Please don't delete this question.)
Page https://ctrlq.org/code/19995-google-script-to-twitter shows a way to create a tweet in Google Script.
I simply pasted the code offered there and successfully followed this instructions:
Go to apps.twitter.com and create a new app
Generate Access Token and Secret
Include Twitter library key «MKvHYYdYA4G5JJHj7hxIcoh8V4oX7X1M_» in Google Script
Activate «Enable Sign in with Twitter» in «App details» in Twitter
Add the «Callbacks URL» in Twitter using my Google Script Project Key: https://script.google.com/macros/d/ML3pMd5z.../usercallback
Now, when I run the code almost nothing happens, but this error message: ReferenceError: "twit" is not defined. (line 22, file "Code").
function sendTweet(status){
status = status || "I found this snippet on #labnol's ctrlq.org";
var twitterKeys= {
TWITTER_CONSUMER_KEY: "1fZn8cBR...",
TWITTER_CONSUMER_SECRET: "588gJxh...",
TWITTER_ACCESS_TOKEN: "112c5e0...",
TWITTER_ACCESS_SECRET: "DcvV614.",
};
var props = PropertiesService.getScriptProperties();
props.setProperties(twitterKeys);
var service = new Twitter.OAuth(props);
if ( service.hasAccess() ) {
var response = twit.sendTweet(status); // ← THIS IS LINE 21
if (response) {
Logger.log("Tweet ID " + response.id_str);
} else {
// Tweet could not be sent
// Go to View -> Logs to see the error message
}
}
}
Please tell me how to fix this specific code.
Found the answer in https://medium.com/javascript-in-plain-english/i-made-a-twitter-bot-from-nothing-but-a-google-sheet-ef0ba6e1b194 and simply added the missing variable after var service = new Twitter.OAuth(props);:
var twit = new Twitter.OAuth(props);
The final code is like so:
function sendTweet(){
var status = "Text to be tweeted goes here.";
var twitterKeys= {
TWITTER_CONSUMER_KEY: "1fZBR...",
TWITTER_CONSUMER_SECRET: "Ig...tzpa4E...",
TWITTER_ACCESS_TOKEN: "1125...90...",
TWITTER_ACCESS_SECRET: "...22GrDcv...",
};
var props = PropertiesService.getScriptProperties();
props.setProperties(twitterKeys);
var service = new Twitter.OAuth(props);
var twit = new Twitter.OAuth(props); // ← THIS WAS THE MISSING VARIABLE
if ( service.hasAccess() ) {
var response = twit.sendTweet(status);
if (response) {
Logger.log("Tweet ID " + response.id_str);
} else { }
}
}

Google Slides: newly inserted table not found

I´m wondering what is going on. I have two functions which both are working good when called one after one:
function createTable() {
var slidesPage = SlidesApp.openById('1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI').getSlides()[0];
var table = slidesPage.insertTable(7, 4);
}
function changeColumnWidth() {
var slidesPage = SlidesApp.openById('1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI').getSlides()[0];
var tableId = slidesPage.getTables()[0].getObjectId();
var requests = [{
updateTableColumnProperties: {
objectId: tableId,
"columnIndices": [ 1, 3],
"tableColumnProperties": {
"columnWidth": {
"magnitude": 80,
"unit": "PT"
}
},
"fields": "columnWidth"
}
}];
var createSlideResponse = Slides.Presentations.batchUpdate({
requests: requests
}, '1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI');
}
But trying to combine these two functions like:
function combined() {
createTable();
changeColumnWidth();
}
I´m getting Error:
Invalid requests[0].updateTableColumnProperties: The object (SLIDES_API456304911_0) could not be found.
Wondering if the insertTable method is asynchronous and therefore the created table is not ready?
Thanks for any help.
How about this modification? Please think of this as one of several workarounds. In my workaround, I used saveAndClose() for your situation. Using this, I thought to separate the process of SlidesApp and Slides API.
Modification points :
Save and close the slide using saveAndClose() after the table was inserted.
Return an object ID of inserted table to use at changeColumnWidth().
At changeColumnWidth(), the table is modified by Slides API using the received object ID.
Modified script :
function combined() {
var tableId = createTable(); // Modified
changeColumnWidth(tableId); // Modified
}
function createTable() {
var slide = SlidesApp.openById('1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI'); // Modified
var slidesPage = slide.getSlides()[9]; // Modified
var table = slidesPage.insertTable(7, 4);
slide.saveAndClose(); // Added
return table.getObjectId();
}
function changeColumnWidth(tableId) { // Modified
// var slidesPage = SlidesApp.openById('1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI').getSlides()[0]; // This line is not used.
// var tableId = slidesPage.getTables()[0].getObjectId(); // This line is not used because slidesPage.getTables().length becomes 0.
var requests = [{
updateTableColumnProperties: {
objectId: tableId,
"columnIndices": [ 1, 3],
"tableColumnProperties": {
"columnWidth": {
"magnitude": 80,
"unit": "PT"
}
},
"fields": "columnWidth"
}
}];
var createSlideResponse = Slides.Presentations.batchUpdate({
requests: requests
}, '1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI');
}
Note :
For the slide which is saved and closed by saveAndClose(), when the slide is reopened, the inserted table cannot be retrieved. When the table is tried to be retrieved using getTables() again, the length becomes 0. But at Slides API, the object ID of table can be retrieved. So I thought that the issue might be able to be solved by returning the object ID of table after the table was inserted.
But I couldn't understand about the reason that the values retrieved by getTables() from the reopened Slide become "0" yet. I'm sorry.
Reference :
saveAndClose()
If this workaround was not what you want, I'm sorry.
To achieve your goal - create a table with a specified layout and specific column sizes in one function - you should use the Slides API for the entire task. The Slides API lets you both create and modify the same element in the same batch request, if you provided a unique object ID for it. Otherwise, you have to first create the element, then send the modification request using the objectId found in the response to the first request. This second approach is essentially the behavior you were experiencing when the function calls were done separately.
There are restrictions on user-supplied IDs, naturally:
objectId string: A user-supplied object ID.If you specify an ID, it must be unique among all pages and page elements in the presentation. The ID must start with an alphanumeric character or an underscore (matches regex [a-zA-Z0-9_] ); remaining characters may include those as well as a hyphen or colon (matches regex [a-zA-Z0-9_-:] ). The length of the ID must not be less than 5 or greater than 50.If you don't specify an ID, a unique one is generated.
Given that hyphens are allowed, we can use the Utilites.getUuid() method to help supply our own unique object IDs.
When mixing SlidesApp and Slides, it is very likely that internal Google optimizations (e.g. write-caching) change the operation order. By restricting to a single service for related task operations, we can ensure that the objects we need are available when needed.
This example uses two methods that make Request objects for batchUpdate and ultimately creates a presentation, adds a blank slide, adds a table and modifies it, and then creates another blank slide.
function makeCreateTableRequest_(slideId, rows, columns, shouldSupplyID) {
const tablerq = {
rows: rows,
columns: columns,
elementProperties: {
pageObjectId: slideId,
/** size: {
height: {...},
width: {...}
},
transform: { ... } */
}
};
// If asked to use a custom ID (e.g. also going to modify this table), use a unique one.
if (shouldSupplyID)
tablerq.objectId = ("table" + Utilities.getUuid()).slice(0, 50);
return {createTable: tablerq};
}
function makeModifyTableColumnPropsRequest_(tableId, newWidthDimension, indicesArray) {
const rq = {
objectId: tableId,
fields: "columnWidth" // There are no other fields for this request as of 2018-07
};
if (newWidthDimension && newWidthDimension.magnitude !== undefined && newWidthDimension.unit)
rq.tableColumnProperties = { columnWidth: newWidthDimension };
if (indicesArray && indicesArray.length)
rq.columnIndices = indicesArray;
return {updateTableColumnProperties: rq};
}
function createPresentation_() {
const newPres = { title: "API-created Presentation" };
// Presentations are huge... limit the metadata sent back to us.
const fields = "presentationId,pageSize,title"
+ ",slides(objectId,pageType,pageElements(objectId,size,title,description))"
+ ",masters(objectId,pageType,pageElements(objectId,size,title,description))"
+ ",layouts(objectId,pageType,pageElements(objectId,size,title,description))";
const createdMetadata = Slides.Presentations.create(newPres, {fields: fields});
console.log({message:"Created a Presentation", response: createdMetadata});
return createdMetadata;
}
function addSlide_(pId) {
const response = Slides.Presentations.batchUpdate({ requests: [{ createSlide: {} }] }, pId);
return response.replies[0].createSlide.objectId;
}
function foo() {
const pres = createPresentation_();
const newSlideId = addSlide_(pres.presentationId);
// Get requests to add and to modify tables.
const openingTableRq = makeCreateTableRequest_(pres.slides[0].objectId, 2, 4);
const newTableRq = makeCreateTableRequest_(newSlideId, 7, 4, true);
const changeWidthRq = makeModifyTableColumnPropsRequest_(newTableRq.createTable.objectId, {magnitude: 80, unit: "PT"}, [0]);
// Add and update the desired table, then create a new slide.
var response = Slides.Presentations.batchUpdate({
requests: [
openingTableRq, // will have reply
newTableRq, // will have reply
changeWidthRq, // no reply
{ createSlide: {} } // will have reply
]
}, pres.presentationId);
console.log({message: "Performed updates to the created presentation", response: response});
}

How to create an object of specific type from JSON in Parse

I have a Cloud Code script that pulls some JSON from a service. That JSON includes an array of objects. I want to save those to Parse, but using a specific Parse class. How can I do it?
Here's my code.
Parse.Cloud.httpRequest({
url: 'http://myservicehost.com',
headers: {
'Authorization': 'XXX'
},
success: function(httpResponse) {
console.log("Success!");
var json = JSON.parse(httpResponse.text);
var recipes = json.results;
for(int i=0; i<recipes.length; i++) {
var Recipe = Parse.Object.extend("Recipe");
var recipeFromJSON = recipes[i];
// how do i save recipeFromJSON into Recipe without setting all the fields one by one?
}
}
});
I think I got it working. You need to set the className property in the JSON data object to your class name. (Found it in the source code) But I did only try this on the client side though.
for(int i=0; i<recipes.length; i++) {
var recipeFromJSON = recipes[i];
recipeFromJSON.className = "Recipe";
var recipeParseObject = Parse.Object.fromJSON(recipeFromJSON);
// do stuff with recipeParseObject
}
Example from this page https://parse.com/docs/js/guide
var GameScore = Parse.Object.extend("GameScore");
var gameScore = new GameScore();
gameScore.save({
score: 1337,
playerName: "Sean Plott",
cheatMode: false
}, {
success: function(gameScore) {
// The object was saved successfully.
},
error: function(gameScore, error) {
// The save failed.
// error is a Parse.Error with an error code and message.
}
});
IHMO this question is not a duplicate of How to use Parse.Object fromJSON? [duplicate]
In this question the JSON has not been generated by the Parse.Object.toJSON function itself, but comes from another service.
const object = new Parse.Object('MyClass')
const asJson = object.toJSON();
// asJson.className = 'MyClass';
Parse.Object.fromJSON(asJson);
// Without L3 this results into:
// Error: Cannot create an object without a className
// It makes no sense (to me) why the Parse.Object.toJSON is not reversible

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});