La génèse

Je possède un Google Home qui ne sert pas vraiment à grand chose, et j'aurai aimé voir une fonctionnalité particulière sur celui-ci.

En effet grâce aux outils de Google, avec ma femme nous partageons mutuellement nos positions, et j'aurai bien aime que mon Google Home puisse me dire où se trouve ma femme...

Après quelques recherchent sur le Web il est apparu qu'il ne savait pas faire cela.

Encore une mission pour Canard !

Google Home Mini

La recherche et le POC

Quand je vais sur Google Maps, celui-ci fait apparaître ma position sur la carte ainsi que celle de ma femme.

Google Maps

Donc forcément, l'info est trouvable dans les différentes requêtes lancées par le navigateur.

Avec la console développeur de Chrome, on peut voir que ce sont plus de 300 requêtes qui sont envoyées pour afficher une Google Map.
Parmis ces requêtes l'une porte les informations que je veux récupérer.

En faisant une recherche avec la console développeur j'arrive à cibler rapidement la requête portant les informations (en cherchant le prénom de ma femme, que Google affiche sur la carte).

La requête magique est la suivante :

https://www.google.com/maps/preview/locationsharing/read?authuser=0&hl=fr&gl=fr&pb=

En la détaillant un peu on se rend compte qu'elle est tout de même bien foireuse.

Le résultat de cette requête est censé être du json, mais il est servi dans un fichier en attachement portant le nom "f.txt". En effet dans les header on trouve :

content-disposition: attachment; filename="f.txt"
content-type: application/json; charset=UTF-8

En regardant le contenu du fichier on trouve du json un peut WTF (que j'ai anonymisé):

)]}'
[[[["blabla","https://lh4.googleusercontent.com/blabla/photo.jpg",null,"Ma femme",null,null,null,"blabla"]
,[null,[null,coordonnée,coordonnée]
,blabla,16,"Adresse en toute lettre",null,"FR",code postal]
,null,1,"blabla",null,["000","https://lh4.googleusercontent.com/blabla/photo.jpg","Ma femme","Ma femme"]
,0,null,null,null,null,null,[1,83]
]
]
,null,"blabla","blabla",null,null,"blabla",30,blabla,[-blabla,[null,[null,coordonnée,coordonnée]
,1533885618287,17,"Adresse en toute lettre",null,"FR",code postal]
]
,null,[1,[[[30000,60000]
,null,3600000]
,[[30000,60000]
,null,null,1]
]
,null,1,null,null,null,null,1,3600]
]

Celui-ci porte directement en toute lettre l'adresse où se trouve ma femme, et l'adresse où je me trouve.

Pour un POC j'ai ensuite utilisé Postman pour simuler la même requête sur les serveurs de Google. Le retour n'était pas très glorieux mais attendu :

[
null,
null,
"blabla",
"blabla",
null,
null,
"blabla",
1800,
1533887081028
]

Le résultat de la requête étant lié au compte Google il devait forcément y avoir une authentification passée quelque part.
En examinant dans Chrome, pas de header particuliers, mais quelques cookies servant surêment à authentifier le compte Google.

A l'aide de l'extension Chrome Cookie Inspector j'ai sauvegardé les cookies de la requête et je les ai réinjectés dans Postman.
Et bingo, le json de retour contenait bien nos positions...

Le POC étant ok, il ne restait plus qu'à réaliser une petit webapp réalisant ces appels.

Le développement

Comme à mon habitude j'ai développé cela en Grails 2.2.0 qui est une antiquité mais que je connais bien.
Ci-dessous les parties intéressantes du développement : Le service qui envoie la requête http.

import groovyx.net.http.HTTPBuilder
import org.apache.http.impl.cookie.BasicClientCookie
import groovyx.net.http.ContentType
import groovyx.net.http.Method
import groovy.json.JsonSlurper

class GooglemapsService {
	
	// petite fonction pour créer un cookie à partir du json
	static parseCookie(m) {
		def cookie = new BasicClientCookie(m.name, m.value)
		m.findAll { k,v -> (k in ['path', 'domain']) }
			.each { k,v ->
				cookie[k] = m[k] 
			}
		cookie
	}
	
	// la fonction principale qui renverra le json de Google
	def getPosition() {
	
		// lire les cookies à partir du fichier json sauvegardé par Cookie Inspector
		// en développement
		def inputFile = new File("grails-app/conf/export.json")
		if (!inputFile.exists()) {
			// en "prod" hébergé sur le pi
			inputFile = new File("/home/pi/war/export.json")
		}
		def InputJSON = new JsonSlurper().parseText(inputFile.text)
		
		def url = "https://www.google.com/maps/preview/locationsharing/read?authuser=0&hl=fr&gl=fr&pb="
		def retour
		try {
			def http = new HTTPBuilder(url)
			
			// injection des cookies
			def cookieStore = http.getClient().getCookieStore()
			InputJSON.each{
				cookieStore.addCookie(parseCookie(it))
			}
			
			http.request(url, Method.GET, ContentType.URLENC) { 
				headers.Accept = 'application/json'
				
				response.success = { resp, json  ->
					log.info("getPosition : Success! ${resp.status}")
					def jsonSlurper = new JsonSlurper()
					// le json de google étant un peu foireux, il convient de le retravailler un peu
					// il commence par 6 caractères à virer qui sont ")]}'\n"
					// et en plus il renvoie des caractères unicode alors qu'il déclare envoyer de l'UTF-8
					// TODO traiter les autres caractères unicodes
					def object = jsonSlurper.parseText(
						json.keySet().toString()
							.substring(6).replaceAll("\ufffd", "é"))
					retour = object
				}
			
				response.failure = { resp ->
					log.error("getPosition : Request failed with status ${resp.status} ${resp.statusLine}")
					retour = null
				}
			}
		}
		catch (Exception e)
		{
			log.error("getPosition : Exception : ${e.message}",e)
			retour = null
		}
		retour
	}
}


Et son exploitation dans le controleur :

def json = googlemapsService.getPosition()
def message = 'Désolé il y a eu une erreur'

if(json != null) {
	if (personne.toLowerCase() == 'marcel') {
		// le flux est constitué d'un empilement de tableaux
		emplacement = json[9][1][4]	
	} else {
		// le flux est constitué d'un empilement de tableaux
		emplacement = json[0][0][1][4]
	}
	message = "${personne} se trouve à ${emplacement}"
}
resGlobal = [
	'speech' : message,
	'displayText'  : message,
	'source' : 'Netatmo'
]


Avec ce code, compilé en WAR et déployé sur le Pi j'arrive à interroger Google pour récupérer nos positions.

Ensuite, j'ai intégré cette nouvelle fonctionnalité dans mon chatBot dialogflow qui est intégré sur Google Home/Assistant. (je ferai un article dessus une prochaine fois).

Celui-ci s'appele netatmo car il sert à la base à interroger ma station météo. Il devrait bientôt s'appeler HAL 9000 à la place :-)


Google Assistant

Mission accomplie


Partager sur Twitter : icône Twitter ou me contacter :
10/08/2018