Add redir

This commit is contained in:
2025-12-02 17:00:19 +01:00
parent 60f04f1aa8
commit 80b58147ed
16 changed files with 198 additions and 195 deletions

View File

@@ -1,8 +1,8 @@
package com.imgfloat.app.controller;
import com.imgfloat.app.model.AdminRequest;
import com.imgfloat.app.model.ImageLayer;
import com.imgfloat.app.model.ImageRequest;
import com.imgfloat.app.model.Asset;
import com.imgfloat.app.model.AssetRequest;
import com.imgfloat.app.model.TransformRequest;
import com.imgfloat.app.model.VisibilityRequest;
import com.imgfloat.app.service.ChannelDirectoryService;
@@ -61,71 +61,71 @@ public class ChannelApiController {
return ResponseEntity.ok().body(removed);
}
@GetMapping("/images")
public Collection<ImageLayer> listImages(@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken authentication) {
@GetMapping("/assets")
public Collection<Asset> listAssets(@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
if (!channelDirectoryService.isBroadcaster(broadcaster, login)
&& !channelDirectoryService.isAdmin(broadcaster, login)) {
throw new ResponseStatusException(FORBIDDEN, "Not authorized");
}
return channelDirectoryService.getImagesForAdmin(broadcaster);
return channelDirectoryService.getAssetsForAdmin(broadcaster);
}
@GetMapping("/images/visible")
public Collection<ImageLayer> listVisible(@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken authentication) {
@GetMapping("/assets/visible")
public Collection<Asset> listVisible(@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
if (!channelDirectoryService.isBroadcaster(broadcaster, login)) {
throw new ResponseStatusException(FORBIDDEN, "Only broadcaster can load public overlay");
}
return channelDirectoryService.getVisibleImages(broadcaster);
return channelDirectoryService.getVisibleAssets(broadcaster);
}
@PostMapping("/images")
public ResponseEntity<ImageLayer> createImage(@PathVariable("broadcaster") String broadcaster,
@Valid @RequestBody ImageRequest request,
OAuth2AuthenticationToken authentication) {
@PostMapping("/assets")
public ResponseEntity<Asset> createAsset(@PathVariable("broadcaster") String broadcaster,
@Valid @RequestBody AssetRequest request,
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureAuthorized(broadcaster, login);
return channelDirectoryService.createImage(broadcaster, request)
return channelDirectoryService.createAsset(broadcaster, request)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Channel not found"));
}
@PutMapping("/images/{imageId}/transform")
public ResponseEntity<ImageLayer> transform(@PathVariable("broadcaster") String broadcaster,
@PathVariable("imageId") String imageId,
@Valid @RequestBody TransformRequest request,
OAuth2AuthenticationToken authentication) {
@PutMapping("/assets/{assetId}/transform")
public ResponseEntity<Asset> transform(@PathVariable("broadcaster") String broadcaster,
@PathVariable("assetId") String assetId,
@Valid @RequestBody TransformRequest request,
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureAuthorized(broadcaster, login);
return channelDirectoryService.updateTransform(broadcaster, imageId, request)
return channelDirectoryService.updateTransform(broadcaster, assetId, request)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Image not found"));
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
}
@PutMapping("/images/{imageId}/visibility")
public ResponseEntity<ImageLayer> visibility(@PathVariable("broadcaster") String broadcaster,
@PathVariable("imageId") String imageId,
@RequestBody VisibilityRequest request,
OAuth2AuthenticationToken authentication) {
@PutMapping("/assets/{assetId}/visibility")
public ResponseEntity<Asset> visibility(@PathVariable("broadcaster") String broadcaster,
@PathVariable("assetId") String assetId,
@RequestBody VisibilityRequest request,
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureAuthorized(broadcaster, login);
return channelDirectoryService.updateVisibility(broadcaster, imageId, request)
return channelDirectoryService.updateVisibility(broadcaster, assetId, request)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Image not found"));
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
}
@DeleteMapping("/images/{imageId}")
@DeleteMapping("/assets/{assetId}")
public ResponseEntity<?> delete(@PathVariable("broadcaster") String broadcaster,
@PathVariable("imageId") String imageId,
@PathVariable("assetId") String assetId,
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureAuthorized(broadcaster, login);
boolean removed = channelDirectoryService.deleteImage(broadcaster, imageId);
boolean removed = channelDirectoryService.deleteAsset(broadcaster, assetId);
if (!removed) {
throw new ResponseStatusException(NOT_FOUND, "Image not found");
throw new ResponseStatusException(NOT_FOUND, "Asset not found");
}
return ResponseEntity.ok().build();
}

View File

@@ -3,7 +3,7 @@ package com.imgfloat.app.model;
import java.time.Instant;
import java.util.UUID;
public class ImageLayer {
public class Asset {
private final String id;
private String url;
private double x;
@@ -14,7 +14,7 @@ public class ImageLayer {
private boolean hidden;
private final Instant createdAt;
public ImageLayer(String url, double width, double height) {
public Asset(String url, double width, double height) {
this.id = UUID.randomUUID().toString();
this.url = url;
this.width = width;

View File

@@ -0,0 +1,66 @@
package com.imgfloat.app.model;
public class AssetEvent {
public enum Type {
CREATED,
UPDATED,
VISIBILITY,
DELETED
}
private Type type;
private String channel;
private Asset payload;
private String assetId;
public static AssetEvent created(String channel, Asset asset) {
AssetEvent event = new AssetEvent();
event.type = Type.CREATED;
event.channel = channel;
event.payload = asset;
event.assetId = asset.getId();
return event;
}
public static AssetEvent updated(String channel, Asset asset) {
AssetEvent event = new AssetEvent();
event.type = Type.UPDATED;
event.channel = channel;
event.payload = asset;
event.assetId = asset.getId();
return event;
}
public static AssetEvent visibility(String channel, Asset asset) {
AssetEvent event = new AssetEvent();
event.type = Type.VISIBILITY;
event.channel = channel;
event.payload = asset;
event.assetId = asset.getId();
return event;
}
public static AssetEvent deleted(String channel, String assetId) {
AssetEvent event = new AssetEvent();
event.type = Type.DELETED;
event.channel = channel;
event.assetId = assetId;
return event;
}
public Type getType() {
return type;
}
public String getChannel() {
return channel;
}
public Asset getPayload() {
return payload;
}
public String getAssetId() {
return assetId;
}
}

View File

@@ -3,7 +3,7 @@ package com.imgfloat.app.model;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public class ImageRequest {
public class AssetRequest {
@NotBlank
private String url;

View File

@@ -8,12 +8,12 @@ import java.util.concurrent.ConcurrentHashMap;
public class Channel {
private final String broadcaster;
private final Set<String> admins;
private final Map<String, ImageLayer> images;
private final Map<String, Asset> assets;
public Channel(String broadcaster) {
this.broadcaster = broadcaster.toLowerCase();
this.admins = ConcurrentHashMap.newKeySet();
this.images = new ConcurrentHashMap<>();
this.assets = new ConcurrentHashMap<>();
}
public String getBroadcaster() {
@@ -24,8 +24,8 @@ public class Channel {
return Collections.unmodifiableSet(admins);
}
public Map<String, ImageLayer> getImages() {
return images;
public Map<String, Asset> getAssets() {
return assets;
}
public boolean addAdmin(String username) {

View File

@@ -1,66 +0,0 @@
package com.imgfloat.app.model;
public class ImageEvent {
public enum Type {
CREATED,
UPDATED,
VISIBILITY,
DELETED
}
private Type type;
private String channel;
private ImageLayer payload;
private String imageId;
public static ImageEvent created(String channel, ImageLayer layer) {
ImageEvent event = new ImageEvent();
event.type = Type.CREATED;
event.channel = channel;
event.payload = layer;
event.imageId = layer.getId();
return event;
}
public static ImageEvent updated(String channel, ImageLayer layer) {
ImageEvent event = new ImageEvent();
event.type = Type.UPDATED;
event.channel = channel;
event.payload = layer;
event.imageId = layer.getId();
return event;
}
public static ImageEvent visibility(String channel, ImageLayer layer) {
ImageEvent event = new ImageEvent();
event.type = Type.VISIBILITY;
event.channel = channel;
event.payload = layer;
event.imageId = layer.getId();
return event;
}
public static ImageEvent deleted(String channel, String imageId) {
ImageEvent event = new ImageEvent();
event.type = Type.DELETED;
event.channel = channel;
event.imageId = imageId;
return event;
}
public Type getType() {
return type;
}
public String getChannel() {
return channel;
}
public ImageLayer getPayload() {
return payload;
}
public String getImageId() {
return imageId;
}
}

View File

@@ -1,9 +1,9 @@
package com.imgfloat.app.service;
import com.imgfloat.app.model.Asset;
import com.imgfloat.app.model.AssetEvent;
import com.imgfloat.app.model.AssetRequest;
import com.imgfloat.app.model.Channel;
import com.imgfloat.app.model.ImageEvent;
import com.imgfloat.app.model.ImageLayer;
import com.imgfloat.app.model.ImageRequest;
import com.imgfloat.app.model.TransformRequest;
import com.imgfloat.app.model.VisibilityRequest;
import org.springframework.messaging.simp.SimpMessagingTemplate;
@@ -45,55 +45,55 @@ public class ChannelDirectoryService {
return removed;
}
public Collection<ImageLayer> getImagesForAdmin(String broadcaster) {
return getOrCreateChannel(broadcaster).getImages().values();
public Collection<Asset> getAssetsForAdmin(String broadcaster) {
return getOrCreateChannel(broadcaster).getAssets().values();
}
public Collection<ImageLayer> getVisibleImages(String broadcaster) {
return getOrCreateChannel(broadcaster).getImages().values().stream()
.filter(image -> !image.isHidden())
public Collection<Asset> getVisibleAssets(String broadcaster) {
return getOrCreateChannel(broadcaster).getAssets().values().stream()
.filter(asset -> !asset.isHidden())
.toList();
}
public Optional<ImageLayer> createImage(String broadcaster, ImageRequest request) {
public Optional<Asset> createAsset(String broadcaster, AssetRequest request) {
Channel channel = getOrCreateChannel(broadcaster);
ImageLayer layer = new ImageLayer(request.getUrl(), request.getWidth(), request.getHeight());
channel.getImages().put(layer.getId(), layer);
messagingTemplate.convertAndSend(topicFor(broadcaster), ImageEvent.created(broadcaster, layer));
return Optional.of(layer);
Asset asset = new Asset(request.getUrl(), request.getWidth(), request.getHeight());
channel.getAssets().put(asset.getId(), asset);
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, asset));
return Optional.of(asset);
}
public Optional<ImageLayer> updateTransform(String broadcaster, String imageId, TransformRequest request) {
public Optional<Asset> updateTransform(String broadcaster, String assetId, TransformRequest request) {
Channel channel = getOrCreateChannel(broadcaster);
ImageLayer layer = channel.getImages().get(imageId);
if (layer == null) {
Asset asset = channel.getAssets().get(assetId);
if (asset == null) {
return Optional.empty();
}
layer.setX(request.getX());
layer.setY(request.getY());
layer.setWidth(request.getWidth());
layer.setHeight(request.getHeight());
layer.setRotation(request.getRotation());
messagingTemplate.convertAndSend(topicFor(broadcaster), ImageEvent.updated(broadcaster, layer));
return Optional.of(layer);
asset.setX(request.getX());
asset.setY(request.getY());
asset.setWidth(request.getWidth());
asset.setHeight(request.getHeight());
asset.setRotation(request.getRotation());
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, asset));
return Optional.of(asset);
}
public Optional<ImageLayer> updateVisibility(String broadcaster, String imageId, VisibilityRequest request) {
public Optional<Asset> updateVisibility(String broadcaster, String assetId, VisibilityRequest request) {
Channel channel = getOrCreateChannel(broadcaster);
ImageLayer layer = channel.getImages().get(imageId);
if (layer == null) {
Asset asset = channel.getAssets().get(assetId);
if (asset == null) {
return Optional.empty();
}
layer.setHidden(request.isHidden());
messagingTemplate.convertAndSend(topicFor(broadcaster), ImageEvent.visibility(broadcaster, layer));
return Optional.of(layer);
asset.setHidden(request.isHidden());
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.visibility(broadcaster, asset));
return Optional.of(asset);
}
public boolean deleteImage(String broadcaster, String imageId) {
public boolean deleteAsset(String broadcaster, String assetId) {
Channel channel = getOrCreateChannel(broadcaster);
ImageLayer removed = channel.getImages().remove(imageId);
Asset removed = channel.getAssets().remove(assetId);
if (removed != null) {
messagingTemplate.convertAndSend(topicFor(broadcaster), ImageEvent.deleted(broadcaster, imageId));
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.deleted(broadcaster, assetId));
return true;
}
return false;