I am trying to create issues in a Github repo via a Javascript client. My code is like so
const github = `https://api.github.com/repos/${owner}/${repo}/issues`;
const msg = JSON.stringify({
"title": `problem with record id: ${recId}`,
"body": "something is wrong in Houston",
"assignee": "flooba",
"milestone": 1,
"labels": [
"images"
]
});
const x = new XMLHttpRequest();
x.open('POST', github, true);
x.onreadystatechange = function() {
if (x.readyState == 4) {
// show status message, all good
}
};
x.setRequestHeader("Content-type", "application/json");
x.setRequestHeader("Authorization", "Basic " + btoa("username:password")); //**
x.send(msg);
[**] username and password are the creds for the :repo, naturally.
When I try the above code, I get http 422. Suggestions?
Note: In the actual working code, I will put the credentials on the server side and invoke the server side process from the web client.
Related
CURRENTLY
I have a Google Sheets App Script 'web app'
Script in Goolge Sheets
function doPost(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet1");
sheet.getRange("A1").setValue("Hello!")
return "Success!"
}
Google Apps Script Web App Config:
Execute as: Me // or as User. I've tried both.
Who has access: Anyone within MyOrganisation
I want to make a POST request to the above Web App from AWS Lambda.
AWS Lambda .js:
const { GoogleSpreadsheet } = require("google-spreadsheet");
const doc = new GoogleSpreadsheet(
{spreadsheetId}
);
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n"),
});
let token = doc["jwtClient"]["credentials"]["access_token"];
await new Promise((resolve, reject) => {
const options = {
host: 'script.google.com',
path: "/macros/s/{myscriptid}/exec", //<-- my web app path!
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer "+ token
}
};
//create the request object with the callback with the result
const req = HTTPS.request(options, (res) => {
resolve(JSON.stringify(res.statusCode));
});
// handle the possible errors
req.on('error', (e) => {
reject(e.message);
});
//do the request
req.write(JSON.stringify(data));
//finish the request
req.end();
});
console.log("response:"+JSON.stringify(response))
GCP Service Account
I have a GCP Service Account, with permission to Google Sheets API, and otherwise unrestricted access.
This Service account has EDIT access to the Google Sheet with the doPost(e) script.
Token Output:
"jwtClient": {
"_events": {},
"_eventsCount": 0,
"transporter": {},
"credentials": {
"access_token": "somelongvalue...............", //<-- what I use
"token_type": "Bearer",
"expiry_date": 1661662492000,
"refresh_token": "jwt-placeholder"
},
"certificateCache": {},
"certificateExpiry": null,
"certificateCacheFormat": "PEM",
"refreshTokenPromises": {},
"eagerRefreshThresholdMillis": 300000,
"forceRefreshOnFailure": false,
"email": "serviceaccount#appspot.gserviceaccount.com",
"key": "-----BEGIN PRIVATE KEY-----\nsomelongvalue=\n-----END PRIVATE KEY-----\n",
"scopes": [
"https://www.googleapis.com/auth/spreadsheets"
],
"subject": null,
"gtoken": {
"key": "-----BEGIN PRIVATE KEY-----\nsomelongvalue=\n-----END PRIVATE KEY-----\n",
"rawToken": {
"access_token": "somelongvalue...............",
"expires_in": 3599,
"token_type": "Bearer"
},
"iss": "serviceaccount#appspot.gserviceaccount.com",
"sub": null,
"scope": "https://www.googleapis.com/auth/spreadsheets",
"expiresAt": 1661662492000
}
}
ISSUE
Current response:
response:"401"
I cannot find any Google documentation on how to setup the headers to authenticate a request (from my service account) to my organisation restricted web app.
When the Web App is open to "Anyone" then it runs fine, but as soon as I restrict to MyOrganisation, I struggle to find a way to authenticate my POST request.
HELP!
How do I set up a POST request to my Google Sheets web app such that it can be protected by authentication? Right now, I'd be happy to find ANY means to authenticate this request (not necessarily a service account) that doesn't leave it completed open to public.
Should I use this hack?
One idea I had was to put a "secret" into my lambda function, and then make the web app public. The web app would check the secret, if if matched, would execute the function.
Modification points:
In order to access Web Apps using the access token with a script, the scopes of Drive API are required to be included. Those are https://www.googleapis.com/auth/drive.readonly, https://www.googleapis.com/auth/drive, and so on. Ref
When I saw your showing script, it seems that the access token is retrieved using google-spreadsheet. When I saw the script of google-spreadsheet, it seems that this uses only the scope of https://www.googleapis.com/auth/spreadsheets. Ref
From this situation, I thought that the reason for your current issue might be due to this. If my understanding is correct, how about the following modification? In this modification, the access token is retrieved by googleapis for Node.js from the service account. Ref
Modified script:
Google Apps Script side:
function doPost(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet1");
sheet.getRange("A1").setValue("Hello!")
return ContentService.createTextOutput("Success!"); // Modified
}
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful about this.
You can see the detail of this in the report "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
Node.js side:
const { google } = require("googleapis");
const HTTPS = require("https");
const auth = new google.auth.JWT(
"###", // Please set client_email here.
null,
"###", // Please set private_key here. When you set private_key of service account, please include \n.
["https://www.googleapis.com/auth/drive.readonly"],
null
);
function req(token) {
return new Promise((resolve, reject) => {
const data = { key1: "value1" }; // Please set your value.
const options = {
host: "script.google.com",
path: "/macros/s/{myscriptid}/exec", //<-- my web app path!
method: "POST",
headers: {Authorization: "Bearer " + token},
};
const req = HTTPS.request(options, (res) => {
if (res.statusCode == 302) {
HTTPS.get(res.headers.location, (res) => {
if (res.statusCode == 200) {
res.setEncoding("utf8");
res.on("data", (r) => resolve(r));
}
});
} else {
res.setEncoding("utf8");
res.on("data", (r) => resolve(r));
}
});
req.on("error", (e) => reject(e.message));
req.write(JSON.stringify(data));
req.end();
});
}
auth.getAccessToken().then(({ token }) => {
req(token).then((e) => console.log(e)).catch((e) => console.log(e));
});
When this script is run, when the Web Apps is correctly deployed, the script of Web Apps is run and Success! is returned.
Note:
If this modified script was not useful for your Web Apps setting, please test as follows.
Please confirm whether your service account can access to the Spreadsheet again.
Please share the email address of the service account on the Spreadsheet. From your showing Google Apps Script, I thought that your Google Apps Script is the container-bound script of the Spreadsheet.
Please reflect the latest script to the Web Apps.
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful about this.
You can see the detail of this in the report "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
When you set private_key of service account, please include \n.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
Added:
When you will directly put the value to the Spreadsheet using Sheets API with google-spreadsheet module, you can also use the following script.
const { GoogleSpreadsheet } = require("google-spreadsheet");
const sample = async () => {
const doc = new GoogleSpreadsheet("###"); // Please set your Spreadsheet ID.
await doc.useServiceAccountAuth({
client_email: client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
});
await doc.loadInfo();
const sheet = doc.sheetsByTitle["Sheet1"];
await sheet.loadCells("A1");
sheet.getCell(0, 0).value = "Hello!";
await sheet.saveUpdatedCells();
};
sample();
In this case, your service account is required to be able to access to the Spreadsheet. Please be careful about this.
I'm working on a dialogflow chatbot. It retrieves the user's name and email at the start of the conversation, and I have a sessions-vars output context with a lifespan of 60 to keep these parameters throughout the conversation. I know they are kept by checking diagnostic info and referring to them in chatbot replies as #context.parameter.
Near the end of my conversation path there in an intent called 110checklistemail.sendemail where my chatbot asks the user if they want information emailed to their email or sent in the chat. If the user says "Email it to me" I have webhook call enabled, where it redirects to fulfillment code, pasted below.
I followed a guide to integrate the chatbot with sendgrid, and the email is sent and does work if I prompt for the email at that specific intent. However, if I don't prompt for the email address (eg. the user says 'email it to me') then the agent is not able to send the email because the email parameter is now blank, despite being populated before in a context with a long lifespan.
Fulfillment code is the following:
'use strict';
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');
const sgMail = require('#sendgrid/mail');
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
process.env.SENDGRID_API_KEY = 'SG._APIKEYHERE';
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
function sendEmail(agent) {
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const emailParam = agent.parameters.email;
const msg = {
to: emailParam,
from: 'tyler#mailfence.com',
subject: 'Just a quick note',
text: 'Just saying Hi ${agent.parameters.given-name} from Dialogflow...',
html: 'Just saying <strong>Hi Hi ${agent.parameters.given-name} from Dialogflow</strong>...',
};
console.log(msg);
sgMail.send(msg);
agent.add(`What a beauty!`);
}
// Run the proper function handler based on the matched Dialogflow intent name
let intentMap = new Map();
intentMap.set('110checklistemail.sendemail', sendEmail);
// intentMap.set('your intent name here', yourFunctionHandler);
// intentMap.set('your intent name here', googleAssistantHandler);
agent.handleRequest(intentMap);
});
Snippet from Diagnostic info:
{
"responseId": "130742d9-7453-41c6-8b27-ee8f91d9d02d-5a74d3f9",
"queryResult": {
"queryText": "Email it to me",
"parameters": {
"email": ""
},
"allRequiredParamsPresent": true,
"fulfillmentText": "What a beauty!",
"fulfillmentMessages": [
{
"text": {
"text": [
"What a beauty!"
]
}
}
],
"outputContexts": [
{
"name": "projects/tyler-vhyo/locations/global/agent/sessions/aef22896f9/contexts/1await_checklist_type",
"lifespanCount": 4,
"parameters": {
"Jobtype.original": "custom shower",
"email.original": "",
"Jobtype": "custom shower",
"email": ""
}
},
{
"name": "projects/tyler-vhyo/locations/global/agent/sessions/aef3d36-18d696f9/contexts/session-vars",
"lifespanCount": 54,
"parameters": {
"last-name.original": "",
"email.original": "",
"email": "",
"given-name.original": "Tim",
"Jobtype": "custom shower",
"Jobtype.original": "custom shower",
"given-name": "Tim",
"last-name": ""
}
I was wondering why it wasn't working unless I prompt for the email in that specific intent, and I realised it's because the email parameters are now all blank (and definitely populated before!) I don't want to reprompt for the email when the user already has to input it at the start of the conversation.
How can I solve this issue and get the email to send off the pre-existing email parameter?
Also, how can I get the ${agent.parameters.given-name} code to reference the name parameter correctly in the email body? It hasn't been working for me and I don't know if there is a better way to pull parameters in the email.
I'm an extreme novice at coding at the code above is something I made following a guide. I really do have no idea what is happening so any advice is very welcome and appreciated. Thank you in advance!
Managed to solve the issue. Steps that I think led to it working:
Added all the parameters to 'action and parameters' of the intent
Checkmarked 'Is list'
Added a default value for each parameter referring to itself from the earlier context in the form #session-vars.parametername
I don't understand 100% why it wouldn't work without the above steps, as the intent shouldn't modify or clear the parameters, but it seems to have done the trick.
I am trying to use the Zoho Desk Invoke API (Proxy) to call the Zoho Desk Push-Data to Desk (aka Import) endpoint.
This is what my code looks like:
// Build the payload
const invokeApiRequestPayload = {
"securityContext":"edbsnc50b85a3cd964126073f50499ae29a3d6ed3c31123e535e901cdda1b2a312dc0a66c638e2beb2724fffc355faebabf1acd65c3883227c2d329d0c9f62cbbdf26ba4553375b5b11cab90c57590c6b48a3",
"requestURL":"https://desk.zoho.com.au/api/v1/channels/{{installationId}}/import",
"headers":{
"Content-Type":"application/json"
},
"postBody":{
"data":{
"tickets":[
{
"actor":{
"email":"test#gmail.com",
"name":"Tom Billy",
"extId":"NjPk2E6J83g41uDKsD6DznLzz323"
},
"subject":"testing new ticket",
"createdTime":"2022-07-24T01:13:44.419Z",
"status":"Open",
"extId":"YCdHTnu93pQbyNoyZzId"
}
],
"threads":[
{
"contentType":"text/html",
"createdTime":"2022-07-24T01:13:44.419Z",
"extId":"YGrfU6quGoDESjX5HEZM",
"extParentId":"YCdHTnu93pQbyNoyZzId",
"actor":{
"extId":"NjPk2E6J83g41uDKsD6DznLzz323",
"name":"Tom Billy",
"email":"test#gmail.com"
},
"canReply":true,
"content":"testing one two three<br>"
}
]
}
},
"connectionLinkName":"zohodesk",
"requestType":"POST",
"queryParams":{
"orgId":"7002257443"
}
};
// Generate the Hmac
const stringToHash = 'requestURL=https://desk.zoho.com.au/api/v1/channels/{{installationId}}/import&requestType=POST&queryParams={"orgId":"7002257443"}&postBody={"data":{"tickets":[{"extId":"YCdHTnu93pQbyNoyZzId","status":"Open","subject":"testing new ticket","createdTime":"2022-07-24T01:13:44.419Z","actor":{"extId":"NjPk2E6J83g41uDKsD6DznLzz323","name":"Tom Billy","email":"test#gmail.com"}}],"threads":[{"extId":"YGrfU6quGoDESjX5HEZM","extParentId":"YCdHTnu93pQbyNoyZzId","actor":{"extId":"NjPk2E6J83g41uDKsD6DznLzz323","name":"Tom Billy","email":"test#gmail.com"},"content":"testing one two three<br>","createdTime":"2022-07-24T01:13:44.419Z","canReply":true,"contentType":"text/html"}]}}&headers={"Content-Type":"application/json"}&connectionLinkName=zohodesk';
const hmac = crypto
.createHmac('sha256', 'mysecret123')
.update(stringToHash)
.digest('hex');
// The hmac created from the above code is '2aa21d41882223e2e23ad7004cfdc5a0317db5192fdff84431f0515d4f4e004b'
// Make the Invoke API request using Axios
const axiosOptions = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
hash: hmac,
},
params: {
orgId: '7002257443',
},
};
return await axios
.post(
'https://desk.zoho.com.au/api/v1/invoke',
invokeApiRequestPayload,
axiosOptions
)
.catch((error) => {
functions.logger.error(error);
throw new functions.https.HttpsError('unknown', error.message, error);
});
But I always get back Error 422 UNPROCESSABLE_ENTITY:
{
"errorCode":"UNPROCESSABLE_ENTITY",
"message":"`Extra query parameter '{\"securityContext\":\"edbsnc50b85a3cd964126073f50499ae29a3d6ed3c31123e535e901cdda1b2a312dc0a66c638e2beb2724fffc355faebabf1acd65c3883227c2d329d0c9f62cbbdf26ba4553375b5b11cab90c57590c6b48a3\",\"requestURL\":\"https://desk.zoho.com.au/api/v1/channels/{{installationId}}/import\",\"requestType\":\"POST\",\"postBody\":{\"data\":{\"tickets\":[{\"extId\":\"YCdHTnu93pQbyNoyZzId\",\"status\":\"Open\",\"subject\":\"testing new ticket\",\"createdTime\":\"2022-07-24T01:13:44.419Z\",\"actor\":{\"extId\":\"NjPk2E6J83g41uDKsD6DznLzz323\",\"name\":\"Tom Billy\",\"email\":\"test#gmail.com\"}}],\"threads\":[{\"extId\":\"YGrfU6quGoDESjX5HEZM\",\"extParentId\":\"YCdHTnu93pQbyNoyZzId\",\"actor\":{\"extId\":\"NjPk2E6J83g41uDKsD6DznLzz323\",\"name\":\"Tom Billy\",\"email\":\"test#gmail.com\"},\"content\":\"testing one two three<br>\",\"createdTime\":\"2022-07-24T01:13:44.419Z\",\"canReply\":true,\"contentType\":\"text/html\"}]}},\"headers\":{\"Content-Type\":\"application/json\"},\"queryParams\":{\"orgId\":\"7002257443\"},\"connectionLinkName\":\"zohodesk\"}' is present in the input.`"
}
The Zoho Desk docs have this explanation for the UNPROCESSABLE_ENTITY error code:
This errorCode value appears if the input does not fulfil the
conditions necessary for successfully executing the API.
And looking at the returned error message details it seems to be complaining that I have an "Extra query parameter". That does not make sense to me, because I included it as the payload required by the Zoho Desk Invoke API (Proxy).
Can anyone see what I am doing wrong?
Hi I'm trying to send a POST call to the SparkPost API in a JavaScript web app. It works fine with curl and Postman, but as soon as I try sending from my localhost site I get a 401 Unauthorized error.
My current code looks like the below, but I've tried fetch as well with the same results.
sendEmail(subject, data) {
let textbody = "blah blah blah";
const url = "https://api.sparkpost.com/api/v1/transmissions";
const fetchbody = {
content: {
from: "sandbox#sparkpostbox.com",
subject: subject,
text: textbody
},
recipients: [{address: "myaddress#gmail.com"}]
};
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.addEventListener("readystatechange", function () {
if (this.readyState === 4) {
console.log(this.responseText);
}
});
xhr.open("POST", url);
xhr.setRequestHeader("content-type", "application/json");
xhr.setRequestHeader("accept", "application/json");
xhr.setRequestHeader("authorization", creds);
xhr.setRequestHeader("cache-control", "no-cache");
xhr.send(JSON.stringify(fetchbody));
}
Is there something wrong with my credentials setup here? Note that creds is set to my secret API key string.
Figured it out after chatting with the Sparkpost devs a bit - the service only allows "strict CORS" i.e. requests from server-side, and I was sending the request from client-side code.
I tried to add review comments by using this "set-review" api:
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-review
I can get a json response from this interface, but nothing is changed.
I can't see any new comments from Gerrit web page.
Here is the example of my client side code:
url = '/changes/16148/revisions/1/review'
data = json.dumps({
'message': 'test',
'labels': {},
'comments': {
'tools/docpreview.py': [{
'line': 20,
'message': 'hehe',
}],
},
'notify': 'NONE'
})
pprint(rest.post(url, data=data))
And the response example(Private info was deleted).
This response looks like result of get-review api described here:
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-review
{u'_number': 16148,
...
u'insertions': 20,
u'kind': u'gerritcodereview#change',
u'created': u'2014-11-05 16:23:08.849000000',
...
u'status': u'NEW',
u'subject': u'Add markdown preview.',
u'updated': u'2014-11-05 22:02:32.978000000'}
I struggled with getting this right for a while, finally, the following piece of code in Python works for me:
from requests.auth import HTTPBasicAuth
from pygerrit2.rest import GerritRestAPI
REST_AUTH = HTTPBasicAuth(MISC['auth_username'], MISC['auth_password'])
REST_CLIENT = GerritRestAPI(url=MISC['base_url'], auth=REST_AUTH)
query = "/changes/" + str(change_id) + "/revisions/" + str(cur_rev) + "/review"
REST_CLIENT.post(query, json={
"message": MISC['message'],
"reviewers": [{
"reviewer": MISC['reviewer_bot']
}]
})
# MISC is a dictionary
You should be authenticated, shouldn't you? Therefore your requests would have to go to /a/changes/