diff --git a/.app_version b/.app_version index 4b9fcbec..cb0c939a 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.5.1 +0.5.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index bc501f1c..086fb527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.5.2] — 2024-06-08 + +### Added + +- Test version of google takeout importing service for exports from users' phones + +--- + ## [0.5.1] — 2024-06-07 ### Added diff --git a/app/jobs/import_job.rb b/app/jobs/import_job.rb index 7c1a9282..73f296c6 100644 --- a/app/jobs/import_job.rb +++ b/app/jobs/import_job.rb @@ -22,6 +22,7 @@ class ImportJob < ApplicationJob case source when 'google_semantic_history' then GoogleMaps::SemanticHistoryParser when 'google_records' then GoogleMaps::RecordsParser + when 'google_phone_takeout' then GoogleMaps::PhoneTakeoutParser when 'owntracks' then OwnTracks::ExportParser end end diff --git a/app/models/import.rb b/app/models/import.rb index dcbf8b2a..543683cd 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -8,5 +8,5 @@ class Import < ApplicationRecord include ImportUploader::Attachment(:raw) - enum source: { google_semantic_history: 0, owntracks: 1, google_records: 2 } + enum source: { google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3 } end diff --git a/app/services/google_maps/phone_takeout_parser.rb b/app/services/google_maps/phone_takeout_parser.rb new file mode 100644 index 00000000..a16737e7 --- /dev/null +++ b/app/services/google_maps/phone_takeout_parser.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class GoogleMaps::PhoneTakeoutParser + attr_reader :import, :user_id + + def initialize(import, user_id) + @import = import + @user_id = user_id + end + + def call + points_data = parse_json + + points = 0 + + points_data.each do |point_data| + next if Point.exists?(timestamp: point_data[:timestamp]) + + Point.create( + latitude: point_data[:latitude], + longitude: point_data[:longitude], + timestamp: point_data[:timestamp], + raw_data: point_data[:raw_data], + topic: 'Google Maps Phone Timeline Export', + tracker_id: 'google-maps-phone-timeline-export', + import_id: import.id, + user_id: + ) + + points += 1 + end + + doubles = points_data.size - points + processed = points + doubles + + { raw_points: points_data.size, points:, doubles:, processed: } + end + + private + + def parse_json + import.raw_data['semanticSegments'].flat_map do |segment| + if segment.key?('timelinePath') + segment['timelinePath'].map do |point| + lat, lon = parse_coordinates(point['point']) + timestamp = DateTime.parse(point['time']).to_i + + point_hash(lat, lon, timestamp, segment) + end + elsif segment.key?('visit') + lat, lon = parse_coordinates(segment['visit']['topCandidate']['placeLocation']['latLng']) + timestamp = DateTime.parse(segment['startTime']).to_i + + point_hash(lat, lon, timestamp, segment) + end + end + end + + def parse_coordinates(coordinates) + coordinates.split(', ').map { _1.chomp('°') } + end + + def point_hash(lat, lon, timestamp, raw_data) + { + latitude: lat.to_f, + longitude: lon.to_f, + timestamp:, + raw_data: + } + end +end diff --git a/spec/fixtures/files/google/phone-takeout.json b/spec/fixtures/files/google/phone-takeout.json new file mode 100644 index 00000000..7beb1b7c --- /dev/null +++ b/spec/fixtures/files/google/phone-takeout.json @@ -0,0 +1,106 @@ +{ + "semanticSegments": [ + { + "startTime": "2019-04-03T08:00:00.000+02:00", + "endTime": "2019-04-03T10:00:00.000+02:00", + "timelinePath": [ + { + "point": "50.0506312°, 14.3439906°", + "time": "2019-04-03T08:14:00.000+02:00" + }, + { + "point": "50.0506312°, 14.3439906°", + "time": "2019-04-03T08:46:00.000+02:00" + } + ] + }, + { + "startTime": "2019-04-03T08:13:57.000+02:00", + "endTime": "2019-04-03T20:10:18.000+02:00", + "startTimeTimezoneUtcOffsetMinutes": 120, + "endTimeTimezoneUtcOffsetMinutes": 120, + "visit": { + "hierarchyLevel": 0, + "probability": 0.8500000238418579, + "topCandidate": { + "placeId": "some random id", + "semanticType": "UNKNOWN", + "probability": 0.44970497488975525, + "placeLocation": { + "latLng": "50.0506312°, 14.3439906°" + } + } + } + } + ], + "rawSignals": [ + { + "activityRecord": { + "probableActivities": [ + { + "type": "STILL", + "confidence": 0.9599999785423279 + }, + { + "type": "IN_VEHICLE", + "confidence": 0.009999999776482582 + }, + { + "type": "ON_FOOT", + "confidence": 0.009999999776482582 + }, + { + "type": "WALKING", + "confidence": 0.009999999776482582 + }, + { + "type": "UNKNOWN", + "confidence": 0.009999999776482582 + }, + { + "type": "IN_ROAD_VEHICLE", + "confidence": 0.009999999776482582 + }, + { + "type": "IN_RAIL_VEHICLE", + "confidence": 0.009999999776482582 + }, + { + "type": "IN_ROAD_VEHICLE", + "confidence": 0.009999999776482582 + } + ], + "timestamp": "2024-04-26T20:54:38.000+02:00" + } + }, + { + "activityRecord": { + "probableActivities": [ + { + "type": "STILL", + "confidence": 0.9900000095367432 + }, + { + "type": "UNKNOWN", + "confidence": 0.009999999776482582 + } + ], + "timestamp": "2024-04-26T20:55:45.000+02:00" + } + } + ], + "userLocationProfile": { + "frequentPlaces": [ + { + "placeId": "some random id", + "placeLocation": "50.0506312°, 14.3439906°", + "label": "WORK" + }, + { + "placeId": "some random id", + "placeLocation": "50.0506312°, 14.3439906°", + "label": "HOME" + } + ] + } +} diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index a7f5b482..f016f8a1 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -5,4 +5,8 @@ RSpec.describe Import, type: :model do it { is_expected.to have_many(:points).dependent(:destroy) } it { is_expected.to belong_to(:user) } end + + describe 'enums' do + it { is_expected.to define_enum_for(:source).with_values(google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3) } + end end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index d7ba37fa..d68bef39 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -180,7 +180,7 @@ paths: lat: 52.502397 lon: 13.356718 tid: Swagger - tst: 1717786543 + tst: 1717877268 servers: - url: http://{defaultHost} variables: