Moar federation
This commit is contained in:
parent
a3ff96653a
commit
ef276128c6
6 changed files with 155 additions and 51 deletions
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String, Object> 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<String> 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<String, Object> actorData) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue