Google Cloud Storage upload file to Facebook Attachment API - facebook

I have an app that I am trying to create a Facebook bot for and it needs to send photo cards to users. However, to do so I need to get a Facebook attachment_id because I can't upload my files directly from the web. (https://developers.facebook.com/docs/messenger-platform/reference/attachment-upload-api/)
My app is on Firebase, for some reason when I try the URL it gives me this error:
{
"error": {
"message": "(#546) The type of file you're trying to attach isn't allowed. Please try again with a different format.",
"type": "OAuthException",
"code": 546,
"error_subcode": 1545026,
"fbtrace_id": "H8qfi28cNvp"
}
}
That happens with any URL I try but here is a test https://memes-dev.mymemestore.com/mms-tp1pESD7hgWWLiOUziHz.jpg
So I try to use the Google Cloud Storage nodejs api to get a readstream and send that like so
const stream = gcs().bucket('my bucket').file('mms-tp1pESD7hgWWLiOUziHz.jpg').createReadStream();
const message = new formdata();
message.append('message', '{"attachment":{"type":"image", "payload":{"is_reusable":true}}}');
message.append('filedata', stream);
fetch('https://graph.facebook.com/v2.6/me/message_attachments?access_token=<PAGE ACCESS TOKEN>',
{
method: 'POST',
headers: message.getHeaders(),
body: message
})
.then(res => {
console.log(res);
return res.json();
})
.then(json => {
console.log(json);
});
but that gives me the following error
(#100) Incorrect number of files uploaded. Must upload exactly one file.
Does anyone know what is going on?
Thanks

One thing I forgot to mention was that this all had to be done serverless. But I got around it by downloading the image to a tmpdir using tmpdir = path.join(os.tmpdir(), filename) and then fs.createReadStream(tmpdir) and that is working.

Related

Request works on Postman/Browser, but it doesn't work with axios

I make a GET request to this URL: https://api.mexc.com/api/v3/depth?symbol=BTCUSDT, if I use Postman or just paste it on my browser, it works, but if I use axios like this, it fails:
try {
const { data } = await axios.get(
'https://api.mexc.com/api/v3/depth?symbol=BTCUSDT'
);
console.log('data', data);
} catch (err) {
console.log(`Something went wrong:`, err);
}
You can test it on this link: https://stackblitz.com/edit/js-test-axios-njrh3z?file=index.js
Can someone explain me why please?
Mexc open api required a apikey for use they api.
https://mxcdevelop.github.io/apidocs/#api-key
If you log data response in axios, you will see this in console.
You don't have permission to access "http://api.mexc.com/api/v3/depth?" on this server.<P>\n
Let get an api key then use them api

POST data to Google Sheet web app from AWS Lambda

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.

GCS Signed URL Post Object using XMLHttpRequest with progress listener results in browser sending Options instead of POST

I have successfully implemented upload of an Object using multi-part Post request with Signature, Policy document GCS POST ...etc from the browser using XMLHttpRequest and angular $http .
But when I attach event listener on XMLHttpRequest upload to show a progress bar to the user, the browser sends a Options Method instead of POST. storage.googleapis.com returns 200 ok After that I was expecting a POST to be sent from the browser with the file but that did not happen. Without the upload listener the code works perfectly. Should I move to PUT ? any workaround
factory.uploadFileToUrlXHR = function(file,obj){
var deferred = $q.defer();
var fd = new FormData();
fd.append('key', obj.key);
fd.append('Content-Type',obj.contenttype)
fd.append('GoogleAccessId', obj.googleaccessId);
fd.append('policy', obj.policy);
fd.append('signature', obj.signature);
fd.append('file', file);
var XHR = new XMLHttpRequest();
XHR.addEventListener('load', function(event) {
// alert('Yeah! Data sent and response loaded.');
deferred.resolve(event);
});
XHR.upload.addEventListener("progress",function(evt){
if (evt.lengthComputable) {
$log.info("add upload event-listener" + evt.loaded + "/" + evt.total);
}
}, false);
// Define what happens in case of error
XHR.addEventListener('error', function(event) {
//alert('Oups! Something went wrong.');
deferred.resolve(event);
});
// Set up our request
XHR.open('POST', obj.uri);
// Send our FormData object; HTTP headers are set automatically
XHR.send(fd);
return deferred.promise;
}
I resolved the issue ,
When you use POST from the browser to upload a file to Google Cloud storage append the bucket name to the URL . In the below code obj.uri should be "https://storage.googleapis.com/bucketname
XHR.open('POST', obj.uri); and remove the bucket name from the key. Key should contain the object name.
fd.append('key', obj.key);
if you do not append the bucket name as part of the POST URL, the browser will send the Options request to https://storage.googleapis.com/. GCS will not be able to find the right CORS configuration. CORS configuration is mapped to the bucket Name.
Sample CORS configuration i had applied.
[
{
"origin": ["http://localhost:8282"],
"method": ["OPTIONS","PUT","POST","GET", "HEAD", "DELETE"],
"responseHeader": ["Content-Type"],
"maxAgeSeconds": 3600
}
]

can't exchange token manually v2.7

I'm using Loopback and
I'm trying to user auth for graph api without javascript sdk or passport
I got the code successfully however I can't exchange it with access token
I followed this guide https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/#confirm
my get request is https://graph.facebook.com/v2.7/dialog/oauth?code={xxxx}&client_secret={xxxx}&client_id={xxx}&redirect_uri={myURL}
myURL is the one used to get the code but not be used again if I understand
If I understand correctly I should it the access_token in the body of the response instead I get this error
{
"error": {
"message": "Unknown path components: /oauth",
"type": "OAuthException",
"code": 2500,
"fbtrace_id": "HXe+214tGpW"
}
}
It looks like a bug in the docs. The first call is to www.facebook.com in a browser.
See here for an example client https://github.com/yschimke/oksocial/blob/master/src/main/java/com/baulsupp/oksocial/services/facebook/FacebookAuthFlow.java
The second should be to something like https://graph.facebook.com/v2.7/oauth/access_token
$response = $fb->sendRequest(
'GET',
'/oauth/access_token',
[
'client_id' => $config['client_id'],
'client_secret' => $config['client_secret'],
'grant_type' => 'fb_exchange_token',
'fb_exchange_token' => $short_token
],
$short_token,
null,
'v2.7');

Upload video to Facebook from the server

I have a problem when uploading videos to Facebook.
I use Facebook SDK for Android: https://github.com/facebook/facebook-android-sdk to get the access token with the following permissions: "publish_stream","email","video_upload","publish_actions"
Then I send token to the server, which should upload the video. The server tries to upload video using following code:
string fullurl = string.Format("https://graph-video.facebook.com/me/videos?title={0}&description={1}&access_token={2}", HttpUtility.UrlEncode(fileName), HttpUtility.UrlEncode(description), token);
Facebook returns an error:
System.Net.WebException: The remote server returned an error: (400) Bad Request.
at System.Net.WebClient.UploadFile(Uri address, String method, String fileName)
at System.Net.WebClient.UploadFile(String address, String fileName)
But, when I check token with the following link: https://graph.facebook.com/me?access_token=myToken Facebook returns user info. That means that token is valid. But, if this request returns error code, where I can find information about error codes?
This solution works perfectly for some users, for other users it works unstable ( 4 of 6 videos uploading failed. Facebook returns that: "connection closed by remote server"). And have not worked for some users with error: "The remote server returned an error: (400) Bad Request".
What is wrong in my code?
I've solve the problem. I have start using facebook SDK for .NET http://facebooksdk.net/ and it works for me.
Facebook.FacebookClient fb = new FacebookClient(facebookEditTask.facebookToken);
fb.PostCompleted += (o, e) =>
{
if (e.Cancelled || e.Error != null)
{
return;
}
var result = e.GetResultData();
facebookVideoUrl = CreateLinkToVideo(result.ToString());
};
dynamic parameters = new ExpandoObject();
parameters.source = new FacebookMediaStream
{
ContentType = "video/mp4",
FileName = Path.GetFileName(facebookEditTask.FilePath)
}.SetValue(File.OpenRead(facebookEditTask.FilePath));
parameters.message = fbMessage;
Task t = fb.PostTaskAsync("me/videos",
new { message = fbMessage, parameters.source });