I am trying to obtain the list of places the user has saved on Google Maps. Now I know there isnt an API for this (for whatever reason), but I saw here:
"My Places" Google Maps API
That apparently there used to be a way to obtain the URL, but it does not seem to work with my list of places.
E.g.
https://www.google.com/maps/#46.889424,0.1194148,6z/data=!4m3!11m2!2s1KbZtik1IdXyNhwfXEb3P9vaZvzU!3e3
Does not seem to work if I append &output=kml or &output=json
I created this list on Google Maps, then hit share and obtained that link.
I even tried parsing the resulting HTML but it seems everything is handled by some Javascript Engine and I can't find any reference to Google Ids there --- I dont even know how they handle clicks!
Any help? There must be a way to retrieve this information programmatically!
EDIT:
I managed to get something working by visiting the shared link, then processing the html and storing the window.APP_INITIALIZATION_STATE variable. I then convert it to an javascript array and loop over it. Deep inside the array/map structure, I managed to get the google name and google place id out of that array. That seems to work a bit, but when trying with lists over 20 items long, google only gets the first 20 and is waiting for the user to 'scroll down' to get the next 20. That seems to trigger another call to get the next 20 results and looks a bit like:
https://www.google.com/search?tbm=map&fp=1&authuser=0&hl=en&gl=nl&pb=!4m8!1m3!1d54065472.4384380........
I can see the original feature id being included at the end of the url, but have no idea how to construct this url in full though to get the next 20 items.... Any ideas?
Your saved places list actually has what you call a feature ID attribute, this isn't a common practice and Google frowns upon this technique but take a look at this URL:
https://www.google.com/maps/preview/entity?authuser=0&hl=en&gl=us&pb=!1m10!1s0x0%3A0x3743ae09a161976b!3m8!1m3!1d14318.72623152007!2d-98.2296425!3d26.2070353!3m2!1i1024!2i768!4f13.1!12m3!2m2!1i392!2i106!13m57!2m2!1i203!2i100!3m2!2i4!5b1!6m6!1m2!1i86!2i86!1m2!1i408!2i200!7m42!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e3!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e3!2b1!3e2!1m3!1e9!2b1!3e2!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e4!2b1!4b1!9b0!14m3!1snyc5W-WeHY3r5gLwkoRI!7e81!15i10112!15m19!2b1!5m4!2b1!3b1!5b1!6b1!10m1!8e3!14m1!3b1!17b1!24b1!25b1!26b1!30m1!2b1!36b1!52b1!53b1!21m28!1m6!1m2!1i0!2i0!2m2!1i458!2i768!1m6!1m2!1i974!2i0!2m2!1i1024!2i768!1m6!1m2!1i0!2i0!2m2!1i1024!2i20!1m6!1m2!1i0!2i748!2m2!1i1024!2i768!22m1!1e81!29m0!30m1!3b1
Highlighted is the feature ID from the link you posted:
https://www.google.com/maps/#46.889424,0.1194148,6z/data=!4m3!11m2!2s1KbZtik1IdXyNhwfXEb3P9vaZvzU!3e3
Along with other maps parameters; when you hit that link you're actually manually triggering the same callback that Google's own scripts in maps use to parse the data to feed back to the maps UI; if you look at array item 2, or {c:..} you'll find a stringified array with the contents of your list, now depending on the program language you're using all it takes is a little tweaking (find/replace, loop through, lint and trim, etc.) to this array and you can pull your results; the cool thing is if you add or remove a place the next time you hit that end point it's updated in real-time.
Some people may call it a "hack"; but it gets the job done. :)
Hope I pointed you to a direction in the event you haven't found a solution; give this a shot.
Note the URL has to be pasted in its entirety, SO truncated the hyperlink; copy and paste the whole thing in one shot and a text file from Google with the arrays will be produced; in my case I curl the URLs I need and parse the returned strings as needed to pull data from Google where their API has limitations. Just a tip. :)
Also check Joel's Answer who did some research and refined some of the following information.
Pagination
You can use this tool to decrypt the pb-parameter. PB stands for protocol buffer (protobuf) and Google uses its own kind of it for maps. You can find different decoders for this by googling it.
In my case, the pagination was done via one parameter (8iX0). It seems, that it always comes with another similar parameter (7i20) but I don't know that it does. I can't yet confirm that this is always the case, but from my experience you're basically looking for two integers that are 20/40/60 etc. apart.
Here's what this looks like for me:
page 2 (7i20, 8i20)
page 3 (7i20, 8i40)
page 4 (7i20, 8i60)
From this information, I tried 7i20 8i00 for page 1, that seemed to work. For lists with >100 items, it just continues like that (8i120, 8i140 etc.)
Here's a code snippet in python (quick & dirty). Make sure to add (long) delays if your list has many pages as you will get rate-limited by captchas eventually if you don't. Notice the 8i%s0 in the url, make sure to put the %s back when you paste your pb-block.
url = "https://www.google.com:443/search?tbm=map&pb=!7i20!8i%s0!..."
headers = {"Referer": "https://www.google.com/"}
def fetch_stops_from_maps():
new_results = -1
page = 0
results = []
while new_results != 0:
new_results = 0
x = requests.get(url % page, headers=headers)
txt = html.unescape(x.text)
txt = txt.split("\n")[1]
results = re.findall(r"\[null,null,[0-9]{1,2}\.[0-9]{4,15},[0-9]{1,2}\.[0-9]{4,15}]", txt)
print(len(results))
for cord in results:
# curr = the description you can manually type in when saving
curr = txt.split(cord)[1].split("\"]]")[0]
curr = curr[curr.rindex(",\"") + 2:]
cords = str(cord).split(",")
lat = cords[2]
lon = cords[3][:-1]
results.append(s)
new_results += 1
page += 2
Actually getting the correct url
Getting the correct url currently seems to be the hardest part when doing this and I have not fully figured this out aswell. However, for my use-case this is not really important, so I extracted the correct pb-block once and called it a day.
As explained in the other answers, the id of the list is visible in the basic url (here, the 2sXX...) when you navigate to the list in your browser. It seems to usually be 24-32 (?) characters long.
.../maps/<coords>/data=!4m3!11m2!2sXXXX...XXXX!3e3
If you have this id, you can put it into an existing protobuf-block and it may work (I only tested this with 3 different lists, which were all created by the same account, so this theory is far from proven).
Now, how do you get the block? I would just share the one I have, but because I only understand parts of what it does, I fear that it may contain some personal info. Instead, I will share my process of getting it. For this I use Burpsuite. It's a program mainly used for web-security testing and has a free community edition, however for our use-case it is the perfect tool, because with it you can easily tinker with requests, change small parts in the request, send it again and immediately see if your changes changed the response. However for extracting the pb-block, one should also be able to use any program that can intercept browser traffic.
Heres the basic rundown with burp:
From GMaps, share a list that has >20 items (this is important) and copy the public link
In Burp, go to the tab "Proxy", make sure "Intercept" is off and click "Open browser" to open the integrated chromium browser
There, paste the link and wait until maps loaded completely
In Burp, turn "Intercept" on, then in google maps, scroll down in the list, until it starts loading new results (always blocks of 20)
Burp now intercepted all requests the browser made since you turned intercepting on. Click "Forward" and go through all requests, until you see a request in the format
GET /search?tbm=map&authuser=0&hl=de&gl=de&pb=!7i20....
This is what you're looking for.
Optionally, you can now right click into the request-text and click "send to repeater", then switch to the repeater-tab. Here you can edit the request and then send it again, being able to see the response immediately. For example, removing the authuser, hl, gl, q, ech, psi url parameters, the request still works flawlessly. If you remove the tch=1 parameter, the response you get will be in a more human readable format.
In the request-text you should now be able to just search for the list-id you got from the link previously and replace it with the id of another list (search bar is at the bottom in burp). As I said, this worked for me, but it may be possible that the pb-block contains some additional metadata that makes lists from different google-accounts or different types of lists incompatible with specific pb-blocks. Just a theory though. Let me know how it goes!
Further automating
I have theorised that one could automate getting the pb-block using requests-html because it can load html-sites fully but it doesn't get updated anymore. Another option (probably the better one) is Selenium Wire, as you should be able to load the page and intercept the requests, like we did in burp. Seems like a whole lot of work tho :D
This was the only API was able to find was this:
https://www.google.com/bookmarks/?output=xml
Used in a browser you would have to first log in through Google's OAuth. It would then return your saved places. Not sure at the moment how you would embedded the authentication to do this programmatically, but this might send you in the right direction.
I was able to extract the data I needed from my google maps list. Below are some comments that expand on some of the other comments here, along with a script that extracts all of the relevant data points from the network response.
Obtaining the underlying URL
You can easily find this URL by just opening the devtools on your browser, going to the network tab, and refreshing the webpage or scrolling down on the list until it loads new results (the list must be larger than 20 results). You should be able to find the network request that starts with https://www.google.com/search?tbm=map&pb... and go from there.
Increase the results size
I was able to increase the number of results returned from the request by changing the value of the 7i20 parameter. From what I can tell, the 71XX parameter is the size of the page, and the 8iXX parameter is the starting point. I haven't tested how large you can make the page limit, but I tested 100 and it seemed to work fine. This should make dealing with larger lists much easier.
Parsing out the data
Instead of using regex to parse out the relevant data from the response, I found that the response is basically just a massive JSON object and I was able to identify the indexes for specific types of data, such as the name of the place, location, notes, etc. See the script below.
If you look at the buildResults function in the script below, you can see the exact indexes used to extract specific pieces of information. This of course may change over time if the network response changes format at all, so use these as a starting point in the case where the specific values aren't at those indexes anymore. Hopefully they would be close to those locations
Script to parse the data (javascript / node.js)
// Insert the raw text content from the network response from the
// https://www.google.com/search?tbm=map&pb... url below.
const rawInput = null
function prepare(input) {
// There are 5 random characters before the JSON object we need to remove
// Also I found that the newlines were messing up the JSON parsing,
// so I removed those and it worked.
const preparedForParsing = input.substring(5).replace(/\n/g, '')
const json = JSON.parse(preparedForParsing)
const results = json[0][1].map(array => array[14])
return results
}
function prepareLookup(data) {
// this function takes a list of indexes as arguments
// constructs them into a line of code and then
// execs the retrieval in a try/catch to handle data not being present
return function lookup(...indexes) {
const indexesWithBrackets = indexes.reduce((acc, cur) => `${acc}[${cur}]`, '')
const cmd = `data${indexesWithBrackets}`
try {
const result = eval(cmd)
return result
} catch(e) {
return null
}
}
}
function buildResults(preparedData) {
const results = []
for (const place of preparedData) {
const lookup = prepareLookup(place)
// Use the indexes below to extract certain pieces of data
// or as a starting point of exploring the data response.
const result = {
address: {
street_address: lookup(183, 1, 2),
city: lookup(183, 1, 3),
zip: lookup(183, 1, 4),
state: lookup(183, 1, 5),
country_code: lookup(183, 1, 6),
},
name: lookup(11),
tags: lookup(13),
notes: lookup(25,15,0,2),
placeId: lookup(78),
phone: lookup(178,0,0),
coordinates: {
long: lookup(208,0,2),
lat: lookup(208,0,3)
}
}
results.push(result)
}
return results
}
const preparedData = prepare(rawInput)
const listResults = buildResults(preparedData)
console.log(listResults)
I am unable to change grades using the Google Classroom API. When I run the code below, lines 2 and 3 run fine. However, line 4 fails with the following message: #ProjectPermissionDenied The Developer Console project is not permitted to make this request.
1) var studentSubmission = {'assignedGrade':'1'};
2) var studentSubmissions = Classroom.Courses.CourseWork.StudentSubmissions.list(courseId, courseWorkId, {userId:'studentEmail#apps.matsuk12.us'});
3) var studentAssignmentId = studentSubmissions['studentSubmissions'][0].id;
4) Classroom.Courses.CourseWork.StudentSubmissions.patch(studentSubmission, courseId, courseWorkId, studentAssignmentId,{'updateMask':'assignedGrade'});
When I go to Project Properties and look at the scopes, here is what I see:
https://www.googleapis.com/auth/classroom.courses
https://www.googleapis.com/auth/classroom.coursework.students
https://www.googleapis.com/auth/classroom.profile.emails
https://www.googleapis.com/auth/classroom.profile.photos
https://www.googleapis.com/auth/classroom.rosters
https://www.googleapis.com/auth/spreadsheets
I am trying to access my own Google Classroom using a container bound script (spreadsheet). Seems like if I have access to the assignments, I should have access to add a grade. Not sure why I can't add the grade. Is there anything I can do to get this code working? The end goal is to be able to grade assignments using a form (not a google form) and have the score automatically pushed to Google Classroom.
I saw a related post that mentions a solution, but it is not clear how to implement it: Permission denied using Classroom.Courses.CourseWork.StudentSubmissions.list(4140802199, 4801051201);
I also see a related bug report here: https://issuetracker.google.com/issues/67748271 (I'm not sure though if this really is a bug, or if this is just how the Google Classroom API works, or if I'm just doing something wrong)
From the posts you linked someone posted this code:
function createCoursework (id) {
Classroom.Courses.CourseWork
.create(id, {
// doesn't work but triggers permissions correctly "courseId": id, "title": 'foo', "description": 'desc', });
}
Essentially you need to put this code somewhere in your projects code. Then, using the, Run menu, execute that function. It won't do anything, but it will initialize the authorization request from google; by means of a popup. After that you should be good to delete that function and use that scope.
I am creating application which can use google fit api.
I want to get all the activities(Movements) available in the google fit. Here the list of activities in google fit Reference.
Edited
I know the way how to get the activities which performed by user, But i want complete list of activities which available in the google fit API (Not only the activity which performed by user, need whole list of activities) like the list available in the above link.
The Google Fit activities are listed in the FitnessActivities class.
You can programmatically get a list of all these fields using:
FitnessActivities.class.getFields()
Had similar problem when started playing with Google Fit API on Android.
There are videos with code samples as well as more detailed API documentation on Google Fit website.
It helped me a lot -- https://developers.google.com/fit/android/get-started
Check both videos and later how to save and get data types:
https://developers.google.com/fit/android/data-types
To have some data available install Google Fit app on your android phone. Use it for a while and then you will have some real data in Google Fit database available.
EDIT:
If I get your edited question correctly, then you need something like the following code.
Please note that I use this in my own app that lists activities recorded by Google Fit Andorid app.
I'm not sure if it will list other activities, for example custom data types recorded by other apps.
Request "activites" (like STILL, RUNNING, WALKING) from Google Fit:
DataReadRequest readRequest = new DataReadRequest.Builder()
.read(DataType.TYPE_ACTIVITY_SEGMENT)
// maybe you want to limit data to specific time range?
//.setTimeRange(today.startTime, today.endTime, TimeUnit.MILLISECONDS)
.build();
Then parse the response. While parsing there will be activity time available:
Fitness.HistoryApi.readData(mClient, readRequest).setResultCallback(new ResultCallback<DataReadResult>() {
#Override
public void onResult(DataReadResult dataReadResult) {
for (DataSet dataSet : dataReadResult.getDataSets()) {
for (DataPoint dataPoint : dataSet.getDataPoints()) {
DataType dataType = dataPoint.getDataType();
if (dataType.equals(DataType.TYPE_ACTIVITY_SEGMENT)) {
String activity = FitnessActivities.getValue(dataPoint);
/* process as needed */
/* the `activitity' string contains values as described here:
* https://developer.android.com/reference/com/google/android/gms/fitness/FitnessActivities.html
*/
}
}
}
}
});
Like I said it works for me -- in my own app I list activities (and their type, ie. walking, running, etc) recorded by Google Fit app for Android.
Hope this can help others...
List<Session> sessions = sessionReadResponse.getSessions();
for (Session session : sessions) {
dumpSession(session);
Log.i(TAG, "Activity Name: "+sessions.get(position).getActivity());
position++;
List<DataSet> dataSets = sessionReadResponse.getDataSet(session);
for (DataSet dataSet : dataSets) {
dumpDataSet(dataSet);
}
}
Good Evening!
I've been looking into the possibility of using GAS(Google Apps Script) to host a small bit of javascript that lets me use the new Google finance apps api. The intention being that I'll be using the stock information for a project which involves the use of stock data. I know that there are a few ways to get stock information from Google, but the data that the finanace app returns is more in-line with other sources we are using. (One constraint on this project is that we have multiple sources).
I've written the javascript and I can call a httpc:request to the URL for the script given to me from Google. In the browser the JS returns the json object as I want it, however when the call is made from Erlang I'm getting it in a list of ascii. From checking the values it appears to be a document starting like:
Below is the javascript and the url to see the json:
https://script.google.com/macros/s/AKfycbzEvuuQl4jkrbPCz7hf9Zv4nvIOzqAkBxL1ixslLBxmSEhksQM/exec
function doGet() {
var stock = FinanceApp.getStockInfo('LON:TSCO');
return ContentService.createTextOutput(JSON.stringify(stock))
.setMimeType(ContentService.MimeType.JSON);
}
For the erlang, it's a simple request but I've not been doing erlang long, so perhaps I've messed something up here (The URL being the one mentioned above). I've got crypto / ssl / inets when I'm testing this on the command line.
{ok, {Version, Headers, Body}} = httpc:request(get, URL, []}, [], []).
I think it's also worth mentioning that when i curl it from Cygwin, I get a massive load of HTML also, I've included it below, but if you see it you'll thank me for not posting it in here! http://pastebin.com/UtJHXjRm
I've been updating the script as I go with the new versions but I'm at a bit of a loss as to why it's not returning correctly.
If anyone can give me any pointers I'd be very grateful! I get the feeling that it's not intended to be used this way, perhaps only within other Google products and such.
Cheers!
It would be necessary to review how are you deploying the Web App, specifically the Who has access to the app, to access without authentication should be configured as shown in the image:
See Deploying Your Script as a Web App from the documentation.
In my test, by running:
curl -L https://script.google.com/macros/s/************/exec
Get the following result:
{
"priceopen":358,
"change":2.199981689453125,
"high52":388.04998779296875,
"tradetime":"2013-10-11T15:35:18.000Z",
"currency":"GBX",
"timezone":"Europe/London",
"low52":307,
"quote":357.8999938964844,
"name":"Tesco PLC",
"exchange":"LON",
"marketcap":28929273763,
"symbol":"TSCO",
"volumedelay":0,
"shares":8083060703,
"pe":23.4719295501709,
"eps":0.15248000621795654,
"price":357.8999938964844,
"has_stock_data":true,
"volumeavg":14196534,
"volume":8885809,
"changepct":0.6184935569763184,
"high":359.5,
"datadelay":0,
"low":355.8999938964844,
"closeyest":355.70001220703125
}
Possibly your GET is not following the REDIRECT that happens when you use contentService. Look at the html returned there is a redirect in there.
I'm looking for examples of a pattern where a demon script running within a GoogleAppsForBusiness domain can parse incoming email messages. Some messages will will contain a call to yet a different GAScript that could, for example, change the ACL setting of a specific document.
I'm assuming someone else has already implemented this pattern but not sure how I go about finding examples.
thx
You can find script examples in the Apps Script user guide and tutorials. You may also search for related discussions on the forum. But I don't think there's one that fits you exactly, all code is out there for sure, but not on a single script.
It's possible that someone wrote such script and never published it. Since it's somewhat straightforward to do and everyone's usage is different. For instance, how do you plan on marking your emails (the ones you've already read, executed, etc)? It may be nice to use a gmail filter to help you out, putting the "command" emails in a label right away, and the script just remove the label (and possibly set another one). Point is, see how it can differ a lot.
Also, I think it's easier if you can keep all functions in the same script project. Possibly just on different files. As calling different scripts is way more complicated.
Anyway, he's how I'd start it:
//set a time-driven trigger to run this function on the desired frequency
function monitorEmails() {
var label = GmailApp.getUserLabelByName('command');
var doneLabel = GmailApp.getUserLabelByName('executed');
var cmds = label.getThreads();
var max = Math.min(cmds.length,5);
for( var i = 0; i < max; ++i ) {
var email = cmds[i].getMessages()[0];
var functionName = email.getBody();
//you may need to do extra parsing here, depending on your usage
var ret = undefined;
try {
ret = this[functionName]();
} catch(err) {
ret = err;
}
//replying the function return value to the email
//this may make sense or not
if( ret !== undefined )
email.reply(ret);
cmds[i].removeLabel(label).addLabel(doneLabel);
}
}
ps: I have not tested this code
You can create a google app that will be triggered by an incoming email message sent to a special address for the app. The message is converted to an HTTP POST which your app receives.
More details here:
https://developers.google.com/appengine/docs/python/mail/receivingmail
I havn't tried this myself yet but will be doing so in the next few days.
There are two ways. First you can use Google pub/sub and handle incomming notifications in your AppScrit endpoint. The second is to use the googleapis npm package inside your AppScript code an example here. Hope it helps.
These are the steps:
made a project on https://console.cloud.google.com/cloudpubsub/topicList?project=testmabs thing?
made a pubsub topic
made a subscription to the webhook url
added that url to the sites i own, i guess? I think I had to do DNS things to confirm i own it, and the error was super vague to figure out that was what i had to do, when trying to add the subscription
added permission to the topic for "gmail-api-push#system.gserviceaccount.com" as publisher (I also added ....apps.googleusercontent.com and youtrackapiuser.caps#gmail.com but i dont think I needed them)
created oauth client info and downloaded it in the credentials section of the google console. (oauthtrash.json)