mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-08 00:53:53 -05:00
Add latest location and avatar to owntracks friends (#619)
This commit is contained in:
@@ -19,19 +19,19 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GpxSender {
|
public class GpxSender {
|
||||||
|
|
||||||
private static class TrackPoint {
|
private static class TrackPoint {
|
||||||
public final double latitude;
|
public final double latitude;
|
||||||
public final double longitude;
|
public final double longitude;
|
||||||
public final Instant timestamp;
|
public final Instant timestamp;
|
||||||
|
|
||||||
public TrackPoint(double latitude, double longitude, Instant timestamp) {
|
public TrackPoint(double latitude, double longitude, Instant timestamp) {
|
||||||
this.latitude = latitude;
|
this.latitude = latitude;
|
||||||
this.longitude = longitude;
|
this.longitude = longitude;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class OwntracksMessage {
|
private static class OwntracksMessage {
|
||||||
public String _type = "location";
|
public String _type = "location";
|
||||||
public double acc;
|
public double acc;
|
||||||
@@ -48,7 +48,7 @@ public class GpxSender {
|
|||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
System.err.println("Usage: java -jar gpx-sender.jar <gpx-file> --url <reitti-url> --token <api-token> [--interval <seconds>] [--original-time]");
|
System.err.println("Usage: java -jar gpx-sender.jar <gpx-file> --url <reitti-url> --token <api-token> [--interval <seconds>] [--original-time] [--verbose]");
|
||||||
System.err.println("Example: java -jar gpx-sender.jar track.gpx --url http://localhost:8080 --token your-api-token --interval 15");
|
System.err.println("Example: java -jar gpx-sender.jar track.gpx --url http://localhost:8080 --token your-api-token --interval 15");
|
||||||
System.err.println(" java -jar gpx-sender.jar track.gpx --url http://localhost:8080 --token your-api-token --original-time");
|
System.err.println(" java -jar gpx-sender.jar track.gpx --url http://localhost:8080 --token your-api-token --original-time");
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
@@ -59,6 +59,7 @@ public class GpxSender {
|
|||||||
String apiToken = null;
|
String apiToken = null;
|
||||||
double intervalSeconds = 15.0;
|
double intervalSeconds = 15.0;
|
||||||
boolean useOriginalTime = false;
|
boolean useOriginalTime = false;
|
||||||
|
boolean verboseOutput = false;
|
||||||
|
|
||||||
// Parse named parameters
|
// Parse named parameters
|
||||||
for (int i = 1; i < args.length; i++) {
|
for (int i = 1; i < args.length; i++) {
|
||||||
@@ -82,6 +83,10 @@ public class GpxSender {
|
|||||||
case "-ot":
|
case "-ot":
|
||||||
useOriginalTime = true;
|
useOriginalTime = true;
|
||||||
break;
|
break;
|
||||||
|
case "--verbose":
|
||||||
|
case "-v":
|
||||||
|
verboseOutput = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
System.err.println("Unknown parameter: " + args[i]);
|
System.err.println("Unknown parameter: " + args[i]);
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
@@ -107,8 +112,11 @@ public class GpxSender {
|
|||||||
} else {
|
} else {
|
||||||
System.out.println("Interval: " + intervalSeconds + " seconds");
|
System.out.println("Interval: " + intervalSeconds + " seconds");
|
||||||
}
|
}
|
||||||
|
if (verboseOutput) {
|
||||||
|
System.out.println("Verbose output enabled");
|
||||||
|
}
|
||||||
|
|
||||||
sendTrackPoints(trackPoints, reittiUrl, apiToken, intervalSeconds, useOriginalTime);
|
sendTrackPoints(trackPoints, reittiUrl, apiToken, intervalSeconds, useOriginalTime, verboseOutput);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("Error: " + e.getMessage());
|
System.err.println("Error: " + e.getMessage());
|
||||||
@@ -119,44 +127,44 @@ public class GpxSender {
|
|||||||
|
|
||||||
private static List<TrackPoint> parseGpxFile(String gpxFile) throws Exception {
|
private static List<TrackPoint> parseGpxFile(String gpxFile) throws Exception {
|
||||||
List<TrackPoint> trackPoints = new ArrayList<>();
|
List<TrackPoint> trackPoints = new ArrayList<>();
|
||||||
|
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
Document document = builder.parse(new File(gpxFile));
|
Document document = builder.parse(new File(gpxFile));
|
||||||
|
|
||||||
NodeList trkptNodes = document.getElementsByTagName("trkpt");
|
NodeList trkptNodes = document.getElementsByTagName("trkpt");
|
||||||
|
|
||||||
for (int i = 0; i < trkptNodes.getLength(); i++) {
|
for (int i = 0; i < trkptNodes.getLength(); i++) {
|
||||||
Element trkpt = (Element) trkptNodes.item(i);
|
Element trkpt = (Element) trkptNodes.item(i);
|
||||||
|
|
||||||
double lat = Double.parseDouble(trkpt.getAttribute("lat"));
|
double lat = Double.parseDouble(trkpt.getAttribute("lat"));
|
||||||
double lon = Double.parseDouble(trkpt.getAttribute("lon"));
|
double lon = Double.parseDouble(trkpt.getAttribute("lon"));
|
||||||
|
|
||||||
NodeList timeNodes = trkpt.getElementsByTagName("time");
|
NodeList timeNodes = trkpt.getElementsByTagName("time");
|
||||||
Instant timestamp = null;
|
Instant timestamp = null;
|
||||||
if (timeNodes.getLength() > 0) {
|
if (timeNodes.getLength() > 0) {
|
||||||
String timeStr = timeNodes.item(0).getTextContent();
|
String timeStr = timeNodes.item(0).getTextContent();
|
||||||
timestamp = Instant.parse(timeStr);
|
timestamp = Instant.parse(timeStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackPoints.add(new TrackPoint(lat, lon, timestamp));
|
trackPoints.add(new TrackPoint(lat, lon, timestamp));
|
||||||
}
|
}
|
||||||
|
|
||||||
return trackPoints;
|
return trackPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sendTrackPoints(List<TrackPoint> trackPoints, String reittiUrl, String apiToken, double intervalSeconds, boolean useOriginalTime) throws Exception {
|
private static void sendTrackPoints(List<TrackPoint> trackPoints, String reittiUrl, String apiToken, double intervalSeconds, boolean useOriginalTime, boolean verboseOutput) throws Exception {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
objectMapper.registerModule(new JavaTimeModule());
|
||||||
|
|
||||||
// Start from current time and send points with their original intervals
|
// Start from current time and send points with their original intervals
|
||||||
Instant startTime = Instant.now();
|
Instant startTime = Instant.now();
|
||||||
|
|
||||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||||
|
|
||||||
for (int i = 0; i < trackPoints.size(); i++) {
|
for (int i = 0; i < trackPoints.size(); i++) {
|
||||||
TrackPoint point = trackPoints.get(i);
|
TrackPoint point = trackPoints.get(i);
|
||||||
|
|
||||||
// Calculate timestamp based on mode
|
// Calculate timestamp based on mode
|
||||||
Instant adjustedTime;
|
Instant adjustedTime;
|
||||||
if (useOriginalTime) {
|
if (useOriginalTime) {
|
||||||
@@ -170,14 +178,13 @@ public class GpxSender {
|
|||||||
} else if (trackPoints.get(i - 1).timestamp != null && point.timestamp != null) {
|
} else if (trackPoints.get(i - 1).timestamp != null && point.timestamp != null) {
|
||||||
// Calculate time difference from previous point and add to previous adjusted time
|
// Calculate time difference from previous point and add to previous adjusted time
|
||||||
TrackPoint prevPoint = trackPoints.get(i-1);
|
TrackPoint prevPoint = trackPoints.get(i-1);
|
||||||
long durationFromPrev = point.timestamp.getEpochSecond() - prevPoint.timestamp.getEpochSecond();
|
|
||||||
adjustedTime = startTime.plusMillis((long) (i * intervalSeconds * 1000));
|
adjustedTime = startTime.plusMillis((long) (i * intervalSeconds * 1000));
|
||||||
} else {
|
} else {
|
||||||
// Fallback: distribute points evenly from start time
|
// Fallback: distribute points evenly from start time
|
||||||
adjustedTime = startTime.plusMillis((long) (i * intervalSeconds * 1000));
|
adjustedTime = startTime.plusMillis((long) (i * intervalSeconds * 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Owntracks message
|
// Create Owntracks message
|
||||||
OwntracksMessage message = new OwntracksMessage(
|
OwntracksMessage message = new OwntracksMessage(
|
||||||
point.latitude,
|
point.latitude,
|
||||||
@@ -185,41 +192,69 @@ public class GpxSender {
|
|||||||
adjustedTime.getEpochSecond(),
|
adjustedTime.getEpochSecond(),
|
||||||
10.0
|
10.0
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send HTTP request
|
// Send HTTP request
|
||||||
String url = reittiUrl + "/api/v1/ingest/owntracks";
|
String url = reittiUrl + "/api/v1/ingest/owntracks";
|
||||||
HttpPost post = new HttpPost(url);
|
HttpPost post = new HttpPost(url);
|
||||||
post.setHeader("Authorization", "Bearer " + apiToken);
|
post.setHeader("Authorization", "Bearer " + apiToken);
|
||||||
post.setHeader("Content-Type", "application/json");
|
post.setHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
String json = objectMapper.writeValueAsString(message);
|
String json = objectMapper.writeValueAsString(message);
|
||||||
post.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
|
post.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
|
||||||
|
|
||||||
System.out.printf("Sending point %d/%d: lat=%.6f, lon=%.6f, time=%s%n",
|
System.out.printf("Sending point %d/%d: lat=%.6f, lon=%.6f, time=%s%n",
|
||||||
i + 1, trackPoints.size(), point.latitude, point.longitude,
|
i + 1, trackPoints.size(), point.latitude, point.longitude, adjustedTime);
|
||||||
adjustedTime.toString());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
httpClient.execute(post, response -> {
|
httpClient.execute(post, response -> {
|
||||||
int statusCode = response.getCode();
|
int statusCode = response.getCode();
|
||||||
if (statusCode >= 200 && statusCode < 300) {
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
System.out.println("✓ Sent successfully");
|
System.out.println("✓ Sent successfully");
|
||||||
|
// Try to read and pretty-print the response body if it exists and verbose is enabled
|
||||||
|
if (verboseOutput) {
|
||||||
|
try {
|
||||||
|
String responseBody = new String(response.getEntity().getContent().readAllBytes());
|
||||||
|
if (!responseBody.isEmpty()) {
|
||||||
|
Object jsonResponse = objectMapper.readValue(responseBody, Object.class);
|
||||||
|
String prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonResponse);
|
||||||
|
System.out.println("Response:");
|
||||||
|
System.out.println(prettyJson);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If we can't parse as JSON, just print the raw response
|
||||||
|
try {
|
||||||
|
String rawResponse = new String(response.getEntity().getContent().readAllBytes());
|
||||||
|
if (!rawResponse.isEmpty()) {
|
||||||
|
System.out.println("Response: " + rawResponse);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Ignore if we can't read the response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
System.err.println("✗ Failed with status: " + statusCode);
|
System.err.println("✗ Failed with status: " + statusCode);
|
||||||
|
// Print error response if available
|
||||||
|
try {
|
||||||
|
String errorBody = new String(response.getEntity().getContent().readAllBytes());
|
||||||
|
System.err.println("Error response: " + errorBody);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore if we can't read the error body
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("✗ Error sending point: " + e.getMessage());
|
System.err.println("✗ Error sending point: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait before sending next point (except for the last one)
|
// Wait before sending next point (except for the last one)
|
||||||
if (i < trackPoints.size() - 1) {
|
if (i < trackPoints.size() - 1) {
|
||||||
Thread.sleep((long) (intervalSeconds * 1000));
|
Thread.sleep((long) (intervalSeconds * 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("Finished sending all track points");
|
System.out.println("Finished sending all track points");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package com.dedicatedcode.reitti.controller;
|
|||||||
|
|
||||||
import com.dedicatedcode.reitti.model.security.User;
|
import com.dedicatedcode.reitti.model.security.User;
|
||||||
import com.dedicatedcode.reitti.service.integration.ReittiIntegrationService;
|
import com.dedicatedcode.reitti.service.integration.ReittiIntegrationService;
|
||||||
import org.springframework.dao.EmptyResultDataAccessException;
|
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -15,36 +13,21 @@ import java.util.concurrent.TimeUnit;
|
|||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/reitti-integration")
|
@RequestMapping("/reitti-integration")
|
||||||
public class ReittiIntegrationController {
|
public class ReittiIntegrationController {
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final ReittiIntegrationService reittiIntegrationService;
|
private final ReittiIntegrationService reittiIntegrationService;
|
||||||
|
|
||||||
public ReittiIntegrationController(JdbcTemplate jdbcTemplate, ReittiIntegrationService reittiIntegrationService) {
|
public ReittiIntegrationController(ReittiIntegrationService reittiIntegrationService) {
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
|
||||||
this.reittiIntegrationService = reittiIntegrationService;
|
this.reittiIntegrationService = reittiIntegrationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/avatar/{integrationId}")
|
@GetMapping("/avatar/{integrationId}")
|
||||||
public ResponseEntity<byte[]> getAvatar(@PathVariable Long integrationId) {
|
public ResponseEntity<byte[]> getAvatar(@AuthenticationPrincipal User user, @PathVariable Long integrationId) {
|
||||||
|
return this.reittiIntegrationService.getAvatar(user, integrationId).map(avatarData -> {
|
||||||
Map<String, Object> result;
|
HttpHeaders headers = new HttpHeaders();
|
||||||
try {
|
headers.setContentType(MediaType.parseMediaType(avatarData.mimeType()));
|
||||||
result = jdbcTemplate.queryForMap(
|
headers.setContentLength(avatarData.imageData().length);
|
||||||
"SELECT mime_type, binary_data FROM remote_user_info WHERE integration_id = ?",
|
headers.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS));
|
||||||
integrationId
|
return new ResponseEntity<>(avatarData.imageData(), headers, HttpStatus.OK);
|
||||||
);
|
}).orElse(ResponseEntity.notFound().cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)).build());
|
||||||
} catch (EmptyResultDataAccessException ignored) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
String contentType = (String) result.get("mime_type");
|
|
||||||
byte[] imageData = (byte[]) result.get("binary_data");
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.parseMediaType(contentType));
|
|
||||||
headers.setContentLength(imageData.length);
|
|
||||||
headers.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS));
|
|
||||||
|
|
||||||
return new ResponseEntity<>(imageData, headers, HttpStatus.OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -135,8 +137,15 @@ public class OwntracksIngestionApiController {
|
|||||||
try {
|
try {
|
||||||
ReittiRemoteInfo info = reittiIntegrationService.getInfo(integration);
|
ReittiRemoteInfo info = reittiIntegrationService.getInfo(integration);
|
||||||
String tid = generateTid(info.userInfo().username());
|
String tid = generateTid(info.userInfo().username());
|
||||||
friendsData.add(new OwntracksFriendResponse(tid, info.userInfo().displayName(), null, null));
|
|
||||||
} catch (RequestFailedException | RequestTemporaryFailedException e) {
|
OwntracksFriendResponse owntracksFriendResponse = reittiIntegrationService.getAvatar(user, integration.getId())
|
||||||
|
.map(avatarData -> new OwntracksFriendResponse(tid, info.userInfo().displayName(), avatarData.imageData(), avatarData.mimeType()))
|
||||||
|
.orElse(new OwntracksFriendResponse(tid, info.userInfo().displayName(), null, null));
|
||||||
|
|
||||||
|
friendsData.add(owntracksFriendResponse);
|
||||||
|
Optional<LocationPoint> latestLocation = reittiIntegrationService.findLatest(user, integration.getId());
|
||||||
|
latestLocation.ifPresent(location -> friendsData.add(new OwntracksFriendResponse(tid, info.userInfo().displayName(), location.getLatitude(), location.getLongitude(), Instant.parse(location.getTimestamp()).getEpochSecond())));
|
||||||
|
} catch (RequestFailedException | RequestTemporaryFailedException e) {
|
||||||
logger.warn("Couldn't fetch info for integration {}", integration.getId(), e);
|
logger.warn("Couldn't fetch info for integration {}", integration.getId(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.dedicatedcode.reitti.service.integration;
|
|||||||
|
|
||||||
import com.dedicatedcode.reitti.dto.*;
|
import com.dedicatedcode.reitti.dto.*;
|
||||||
import com.dedicatedcode.reitti.model.geo.GeoPoint;
|
import com.dedicatedcode.reitti.model.geo.GeoPoint;
|
||||||
|
import com.dedicatedcode.reitti.model.geo.RawLocationPoint;
|
||||||
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
|
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
|
||||||
import com.dedicatedcode.reitti.model.integration.ReittiIntegration;
|
import com.dedicatedcode.reitti.model.integration.ReittiIntegration;
|
||||||
import com.dedicatedcode.reitti.model.security.RemoteUser;
|
import com.dedicatedcode.reitti.model.security.RemoteUser;
|
||||||
@@ -15,11 +16,14 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.RestClientException;
|
import org.springframework.web.client.RestClientException;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import javax.xml.stream.Location;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
@@ -37,50 +41,24 @@ public class ReittiIntegrationService {
|
|||||||
|
|
||||||
private final String advertiseUri;
|
private final String advertiseUri;
|
||||||
private final ReittiIntegrationJdbcService jdbcService;
|
private final ReittiIntegrationJdbcService jdbcService;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
private final AvatarService avatarService;
|
private final AvatarService avatarService;
|
||||||
private final Map<Long, String> integrationSubscriptions = new ConcurrentHashMap<>();
|
private final Map<Long, String> integrationSubscriptions = new ConcurrentHashMap<>();
|
||||||
private final Map<String, Long> userForSubscriptions = new ConcurrentHashMap<>();
|
private final Map<String, Long> userForSubscriptions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public ReittiIntegrationService(@Value("${reitti.server.advertise-uri}") String advertiseUri, ReittiIntegrationJdbcService jdbcService,
|
public ReittiIntegrationService(@Value("${reitti.server.advertise-uri}") String advertiseUri,
|
||||||
|
ReittiIntegrationJdbcService jdbcService,
|
||||||
|
JdbcTemplate jdbcTemplate,
|
||||||
RestTemplate restTemplate,
|
RestTemplate restTemplate,
|
||||||
AvatarService avatarService) {
|
AvatarService avatarService) {
|
||||||
this.advertiseUri = advertiseUri;
|
this.advertiseUri = advertiseUri;
|
||||||
this.jdbcService = jdbcService;
|
this.jdbcService = jdbcService;
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
this.avatarService = avatarService;
|
this.avatarService = avatarService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<UserTimelineData> getTimelineData(User user, LocalDate selectedDate, ZoneId userTimezone) {
|
|
||||||
return this.jdbcService
|
|
||||||
.findAllByUser(user)
|
|
||||||
.stream().filter(integration -> integration.isEnabled() && VALID_INTEGRATION_STATUS.contains(integration.getStatus()))
|
|
||||||
.map(integration -> {
|
|
||||||
|
|
||||||
log.debug("Fetching user timeline data for [{}]", integration);
|
|
||||||
try {
|
|
||||||
RemoteUser remoteUser = handleRemoteUser(integration);
|
|
||||||
List<TimelineEntry> timelineEntries = loadTimeLineEntries(integration, selectedDate, userTimezone);
|
|
||||||
integration = update(integration.withStatus(ReittiIntegration.Status.ACTIVE).withLastUsed(LocalDateTime.now()));
|
|
||||||
return new UserTimelineData("remote:" + integration.getId(),
|
|
||||||
remoteUser.getDisplayName(),
|
|
||||||
this.avatarService.generateInitials(remoteUser.getDisplayName()),
|
|
||||||
"/reitti-integration/avatar/" + integration.getId(),
|
|
||||||
integration.getColor(),
|
|
||||||
timelineEntries,
|
|
||||||
String.format("/reitti-integration/raw-location-points/%d?date=%s&timezone=%s", integration.getId(), selectedDate, userTimezone),
|
|
||||||
String.format("/reitti-integration/visits/%d?date=%s&timezone=%s", integration.getId(), selectedDate, userTimezone));
|
|
||||||
} catch (RequestFailedException e) {
|
|
||||||
log.error("couldn't fetch user info for [{}]", integration, e);
|
|
||||||
update(integration.withStatus(ReittiIntegration.Status.FAILED).withLastUsed(LocalDateTime.now()).withEnabled(false));
|
|
||||||
} catch (RequestTemporaryFailedException e) {
|
|
||||||
log.warn("couldn't temporarily fetch user info for [{}]", integration, e);
|
|
||||||
update(integration.withStatus(ReittiIntegration.Status.RECOVERABLE).withLastUsed(LocalDateTime.now()));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<UserTimelineData> getTimelineDataRange(User user, LocalDate startDate, LocalDate endDate, ZoneId userTimezone) {
|
public List<UserTimelineData> getTimelineDataRange(User user, LocalDate startDate, LocalDate endDate, ZoneId userTimezone) {
|
||||||
return this.jdbcService
|
return this.jdbcService
|
||||||
.findAllByUser(user)
|
.findAllByUser(user)
|
||||||
@@ -146,6 +124,24 @@ public class ReittiIntegrationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<AvatarService.AvatarData> getAvatar(User user, Long integrationId) {
|
||||||
|
Map<String, Object> result;
|
||||||
|
try {
|
||||||
|
result = jdbcTemplate.queryForMap(
|
||||||
|
"SELECT mime_type, binary_data FROM remote_user_info WHERE integration_id = ?",
|
||||||
|
integrationId
|
||||||
|
);
|
||||||
|
} catch (EmptyResultDataAccessException ignored) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = (String) result.get("mime_type");
|
||||||
|
byte[] imageData = (byte[]) result.get("binary_data");
|
||||||
|
|
||||||
|
return Optional.of(new AvatarService.AvatarData(contentType,imageData, -1));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public ProcessedVisitResponse getVisits(User user, Long integrationId, String startDate, String endDate, Integer zoom, String timezone) {
|
public ProcessedVisitResponse getVisits(User user, Long integrationId, String startDate, String endDate, Integer zoom, String timezone) {
|
||||||
return this.jdbcService
|
return this.jdbcService
|
||||||
.findByIdAndUser(integrationId,user)
|
.findByIdAndUser(integrationId,user)
|
||||||
@@ -192,6 +188,51 @@ public class ReittiIntegrationService {
|
|||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.findFirst().orElse(null);
|
.findFirst().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<LocationPoint> findLatest(User user, Long integrationId) {
|
||||||
|
return this.jdbcService
|
||||||
|
.findByIdAndUser(integrationId, user)
|
||||||
|
.filter(integration -> integration.isEnabled() && VALID_INTEGRATION_STATUS.contains(integration.getStatus()))
|
||||||
|
.map(integration -> {
|
||||||
|
|
||||||
|
log.debug("Fetching latest location ata for [{}]", integration);
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("X-API-TOKEN", integration.getToken());
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
|
String rawLocationDataUrl = integration.getUrl().endsWith("/") ?
|
||||||
|
integration.getUrl() + "api/v1/latest-location" :
|
||||||
|
integration.getUrl() + "/api/v1/latest-location";
|
||||||
|
ResponseEntity<Map> remoteResponse = restTemplate.exchange(
|
||||||
|
rawLocationDataUrl,
|
||||||
|
HttpMethod.GET,
|
||||||
|
entity,
|
||||||
|
Map.class
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remoteResponse.getStatusCode().is2xxSuccessful() && remoteResponse.getStatusCode().is2xxSuccessful() && remoteResponse.getBody() != null && remoteResponse.getBody().containsKey("hasLocation")) {
|
||||||
|
update(integration.withStatus(ReittiIntegration.Status.ACTIVE).withLastUsed(LocalDateTime.now()));
|
||||||
|
if (!remoteResponse.getBody().get("hasLocation").equals(true)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return parseLocationPoint(remoteResponse.getBody().get("point"));
|
||||||
|
}
|
||||||
|
} else if (remoteResponse.getStatusCode().is4xxClientError()) {
|
||||||
|
throw new RequestFailedException(rawLocationDataUrl, remoteResponse.getStatusCode(), remoteResponse.getBody());
|
||||||
|
} else {
|
||||||
|
throw new RequestTemporaryFailedException(rawLocationDataUrl, remoteResponse.getStatusCode(), remoteResponse.getBody());
|
||||||
|
}
|
||||||
|
} catch (RequestFailedException e) {
|
||||||
|
log.error("couldn't fetch user info for [{}]", integration, e);
|
||||||
|
update(integration.withStatus(ReittiIntegration.Status.FAILED).withLastUsed(LocalDateTime.now()).withEnabled(false));
|
||||||
|
} catch (RequestTemporaryFailedException e) {
|
||||||
|
log.warn("couldn't temporarily fetch user info for [{}]", integration, e);
|
||||||
|
update(integration.withStatus(ReittiIntegration.Status.RECOVERABLE).withLastUsed(LocalDateTime.now()));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
public List<LocationPoint> getRawLocationData(User user, Long integrationId, String startDate, String endDate, Integer zoom, String timezone) {
|
public List<LocationPoint> getRawLocationData(User user, Long integrationId, String startDate, String endDate, Integer zoom, String timezone) {
|
||||||
return this.jdbcService
|
return this.jdbcService
|
||||||
.findByIdAndUser(integrationId,user)
|
.findByIdAndUser(integrationId,user)
|
||||||
@@ -249,36 +290,6 @@ public class ReittiIntegrationService {
|
|||||||
return integration;
|
return integration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<TimelineEntry> loadTimeLineEntries(ReittiIntegration integration, LocalDate selectedDate, ZoneId userTimezone) throws RequestFailedException, RequestTemporaryFailedException {
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.set("X-API-TOKEN", integration.getToken());
|
|
||||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
|
||||||
|
|
||||||
String timelineUrl = integration.getUrl().endsWith("/") ?
|
|
||||||
integration.getUrl() + "api/v1/reitti-integration/timeline?date={date}&timezone={timezone}" :
|
|
||||||
integration.getUrl() + "/api/v1/reitti-integration/timeline?date={date}&timezone={timezone}";
|
|
||||||
|
|
||||||
|
|
||||||
ParameterizedTypeReference<List<TimelineEntry>> typeRef = new ParameterizedTypeReference<>() {};
|
|
||||||
ResponseEntity<List<TimelineEntry>> remoteResponse = restTemplate.exchange(
|
|
||||||
timelineUrl,
|
|
||||||
HttpMethod.GET,
|
|
||||||
entity,
|
|
||||||
typeRef,
|
|
||||||
selectedDate,
|
|
||||||
userTimezone.getId()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remoteResponse.getStatusCode().is2xxSuccessful()) {
|
|
||||||
return remoteResponse.getBody();
|
|
||||||
} else if (remoteResponse.getStatusCode().is4xxClientError()) {
|
|
||||||
throw new RequestFailedException(timelineUrl, remoteResponse.getStatusCode(), remoteResponse.getBody());
|
|
||||||
} else {
|
|
||||||
throw new RequestTemporaryFailedException(timelineUrl, remoteResponse.getStatusCode(), remoteResponse.getBody());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<TimelineEntry> loadTimeLineEntriesRange(ReittiIntegration integration, LocalDate startDate, LocalDate endDate, ZoneId userTimezone) throws RequestFailedException, RequestTemporaryFailedException {
|
private List<TimelineEntry> loadTimeLineEntriesRange(ReittiIntegration integration, LocalDate startDate, LocalDate endDate, ZoneId userTimezone) throws RequestFailedException, RequestTemporaryFailedException {
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
@@ -289,7 +300,6 @@ public class ReittiIntegrationService {
|
|||||||
integration.getUrl() + "api/v1/reitti-integration/timeline?startDate={startDate}&endDate={endDate}&timezone={timezone}" :
|
integration.getUrl() + "api/v1/reitti-integration/timeline?startDate={startDate}&endDate={endDate}&timezone={timezone}" :
|
||||||
integration.getUrl() + "/api/v1/reitti-integration/timeline?startDate={startDate}&endDate={endDate}&timezone={timezone}";
|
integration.getUrl() + "/api/v1/reitti-integration/timeline?startDate={startDate}&endDate={endDate}&timezone={timezone}";
|
||||||
|
|
||||||
|
|
||||||
ParameterizedTypeReference<List<TimelineEntry>> typeRef = new ParameterizedTypeReference<>() {};
|
ParameterizedTypeReference<List<TimelineEntry>> typeRef = new ParameterizedTypeReference<>() {};
|
||||||
ResponseEntity<List<TimelineEntry>> remoteResponse = restTemplate.exchange(
|
ResponseEntity<List<TimelineEntry>> remoteResponse = restTemplate.exchange(
|
||||||
timelineUrl,
|
timelineUrl,
|
||||||
@@ -353,9 +363,9 @@ public class ReittiIntegrationService {
|
|||||||
log.warn("Advertise URI is null or empty, remote updates are disabled. Consider setting 'reitti.server.advertise-uri'");
|
log.warn("Advertise URI is null or empty, remote updates are disabled. Consider setting 'reitti.server.advertise-uri'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ReittiIntegration> activeIntegrations = getActiveIntegrationsForUser(user);
|
List<ReittiIntegration> activeIntegrations = getActiveIntegrationsForUser(user);
|
||||||
|
|
||||||
for (ReittiIntegration integration : activeIntegrations) {
|
for (ReittiIntegration integration : activeIntegrations) {
|
||||||
try {
|
try {
|
||||||
registerSubscriptionOnIntegration(integration, user);
|
registerSubscriptionOnIntegration(integration, user);
|
||||||
@@ -383,7 +393,7 @@ public class ReittiIntegrationService {
|
|||||||
log.warn("No advertise URI configured, skipping subscription registration for integration: [{}]", integration.getId());
|
log.warn("No advertise URI configured, skipping subscription registration for integration: [{}]", integration.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.set("X-API-TOKEN", integration.getToken());
|
headers.set("X-API-TOKEN", integration.getToken());
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
@@ -391,11 +401,11 @@ public class ReittiIntegrationService {
|
|||||||
SubscriptionRequest subscriptionRequest = new SubscriptionRequest();
|
SubscriptionRequest subscriptionRequest = new SubscriptionRequest();
|
||||||
subscriptionRequest.setCallbackUrl(advertiseUri);
|
subscriptionRequest.setCallbackUrl(advertiseUri);
|
||||||
HttpEntity<SubscriptionRequest> entity = new HttpEntity<>(subscriptionRequest, headers);
|
HttpEntity<SubscriptionRequest> entity = new HttpEntity<>(subscriptionRequest, headers);
|
||||||
|
|
||||||
String subscribeUrl = integration.getUrl().endsWith("/") ?
|
String subscribeUrl = integration.getUrl().endsWith("/") ?
|
||||||
integration.getUrl() + "api/v1/reitti-integration/subscribe" :
|
integration.getUrl() + "api/v1/reitti-integration/subscribe" :
|
||||||
integration.getUrl() + "/api/v1/reitti-integration/subscribe";
|
integration.getUrl() + "/api/v1/reitti-integration/subscribe";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ResponseEntity<SubscriptionResponse> response = restTemplate.exchange(
|
ResponseEntity<SubscriptionResponse> response = restTemplate.exchange(
|
||||||
subscribeUrl,
|
subscribeUrl,
|
||||||
@@ -403,7 +413,7 @@ public class ReittiIntegrationService {
|
|||||||
entity,
|
entity,
|
||||||
SubscriptionResponse.class
|
SubscriptionResponse.class
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||||
log.debug("Successfully subscribed to integration: [{}]", integration.getId());
|
log.debug("Successfully subscribed to integration: [{}]", integration.getId());
|
||||||
synchronized (integrationSubscriptions) {
|
synchronized (integrationSubscriptions) {
|
||||||
@@ -482,14 +492,14 @@ public class ReittiIntegrationService {
|
|||||||
if (placesData == null) {
|
if (placesData == null) {
|
||||||
return new ProcessedVisitResponse(Collections.emptyList());
|
return new ProcessedVisitResponse(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ProcessedVisitResponse.PlaceVisitSummary> places = placesData.stream()
|
List<ProcessedVisitResponse.PlaceVisitSummary> places = placesData.stream()
|
||||||
.map(this::parsePlaceVisitSummary)
|
.map(this::parsePlaceVisitSummary)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return new ProcessedVisitResponse(places);
|
return new ProcessedVisitResponse(places);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private ProcessedVisitResponse.PlaceVisitSummary parsePlaceVisitSummary(Map<String, Object> placeData) {
|
private ProcessedVisitResponse.PlaceVisitSummary parsePlaceVisitSummary(Map<String, Object> placeData) {
|
||||||
// Parse place info
|
// Parse place info
|
||||||
@@ -507,7 +517,7 @@ public class ReittiIntegrationService {
|
|||||||
SignificantPlace.PlaceType.valueOf(placeInfo.get("type").toString()),
|
SignificantPlace.PlaceType.valueOf(placeInfo.get("type").toString()),
|
||||||
polygon
|
polygon
|
||||||
);
|
);
|
||||||
|
|
||||||
// Parse visits
|
// Parse visits
|
||||||
List<Map<String, Object>> visitsData = (List<Map<String, Object>>) placeData.get("visits");
|
List<Map<String, Object>> visitsData = (List<Map<String, Object>>) placeData.get("visits");
|
||||||
List<ProcessedVisitResponse.VisitDetail> visits = visitsData.stream()
|
List<ProcessedVisitResponse.VisitDetail> visits = visitsData.stream()
|
||||||
@@ -518,12 +528,12 @@ public class ReittiIntegrationService {
|
|||||||
getLongValue(visitData, "durationSeconds")
|
getLongValue(visitData, "durationSeconds")
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Parse summary data
|
// Parse summary data
|
||||||
long totalDurationMs = getLongValue(placeData, "totalDurationMs"); // Convert to milliseconds
|
long totalDurationMs = getLongValue(placeData, "totalDurationMs"); // Convert to milliseconds
|
||||||
int visitCount = getIntValue(placeData, "visitCount");
|
int visitCount = getIntValue(placeData, "visitCount");
|
||||||
String color = "#3388ff"; // Default color, could be extracted from response if available
|
String color = "#3388ff"; // Default color, could be extracted from response if available
|
||||||
|
|
||||||
return new ProcessedVisitResponse.PlaceVisitSummary(place, visits, totalDurationMs, visitCount, color);
|
return new ProcessedVisitResponse.PlaceVisitSummary(place, visits, totalDurationMs, visitCount, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +553,7 @@ public class ReittiIntegrationService {
|
|||||||
}
|
}
|
||||||
return polygon;
|
return polygon;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getLongValue(Map<String, Object> map, String key) {
|
private Long getLongValue(Map<String, Object> map, String key) {
|
||||||
Object value = map.get(key);
|
Object value = map.get(key);
|
||||||
if (value instanceof Number) {
|
if (value instanceof Number) {
|
||||||
@@ -551,7 +561,7 @@ public class ReittiIntegrationService {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Double getDoubleValue(Map<String, Object> map, String key) {
|
private Double getDoubleValue(Map<String, Object> map, String key) {
|
||||||
Object value = map.get(key);
|
Object value = map.get(key);
|
||||||
if (value instanceof Number) {
|
if (value instanceof Number) {
|
||||||
@@ -559,7 +569,7 @@ public class ReittiIntegrationService {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer getIntValue(Map<String, Object> map, String key) {
|
private Integer getIntValue(Map<String, Object> map, String key) {
|
||||||
Object value = map.get(key);
|
Object value = map.get(key);
|
||||||
if (value instanceof Number) {
|
if (value instanceof Number) {
|
||||||
@@ -568,4 +578,24 @@ public class ReittiIntegrationService {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LocationPoint parseLocationPoint(Object locationObj) {
|
||||||
|
if (locationObj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> locationMap = (Map<String, Object>) locationObj;
|
||||||
|
|
||||||
|
LocationPoint locationPoint = new LocationPoint();
|
||||||
|
locationPoint.setLatitude(getDoubleValue(locationMap, "latitude"));
|
||||||
|
locationPoint.setLongitude(getDoubleValue(locationMap, "longitude"));
|
||||||
|
locationPoint.setTimestamp((String) locationMap.get("timestamp"));
|
||||||
|
locationPoint.setAccuracyMeters(getDoubleValue(locationMap, "accuracyMeters"));
|
||||||
|
|
||||||
|
if (locationMap.containsKey("elevationMeters")) {
|
||||||
|
locationPoint.setElevationMeters(getDoubleValue(locationMap, "elevationMeters"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationPoint;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user