use a .gs file in a web page in App Script - javascript

I'm using a script to extract data from google search console in a sheet.
I built a sidebar to chose on which website the user want to analyse his data.
For that i have a function that can list all sites link to the google account, but i have an error when i try to execute this function in my html file.
I use withSuccessHandler(function) method which sets a callback function to run if the server-side function returns successfully. (i have a OAuth2.0.gs file where is my getService function.
The error is "service.hasAccess is not a function at listAccountSites" where listAccountSites is my function. Here's an extract of my html file:
<script src="OAuth2.0.gs"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script>
$(function() {
var liste = google.script.run.withSuccessHandler(listAccountSites)
.getService();
console.log(liste);
});
function listAccountSites(service){
if (service.hasAccess()) {
var apiURL = "https://www.googleapis.com/webmasters/v3/sites";
var headers = {
"Authorization": "Bearer " + getService().getAccessToken()
};
var options = {
"headers": headers,
"method" : "GET",
"muteHttpExceptions": true
};
var response = UrlFetchApp.fetch(apiURL, options);
var json = JSON.parse(response.getContentText());
Logger.log(json)
console.log('if')
var URLs = []
for (var i in json.siteEntry) {
URLs.push([json.siteEntry[i].siteUrl, json.siteEntry[i].permissionLevel]);
}
/*
newdoc.getRange(1,1).setValue('Sites');
newdoc.getRange(1,3).setValue('URL du site à analyser');
newdoc.getRange(2,1,URLs.length,1).setValues(URLs);
*/
console.log(URLs);
} else {
console.log('else')
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s', authorizationUrl);
Browser.msgBox('Open the following URL and re-run the script: ' + authorizationUrl);
}
return URLs;
}
</script>

i found the solution.
Jquery is useless here, you just have to use google.script.run.yourfunction() to run your gs. function on your html sidebar.

Related

Google Apps Script - use Google Drive pdf with PDF-lib.js

Background:
I am try to fill in a pdf file that:
a) has form fields.
b) that is stored on Google Drive.
with data that is stored in Google spreadsheet.
I am using:
Google Apps Script.
HtmlService
PDF-lib.js in a htmlOutput object generated from a htmlTemplate.
The work flow is:
The showModalDialog_downloadFilledPDFform_NAMOFFORM() function is called from a menu.
The function is:
function showModalDialog_downloadFilledPDFform_NAMOFFORM() {
var pdf_template_file_url = getPDFfileURL("form1.pdf");
var htmlTemplate = HtmlService.createTemplateFromFile('downloadFilledPDFformHTML');
htmlTemplate.dataFromServerTemplate = { pdf_template_file: "form1.pdf", pdf_template_file_url: pdf_template_file_url };
var htmlOutput = htmlTemplate.evaluate();
htmlOutput.setWidth(648.1);
htmlOutput.setHeight(286.300)
SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Download filled PDF');
}
The url that is passed to the htmlTemplate is generated by: "fillPDFForm.gs"
function: fillPDFForm.gs:
var pdfFileNamesAndIDs = [ ]
pdfFileNamesAndIDs.push(["form1.pdf", "1y8F5NgnK50mdtWSR6v1b8pELsbbBJert"])
pdfFileNamesAndIDs.push(["form2.pdf", "1B4BOaI-BqFmhmnFx7FaT-yys-U0vkYKz"])
pdfFileNamesAndIDs.push(["form3.pdf", "17LrJpRA5oBZBqw-2du1H74KxWIX55qYC"])
function getPDFfileURL(fileName) {
var documentID = "";
for (var i in pdfFileNamesAndIDs) {
//console.log(pdfFileNamesAndIDs[i][0]);
if (pdfFileNamesAndIDs[i][0] == fileName) {
documentID = pdfFileNamesAndIDs[i][1];
console.log("documentID: " + documentID);
}
}
var documentFile = DriveApp.getFileById(documentID);
var documentURL = documentFile.getDownloadUrl();
Logger.log("documentURL = "+documentURL);
return documentURL;
}
The Problem:
The URL generated by getPDFfileURL() doesn't work in the html file generated in showModalDialog_downloadFilledPDFform_NAMOFFORM().
The error in Chrome dev console is:
pdf-lib#1.11.0:15 Uncaught (in promise) Error: Failed to parse PDF document (line:0 col:0 offset=0): No PDF header found
at e [as constructor] (pdf-lib#1.11.0:15:189222)
at new e (pdf-lib#1.11.0:15:190065)
at e.parseHeader (pdf-lib#1.11.0:15:401731)
at e.<anonymous> (pdf-lib#1.11.0:15:400782)
at pdf-lib#1.11.0:15:1845
at Object.next (pdf-lib#1.11.0:15:1950)
at pdf-lib#1.11.0:15:887
at new Promise (<anonymous>)
at i (pdf-lib#1.11.0:15:632)
at e.parseDocument (pdf-lib#1.11.0:15:400580)
The basic concept for the html page (shown as a modal dialog box), came from: https://jsfiddle.net/Hopding/0mwfqkv6/3/
The contents of the htmlTemplate are:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<!-- Add Stylesheet -->
<?!= HtmlService.createHtmlOutputFromFile('downloadFilledPDFformCSS').getContent(); ?>
<!-- Add pdf-lib and downloadjs libraries -->
<!-- https://pdf-lib.js.org/ and https://github.com/rndme/download -->
<!-- https://jsfiddle.net/Hopding/0mwfqkv6/3/ -->
<script src="https://unpkg.com/pdf-lib#1.11.0"></script>
<script src="https://unpkg.com/downloadjs#1.4.7"></script>
</head>
<body>
<h2 id="myTitle"></h2>
<p>Click the button to fill form fields in an the following PDF document: <code id="pdf_template_file">pdf-lib</code></p>
<button onclick="fillForm()">Fill PDF</button>
<p class="small">(Your browser will download the resulting file)</p>
</body>
<script>
const data = <?!= JSON.stringify(dataFromServerTemplate) ?>; //Stores the data directly in the javascript code
function removeExtension(filename) {
return filename.substring(0, filename.lastIndexOf('.')) || filename;
}
const pdf_template_file = data.pdf_template_file;
const pdf_template_file_basename = removeExtension(pdf_template_file);
// sample usage
function initialize() {
document.getElementById("myTitle").innerText = pdf_template_file;
//or use jquery: $("#myTitle").text(data.first + " - " + data.last);
document.getElementById("pdf_template_file").innerText = pdf_template_file;
}
// use onload or use jquery to call your initialization after the document loads
window.onload = initialize;
</script>
<script>
const { PDFDocument } = PDFLib;
async function fillForm() {
// Fetch the PDF with form fields
const formUrl = data.pdf_template_file_url
//const formPdfBytes = await fetch(formUrl).then(res => res.arrayBuffer())
const formPdfBytes = await fetch(formUrl, {
redirect: "follow",
mode: 'no-cors',
method: 'GET',
headers: {
'Content-Type': 'application/pdf',
}
}).then(res => res.arrayBuffer());
// Load a PDF with form fields
const pdfDoc = await PDFDocument.load(formPdfBytes);
// Get the form containing all the fields
const form = pdfDoc.getForm()
// Get all fields in the PDF by their names
const invIDField = form.getTextField('invID')
const makeAndModelField = form.getTextField('makeAndModel')
const nameField = form.getTextField('name')
const addressField = form.getTextField('address')
const phoneNumberField = form.getTextField('phoneNumber')
const emailAddressField = form.getTextField('emailAddress')
const dateField = form.getTextField('date')
// Output file name
const INPUT_FNAME = "AN"
const INPUT_LNAME = "Other"
// Fill in the basic info fields
invIDField.setText()
makeAndModelField.setText()
nameField.setText(INPUT_FNAME + " " + INPUT_LNAME)
addressField.setText()
phoneNumberField.setText()
emailAddressField.setText()
dateField.setText()
// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save({updateFieldAppearances: false})
const outputPDFfilename = pdf_template_file_basename + "." + INPUT_FNAME + "_" + INPUT_LNAME + ".pdf"
// Trigger the browser to download the PDF document
download(pdfBytes, outputPDFfilename, "application/pdf");
}
</script>
</html>
I have replicated the contents of the html file on my testing webserver. The server has 3 files: index.html, stykesheet.css and form1.pdf
The pdf (on the web server) is the same pdf file that is stored on Google drive.
On my server the following works:
if I use the pdf file that is in the same folder as the html and css files, a filled pdf is offered for download.
...but the following doesn't work:
if I use the same URL that is generated by getPDFfileURL(), nothing happens and no filled pdf is offered for download.
So the question is:
How do I generate the correct URL (for the pdf file stored in Google Drive), so it can then be used by PDF-lib.js (in the htmlTemplate)?
The answer is:
fetch the pdf file from Google drive as raw bytes.
encode the bytes as base64.
pass the base64 string from the GAS function to the htmlTemplate.
This is the function that gets the pdf file and returns it as a base64 encoded string:
function getPDFfileAsBase64() {
var fileId = "";
var url = "https://drive.google.com/uc?id=" + fileId + "&alt=media";
console.log("url: " + url);
var params = {
method: "get",
headers: {
Authorization: "Bearer " + ScriptApp.getOAuthToken(),
},
};
var bytes = UrlFetchApp.fetch(url, params).getContent();
var encoded = Utilities.base64Encode(bytes);
//console.log("encoded: " + encoded);
return encoded;
}
the function to create a modal dialog is:
function showModalDialog_downloadFilledPDFform_form1() {
// Display a modal dialog box with custom HtmlService content.
var pdf_template_file = "form1.pdf";
var pdf_template_file_AsBase64 = getPDFfileAsBase64(pdf_template_file);
var htmlTemplate = HtmlService.createTemplateFromFile('downloadFilledPDFformHTML');
htmlTemplate.dataFromServerTemplate = { pdf_template_file: pdf_template_file, pdf_template_file_AsBase64: pdf_template_file_AsBase64};
var htmlOutput = htmlTemplate.evaluate();
htmlOutput.setWidth(648.1);
htmlOutput.setHeight(286.300)
SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Download filled PDF');
}

Authorize.net embedded iFrame accept.js response transaction

I have integrated authorize.net accept.js embedded iFrame in my application. Having trouble setting the transaction respone in my lambda function to get the response. I've seen similar questions on stack overflow but nothing worked out for me yet.
Using Nodejs for my backend and angular7 for the front-end.
I successfully get the token from my lambda function so my iframe appears on the ui. I've set \"showReceipt\": false providing url for cancel & continue, since the documentation says I have to set the show receipt parameter "false" in order to communicate with the IFrameCommunicator.html in the ui. But when I click on "Pay" its stuck at "Processing.." for a long time.
Following are the request & response headers respectively from the network tab:
* Cookie: __cfruid=deb63d2f12d9690aeea838cf7f31ada6da92bc1c-1602260930
* Host: test.authorize.net
* Origin: https://test.authorize.net
* Referer: https://test.authorize.net/payment/payment
* Sec-Fetch-Dest: empty
* Sec-Fetch-Mode: cors
* 
Sec-Fetch-Site: same-origin
{"resultCode":"Ok","messageCode":"Ok","transactionData":{"accountType":"Discover","accountNumber":"XXXX0012","transId":"40055282319","responseCode":"4","authorization":"TYIUU7","merchantName":"iMart Inc.","totalAmount":"1999.9","dateTime":"10/09/2020 4:20:27 PM"}}
I'm sure the transaction is happening looking at the response but not sure why it's not connecting with the communicator.
I've read the steps in the documentation and also followed the GitHub sample code-https://github.com/AuthorizeNet/accept-sample-app, which made me more confused since they both say different things at some places. Following are the steps I've accomplished until now :
Created a lambda hosted payment function with all the settings (followed the correct sequence) to get back a token.
Created a hosted payment form to display the iframe.
Able to make a payment --> get the receipt page --> routing to success screen.
What I'm trying to accomplish:
After I make the payment, initial idea was to trigger a different lambda function based on the response from authorize.net without communicating with IFrameCommunicator.html, but as I cannot do that, I want to get a response to initiate the next process at the backend.
Also, we're not storing any user details in our server and not interested in creating a customer profile unless it's a necessary step to get the transaction response. Please suggest the step integration if I can do it in the same lambda function I've created to get a token or I would have to create a different one for this and when will that step be implemented?
I know about the Webhooks but not sure if it's an absolute necessity at this point of time when I'm just trying to implement a simple transaction.
I'm a newbie and I couldn't find a lot of examples related to the same to resolve my issues/confusions. Would highly appreciate if I get a clear explanation on the steps here and where am I going wrong.
Following is the code -
accept-hosted.js Lambda function:
merchantAuthenticationType.setName('*****');
merchantAuthenticationType.setTransactionKey('******');
var transactionRequestType = new ApiContracts.TransactionRequestType();
transactionRequestType.setTransactionType(ApiContracts.TransactionTypeEnum.AUTHCAPTURETRANSACTION);
transactionRequestType.setAmount(Total);
var setting1 = new ApiContracts.SettingType();
var setting2 = new ApiContracts.SettingType();
var setting4 = new ApiContracts.SettingType();
var setting5 = new ApiContracts.SettingType();
var setting6 = new ApiContracts.SettingType();
var setting7 = new ApiContracts.SettingType();
var setting8 = new ApiContracts.SettingType();
var setting9 = new ApiContracts.SettingType();
var setting10 = new ApiContracts.SettingType();
var setting11 = new ApiContracts.SettingType();
setting2.setSettingName("hostedPaymentButtonOptions");
setting2.setSettingValue("{\"text\": \"Pay\"}");
setting1.setSettingName("hostedPaymentReturnOptions");
setting1.setSettingValue(
"{\"showReceipt\": false, \"url\": \"https://iMart.com/success.html\", \"urlText\": \"Continue\", \"cancelUrl\": \"https://iMart.com/error.html\", \"cancelUrlText\": \"Cancel\"}");
setting10.setSettingName("hostedPaymentOrderOptions");
setting10.setSettingValue("{\"show\": false, \"merchantName\": \"iMart Inc.\"}");
setting5.setSettingName("hostedPaymentPaymentOptions");
setting5.setSettingValue("{\"cardCodeRequired\": true, \"showCreditCard\": true, \"showBankAccount\": false}");
setting7.setSettingName("hostedPaymentShippingAddressOptions");
setting7.setSettingValue("{\"show\": false, \"required\": false}");
setting8.setSettingName("hostedPaymentBillingAddressOptions");
setting8.setSettingValue("{\"show\": false, \"required\": false}");
setting6.setSettingName("hostedPaymentSecurityOptions");
setting6.setSettingValue("{\"captcha\": true}");
setting4.setSettingName("hostedPaymentStyleOptions");
setting4.setSettingValue("{\"bgColor\": \"blue\"}");
setting9.setSettingName("hostedPaymentCustomerOptions");
setting9.setSettingValue("{\"showEmail\": false, \"requiredEmail\": false, \"addPaymentProfile\": true }");
setting11.setSettingName("hostedPaymentIFrameCommunicatorUrl");
setting11.setSettingValue("{\"url\": \"https://iMart.com/IFrameCommunicator.html\"}");
var settingList = [];
settingList.push(setting2);
settingList.push(setting10);
settingList.push(setting5);
settingList.push(setting7);
settingList.push(setting8);
settingList.push(setting6);
settingList.push(setting4);
settingList.push(setting9);
settingList.push(setting11);
settingList.push(setting1);
var alist = new ApiContracts.ArrayOfSetting();
alist.setSetting(settingList);
var firstname = new ApiContracts.UserField();
firstname.setName('First Name');
firstname.setValue(firstName);
var lastname = new ApiContracts.UserField();
lastname.setName('Last Name');
lastname.setValue(lastName);
var userFieldList = [];
userFieldList.push(firstname);
userFieldList.push(lastname);
var userFields = new ApiContracts.TransactionRequestType.UserFields();
userFields.setUserField(userFieldList);
var transactionSetting1 = new ApiContracts.SettingType();
transactionSetting1.setSettingName('duplicateWindow');
transactionSetting1.setSettingValue('120');
var transactionSetting2 = new ApiContracts.SettingType();
transactionSetting2.setSettingName('recurringBilling');
transactionSetting2.setSettingValue('false');
var transactionSetting3 = new ApiContracts.SettingType();
transactionSetting3.setSettingName('emailCustomer');
transactionSetting3.setSettingValue('true');
var transactionSetting4 = new ApiContracts.SettingType();
transactionSetting4.setSettingName('headerEmailReceipt');
transactionSetting3.setSettingValue('You are all set!');
var transactionSetting5 = new ApiContracts.SettingType();
transactionSetting5.setSettingName('footerEmailReceipt');
transactionSetting5.setSettingValue('This is the footer');
var getRequest = new ApiContracts.GetHostedPaymentPageRequest();
getRequest.setMerchantAuthentication(merchantAuthenticationType);
getRequest.setTransactionRequest(transactionRequestType);
getRequest.setHostedPaymentSettings(alist);
var ctrl = new ApiControllers.GetHostedPaymentPageController(getRequest.getJSON());
const basicAuth = encode.encode("*****", 'base64');
await axios({
method: 'post',
url: 'https://apitest.authorize.net/xml/v1/request.api',
headers: {
'Authorization': 'Basic '+basicAuth,
'Content-Type': 'application/json'
},
data:JSON.stringify(ctrl._request)
}).then(async (data : any)=>{
if(data.data.token) {
callback(null, data.data) ;
} else callErr(data);
});
async function callErr(data: any){
callback(null, res) ;
}
}
IFrameCommunicator.html:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Iframe Communicator</title>
<script type="text/javascript">
//<![CDATA[
function callParentFunction(str) {
if (str && str.length > 0
&& window.parent
&& window.parent.parent
&& window.parent.parent.AuthorizeNetPopup
&& window.parent.parent.AuthorizeNetPopup.onReceiveCommunication)
{
// Errors indicate a mismatch in domain between the page containing the iframe and this page.
window.parent.parent.AuthorizeNetPopup.onReceiveCommunication(str);
}
}
function receiveMessage(event) {
if (event && event.data) {
callParentFunction(event.data);
}
}
if (window.addEventListener) {
console.log('addEventListener');
console.log(receiveMessage);
window.addEventListener("message", receiveMessage, false);
} else if (window.attachEvent) {
window.attachEvent("onmessage", receiveMessage);
}
if (window.location.hash && window.location.hash.length > 1) { callParentFunction(window.location.hash.substring(1));
}
//]]/>
</script>
</head>
<body>
</body>
</html>
Angular code for showing the iFrame:
<iframe id="add_payment" class="embed-responsive-item panel" name="add_payment" width="100%" frameborder="0" scrolling="yes">
</iframe>
</div>
<form id="send_token" action="" method="post" target="add_payment" >
<input id="token" type="hidden" name="token" />
</form>
I have been struggling a lot since many days now with a time crunch. Would be really helpful if someone provides me with a good insight here. Please let me know if additional info is required. Thank you in advance!!!
Here are the answer for all your question, I hope it works :
1)if you are using iFrame then iFrameCommunicator is mandatory
2)the success url can only be used when you set "showReceipt" as true, here you cannot navigate automatically to yoour success page, this is the link for "Continue" button which appears when "showReceipt" is allowed
3)If you want to trigger any function or want to navigate after the response then add the following code in your html file
<script type="text/javascript">
$(document).ready(function () {
window.CommunicationHandler = {};
function parseQueryString(str) {
var vars = [];
var arr = str.split('&');
var pair;
for (var i = 0; i < arr.length; i++) {
pair = arr[i].split('=');
vars[pair[0]] = unescape(pair[1]);
}
return vars;
}
window.CommunicationHandler.onReceiveCommunication = function (argument) {
console.log('communication handler enter', argument);
var params = parseQueryString(argument.qstr)
switch (params['action']) {
case "resizeWindow":
console.log('resize'); break;
case "successfulSave":
console.log('save'); break;
case "cancel":
console.log('cancel'); break;
case "transactResponse":
sessionStorage.removeItem("HPTokenTime");
console.log('transaction complete');
var transResponse = JSON.parse(params['response']);
console.log('transaction complete1', transResponse);
// window.location.href = '/checkout/complete';
}
}
//send the token
$('#send_hptoken').submit();
});
</script>

asynchronous HTTP (ajax) request works in script tag but not in js file

I have this ajax call here in a script tag at the bottom of my page. Everything works fine! I can set a breakpoint inside the 'updatestatus' action method in my controller. My server gets posted too and the method gets called great! But when I put the javascript inside a js file the ajax call doesn't hit my server. All other code inside runs though, just not the ajax post call to the studentcontroller updatestatus method.
<script>
$(document).ready(function () {
console.log("ready!");
alert("entered student profile page");
});
var statusdropdown = document.getElementById("enumstatus");
statusdropdown.addEventListener("change", function (event) {
var id = "#Model.StudentId";
var url = '#Url.Action("UpdateStatus", "Student")';
var status = $(this).val();
$.post(url, { ID: id, Status: status }, function (data) {
// do something with the returned value e.g. display a message?
// for example - if(data) { // OK } else { // Oops }
});
var e = document.getElementById("enumstatus");
if (e.selectedIndex == 0) {
document.getElementById("statusbubble").style.backgroundColor = "#3fb34f";
} else {
document.getElementById("statusbubble").style.backgroundColor = "#b23f42";
}
}, false);
</script>
Now I put this at the bottom of my page now.
#section Scripts {
#Scripts.Render("~/bundles/studentprofile")
}
and inside my bundle.config file it looks like this
bundles.Add(new ScriptBundle("~/bundles/studentprofile").Include(
"~/Scripts/submitstatus.js"));
and submitstatus.js looks like this. I know it enters and runs this code because it I see the alert message and the background color changes. So the code is running. Its just not posting back to my server.
$(document).ready(function () {
console.log("ready!");
alert("submit status entered");
var statusdropdown = document.getElementById('enumstatus');
statusdropdown.addEventListener("change", function (event) {
var id = "#Model.StudentId";
var url = '#Url.Action("UpdateStatus", "Student")';
var status = $(this).val();
$.post(url, { ID: id, Status: status }, function (data) {
// do something with the returned value e.g. display a message?
// for example - if(data) { // OK } else { // Oops }
});
var e = document.getElementById('enumstatus');
if (e.selectedIndex == 0) {
document.getElementById("statusbubble").style.backgroundColor = "#3fb34f";
} else {
document.getElementById("statusbubble").style.backgroundColor = "#b23f42";
}
}, false);
});
In the console window I'm getting this error message.
POST https://localhost:44301/Student/#Url.Action(%22UpdateStatus%22,%20%22Student%22) 404 (Not Found)
Razor code is not parsed in external files so using var id = "#Model.StudentId"; in the main view will result in (say) var id = 236;, in the external script file it will result in var id = '#Model.StudentId'; (the value is not parsed)
You can either declare the variables in the main view
var id = "#Model.StudentId";
var url = '#Url.Action("UpdateStatus", "Student")';
and the external file will be able to access the values (remove the above 2 lines fro the external script file), or add them as data- attributes of the element, for example (I'm assuming enumstatus is a dropdownlist?)
#Html.DropDownListFor(m => m.enumStatus, yourSelectList, "Please select", new { data_id = Model.StudentId, data_url = Url.Action("UpdateStatus", "Student") })
which will render something like
<select id="enumStatus" name="enumStatus" data-id="236" data-url="/Student/UpdateStatus">
Then in the external file script you can access the values
var statusbubble = $('#statusbubble'); // cache this element
$('#enumStatus').change(function() {
var id = $(this).data('id');
var url = $(this).data('url');
var status = $(this).val();
$.post(url, { ID: id, Status: status }, function (data) {
....
});
// suggest you add/remove class names instead, but if you want inline styles then
if (status == someValue) { // the value of the first option?
statusbubble.css('backgroundColor', '#3fb34f');
} else {
statusbubble.css('backgroundColor', '#b23f42');
};
});

How to post image on twitter in Phonegap using javascript

I'm currently implementing a web smartphone application with Phonegap. On this application, users can post images they take with the phone camera on Facebook. This feature has been succesfully implemented only using javascript, by sending a base 64 encoded image. Now, I want to implement the same feature using Twitter.
I found some very interesting blog posts about this and I'm already be able to update the user status only using javascript... but I can't post images too using the update_with_media Twitter web service.
According too this post, someone says it's impossible to implement this operation without using a server side code (like a php script for example).
So my question is : is it possible to use the update_with_media Twitter web service only with javascript ?
I send you my code to have an overview of the current solution. I've taken this article as working base : http://oodlestechnologies.com/blogs/Twitter-integration-on-PhoneGap-using-ChildBrowser-and-OAuth-for-iOS-and-Android-Platforms
Here is my HTML code.
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<script type="text/javascript" src="../js/jquery/jquery.min.js"></script>
<script type="text/javascript" src="../cordova-2.5.0.js"></script>
<script type="text/javascript" src="../js/childBrowser/childbrowser.js"></script>
<script type="text/javascript" src="../js/helpers/jsOAuth-1.3.6.js"></script>
<script type="text/javascript" src="../js/helpers/twitter.js"></script>
</head>
<body>
<h4>Oodles Twitter App</h4>
<table border="1">
<tr>
<th>Login using Twitter</th>
<th>
<button id="loginBtn" onclick="Twitter.init();">Login</button>
<button id="logoutBtn" onclick="logOut();">Logout</button>
</th>
</tr>
<tr id="tweetText">
<td colspan="2"><textarea id="tweet"></textarea></td>
</tr>
<tr id="tweetBtn">
<td colspan="2" align="right">
<button id="tweeter" onclick="Twitter.tweet();">Tweet</button>
</td>
</tr>
<tr><td colspan="2"><div id="welcome">Please Login to use this app</div></td></tr>
</table>
<br/>
<br/>
<button onclick="javascript:location.reload();">Recharger la page</button>
</body>
</html>
Here is my twitter.js code : (The point is in the post method)
$(document).ready(function() {
document.addEventListener("deviceready", onDeviceReady, false);
});
function onDeviceReady() {
var root = this;
cb = window.plugins.childBrowser;
if (!localStorage.getItem(twitterKey)) {
$("#loginBtn").show();
$("#logoutBtn").hide();
$("tweetBtn").hide();
$("tweetText").hide();
}
else {
$("#loginBtn").hide();
$("#logoutBtn").show();
$("tweetBtn").show();
$("tweetText").show();
}
if (cb != null) {
cb.onLocationChange = function(loc) {
root.locChanged(loc);
};
cb.onClose = function() {
root.onCloseBrowser()
};
cb.onOpenExternal = function() {
root.onOpenExternal();
};
}
}
function onCloseBrowser() {
console.log("onCloseBrowser!");
}
function locChanged(loc) {
console.log("locChanged!");
}
function onOpenExternal() {
console.log("onOpenExternal!");
}
// Consumer key : ...
// Consumer secret : ...
// GLOBAL VARS
var oauth; // It Holds the oAuth data request
var requestParams; // Specific param related to request
var options = {consumerKey: '...', consumerSecret: '...', callbackUrl: "http://www.google.fr"};
var twitterKey = "twtrKey"; // This key is used for storing Information related
var Twitter = {
init: function() {
// Apps storedAccessData , Apps Data in Raw format
var storedAccessData, rawData = localStorage.getItem(twitterKey);
// here we are going to check whether the data about user is already with us.
if (localStorage.getItem(twitterKey) !== null) {
// when App already knows data
storedAccessData = JSON.parse(rawData); //JSON parsing
//options.accessTokenKey = storedAccessData.accessTokenKey; // data will be saved when user first time signin
options.accessTokenSecret = storedAccessData.accessTokenSecret; // data will be saved when user first first signin
// javascript OAuth take care of everything for app we need to provide just the options
oauth = OAuth(options);
oauth.get('https://api.twitter.com/1/account/verify_credentials.json?skip_status=true',
function(data) {
var entry = JSON.parse(data.text);
console.log("USERNAME: " + entry.screen_name);
}
);
}
else {
// we have no data for save user
oauth = OAuth(options);
oauth.get('https://api.twitter.com/oauth/request_token',
function(data) {
requestParams = data.text;
cb.showWebPage('https://api.twitter.com/oauth/authorize?' + data.text); // This opens the Twitter authorization / sign in page
cb.onLocationChange = function(loc) {
Twitter.success(loc);
}; // Here will will track the change in URL of ChildBrowser
},
function(data) {
console.log("ERROR: " + JSON.stringify(data));
}
);
}
},
/*
When ChildBrowser's URL changes we will track it here.
We will also be acknowledged was the request is a successful or unsuccessful
*/
success: function(loc) {
// Here the URL of supplied callback will Load
/*
Here Plugin will check whether the callback Url matches with the given Url
*/
if (loc.indexOf("http://www.google.fr") >= 0) {
// Parse the returned URL
var index, verifier = '';
var params = loc.substr(loc.indexOf('?') + 1);
params = params.split('&');
for (var i = 0; i < params.length; i++) {
var y = params[i].split('=');
if (y[0] === 'oauth_verifier') {
verifier = y[1];
}
}
// Here we are going to change token for request with token for access
/*
Once user has authorised us then we have to change the token for request with token of access
here we will give data to localStorage.
*/
oauth.get('https://api.twitter.com/oauth/access_token?oauth_verifier=' + verifier + '&' + requestParams,
function(data) {
var accessParams = {};
var qvars_tmp = data.text.split('&');
for (var i = 0; i < qvars_tmp.length; i++) {
var y = qvars_tmp[i].split('=');
accessParams[y[0]] = decodeURIComponent(y[1]);
}
$('#oauthStatus').html('<span style="color:green;">Success!</span>');
$('#stage-auth').hide();
$('#stage-data').show();
oauth.setAccessToken([accessParams.oauth_token, accessParams.oauth_token_secret]);
// Saving token of access in Local_Storage
var accessData = {};
accessData.accessTokenKey = accessParams.oauth_token;
accessData.accessTokenSecret = accessParams.oauth_token_secret;
// Configuring Apps LOCAL_STORAGE
console.log("TWITTER: Storing token key/secret in localStorage");
localStorage.setItem(twitterKey, JSON.stringify(accessData));
oauth.get('https://api.twitter.com/1/account/verify_credentials.json?skip_status=true',
function(data) {
var entry = JSON.parse(data.text);
console.log("TWITTER USER: " + entry.screen_name);
$("#welcome").show();
document.getElementById("welcome").innerHTML = "welcome " + entry.screen_name;
successfulLogin();
// Just for eg.
app.init();
},
function(data) {
console.log("ERROR: " + data);
}
);
// Now we have to close the child browser because everthing goes on track.
window.plugins.childBrowser.close();
},
function(data) {
console.log(data);
}
);
}
else {
// Just Empty
}
},
tweet: function() {
var storedAccessData, rawData = localStorage.getItem(twitterKey);
storedAccessData = JSON.parse(rawData); // Paring Json
options.accessTokenKey = storedAccessData.accessTokenKey; // it will be saved on first signin
options.accessTokenSecret = storedAccessData.accessTokenSecret; // it will be save on first login
// javascript OAuth will care of else for app we need to send only the options
oauth = OAuth(options);
oauth.get('https://api.twitter.com/1/account/verify_credentials.json?skip_status=true',
function(data) {
var entry = JSON.parse(data.text);
Twitter.post();
}
);
},
/*
We now have the data to tweet
*/
post: function() {
alert('Post !');
var theTweet = $("#tweet").val(); // You can change it with what else you likes.
oauth.post('https://upload.twitter.com/1/statuses/update_with_media.json',
{
'status': theTweet,
'media': //HERE IS THE PROBLEM, WHAT TO DO HERE ?
}, "multipart/form-data",
function(data)
{
alert('Data 1 !');
console.log('------Data1 : ' + data);
var entry = JSON.parse(data.text);
console.log(entry);
done();
},
function(data) {
//var json_result = JSON.parse(data);
//alert(json_result.text.error);
var entry = JSON.stringify(data);
console.log('------Data2 : ' + entry);
}
);
}
}
function done() {
alert("OKKK !");
$("#tweet").val('');
}
function successfulLogin() {
$("#loginBtn").hide();
$("#logoutBtn,#tweet,#tweeter,#tweetBtn,#tweetText").show();
}
function logOut() {
//localStorage.clear();
window.localStorage.removeItem(twitterKey);
document.getElementById("welcome").innerHTML = "Please Login to use this app";
$("#loginBtn").show();
$("#logoutBtn,#tweet,#tweeter,#tweetText,#tweetBtn").hide();
}
After many tests (sending a base64 image, sending a blob, sending a binary file, ...) here is the return message from Twitter I have :
{\"errors\":[{\"message\":\"Internal
error\",\"code\":131}]}","xml":"","requestHeaders":{"Content-Type":"multipart/form-data"},"responseHeaders":{"date":"Fri,
19 Apr 2013 15:45:28
GMT","content-encoding":"deflate","strict-transport-security":"max-age=631138519","status":"500
Internal Server
Error","server":"tfe","content-type":"application/json;
charset=utf-8","version":"HTTP/1.1"}}
A "solution" (by send a blob) have been posted on the Twitter dev forum but not working for me : dev.twitter.com/discussions/6969
Does anyone want to implement the same feature or have a solution ? Thank you !
------ EDITED :
I just want to use Javascript and I don't want to implement any server-side solution (no PHP, C#, Java...).
According to the docs, Twitter requires the multipart/form-data enctype, which means a base64 string isn't going to work.
Unlike POST statuses/update, this method expects raw multipart data. Your POST request's Content-Type should be set to multipart/form-data with the media[] parameter ~ https://dev.twitter.com/docs/api/1/post/statuses/update_with_media
However, you could host an endpoint that takes base64, converts it to a real file, and forwards the request to Twitter. For example (untested):
<?php
$base64 = $_POST['image'];
$data = base64_decode( $base64 );
// Make name unique to avoid conflicts.
$temp_file = uniqid() . $_POST['name'];
// Save the file to a temp location.
file_put_contents( $temp_file, $data );
$temp_info = pathinfo( $temp_file );
$temp_type = $temp_info['extension'];
$temp_name = basename( $temp_file, '.' . $temp_type );
// OAuth library recommended by Twitter: https://github.com/themattharris/tmhOAuth
// See original: https://github.com/themattharris/tmhOAuth-examples/blob/master/images.php
require 'tmhOAuth.php';
require 'tmhUtilities.php';
$tmhOAuth = new tmhOAuth( array(
'consumer_key' => $_POST['consumer_key'],
'consumer_secret' => $_POST['consumer_secret'],
'user_token' => $_POST['user_token'],
'user_secret' => $_POST['user_secret'],
));
// note the type and filename are set here as well
// Edit: Not sure if the `type` and `filename` params are necessary.
$params = array( 'media[]' => "#{$temp_file};type={$temp_type};filename={$temp_name}" );
$code = $tmhOAuth->request( 'POST', $tmhOAuth->url( '1/status/update_with_media' ),
$params,
true, // use auth
true // multipart
);
// Remove temp file.
unlink( $temp_file );
if ( $code == 200 ) {
tmhUtilities::pr( json_decode( $tmhOAuth->response['response'] ) );
}
tmhUtilities::pr( htmlentities( $tmhOAuth->response['response'] ) );
?>
And you might call it like:
$.ajax({
// You'll want to use https to protect the oauth info.
url: "https://mysite.com/proxy.php",
type: "POST",
data: {
image: "base64 data...",
name: "foo.png",
consumer_key: options.consumerKey,
consumer_secret: options.consumerSecret,
user_token: options.accessTokenKey,
user_secret: options.accessTokenSecret
},
success: function( data ) {
console.log( data );
}
});
For anyone trying to post images to Twitter using client JS, I was able to post to twitter using the solution by gary-buynary-co-za (https://github.com/bytespider/jsOAuth/pull/11) at the end of this forum. Pretty much ended up using Phonegap FileTransfer and FileTransferOptions objects for transferring image to twitter api, but used jsOAuth for preparing FileTransferOptions headers and signatures. The solution could definitely be cleaned up though.

Ajax.Updater POST params not transmitted by Firefox

I am debugging code written by someone else - I am also a newbie at Ajax and JS. The code shown below uses Ajax (Prototype JavaScript framework, version 1.6.0.1) to submit a simple contact form and update the page with the response. It works perfectly with IE8 and fails with Firefox. I have traced the failure to the post parameters. The target file, mail.php, is not receiving any POST parameters when Firefox is the browser. Not just the values are missing - the params are not defined in the post. The URL is valid. The form, the Ajax JS files and the mail.php script are all on the same domain.
I have tried variations on defining var params as shown in several other Stack Overflow posts (inline, different assignment methods, etc). All produced the same results as the code below.
Any clue what is wrong?
FF results
Please try again. responseText='', url='http://domain.com/builder/mail.php',
form_name='dom', form_message='testing', form_email='dom#domain.com',
form_to='info#domain.com'
IE8 results
Message Sent!
JS function
function sendForm(F)
{
$("contact_form").hide();
$("ajax-loader").show();
var url = site_url+'mail.php';
var form_message = escape($("message").value);
var form_name = escape($("name").value);
var form_address = escape($("address").value);
var form_phone = escape($("phone").value);
var form_email = escape($("email").value);
var form_how_learn = escape($("how_learn").value);
var form_to = escape($("to").value);
var params = {message: form_message,
name: form_name,
address: form_address,
phone: form_phone,
email: form_email,
how_learn: form_how_learn,
to: form_to};
var ajax = new Ajax.Updater(
'contact_form',
url,
{
method: 'post',
parameters: params,
onComplete: function (response)
{
$("ajax-loader").hide();
if (response.responseText == "ok")
{
$("contact_form").innerHTML = "<p style=\"color:green;\">Message Sent!</p>";
$("contact_form").show();
}
else
{
// Debug code added
$("contact_form").innerHTML = "<p style=\"color:red;\">Please try again."+
" responseText='"+response.responseText+"',"+
" url='"+url+"',"+
" form_name='"+form_name+"',"+
" form_message='"+form_message+"',"+
" form_email='"+form_email+"',"+
" form_to='"+form_to+"'"+
"</p>";
$("contact_form").show();
}
},
onFailure: function ()
{
$("ajax-loader").hide();
$("contact_form").innerHTML = "<p style=\"color:red;\">Please try again.</p>";
$("contact_form").show();
}
}
);
return false;
}

Categories