How to interact Google Spreadsheets with Telegram bot using Node? - node.js

I'm using Service Account's to manipulate my own spreadsheet, I know my script is working because I've tried it direct by Node (running node myScript.js)
My script is like this:
const { GoogleSpreadsheet } = require('google-spreadsheet');
async function start() {
const creds = require('./creds-from-google.json');
const sheetID = 'my spredsheets URL id here';
const doc = new GoogleSpreadsheet(sheetID);
await doc.useServiceAccountAuth({
client_email: creds.client_email,
private_key: creds.private_key,
});
await doc.loadInfo(); // loads document properties and worksheets
console.log(doc.title);
const sheet = doc.sheetsByIndex[0];
console.log(sheet.title);
const data = {
'Title': 'my title here',
'Content': 'my content here'
};
return sheet.addRow(data);
}
start();
But when I execute this code by Telegram bot, it's getting 401 error.
Error message:
Google API error - [401] 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.

Related

Google Tokens after a while have invalid permission

I am using the Google Calendar API in my Node.js app to fetch events from a user's calendar. I am using the googleapis package and have followed the steps to authenticate and authorize the API.
Everything was working fine for the first few days, but now I am receiving the following error message when trying to fetch events:
{
"error": "invalid_grant",
"error_description": "Bad Request"
}
I am not sure why this is happening, as I have not made any changes to my code or the way I am using the API. I have tried refreshing the access token, but this did not resolve the issue.
Here's the relevant code I am using:
const { google } = require('googleapis');
const calendar = google.calendar('v3');
const OAuth2 = google.auth.OAuth2;
const auth = new OAuth2(CLIENT_ID, CLIENT_SECRET, redirect);
auth.forceRefreshOnFailure = true;
// tokens is from DB where are stroed as they come as a response from Google
auth.setCredentials(tokens);
// Check if calendar is available
const from = new Date();
const to = new Date(24 * 60 * 60 * 1000 + +from);
try {
const freeBusyQuery = {
requestBody: {
items: [{ id: 'primary' }],
timeMin: from.toISOString(),
timeMax: to.toISOString(),
},
fields: 'calendars,groups,kind,timeMax,timeMin',
alt: 'json',
auth: auth.client,
};
const response = await calendar.freebusy.query(freeBusyQuery);
return !!response.data;
} catch (error) {
// Couple days works ok and after always get error on this request with stored Token
console.error(error);
return false;
}
And an example of body for auth:
const options = {
access_type: 'offline',
prompt: 'consent',
include_granted_scopes: true,
scopes: 'openid email profile https://www.googleapis.com/auth/calendar'
}
I am using version 95.0.0 of the googleapis package.
Publishing status of consent screen is In production.
Can someone help me understand why I am receiving the permission_denied error and how I can resolve it?

Chrome Extensions - token from "launchWebAuthFlow" expires after an hour and need user interaction to get new one

I am using launchWebAuthFlow in a service worker to authenticate users who choose to backup the extension settings in their Google Drive.
When a user clicks the login button, it sends a message to the service worker (MV3, perviously "background script"), who does this:
const redirectUrl = await browser.identity.launchWebAuthFlow({
'url': _createAuthEndpoint(),
'interactive': true
})
const url = new URL(redirectUrl);
const urlParams = new URLSearchParams(url.hash.slice(1));
const params = Object.fromEntries(urlParams.entries());
await browser.storage.local.set({googleToken: params.access_token});
Helper method to construct auth url:
function _createAuthEndpoint() {
const redirectURL = browser.identity.getRedirectURL();
const { oauth2 } = browser.runtime.getManifest();
const clientId = oauth2.client_id;
const authParams = new URLSearchParams({
client_id: clientId,
response_type: 'token',
redirect_uri: redirectURL,
scope: 'openid ' + oauth2.scopes.join(' '),
});
return `https://accounts.google.com/o/oauth2/auth?${authParams.toString()}`;
}
It works well for about an hour, after that the token get invalidated and I need to get a new token. If i try to use launchWebAuthFlow with interactive: false I get an error "user interaction required"
Is there a way to have the token refresh without user interaction?

YouTube Data v3 API - How to request all videos from a channel?

