iobroker.google-sharedlocations2 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/admin/i18n/de.json +15 -3
- package/admin/i18n/en.json +14 -2
- package/admin/i18n/es.json +15 -3
- package/admin/i18n/fr.json +15 -3
- package/admin/i18n/it.json +15 -3
- package/admin/i18n/nl.json +15 -3
- package/admin/i18n/pl.json +15 -3
- package/admin/i18n/pt.json +15 -3
- package/admin/i18n/ru.json +15 -3
- package/admin/i18n/uk.json +15 -3
- package/admin/i18n/zh-cn.json +15 -3
- package/admin/jsonConfig.json +9 -14
- package/io-package.json +55 -3
- package/package.json +1 -1
- package/src/lib/Cookie.ts +278 -63
- package/src/main.ts +33 -57
package/README.md
CHANGED
|
@@ -27,6 +27,16 @@ Copyright and trademark of Google are property of Google.
|
|
|
27
27
|
Placeholder for the next version (at the beginning of the line):
|
|
28
28
|
### **WORK IN PROGRESS**
|
|
29
29
|
-->
|
|
30
|
+
### 0.3.0 (2026-02-09)
|
|
31
|
+
* (Garfonso) do not update states if no new position is available.
|
|
32
|
+
* (Garfonso) fixed: refresh via browser
|
|
33
|
+
* (Garfonso) added: force refresh via browser by setting "forceRefresh" state to true
|
|
34
|
+
* (Garfonso) changed: now store complete cookies array
|
|
35
|
+
|
|
36
|
+
### 0.2.0 (2026-02-03)
|
|
37
|
+
* (Garfonso) now using data directory to store chrome data
|
|
38
|
+
* (Garfonso) try to use existing cookie in browser to refresh cookie without login.
|
|
39
|
+
|
|
30
40
|
### 0.1.1 (2026-02-02)
|
|
31
41
|
* (Garfonso) improved recovery from login errors
|
|
32
42
|
|
package/admin/i18n/de.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Fences erstellen boolesche Zustände für jeden Fence, um anzuzeigen, ob sich der Benutzer innerhalb des Bereichs befindet",
|
|
3
|
+
"Geofencing Settings": "Geofencing-Einstellungen",
|
|
4
|
+
"Google Settings": "Google-Einstellungen",
|
|
5
|
+
"ID of state": "ID des Staates",
|
|
6
|
+
"Instance of iobroker.places to use": "Zu verwendende Instanz von iobroker.places",
|
|
7
|
+
"Latitude": "Breitengrad",
|
|
8
|
+
"Longitude": "Längengrad",
|
|
9
|
+
"Name of fence": "Name des Bereichs",
|
|
10
|
+
"Password of ioBroker Google Account": "Passwort des ioBroker-Google-Kontos",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Abfrageintervall (in Sekunden, mindestens 60 Sekunden)",
|
|
12
|
+
"Radius (m)": "Radius (m)",
|
|
13
|
+
"User": "Benutzer",
|
|
14
|
+
"Username of ioBroker Google Account": "Benutzername des ioBroker-Google-Kontos",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Adaptereinstellungen für google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Bitte geben Sie die Anmeldeinformationen eines ioBroker-Google-Kontos an, mit dem Sie Ihren Standort über die Google Maps-App teilen. Es wird dringend empfohlen, zu diesem Zweck ein dediziertes Google-Konto zu erstellen."
|
|
17
|
+
}
|
package/admin/i18n/en.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"google-sharedlocations2 adapter settings": "Adapter settings for google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"Google Settings": "Google Settings",
|
|
4
|
+
"labelAccountHint": "Please provide the credentials of an ioBroker Google Account which you share your location with using the Google Maps App. It is strongly recommended to create a dedicated Google Account for this purpose.",
|
|
5
|
+
"Username of ioBroker Google Account": "Username of ioBroker Google Account",
|
|
6
|
+
"Password of ioBroker Google Account": "Password of ioBroker Google Account",
|
|
7
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Polling Interval (in seconds, at least 60 seconds)",
|
|
8
|
+
"Geofencing Settings": "Geofencing Settings",
|
|
9
|
+
"Instance of iobroker.places to use": "Instance of iobroker.places to use",
|
|
10
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Fences, will create boolean states for each fence to indicate if user is within the fence",
|
|
11
|
+
"Name of fence": "Name of fence",
|
|
12
|
+
"Latitude": "Latitude",
|
|
13
|
+
"Longitude": "Longitude",
|
|
14
|
+
"Radius (m)": "Radius (m)",
|
|
15
|
+
"User": "User",
|
|
16
|
+
"ID of state": "ID of state"
|
|
5
17
|
}
|
package/admin/i18n/es.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Cercas, creará estados booleanos para cada cerca para indicar si el usuario está dentro de la cerca.",
|
|
3
|
+
"Geofencing Settings": "Configuración de geocercas",
|
|
4
|
+
"Google Settings": "Configuración de Google",
|
|
5
|
+
"ID of state": "identificación del estado",
|
|
6
|
+
"Instance of iobroker.places to use": "Instancia de iobroker.lugares para usar",
|
|
7
|
+
"Latitude": "Latitud",
|
|
8
|
+
"Longitude": "Longitud",
|
|
9
|
+
"Name of fence": "nombre de la valla",
|
|
10
|
+
"Password of ioBroker Google Account": "Contraseña de la cuenta de Google de ioBroker",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Intervalo de sondeo (en segundos, al menos 60 segundos)",
|
|
12
|
+
"Radius (m)": "Radio (m)",
|
|
13
|
+
"User": "Usuario",
|
|
14
|
+
"Username of ioBroker Google Account": "Nombre de usuario de la cuenta de Google de ioBroker",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Ajustes del adaptador para google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Proporcione las credenciales de una cuenta de Google de ioBroker con la que comparte su ubicación mediante la aplicación Google Maps. Se recomienda encarecidamente crear una cuenta de Google dedicada para este fin."
|
|
17
|
+
}
|
package/admin/i18n/fr.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Clôtures, créera des états booléens pour chaque clôture pour indiquer si l'utilisateur se trouve à l'intérieur de la clôture",
|
|
3
|
+
"Geofencing Settings": "Paramètres de géolocalisation",
|
|
4
|
+
"Google Settings": "Paramètres Google",
|
|
5
|
+
"ID of state": "ID de l'état",
|
|
6
|
+
"Instance of iobroker.places to use": "Instance de iobroker.places à utiliser",
|
|
7
|
+
"Latitude": "Latitude",
|
|
8
|
+
"Longitude": "Longitude",
|
|
9
|
+
"Name of fence": "Nom de la clôture",
|
|
10
|
+
"Password of ioBroker Google Account": "Mot de passe du compte Google ioBroker",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Intervalle d'interrogation (en secondes, au moins 60 secondes)",
|
|
12
|
+
"Radius (m)": "Rayon (m)",
|
|
13
|
+
"User": "Utilisateur",
|
|
14
|
+
"Username of ioBroker Google Account": "Nom d'utilisateur du compte Google ioBroker",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Paramètres d'adaptateur pour google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Veuillez fournir les informations d'identification d'un compte Google ioBroker avec lequel vous partagez votre position à l'aide de l'application Google Maps. Il est fortement recommandé de créer un compte Google dédié à cet effet."
|
|
17
|
+
}
|
package/admin/i18n/it.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Recinti: creerà stati booleani per ciascun recinto per indicare se l'utente si trova all'interno del recinto",
|
|
3
|
+
"Geofencing Settings": "Impostazioni del geofencing",
|
|
4
|
+
"Google Settings": "Impostazioni di Google",
|
|
5
|
+
"ID of state": "ID dello stato",
|
|
6
|
+
"Instance of iobroker.places to use": "Istanza di iobroker.places da utilizzare",
|
|
7
|
+
"Latitude": "Latitudine",
|
|
8
|
+
"Longitude": "Longitudine",
|
|
9
|
+
"Name of fence": "Nome della recinzione",
|
|
10
|
+
"Password of ioBroker Google Account": "Password dell'account Google ioBroker",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Intervallo di polling (in secondi, almeno 60 secondi)",
|
|
12
|
+
"Radius (m)": "Raggio (m)",
|
|
13
|
+
"User": "Utente",
|
|
14
|
+
"Username of ioBroker Google Account": "Nome utente dell'account Google ioBroker",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Impostazioni dell'adattatore per google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Fornisci le credenziali di un account Google ioBroker con cui condividi la tua posizione utilizzando l'app Google Maps. Si consiglia vivamente di creare un account Google dedicato a questo scopo."
|
|
17
|
+
}
|
package/admin/i18n/nl.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Hekken, creëert booleaanse toestanden voor elk hek om aan te geven of de gebruiker zich binnen het hek bevindt",
|
|
3
|
+
"Geofencing Settings": "Geofencing-instellingen",
|
|
4
|
+
"Google Settings": "Google-instellingen",
|
|
5
|
+
"ID of state": "Identiteitskaart van de staat",
|
|
6
|
+
"Instance of iobroker.places to use": "Instantie van iobroker.places om te gebruiken",
|
|
7
|
+
"Latitude": "Breedte",
|
|
8
|
+
"Longitude": "Lengte",
|
|
9
|
+
"Name of fence": "Naam hek",
|
|
10
|
+
"Password of ioBroker Google Account": "Wachtwoord van ioBroker Google-account",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Polling-interval (in seconden, minimaal 60 seconden)",
|
|
12
|
+
"Radius (m)": "Straal (m)",
|
|
13
|
+
"User": "Gebruiker",
|
|
14
|
+
"Username of ioBroker Google Account": "Gebruikersnaam van ioBroker Google-account",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Adapterinstellingen voor google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Geef de inloggegevens op van een ioBroker Google-account waarmee u uw locatie deelt via de Google Maps-app. Het wordt sterk aanbevolen om hiervoor een speciaal Google-account aan te maken."
|
|
17
|
+
}
|
package/admin/i18n/pl.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Ogrodzenia utworzy stany logiczne dla każdego ogrodzenia, aby wskazać, czy użytkownik znajduje się w obrębie ogrodzenia",
|
|
3
|
+
"Geofencing Settings": "Ustawienia geofencingu",
|
|
4
|
+
"Google Settings": "Ustawienia Google",
|
|
5
|
+
"ID of state": "Identyfikator stanu",
|
|
6
|
+
"Instance of iobroker.places to use": "Instancja iobroker.places do użycia",
|
|
7
|
+
"Latitude": "Szerokość",
|
|
8
|
+
"Longitude": "Długość geograficzna",
|
|
9
|
+
"Name of fence": "Nazwa ogrodzenia",
|
|
10
|
+
"Password of ioBroker Google Account": "Hasło do konta Google ioBroker",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Interwał odpytywania (w sekundach, co najmniej 60 sekund)",
|
|
12
|
+
"Radius (m)": "Promień (m)",
|
|
13
|
+
"User": "Użytkownik",
|
|
14
|
+
"Username of ioBroker Google Account": "Nazwa użytkownika konta Google ioBroker",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Ustawienia adaptera dla google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Podaj dane uwierzytelniające konta Google ioBroker, któremu udostępniasz swoją lokalizację za pomocą aplikacji Google Maps. Zdecydowanie zaleca się utworzenie w tym celu dedykowanego konta Google."
|
|
17
|
+
}
|
package/admin/i18n/pt.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Cercas, criará estados booleanos para cada cerca para indicar se o usuário está dentro da cerca",
|
|
3
|
+
"Geofencing Settings": "Configurações de cerca geográfica",
|
|
4
|
+
"Google Settings": "Configurações do Google",
|
|
5
|
+
"ID of state": "ID do estado",
|
|
6
|
+
"Instance of iobroker.places to use": "Instância de iobroker.places para usar",
|
|
7
|
+
"Latitude": "Latitude",
|
|
8
|
+
"Longitude": "Longitude",
|
|
9
|
+
"Name of fence": "Nome da cerca",
|
|
10
|
+
"Password of ioBroker Google Account": "Senha da conta do Google ioBroker",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Intervalo de pesquisa (em segundos, pelo menos 60 segundos)",
|
|
12
|
+
"Radius (m)": "Raio (m)",
|
|
13
|
+
"User": "Usuário",
|
|
14
|
+
"Username of ioBroker Google Account": "Nome de usuário da conta do Google ioBroker",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Configurações do adaptador para google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Forneça as credenciais de uma conta do Google ioBroker com a qual você compartilha sua localização usando o aplicativo Google Maps. É altamente recomendável criar uma Conta do Google dedicada para essa finalidade."
|
|
17
|
+
}
|
package/admin/i18n/ru.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Заборы: будут создавать логические состояния для каждого забора, чтобы указать, находится ли пользователь внутри забора.",
|
|
3
|
+
"Geofencing Settings": "Настройки геозон",
|
|
4
|
+
"Google Settings": "Настройки Google",
|
|
5
|
+
"ID of state": "идентификатор штата",
|
|
6
|
+
"Instance of iobroker.places to use": "Экземпляр iobroker.places для использования",
|
|
7
|
+
"Latitude": "Широта",
|
|
8
|
+
"Longitude": "Долгота",
|
|
9
|
+
"Name of fence": "Название забора",
|
|
10
|
+
"Password of ioBroker Google Account": "Пароль учетной записи Google ioBroker",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Интервал опроса (в секундах, не менее 60 секунд)",
|
|
12
|
+
"Radius (m)": "Радиус (м)",
|
|
13
|
+
"User": "Пользователь",
|
|
14
|
+
"Username of ioBroker Google Account": "Имя пользователя учетной записи Google ioBroker",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Настройки адаптера для google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Укажите учетные данные учетной записи Google ioBroker, с которой вы делитесь своим местоположением с помощью приложения Google Maps. Для этой цели настоятельно рекомендуется создать специальную учетную запись Google."
|
|
17
|
+
}
|
package/admin/i18n/uk.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "Fences створить логічні стани для кожної огорожі, щоб вказати, чи перебуває користувач у межах огорожі",
|
|
3
|
+
"Geofencing Settings": "Налаштування геозон",
|
|
4
|
+
"Google Settings": "Налаштування Google",
|
|
5
|
+
"ID of state": "ID держ",
|
|
6
|
+
"Instance of iobroker.places to use": "Примірник iobroker.places для використання",
|
|
7
|
+
"Latitude": "Широта",
|
|
8
|
+
"Longitude": "Довгота",
|
|
9
|
+
"Name of fence": "Назва огорожі",
|
|
10
|
+
"Password of ioBroker Google Account": "Пароль облікового запису Google ioBroker",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "Інтервал опитування (у секундах, не менше 60 секунд)",
|
|
12
|
+
"Radius (m)": "Радіус (м)",
|
|
13
|
+
"User": "Користувач",
|
|
14
|
+
"Username of ioBroker Google Account": "Ім'я користувача облікового запису Google ioBroker",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "Налаштування адаптера для google-sharedlocations2",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "Надайте облікові дані облікового запису Google ioBroker, якому ви надаєте доступ до свого місцезнаходження за допомогою програми Google Maps. Для цього наполегливо рекомендується створити спеціальний обліковий запис Google."
|
|
17
|
+
}
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Fences, will create boolean states for each fence to indicate if user is within the fence": "栅栏,将为每个栅栏创建布尔状态以指示用户是否在栅栏内",
|
|
3
|
+
"Geofencing Settings": "地理围栏设置",
|
|
4
|
+
"Google Settings": "谷歌设置",
|
|
5
|
+
"ID of state": "州ID",
|
|
6
|
+
"Instance of iobroker.places to use": "要使用的 iobroker.places 实例",
|
|
7
|
+
"Latitude": "纬度",
|
|
8
|
+
"Longitude": "经度",
|
|
9
|
+
"Name of fence": "围栏名称",
|
|
10
|
+
"Password of ioBroker Google Account": "ioBroker Google 账户密码",
|
|
11
|
+
"Polling Interval (in seconds, at least 60 seconds)": "轮询间隔(以秒为单位,至少 60 秒)",
|
|
12
|
+
"Radius (m)": "半径(米)",
|
|
13
|
+
"User": "用户",
|
|
14
|
+
"Username of ioBroker Google Account": "ioBroker Google 帐户的用户名",
|
|
2
15
|
"google-sharedlocations2 adapter settings": "google-sharedlocations2的适配器设置",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
}
|
|
16
|
+
"labelAccountHint": "请提供您使用 Google 地图应用程序共享您的位置的 ioBroker Google 帐户的凭据。强烈建议为此目的创建一个专用的 Google 帐户。"
|
|
17
|
+
}
|
package/admin/jsonConfig.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"i18n": true,
|
|
3
3
|
"type": "tabs",
|
|
4
|
-
"items":
|
|
5
|
-
{
|
|
4
|
+
"items": {
|
|
5
|
+
"_AccountSettings": {
|
|
6
6
|
"type": "panel",
|
|
7
7
|
"label": "Google Settings",
|
|
8
8
|
"items": {
|
|
9
9
|
"_explanation": {
|
|
10
10
|
"type": "staticText",
|
|
11
|
-
"label": "
|
|
11
|
+
"label": "labelAccountHint",
|
|
12
12
|
"newLine": true,
|
|
13
13
|
"xs": 12,
|
|
14
14
|
"sm": 12,
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
-
{
|
|
56
|
+
"_GeofencingSettings": {
|
|
57
57
|
"type": "panel",
|
|
58
58
|
"label": "Geofencing Settings",
|
|
59
59
|
"items": {
|
|
60
60
|
"placesInstance": {
|
|
61
61
|
"type": "instance",
|
|
62
|
-
"label": "Instance of iobroker.places to use
|
|
62
|
+
"label": "Instance of iobroker.places to use",
|
|
63
63
|
"adapter": "places",
|
|
64
64
|
"allowDeactivate": true,
|
|
65
65
|
"newLine": true,
|
|
@@ -112,24 +112,19 @@
|
|
|
112
112
|
"command": "getUsers",
|
|
113
113
|
"attr": "user",
|
|
114
114
|
"multiple": false,
|
|
115
|
-
"title": "
|
|
115
|
+
"title": "User",
|
|
116
116
|
"width": "20%",
|
|
117
117
|
"default": ""
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"type": "text",
|
|
121
|
-
"title": "
|
|
121
|
+
"title": "ID of state",
|
|
122
122
|
"width": "15%",
|
|
123
|
-
"attr": "fenceId"
|
|
124
|
-
"defaultFunc": "data.name + data.user",
|
|
125
|
-
"alsoDependsOn": [
|
|
126
|
-
"name",
|
|
127
|
-
"user"
|
|
128
|
-
]
|
|
123
|
+
"attr": "fenceId"
|
|
129
124
|
}
|
|
130
125
|
]
|
|
131
126
|
}
|
|
132
127
|
}
|
|
133
128
|
}
|
|
134
|
-
|
|
129
|
+
}
|
|
135
130
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "google-sharedlocations2",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.3.0": {
|
|
7
|
+
"en": "do not update states if no new position is available.\nfixed: refresh via browser\nadded: force refresh via browser by setting \"forceRefresh\" state to true\nchanged: now store complete cookies array",
|
|
8
|
+
"de": "aktualisiere zustände nicht, wenn keine neue position verfügbar ist.\nrepariert: cookie refresh über browser\nneu: Refresh über Browser durch \"forceRefresh\" Zustand auf true setzen erzwingen\ngeändert: jetzt komplette cookies-array speichern",
|
|
9
|
+
"ru": "не обновляйте состояния, если нет новой позиции.\nобновление через браузер\nдобавлено: принудительное обновление через браузер, настроив состояние «forceRefresh» на истинное\nизменено: теперь храните полный массив cookie",
|
|
10
|
+
"pt": "não atualizar estados se nenhuma nova posição estiver disponível.\ncorrigido: atualizar via navegador\nadded: forçar a atualização via navegador definindo o estado \"forceRefresh\" para true\nalterado: agora armazenar o array completo de cookies",
|
|
11
|
+
"nl": "niet bijwerken indien er geen nieuwe positie beschikbaar is.\nvast: vernieuwen via browser\ntoegevoegd: force refresh via browser door \"forceRefresh\" in te stellen\ngewijzigd: nu volledige cookies-array opslaan",
|
|
12
|
+
"fr": "ne mettez pas à jour les états si aucun nouveau poste n'est disponible.\ncorrigé: rafraîchir via le navigateur\najouté: force rafraîchir via le navigateur en définissant l'état \"forceRefresh\" à true\nmodifié : stockez maintenant le tableau complet des cookies",
|
|
13
|
+
"it": "non aggiornare gli stati se non è disponibile nessuna nuova posizione.\nfisso: rinfrescare tramite browser\naggiunto: forza rinfresco tramite browser impostando lo stato \"forceRefresh\" a true\ncambiato: ora memorizzare l'array completo dei cookie",
|
|
14
|
+
"es": "no actualizar estados si no hay nueva posición disponible.\nfijo: refrescante a través del navegador\nañadido: actualización de fuerza a través del navegador mediante el establecimiento del estado \"forceRefresh\" a la verdad\ncambiado: ahora almacena la gama completa de cookies",
|
|
15
|
+
"pl": "nie aktualizować stanów, jeśli nie jest dostępna nowa pozycja.\nstałe: odświeżenie przez przeglądarkę\ndodane: siła odświeżania przez przeglądarkę poprzez ustawienie stanu \"forceRefresh\" na true\nzmienione: teraz przechowywać pełną tablicę cookies",
|
|
16
|
+
"uk": "не оновлювати стани, якщо немає нової позиції.\nвиправлено: оновлення через браузер\nдодано: сила освіження через браузер за допомогою налаштування \"forceRefresh\" стану true\nзмінено: тепер зберігає повний масив файлів cookie",
|
|
17
|
+
"zh-cn": "如果没有新职位,则不更新状态.\n固定: 通过浏览器刷新\n添加: 通过浏览器将“ ForceRefresh” 状态设置为真以强制刷新\n更改: 现在存储完整的 cookie 阵列"
|
|
18
|
+
},
|
|
19
|
+
"0.2.0": {
|
|
20
|
+
"en": "now using data directory to store chrome data\ntry to use existing cookie in browser to refresh cookie without login.",
|
|
21
|
+
"de": "jetzt mit data directory, um chromdaten zu speichern\nversuchen, existierenden cookie im browser zu verwenden, um cookie ohne anmeldung zu aktualisieren.",
|
|
22
|
+
"ru": "теперь используя директор данных для хранения хромированных данных\nпопробуйте использовать существующий cookie в браузере, чтобы обновить cookie без входа в систему.",
|
|
23
|
+
"pt": "agora usando o diretor de dados para armazenar dados de cromo\ntentar usar cookie existente no navegador para atualizar cookie sem login.",
|
|
24
|
+
"nl": "nu met behulp van data director om chroom gegevens op te slaan\nprobeer bestaande cookie in de browser te gebruiken om cookie te vernieuwen zonder in te loggen.",
|
|
25
|
+
"fr": "maintenant en utilisant le directeur de données pour stocker des données chrome\nessayez d'utiliser le cookie existentint dans le navigateur pour rafraîchir le cookie sans vous connecter.",
|
|
26
|
+
"it": "ora utilizzando il data director per memorizzare i dati cromati\nprovare a utilizzare cookie esisteint nel browser per aggiornare i cookie senza effettuare il login.",
|
|
27
|
+
"es": "ahora utilizando el director de datos para almacenar datos de cromo\ntratar de utilizar la cookie existente en el navegador para refrescar la cookie sin login.",
|
|
28
|
+
"pl": "teraz za pomocą dyrektora danych do przechowywania danych chromowych\nspróbuj użyć plików cookie existint w przeglądarce, aby odświeżyć pliki cookie bez logowania.",
|
|
29
|
+
"uk": "тепер використовуючи каталог даних для зберігання хромованих даних\nспробуйте використовувати наявний cookie-файли в браузері, щоб оновити cookie без реєстрації.",
|
|
30
|
+
"zh-cn": "现在使用数据总监存储铬数据\n尝试在浏览器中使用存在 cookie来刷新 cookie而不登录."
|
|
31
|
+
},
|
|
6
32
|
"0.1.1": {
|
|
7
33
|
"en": "improved recovery from login errors",
|
|
8
34
|
"de": "verbesserte wiederherstellung von login-fehlern",
|
|
@@ -218,14 +244,40 @@
|
|
|
218
244
|
"type": "state",
|
|
219
245
|
"common": {
|
|
220
246
|
"role": "state",
|
|
221
|
-
"name": "Current authentication cookies",
|
|
247
|
+
"name": "Current authentication cookies (write only)",
|
|
222
248
|
"desc": "Overwrite to set cookies manually. Set empty to login again.",
|
|
223
249
|
"type": "string",
|
|
224
|
-
"read":
|
|
250
|
+
"read": false,
|
|
225
251
|
"write": true,
|
|
226
252
|
"def": ""
|
|
227
253
|
}
|
|
228
254
|
},
|
|
255
|
+
{
|
|
256
|
+
"_id": "info.cookieStore",
|
|
257
|
+
"type": "state",
|
|
258
|
+
"common": {
|
|
259
|
+
"role": "state",
|
|
260
|
+
"name": "JSON cookie store with all data",
|
|
261
|
+
"desc": "please don't mess up",
|
|
262
|
+
"type": "object",
|
|
263
|
+
"read": true,
|
|
264
|
+
"write": false,
|
|
265
|
+
"def": "[]"
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"_id": "info.forceRefreshWithBrowser",
|
|
270
|
+
"type": "state",
|
|
271
|
+
"common": {
|
|
272
|
+
"role": "button",
|
|
273
|
+
"name": "Start Browser to refresh cookies",
|
|
274
|
+
"desc": "Set to true to start the browser and refresh cookies using existing login in browser. Will reset to false automatically after starting the browser.",
|
|
275
|
+
"type": "boolean",
|
|
276
|
+
"read": false,
|
|
277
|
+
"write": true,
|
|
278
|
+
"def": false
|
|
279
|
+
}
|
|
280
|
+
},
|
|
229
281
|
{
|
|
230
282
|
"_id": "fences",
|
|
231
283
|
"type": "folder",
|
package/package.json
CHANGED
package/src/lib/Cookie.ts
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import type { GoogleSharedlocations2 } from '../main';
|
|
3
3
|
import puppeteer from 'puppeteer';
|
|
4
|
-
import type { Browser } from 'puppeteer';
|
|
4
|
+
import type { Browser, Page, CookieData } from 'puppeteer';
|
|
5
|
+
import { mkdir } from 'fs/promises';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Helper class to manage Google cookies.
|
|
8
9
|
*/
|
|
9
10
|
export class Cookie {
|
|
10
|
-
|
|
11
|
+
cookies: CookieData[] = [];
|
|
11
12
|
username?: string;
|
|
12
13
|
password?: string;
|
|
13
14
|
adapter: GoogleSharedlocations2;
|
|
14
15
|
log;
|
|
15
16
|
private browser: Browser | null = null;
|
|
17
|
+
dataDir: string;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Construct cookie helper
|
|
19
21
|
*
|
|
20
22
|
* @param adapter - adapter instance
|
|
23
|
+
* @param dataDir - data directory of the instance to store browser data to.
|
|
21
24
|
*/
|
|
22
|
-
constructor(adapter: GoogleSharedlocations2) {
|
|
23
|
-
this.currentCookie = '';
|
|
25
|
+
constructor(adapter: GoogleSharedlocations2, dataDir: string) {
|
|
24
26
|
this.username = '';
|
|
25
27
|
this.password = '';
|
|
26
28
|
this.adapter = adapter;
|
|
27
29
|
this.log = adapter.log;
|
|
30
|
+
this.dataDir = dataDir;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -33,16 +36,36 @@ export class Cookie {
|
|
|
33
36
|
async init(): Promise<void> {
|
|
34
37
|
this.username = this.adapter.config.googleUsername;
|
|
35
38
|
this.password = this.adapter.config.googlePassword;
|
|
39
|
+
this.log = this.adapter.log; // does not exist during construction...
|
|
36
40
|
try {
|
|
37
|
-
|
|
41
|
+
//ensure data dir exists
|
|
42
|
+
await mkdir(this.dataDir, { recursive: true }); //recursive true should prevent error if already exists.
|
|
43
|
+
const state = await this.adapter.getStateAsync('info.cookieStore');
|
|
44
|
+
const stringState = await this.adapter.getStateAsync('info.currentCookies');
|
|
38
45
|
if (state && state.val && typeof state.val === 'string') {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
try {
|
|
47
|
+
this.cookies = JSON.parse(state.val);
|
|
48
|
+
this.log?.debug(`Loaded ${this.cookies.length} cookies from state.`);
|
|
49
|
+
if (this.isValid()) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
this.log?.error(
|
|
54
|
+
`Error parsing cookies from state: ${(e as Error).message}, using string as cookie.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (stringState && stringState.val && typeof stringState.val === 'string') {
|
|
60
|
+
this.log?.debug('Loaded cookie string from state, trying to convert to new format.');
|
|
61
|
+
this.readCookieFromString(stringState.val);
|
|
62
|
+
if (this.isValid()) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
45
65
|
}
|
|
66
|
+
|
|
67
|
+
this.log?.debug('No cookie found in states, trying to log in to get new one.');
|
|
68
|
+
await this.loginToGetNewCookies();
|
|
46
69
|
} catch (err: any) {
|
|
47
70
|
this.log?.error(`Error loading cookie from state: ${err}`);
|
|
48
71
|
}
|
|
@@ -53,12 +76,35 @@ export class Cookie {
|
|
|
53
76
|
*/
|
|
54
77
|
async storeCookie(): Promise<void> {
|
|
55
78
|
try {
|
|
56
|
-
await this.adapter.
|
|
79
|
+
await this.adapter.setState('info.cookieStore', JSON.stringify(this.cookies), true);
|
|
57
80
|
} catch (err: any) {
|
|
58
81
|
this.log?.error(`Error storing cookie: ${err}`);
|
|
59
82
|
}
|
|
60
83
|
}
|
|
61
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Read cookie from a string in the format "name=value; name2=value2" and store it in the cookies array.
|
|
87
|
+
*
|
|
88
|
+
* @param cookieString - cookie string to read from
|
|
89
|
+
*/
|
|
90
|
+
readCookieFromString(cookieString: string): void {
|
|
91
|
+
this.cookies = cookieString
|
|
92
|
+
.split(';')
|
|
93
|
+
.map(pair => {
|
|
94
|
+
const parts = pair.trim().split('=');
|
|
95
|
+
return parts.length >= 2
|
|
96
|
+
? {
|
|
97
|
+
name: parts[0].trim(),
|
|
98
|
+
value: parts.slice(1).join('=').trim(),
|
|
99
|
+
domain: '.google.com',
|
|
100
|
+
path: '/',
|
|
101
|
+
secure: true,
|
|
102
|
+
}
|
|
103
|
+
: null;
|
|
104
|
+
})
|
|
105
|
+
.filter(c => c !== null);
|
|
106
|
+
}
|
|
107
|
+
|
|
62
108
|
/**
|
|
63
109
|
* Augment the current cookie with data from the 'set-cookie' header.
|
|
64
110
|
*
|
|
@@ -67,25 +113,64 @@ export class Cookie {
|
|
|
67
113
|
async augmentCookieFromHeader(headers: Record<string, any>): Promise<void> {
|
|
68
114
|
if (headers['set-cookie'] && headers['set-cookie'].length) {
|
|
69
115
|
this.log?.debug('New header received.');
|
|
70
|
-
const oldLength = this.
|
|
71
|
-
const cookies = this.currentCookie.split('; ').map(c => c.split('='));
|
|
116
|
+
const oldLength = this.cookies.length;
|
|
72
117
|
|
|
73
118
|
//split old cookie and new cookie. Update single values.
|
|
74
119
|
for (const header of headers['set-cookie']) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
120
|
+
//console.log('Processing header cookie:', header);
|
|
121
|
+
const keyValues = header.split('; ');
|
|
122
|
+
const [name, value] = keyValues.shift().split('='); // first part is cookie, rest are attributes like path, secure etc.
|
|
123
|
+
const cookie = {
|
|
124
|
+
name: name.trim(),
|
|
125
|
+
value: value.trim(),
|
|
126
|
+
domain: '.google.com',
|
|
127
|
+
} as CookieData;
|
|
128
|
+
for (const kv of keyValues) {
|
|
129
|
+
const [k, v] = kv.split('=');
|
|
130
|
+
switch (k.toLowerCase()) {
|
|
131
|
+
case 'domain':
|
|
132
|
+
cookie.domain = v ? v.trim() : '.google.com';
|
|
133
|
+
break;
|
|
134
|
+
case 'path':
|
|
135
|
+
cookie.path = v ? v.trim() : '/';
|
|
136
|
+
break;
|
|
137
|
+
case 'secure':
|
|
138
|
+
cookie.secure = true;
|
|
139
|
+
break;
|
|
140
|
+
case 'httponly':
|
|
141
|
+
cookie.httpOnly = true;
|
|
142
|
+
break;
|
|
143
|
+
case 'samesite':
|
|
144
|
+
cookie.sameSite = v ? v.trim() : 'Lax';
|
|
145
|
+
break;
|
|
146
|
+
case 'expires':
|
|
147
|
+
cookie.expires = new Date(v).getTime() / 1000; //puppeteer expects expires in seconds, not milliseconds
|
|
148
|
+
break;
|
|
149
|
+
case 'priority':
|
|
150
|
+
cookie.priority = v ? v.trim() : 'Medium';
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
this.log.debug(`Unknown cookie attribute: ${k}=${v}`);
|
|
83
154
|
}
|
|
84
155
|
}
|
|
156
|
+
const cIndex = this.cookies.findIndex(c => c.name === name);
|
|
157
|
+
if (cIndex < 0) {
|
|
158
|
+
this.log.debug(`Adding new cookie from header: ${cookie.name}=${cookie.value}`);
|
|
159
|
+
this.cookies.push(cookie); //add
|
|
160
|
+
} else {
|
|
161
|
+
this.log.debug(`Updating cookie from header: ${cookie.name}=${cookie.value}`);
|
|
162
|
+
this.cookies[cIndex] = cookie; //update
|
|
163
|
+
}
|
|
85
164
|
}
|
|
86
165
|
|
|
87
|
-
this.
|
|
88
|
-
|
|
166
|
+
this.cookies
|
|
167
|
+
.filter(c => c.expires && c.expires < Date.now() / 1000)
|
|
168
|
+
.forEach(c =>
|
|
169
|
+
this.log.debug(`Cookie ${c.name} expired at ${new Date(c.expires! * 1000).toISOString()}`),
|
|
170
|
+
);
|
|
171
|
+
this.cookies = this.cookies.filter(c => !c.expires || c.expires > Date.now() / 1000); //remove expired cookies
|
|
172
|
+
|
|
173
|
+
this.log?.debug(`Cookie updated. Length: ${oldLength} -> ${this.cookies.length}`);
|
|
89
174
|
return this.storeCookie();
|
|
90
175
|
}
|
|
91
176
|
}
|
|
@@ -93,12 +178,12 @@ export class Cookie {
|
|
|
93
178
|
/**
|
|
94
179
|
* Improve the current cookie by making a request to Google My Account page.
|
|
95
180
|
*/
|
|
96
|
-
async improveCookie(): Promise<
|
|
181
|
+
async improveCookie(): Promise<boolean> {
|
|
97
182
|
//see https://github.com/costastf/locationsharinglib/blob/master/locationsharinglib/locationsharinglib.py#L105
|
|
98
183
|
const options = {
|
|
99
184
|
url: 'https://myaccount.google.com/?hl=en',
|
|
100
185
|
headers: {
|
|
101
|
-
Cookie: this.
|
|
186
|
+
Cookie: this.cookies.map(c => `${c.name}=${c.value}`).join('; '),
|
|
102
187
|
},
|
|
103
188
|
method: 'get',
|
|
104
189
|
};
|
|
@@ -108,12 +193,159 @@ export class Cookie {
|
|
|
108
193
|
|
|
109
194
|
if (response.status !== 200) {
|
|
110
195
|
this.log?.error(`Failed improving cookie: ${response.status}`);
|
|
111
|
-
|
|
112
|
-
await this.augmentCookieFromHeader(response.headers);
|
|
196
|
+
return false;
|
|
113
197
|
}
|
|
198
|
+
await this.augmentCookieFromHeader(response.headers);
|
|
199
|
+
return true;
|
|
114
200
|
} catch (err: any) {
|
|
115
201
|
this.log?.error(err);
|
|
116
202
|
this.log?.info('Connection to google maps failure.');
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Start a puppeteer browser instance and return a new page. Sets up user agent and hides automation flag.
|
|
209
|
+
*
|
|
210
|
+
* @returns puppeteer page or undefined if browser could not be started
|
|
211
|
+
*/
|
|
212
|
+
private async startBrowser(): Promise<Page | undefined> {
|
|
213
|
+
if (this.browser) {
|
|
214
|
+
this.log.info('Seems we are already trying to log in. Aborting new login attempt.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.log.debug('Starting browser.');
|
|
218
|
+
this.browser = await puppeteer.launch({
|
|
219
|
+
headless: true,
|
|
220
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled'],
|
|
221
|
+
ignoreDefaultArgs: ['--enable-automation'], //h// ide automation flag, did not help.
|
|
222
|
+
userDataDir: this.dataDir,
|
|
223
|
+
});
|
|
224
|
+
this.log.debug('browser started, opening new page.');
|
|
225
|
+
const page = await this.browser.newPage();
|
|
226
|
+
//hide puppeteer automation flag
|
|
227
|
+
await page.evaluateOnNewDocument(() => {
|
|
228
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
|
229
|
+
});
|
|
230
|
+
await page.setUserAgent({
|
|
231
|
+
userAgent:
|
|
232
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return page;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Request location Data from Google Maps
|
|
240
|
+
*
|
|
241
|
+
* @returns Array of location data or undefined if request failed
|
|
242
|
+
*/
|
|
243
|
+
async sendRequest(): Promise<Array<any> | undefined> {
|
|
244
|
+
if (!this.isValid()) {
|
|
245
|
+
this.log.error('Cannot send request, no cookies available!');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
//send request with current cookies
|
|
250
|
+
this.log.debug('Sending request with current cookies');
|
|
251
|
+
const options = {
|
|
252
|
+
method: 'GET',
|
|
253
|
+
url: 'https://www.google.com/maps/rpc/locationsharing/read',
|
|
254
|
+
headers: {
|
|
255
|
+
Cookie: this.cookies.map(c => `${c.name}=${c.value}`).join('; '),
|
|
256
|
+
},
|
|
257
|
+
params: {
|
|
258
|
+
authuser: 2,
|
|
259
|
+
hl: 'en',
|
|
260
|
+
gl: 'us',
|
|
261
|
+
//pb is place on map. Is irrelevant, set to google head quarters here.
|
|
262
|
+
pb: '!1m7!8m6!1m3!1i14!2i8413!3i5385!2i6!3x4095!2m3!1e0!2sm!3i407105169!3m7!2sen!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e1!5m4!1e4!8m2!1e0!1e1!6m9!1e12!2i2!26m1!4b1!30m1!1f1.3953487873077393!39b1!44e1!50e0!23i4111425',
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const response = await axios.request(options);
|
|
268
|
+
this.log.debug(`Request successful, response code: ${response.status}`);
|
|
269
|
+
const data = response.data.split('\n').slice(1).join('\n');
|
|
270
|
+
const locationData = JSON.parse(data);
|
|
271
|
+
const locations = locationData[0];
|
|
272
|
+
if (locations && locations.length > 0) {
|
|
273
|
+
await this.augmentCookieFromHeader(response.headers);
|
|
274
|
+
return locations;
|
|
275
|
+
}
|
|
276
|
+
this.log.info('No shared locations found in the response, probably not logged in.');
|
|
277
|
+
} catch (e) {
|
|
278
|
+
this.log.error(`Error during request: ${(e as Error).message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get cookies from the given page and store them. Also closes Browser.
|
|
284
|
+
*
|
|
285
|
+
* @param page - puppeteer page
|
|
286
|
+
*/
|
|
287
|
+
private async getCookiesFromPage(page: Page): Promise<void> {
|
|
288
|
+
//using deprecated function, but browser.cookies just does not work...???
|
|
289
|
+
const cookies = await page.cookies();
|
|
290
|
+
const browserCookies = await this.browser!.cookies();
|
|
291
|
+
this.log.debug(`Got ${cookies.length} cookies from page, ${browserCookies.length} from browser.cookies().`);
|
|
292
|
+
|
|
293
|
+
this.cookies = cookies.filter(c => c.domain.includes('google')); //only keep google cookies, maybe some other cookies are set during login which we do not want to store.
|
|
294
|
+
await this.browser!.close();
|
|
295
|
+
if (this.isValid()) {
|
|
296
|
+
this.log.warn('Cookie string seems too short, login probably failed!');
|
|
297
|
+
} else {
|
|
298
|
+
this.log.info(`Obtained new cookies from Google login with length ${this.cookies.length}.`);
|
|
299
|
+
this.cookies = cookies;
|
|
300
|
+
await this.storeCookie();
|
|
301
|
+
}
|
|
302
|
+
this.browser = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Refresh the current cookie by using puppeteer to load Google Maps with existing cookie.
|
|
307
|
+
*
|
|
308
|
+
* @returns true if refresh was successful
|
|
309
|
+
*/
|
|
310
|
+
async refreshCookieWithBrowser(): Promise<boolean> {
|
|
311
|
+
if (this.browser) {
|
|
312
|
+
this.log.info('Seems we are already trying to log in. Aborting new login attempt.');
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.log.debug('Trying to refresh cookie by loading Google Maps with existing cookie in Browser.');
|
|
317
|
+
const page = await this.startBrowser();
|
|
318
|
+
if (!page) {
|
|
319
|
+
this.log.error('Could not start browser for cookie refresh.');
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const cookieArray = [...this.cookies];
|
|
324
|
+
// somehow we stored wrong cookies... :-/ Try to clean up here.
|
|
325
|
+
while (cookieArray.length > 0) {
|
|
326
|
+
try {
|
|
327
|
+
//await page.setCookie(...cookieArray);
|
|
328
|
+
await this.browser!.setCookie(...cookieArray);
|
|
329
|
+
break;
|
|
330
|
+
} catch (e) {
|
|
331
|
+
this.log.error(`Error setting cookies in browser: ${(e as Error).message}, trying again...`);
|
|
332
|
+
const cookie = cookieArray.pop(); //remove last cookie and try again, maybe some cookies are not valid for puppeteer or something.
|
|
333
|
+
console.log('Removed cookie:', cookie);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
await page.goto('https://www.google.com/maps', { waitUntil: 'networkidle2', timeout: 60000 });
|
|
339
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
340
|
+
|
|
341
|
+
await this.sendRequest();
|
|
342
|
+
await this.getCookiesFromPage(page);
|
|
343
|
+
return true;
|
|
344
|
+
} catch (e) {
|
|
345
|
+
this.log.error(
|
|
346
|
+
`Error during cookie refresh: ${(e as Error).message}, ${e instanceof Error ? e.stack : ''}`,
|
|
347
|
+
);
|
|
348
|
+
return false;
|
|
117
349
|
}
|
|
118
350
|
}
|
|
119
351
|
|
|
@@ -122,6 +354,15 @@ export class Cookie {
|
|
|
122
354
|
*/
|
|
123
355
|
async loginToGetNewCookies(): Promise<boolean> {
|
|
124
356
|
try {
|
|
357
|
+
if (this.isValid()) {
|
|
358
|
+
this.log.info('Current cookie seems valid, trying refresh.');
|
|
359
|
+
await this.refreshCookieWithBrowser();
|
|
360
|
+
if (this.isValid()) {
|
|
361
|
+
this.log.info('Cookie refresh successful, no need to login again.');
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
125
366
|
if (this.browser) {
|
|
126
367
|
this.log.info('Seems we are already trying to log in. Aborting new login attempt.');
|
|
127
368
|
return false;
|
|
@@ -133,23 +374,11 @@ export class Cookie {
|
|
|
133
374
|
|
|
134
375
|
this.log.info('Trying to login to Google to get new cookies.');
|
|
135
376
|
//testing puppeteer:
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
this.log.debug('browser started, opening new page.');
|
|
143
|
-
const page = await this.browser.newPage();
|
|
144
|
-
|
|
145
|
-
//hide puppeteer automation flag
|
|
146
|
-
await page.evaluateOnNewDocument(() => {
|
|
147
|
-
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
|
148
|
-
});
|
|
149
|
-
await page.setUserAgent({
|
|
150
|
-
userAgent:
|
|
151
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
152
|
-
});
|
|
377
|
+
const page = await this.startBrowser();
|
|
378
|
+
if (!page) {
|
|
379
|
+
this.log.error('Could not start browser for login.');
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
153
382
|
|
|
154
383
|
this.log.debug('going to google login page.');
|
|
155
384
|
await page.goto(
|
|
@@ -177,23 +406,7 @@ export class Cookie {
|
|
|
177
406
|
|
|
178
407
|
await page.goto('https://www.google.com/maps');
|
|
179
408
|
this.log.debug('getting cookies.');
|
|
180
|
-
|
|
181
|
-
const cookies = await page.cookies();
|
|
182
|
-
|
|
183
|
-
this.currentCookie = cookies
|
|
184
|
-
.filter(c => c.domain.includes('google'))
|
|
185
|
-
.map(c => `${c.name}=${c.value}`)
|
|
186
|
-
.join('; ');
|
|
187
|
-
//this.log.debug(this._cookies);
|
|
188
|
-
//console.log(this._cookies);
|
|
189
|
-
await this.browser.close();
|
|
190
|
-
if (this.currentCookie.length < 50) {
|
|
191
|
-
this.log.warn('Cookie string seems too short, login probably failed!');
|
|
192
|
-
} else {
|
|
193
|
-
this.log.info('Obtained new cookies from Google login.');
|
|
194
|
-
await this.storeCookie();
|
|
195
|
-
}
|
|
196
|
-
this.browser = null;
|
|
409
|
+
await this.getCookiesFromPage(page);
|
|
197
410
|
return true;
|
|
198
411
|
} catch (e) {
|
|
199
412
|
this.log.error(`Error in puppeteer: ${(e as Error).message}`);
|
|
@@ -224,6 +437,8 @@ export class Cookie {
|
|
|
224
437
|
* Check if the current cookie is valid.
|
|
225
438
|
*/
|
|
226
439
|
isValid(): boolean {
|
|
227
|
-
|
|
440
|
+
// we can not really check if cookie is valid without sending a request, but if the cookie string is very short, it is probably not valid.
|
|
441
|
+
// maybe change that to some check against the array length in future?
|
|
442
|
+
return this.cookies.map(c => `${c.name}=${c.value}`).join('; ').length > 50;
|
|
228
443
|
}
|
|
229
444
|
}
|
package/src/main.ts
CHANGED
|
@@ -9,7 +9,6 @@ import * as utils from '@iobroker/adapter-core';
|
|
|
9
9
|
import { User } from './lib/User';
|
|
10
10
|
import { Fence } from './lib/Fence';
|
|
11
11
|
import { Cookie } from './lib/Cookie';
|
|
12
|
-
import axios from 'axios';
|
|
13
12
|
|
|
14
13
|
//used to test timeout against
|
|
15
14
|
const MAX_INT32 = 2 ** 31 - 1; // 2147483647 (hex 0x7FFFFFFF)
|
|
@@ -43,7 +42,7 @@ export class GoogleSharedlocations2 extends utils.Adapter {
|
|
|
43
42
|
// this.on('objectChange', this.onObjectChange.bind(this));
|
|
44
43
|
this.on('message', this.onMessage.bind(this));
|
|
45
44
|
this.on('unload', this.onUnload.bind(this));
|
|
46
|
-
this.cookie = new Cookie(this);
|
|
45
|
+
this.cookie = new Cookie(this, utils.getAbsoluteInstanceDataDir(this));
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
/**
|
|
@@ -55,7 +54,7 @@ export class GoogleSharedlocations2 extends utils.Adapter {
|
|
|
55
54
|
// Reset the connection indicator during startup
|
|
56
55
|
await this.setState('info.connection', false, true);
|
|
57
56
|
await this.cookie.init();
|
|
58
|
-
await this.subscribeStatesAsync('info
|
|
57
|
+
await this.subscribeStatesAsync('info.*');
|
|
59
58
|
|
|
60
59
|
//sanitize polling interval:
|
|
61
60
|
this._pollInterval = this.config.pollInterval;
|
|
@@ -137,62 +136,31 @@ export class GoogleSharedlocations2 extends utils.Adapter {
|
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
private async sendRequest(): Promise<void> {
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const results = await this.cookie.sendRequest();
|
|
140
|
+
if (!results) {
|
|
142
141
|
await this.setState('info.connection', false, true);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
//send request with current cookies
|
|
147
|
-
this.log.debug('Sending request with current cookies');
|
|
148
|
-
const options = {
|
|
149
|
-
method: 'GET',
|
|
150
|
-
url: 'https://www.google.com/maps/rpc/locationsharing/read',
|
|
151
|
-
headers: {
|
|
152
|
-
Cookie: this.cookie.currentCookie,
|
|
153
|
-
},
|
|
154
|
-
params: {
|
|
155
|
-
authuser: 2,
|
|
156
|
-
hl: 'en',
|
|
157
|
-
gl: 'us',
|
|
158
|
-
//pb is place on map. Is irrelevant, set to google head quarters here.
|
|
159
|
-
pb: '!1m7!8m6!1m3!1i14!2i8413!3i5385!2i6!3x4095!2m3!1e0!2sm!3i407105169!3m7!2sen!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e1!5m4!1e4!8m2!1e0!1e1!6m9!1e12!2i2!26m1!4b1!30m1!1f1.3953487873077393!39b1!44e1!50e0!23i4111425',
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const response = await axios.request(options);
|
|
165
|
-
this.log.debug(`Request successful, response code: ${response.status}`);
|
|
166
|
-
const data = response.data.split('\n').slice(1).join('\n');
|
|
167
|
-
const locationData = JSON.parse(data);
|
|
168
|
-
const locations = locationData[0];
|
|
169
|
-
if (locations && locations.length > 0) {
|
|
170
|
-
this._successFullPolls += 1;
|
|
171
|
-
await this.setState('info.connection', true, true);
|
|
172
|
-
for (const location of locations) {
|
|
173
|
-
const user = new User(location);
|
|
174
|
-
if (user.id) {
|
|
175
|
-
this._users[user.id] = user;
|
|
176
|
-
await this.fillIntoObjects(user);
|
|
177
|
-
await this.notifyPlaces(user);
|
|
178
|
-
await this.checkFences(user);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
this.log.info('No shared locations found in the response, probably not logged in.');
|
|
183
|
-
if (this._successFullPolls > 0) {
|
|
184
|
-
//try to get new cookie:
|
|
185
|
-
this._successFullPolls = 0;
|
|
186
|
-
await this.cookie.loginToGetNewCookies();
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
} catch (e) {
|
|
190
|
-
this.log.error(`Error during request: ${(e as Error).message}`);
|
|
191
142
|
if (this._successFullPolls > 0) {
|
|
192
143
|
//try to get new cookie:
|
|
193
144
|
this._successFullPolls = 0;
|
|
194
145
|
await this.cookie.loginToGetNewCookies();
|
|
195
146
|
}
|
|
147
|
+
} else {
|
|
148
|
+
this._successFullPolls += 1;
|
|
149
|
+
await this.setState('info.connection', true, true);
|
|
150
|
+
for (const location of results) {
|
|
151
|
+
const user = new User(location);
|
|
152
|
+
if (user.id) {
|
|
153
|
+
const oldTS = this._users[user.id]?.timestamp || 0;
|
|
154
|
+
this._users[user.id] = user; // should I try to merge stuff here? Or is it always completely filled?
|
|
155
|
+
if (user.timestamp && user.timestamp <= oldTS) {
|
|
156
|
+
this.log.debug(`Ignoring older or same location data for user ${user.id}`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
await this.fillIntoObjects(user);
|
|
160
|
+
await this.notifyPlaces(user);
|
|
161
|
+
await this.checkFences(user);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
196
164
|
}
|
|
197
165
|
}
|
|
198
166
|
|
|
@@ -396,16 +364,24 @@ export class GoogleSharedlocations2 extends utils.Adapter {
|
|
|
396
364
|
if (state.val === '') {
|
|
397
365
|
this.log.info('Current cookies state was cleared, trying to obtain new cookies.');
|
|
398
366
|
this._successFullPolls = 0;
|
|
367
|
+
this.cookie.readCookieFromString(''); //clear old cookie
|
|
399
368
|
await this.cookie.loginToGetNewCookies();
|
|
400
|
-
if (this.cookie.isValid()) {
|
|
401
|
-
await this.sendRequest();
|
|
402
|
-
}
|
|
403
369
|
} else {
|
|
404
370
|
this.log.info(
|
|
405
371
|
'Current cookies state was changed from outside the adapter, updating internal cookie store.',
|
|
406
372
|
);
|
|
407
|
-
this.cookie.
|
|
373
|
+
this.cookie.readCookieFromString(state.val as string);
|
|
374
|
+
}
|
|
375
|
+
if (this.cookie.isValid()) {
|
|
376
|
+
await this.sendRequest();
|
|
377
|
+
}
|
|
378
|
+
} else if (id.endsWith('info.forceRefreshWithBrowser') && state && !state.ack) {
|
|
379
|
+
this.log.info('Force refresh in browser state was triggered, trying to obtain new cookies.');
|
|
380
|
+
await this.cookie.refreshCookieWithBrowser();
|
|
381
|
+
if (this.cookie.isValid()) {
|
|
382
|
+
await this.sendRequest();
|
|
408
383
|
}
|
|
384
|
+
await this.setState('info.forceRefreshWithBrowser', false, true);
|
|
409
385
|
}
|
|
410
386
|
}
|
|
411
387
|
|