From ef276128c6df1cdea548626830f3362c9108c741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Z=C3=B6ller?= Date: Wed, 3 Dec 2025 07:50:57 +0100 Subject: [PATCH] Moar federation --- .../fitpub/controller/ActivityController.java | 24 +++++- .../fitpub/model/dto/ActivityDTO.java | 3 +- .../fitpub/service/ActivityImageService.java | 4 +- .../fitpub/service/FederationService.java | 36 ++++++++ .../fitpub/service/FitFileService.java | 55 ++---------- .../fitpub/util/ActivityFormatter.java | 84 +++++++++++++++++++ 6 files changed, 155 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/operaton/fitpub/util/ActivityFormatter.java diff --git a/src/main/java/org/operaton/fitpub/controller/ActivityController.java b/src/main/java/org/operaton/fitpub/controller/ActivityController.java index a43b28b..0701e02 100644 --- a/src/main/java/org/operaton/fitpub/controller/ActivityController.java +++ b/src/main/java/org/operaton/fitpub/controller/ActivityController.java @@ -12,6 +12,7 @@ import org.operaton.fitpub.repository.UserRepository; import org.operaton.fitpub.service.ActivityImageService; import org.operaton.fitpub.service.FederationService; import org.operaton.fitpub.service.FitFileService; +import org.operaton.fitpub.util.ActivityFormatter; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -153,7 +154,8 @@ public class ActivityController { // Activity type with emoji String activityEmoji = getActivityEmoji(activity.getActivityType()); - content.append(activityEmoji).append(" ").append(activity.getActivityType()); + String formattedType = ActivityFormatter.formatActivityType(activity.getActivityType()); + content.append(activityEmoji).append(" ").append(formattedType); // Metrics on separate lines if (activity.getTotalDistance() != null) { @@ -336,11 +338,31 @@ public class ActivityController { UUID userId = getUserId(userDetails); + // Get activity before deletion to send Delete activity to followers + Activity activity = fitFileService.getActivity(id, userId); + if (activity == null) { + return ResponseEntity.notFound().build(); + } + + // Only send Delete activity if it was previously federated (public or followers-only) + boolean shouldFederate = activity.getVisibility() == Activity.Visibility.PUBLIC || + activity.getVisibility() == Activity.Visibility.FOLLOWERS; + + // Delete from database boolean deleted = fitFileService.deleteActivity(id, userId); if (!deleted) { return ResponseEntity.notFound().build(); } + // Send Delete activity to followers if the activity was federated + if (shouldFederate) { + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + String activityUri = baseUrl + "/activities/" + id; + federationService.sendDeleteActivity(activityUri, user); + } + return ResponseEntity.noContent().build(); } diff --git a/src/main/java/org/operaton/fitpub/model/dto/ActivityDTO.java b/src/main/java/org/operaton/fitpub/model/dto/ActivityDTO.java index 7d217e9..e2d6d22 100644 --- a/src/main/java/org/operaton/fitpub/model/dto/ActivityDTO.java +++ b/src/main/java/org/operaton/fitpub/model/dto/ActivityDTO.java @@ -9,6 +9,7 @@ import lombok.NoArgsConstructor; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.LineString; import org.operaton.fitpub.model.entity.Activity; +import org.operaton.fitpub.util.ActivityFormatter; import java.math.BigDecimal; import java.time.Duration; @@ -91,7 +92,7 @@ public class ActivityDTO { ActivityDTOBuilder builder = ActivityDTO.builder() .id(activity.getId()) .userId(activity.getUserId()) - .activityType(activity.getActivityType().name()) + .activityType(ActivityFormatter.formatActivityType(activity.getActivityType())) .title(activity.getTitle()) .description(activity.getDescription()) .startedAt(activity.getStartedAt()) diff --git a/src/main/java/org/operaton/fitpub/service/ActivityImageService.java b/src/main/java/org/operaton/fitpub/service/ActivityImageService.java index e15c5be..65ee2e0 100644 --- a/src/main/java/org/operaton/fitpub/service/ActivityImageService.java +++ b/src/main/java/org/operaton/fitpub/service/ActivityImageService.java @@ -3,6 +3,7 @@ package org.operaton.fitpub.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.operaton.fitpub.model.entity.Activity; +import org.operaton.fitpub.util.ActivityFormatter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -216,7 +217,8 @@ public class ActivityImageService { // Activity type g2d.setFont(new Font("Arial", Font.PLAIN, 24)); g2d.setColor(new Color(200, 200, 200)); - g2d.drawString(activity.getActivityType().toString(), metadataX, y); + String formattedType = ActivityFormatter.formatActivityType(activity.getActivityType()); + g2d.drawString(formattedType, metadataX, y); y += lineHeight; // Distance diff --git a/src/main/java/org/operaton/fitpub/service/FederationService.java b/src/main/java/org/operaton/fitpub/service/FederationService.java index d6701ea..3f997f4 100644 --- a/src/main/java/org/operaton/fitpub/service/FederationService.java +++ b/src/main/java/org/operaton/fitpub/service/FederationService.java @@ -352,6 +352,42 @@ public class FederationService { } } + /** + * Send a Delete activity to notify followers that an object has been deleted. + * + * @param objectUri the URI of the deleted object (e.g., activity URI) + * @param sender the user who deleted the object + */ + public void sendDeleteActivity(String objectUri, User sender) { + try { + String deleteId = baseUrl + "/activities/delete/" + UUID.randomUUID(); + String actorUri = baseUrl + "/users/" + sender.getUsername(); + + Map deleteActivity = new HashMap<>(); + deleteActivity.put("@context", "https://www.w3.org/ns/activitystreams"); + deleteActivity.put("type", "Delete"); + deleteActivity.put("id", deleteId); + deleteActivity.put("actor", actorUri); + deleteActivity.put("object", objectUri); + deleteActivity.put("published", Instant.now().toString()); + + // For delete activities, we typically also send to public if the object was public + deleteActivity.put("to", List.of("https://www.w3.org/ns/activitystreams#Public")); + deleteActivity.put("cc", List.of(actorUri + "/followers")); + + // Send to all follower inboxes + List inboxes = getFollowerInboxes(sender.getId()); + for (String inbox : inboxes) { + sendActivity(inbox, deleteActivity, sender); + } + + log.info("Sent Delete activity for: {} to {} inboxes", objectUri, inboxes.size()); + + } catch (Exception e) { + log.error("Failed to send Delete activity for: {}", objectUri, e); + } + } + // Helper methods private String extractUsername(String actorUri, Map actorData) { diff --git a/src/main/java/org/operaton/fitpub/service/FitFileService.java b/src/main/java/org/operaton/fitpub/service/FitFileService.java index 89e46f5..39ecc46 100644 --- a/src/main/java/org/operaton/fitpub/service/FitFileService.java +++ b/src/main/java/org/operaton/fitpub/service/FitFileService.java @@ -13,6 +13,7 @@ import org.operaton.fitpub.model.entity.Activity; import org.operaton.fitpub.model.entity.ActivityMetrics; import org.operaton.fitpub.repository.ActivityMetricsRepository; import org.operaton.fitpub.repository.ActivityRepository; +import org.operaton.fitpub.util.ActivityFormatter; import org.operaton.fitpub.util.FitFileValidator; import org.operaton.fitpub.util.FitParser; import org.operaton.fitpub.util.TrackSimplifier; @@ -210,56 +211,14 @@ public class FitFileService { } /** - * Generates a default title for an activity. + * Generates a default title for an activity based on time of day. + * Examples: "Morning Run", "Evening Ride", "Night Walk" */ private String generateTitle(FitParser.ParsedFitData parsedData) { - String activityType = formatActivityType(parsedData.getActivityType()); - String date = parsedData.getStartTime().toLocalDate().toString(); - return String.format("%s - %s", activityType, date); - } - - /** - * Formats activity type for display. - */ - private String formatActivityType(Activity.ActivityType type) { - switch (type) { - case RUN: - return "Run"; - case RIDE: - return "Ride"; - case HIKE: - return "Hike"; - case WALK: - return "Walk"; - case SWIM: - return "Swim"; - case ALPINE_SKI: - return "Alpine Ski"; - case BACKCOUNTRY_SKI: - return "Backcountry Ski"; - case NORDIC_SKI: - return "Nordic Ski"; - case SNOWBOARD: - return "Snowboard"; - case ROWING: - return "Rowing"; - case KAYAKING: - return "Kayaking"; - case CANOEING: - return "Canoeing"; - case INLINE_SKATING: - return "Inline Skating"; - case ROCK_CLIMBING: - return "Rock Climbing"; - case MOUNTAINEERING: - return "Mountaineering"; - case YOGA: - return "Yoga"; - case WORKOUT: - return "Workout"; - default: - return "Activity"; - } + return ActivityFormatter.generateActivityTitle( + parsedData.getStartTime(), + parsedData.getActivityType() + ); } /** diff --git a/src/main/java/org/operaton/fitpub/util/ActivityFormatter.java b/src/main/java/org/operaton/fitpub/util/ActivityFormatter.java new file mode 100644 index 0000000..1c2499b --- /dev/null +++ b/src/main/java/org/operaton/fitpub/util/ActivityFormatter.java @@ -0,0 +1,84 @@ +package org.operaton.fitpub.util; + +import org.operaton.fitpub.model.entity.Activity; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * Utility class for formatting activity-related data for display. + */ +public class ActivityFormatter { + + /** + * Formats an activity type to a human-readable string. + * Converts UPPERCASE_WITH_UNDERSCORES to Title Case. + * + * @param activityType the activity type enum + * @return human-readable activity type (e.g., "Alpine Ski" instead of "ALPINE_SKI") + */ + public static String formatActivityType(Activity.ActivityType activityType) { + if (activityType == null) { + return "Unknown"; + } + + String name = activityType.name(); + + // Split by underscore and capitalize each word + String[] words = name.split("_"); + StringBuilder formatted = new StringBuilder(); + + for (int i = 0; i < words.length; i++) { + String word = words[i].toLowerCase(); + // Capitalize first letter + formatted.append(Character.toUpperCase(word.charAt(0))); + formatted.append(word.substring(1)); + + // Add space between words (except for the last word) + if (i < words.length - 1) { + formatted.append(" "); + } + } + + return formatted.toString(); + } + + /** + * Generates a default activity title based on the time of day and activity type. + * Format: "[Time of Day] [Activity Type]" (e.g., "Morning Run", "Evening Ride") + * + * @param startedAt the activity start time + * @param activityType the activity type + * @return generated title + */ + public static String generateActivityTitle(LocalDateTime startedAt, Activity.ActivityType activityType) { + if (startedAt == null || activityType == null) { + return "Activity"; + } + + String timeOfDay = getTimeOfDay(startedAt.toLocalTime()); + String formattedType = formatActivityType(activityType); + + return timeOfDay + " " + formattedType; + } + + /** + * Determines the time of day based on the hour. + * + * @param time the local time + * @return time of day description (Morning, Afternoon, Evening, Night) + */ + private static String getTimeOfDay(LocalTime time) { + int hour = time.getHour(); + + if (hour >= 5 && hour < 12) { + return "Morning"; + } else if (hour >= 12 && hour < 17) { + return "Afternoon"; + } else if (hour >= 17 && hour < 21) { + return "Evening"; + } else { + return "Night"; + } + } +}