I am writing a Node.js script which will run in Lambda to periodically request the list of every video (public, unlisted, or private) for one of my organization's channels via the YouTube Data v3 API. In order to do this, it appears there are two steps:
Executing the channels/list call https://developers.google.com/youtube/v3/docs/channels/list to get the "Uploads" playlist.
const channelResult = await google.youtube('v3').channels.list({
auth: youtubeApiKey,
part: ['snippet', 'contentDetails'],
id: ["my_channel_id"]
});
Executing the playlistItems/list https://developers.google.com/youtube/v3/docs/playlistItems/list to see all the videos.
const videosResult = await google.youtube('v3').playlistItems.list({
auth: youtubeApiKey,
part: ['snippet', 'status'],
playlistId: "my_uploads_playlsit_id"
});
This only ever executes as a script running the cloud; there is no user interface or browser component. This all appeared to work in my test channel when I set the lone video there to public. But if I set it to private, I get:
The playlist identified with the request's <code>playlistId</code> parameter cannot be found.
What do I have to do in order to still access the Uploads playlist of my channel, and show private videos? Thanks!
With help from #stvar in the original question's comments, I was able to achieve this. The flow is as such:
Enable the YouTube Data API v3 from the Google Developers Console via the Enable APIs and Services.
Create a new OAuth client ID under YouTube Data API v3's "Credentials" pane and select Desktop app.
Save the client_id and client_secret. Make these accessible to your Node app via whatever environment variable method you prefer.
Create a separate script specifically for getting a refresh_token via YouTube Data API v3 OAuth
import { google } from 'googleapis';
import prompts from 'prompts';
console.log("about to execute oauth");
const yt_client_id = process.env.YOUTUBE_CLIENT_ID;
const yt_client_secret = process.env.YOUTUBE_CLIENT_SECRET;
const oauthClient = new google.auth.OAuth2({
clientId: yt_client_id,
clientSecret: yt_client_secret,
redirectUri: 'http://localhost'
});
const authUrl = oauthClient.generateAuthUrl({
access_type: 'offline', //gives you the refresh_token
scope: 'https://www.googleapis.com/auth/youtube.readonly'
});
const codeUrl = await prompts({
type: 'text',
name: 'codeURl',
message: `Please go to \n\n${authUrl}\n\nand paste in resulting localhost uri`
});
const decodedUrl = decodeURIComponent(codeUrl.codeURl);
const code = decodedUrl.split('?code=')[1].split("&scope=")[0];
const token = (await oauthClient.getToken(code)).tokens;
const yt_refresh_token = token.refresh_token;
console.log(`Please save this value into the YOUTUBE_REFRESH_TOKEN env variable for future runs: ${yt_refresh_token}`);
await prompts({
type: 'text',
name: 'blank',
message: 'Hit enter to exit:'
});
process.exit(0);
Save the refresh token in another environment variable, accessible to your main data-fetching script. Use it as such:
import { google } from 'googleapis';
console.log("Beginning youtubeIndexer. Checking for valid oauth.");
const yt_refresh_token = process.env.YOUTUBE_REFRESH_TOKEN;
const yt_client_id = process.env.YOUTUBE_CLIENT_ID;
const yt_client_secret = process.env.YOUTUBE_CLIENT_SECRET;
const yt_channel_id = process.env.YOUTUBE_CHANNEL_ID;
const oauthClient = new google.auth.OAuth2({
clientId: yt_client_id,
clientSecret: yt_client_secret,
redirectUri: 'http://localhost'
});
oauthClient.setCredentials({
refresh_token: yt_refresh_token
});
const youtube = google.youtube("v3");
const channelResult = await youtube.channels.list({
auth: oauthClient,
part: ['snippet', 'contentDetails'],
id: [yt_channel_id]
});
let nextPageToken = undefined;
let videosFetched = 0;
do {
const videosResult = await youtube.playlistItems.list({
auth: oauthClient,
maxResults: 50,
pageToken: nextPageToken,
part: ['snippet', 'status'],
playlistId: channelResult.data.items[0].contentDetails.relatedPlaylists.uploads
});
videosFetched += videosResult.data.items.length;
nextPageToken = videosResult.data.nextPageToken;
videosResult.data.items.map((video, index) => {
//process the files as you need to.
});
} while (nextPageToken);
This last .map() function, marked with the "process the files as you need to" comment will receive every video in the channel, whether it be public, unlisted, or private.
NOTE: I do not know yet how long a given refresh_token will last, but assume that you will regularly need to run the first script again and update the refresh_token used via the second script's environment variable.

Gmail API service account request- Precondition check failed

I'm trying to work with the google API's for the first time, and when I attempt to make a request to the gmail API I'm getting a "precondition check failed" error. I am using a service account authorization, not Oauth2 user consent. Things I've tried:
Authorized "domain wide delegation" for the service account.
Ensured the APP is trusted in the G suite account.
Ensured service account role is "owner"
Enabled domain wide delegation for the client ID of the service account in the g suite admin panel.
This is an adapted sample from the Node client library, but the sample did not use service account auth so I wasn't able to use the sample directly.
const path = require('path');
const {google} = require('googleapis');
const gmail = google.gmail('v1');
async function runSample() {
// Obtain user credentials to use for the request
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve(__dirname, 'google-key.json'),
scopes: ['https://www.googleapis.com/auth/gmail.readonly'],
});
google.options({auth});
const res = await gmail.users.messages.list({userId: 'me'}); // have tried with my gsuite email address as well
console.log(res.data);
return res.data;
}
if (module === require.main) {
runSample().catch(console.error);
}
module.exports = runSample;
Returning error with message: Error: Precondition check failed.
After searching the dark web for eternity, I found a link to a github issue that described how to authenticate as a service using JWT auth.
This is a working version of what I was trying to accomplish:
const path = require('path');
const {google} = require('googleapis');
async getMessageList(userId, qty) {
const JWT = google.auth.JWT;
const authClient = new JWT({
keyFile: path.resolve(__dirname, 'google-key.json'),
scopes: ['https://www.googleapis.com/auth/gmail.readonly'],
subject: 'admin#example.com' // google admin email address to impersonate
});
await authClient.authorize(); // once authorized, can do whatever you want
const gmail = google.gmail({
auth: authClient,
version: 'v1'
});
const response = await gmail.users.messages.list({
includeSpamTrash: false,
maxResults: qty,
q: "",
userId: userId
});
// the data object includes a "messages" array of message data
return response.data;
}

NodeJS Googleapis Service Account authentication

I'm trying to perform authentication on GoogleAPIs using a Service Account. I have a service account set up, with its credentials located at credentials.json. I try to access a private sheet, to which I added the E-Mail address of the service account with editing rights.
Here the code I am using:
const {
google
} = require('googleapis');
const fs = require('fs');
let scopes = ['https://www.googleapis.com/auth/spreadsheets'];
let credentials = require("./credentials.json");
const authClient = new google.auth.JWT(
credentials.client_email,
null,
credentials.private_key,
scopes);
authClient.authorize(function(err, tokens) {
if (err) {
console.log(err);
return;
} else {
authClient.setCredentials(tokens);
}
});
const sheets = google.sheets({
version: 'v4',
authClient
});
let spreadsheetId = //...
let range = //...
const request = {
spreadsheetId: spreadsheetId,
range: range
};
sheets.spreadsheets.values.get(request, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
} else {
console.log('Result: ' + response);
}
});
I guess the API changed over time, since many guides showed different approaches, and in the end none worked for me.
The error is as follows:
The API returned an error: Error: The request is missing a valid API key.
To my understanding, a simple API key should only be necessary for unauthenticated access on public sheets, so I don't get why it is even requiring that. If I add such an API key I get the error
The API returned an error: Error: The caller does not have permission
Using
$ npm list googleapis
`-- googleapis#52.1.0
Any help would be greatly appreciated.
For who still facing googleapis problems within NodeJS Runtime in 2022.
Firstly, redirect into Google-IAM-Admin/ServiceAccount to pick the current working project.
Secondly, click to jump into Service Account that has the following format project#sub-name-id.iam.gserviceaccount.com.
Thirdly, between [Details, Permissions, Keys, Metrics, Logs]. Jump into Keys then Add Key -> Create new Key -> Key type::JSON and save JSON file to your computer.
Here within NodeJS Runtime, I use the following Semantic Version
googleapis#100.0.0
You can create JWT Client and inject into google default auth at google.options({auth: client}); or provide auth-client to specific Service as google.chat({version: 'v1', auth: client});
However, in the following example. I create a GoogleAuth instance and then make an AuthClient after. Which resulted the same behaviour to the JWT Method.
/** Import Node Native Dependencies !*/
import * as path from "path";
/** Import ES6 Default Dependencies !*/
import {google} from "googleapis";
const {client_email, private_key} = require('$/keys/credentials.json');
/**
** #description - Google [[Service Account]] Authenticator.
**/
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve('keys/credentials.json'),
/** Scopes can be specified either as an array or as a single, space-delimited string; ~!*/
scopes: [
"https://www.googleapis.com/auth/chat.bot",
],
});
const client = new google.auth.JWT({
email: client_email,
key: private_key,
/** Scopes can be specified either as an array or as a single, space-delimited string; ~!*/
scopes: [
"https://www.googleapis.com/auth/chat.bot",
],
});
(async () => {
/** #description - Either [[Get Client]] from [Google Auth] or Use directly from [JWT Client] ~!*/
const client = await auth.getClient();
/** #description - Use this Authorized Client as Default Authenticated to fallback from [Non-Authenticated Services] ~!*/
google.options({auth: client});
const chat = google.chat({
version: 'v1',
/** #description - Provide [Authenticated Services] to [Google Chat Service] Instance ~!*/
auth: client,
});
const response = await chat.spaces.members.get({
// Required. Resource name of the attachment, in the form "spaces/x/messages/x/attachments/x".
name: 'spaces',
});
console.log('response', response.data);
return void 0;
})();

Resources