From 80b58147ed6305956540e74392b62498cc90ae3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Tue, 2 Dec 2025 17:00:19 +0100 Subject: [PATCH] Add redir --- Makefile | 3 +- README.md | 11 ++-- .../app/controller/ChannelApiController.java | 66 +++++++++---------- .../app/model/{ImageLayer.java => Asset.java} | 4 +- .../com/imgfloat/app/model/AssetEvent.java | 66 +++++++++++++++++++ .../{ImageRequest.java => AssetRequest.java} | 2 +- .../java/com/imgfloat/app/model/Channel.java | 8 +-- .../com/imgfloat/app/model/ImageEvent.java | 66 ------------------- .../app/service/ChannelDirectoryService.java | 64 +++++++++--------- src/main/resources/application.yml | 2 +- src/main/resources/static/js/admin.js | 36 +++++----- src/main/resources/static/js/broadcast.js | 24 +++---- src/main/resources/templates/admin.html | 12 ++-- .../app/ChannelApiIntegrationTest.java | 16 ++--- .../app/ChannelDirectoryServiceTest.java | 12 ++-- src/test/resources/valid.env | 1 + 16 files changed, 198 insertions(+), 195 deletions(-) rename src/main/java/com/imgfloat/app/model/{ImageLayer.java => Asset.java} (94%) create mode 100644 src/main/java/com/imgfloat/app/model/AssetEvent.java rename src/main/java/com/imgfloat/app/model/{ImageRequest.java => AssetRequest.java} (95%) delete mode 100644 src/main/java/com/imgfloat/app/model/ImageEvent.java diff --git a/Makefile b/Makefile index abaa665..bd1db2c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ APP_NAME=imgfloat .PHONY: run test package docker-build docker-run ssl run: - test -f .env && . ./.env + test -f .env && . ./.env; \ + export TWITCH_REDIRECT_URI=$${TWITCH_REDIRECT_URI:-http://localhost:8080/login/oauth2/code/twitch}; \ mvn spring-boot:run test: diff --git a/README.md b/README.md index d6303f4..3ef8b38 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Spring Boot overlay server for Twitch broadcasters and their channel admins. B - Twitch OAuth (OAuth2 login) with broadcaster and channel admin access controls. - Admin console with Twitch player embed and canvas preview. - Broadcaster overlay view optimized for OBS browser sources. -- Real-time image creation, movement, resize, rotation, visibility toggles, and deletion via STOMP/WebSockets. +- Real-time asset creation, movement, resize, rotation, visibility toggles, and deletion via STOMP/WebSockets. - In-memory channel directory optimized with lock-free collections for fast updates. - Optional SSL with local self-signed keystore support. - Dockerfile, Makefile, CI workflow, and Maven build. @@ -19,9 +19,10 @@ A Spring Boot overlay server for Twitch broadcasters and their channel admins. B ### Local run ```bash -TWITCH_CLIENT_ID=your_id TWITCH_CLIENT_SECRET=your_secret mvn spring-boot:run +TWITCH_CLIENT_ID=your_id TWITCH_CLIENT_SECRET=your_secret \ +TWITCH_REDIRECT_URI=http://localhost:8080/login/oauth2/code/twitch mvn spring-boot:run ``` -The default server port is `8080`. Log in via `/oauth2/authorization/twitch`. +The default server port is `8080`. Log in via `/oauth2/authorization/twitch`. The redirect URI above is what Twitch should be configured to call for local development. ### Enable TLS locally ```bash @@ -32,7 +33,7 @@ mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=8443" ``` ### Make targets -- `make run` – start the dev server. +- `make run` – start the dev server (exports `TWITCH_REDIRECT_URI` to `http://localhost:8080/login/oauth2/code/twitch` if unset). - `make test` – run unit/integration tests. - `make package` – build the runnable jar. - `make docker-build` / `make docker-run` – containerize and run the service. @@ -45,7 +46,7 @@ TWITCH_CLIENT_ID=your_id TWITCH_CLIENT_SECRET=your_secret docker run -p 8080:808 ``` ### OAuth configuration -Spring Boot reads Twitch credentials from `TWITCH_CLIENT_ID` and `TWITCH_CLIENT_SECRET`. The redirect URI is `{baseUrl}/login/oauth2/code/twitch`. +Spring Boot reads Twitch credentials from `TWITCH_CLIENT_ID` and `TWITCH_CLIENT_SECRET`. The redirect URI comes from `TWITCH_REDIRECT_URI` (defaulting to `{baseUrl}/login/oauth2/code/twitch`). ### CI GitHub Actions runs `mvn verify` on pushes and pull requests via `.github/workflows/ci.yml`. diff --git a/src/main/java/com/imgfloat/app/controller/ChannelApiController.java b/src/main/java/com/imgfloat/app/controller/ChannelApiController.java index 7e99d99..74c53ca 100644 --- a/src/main/java/com/imgfloat/app/controller/ChannelApiController.java +++ b/src/main/java/com/imgfloat/app/controller/ChannelApiController.java @@ -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 listImages(@PathVariable("broadcaster") String broadcaster, - OAuth2AuthenticationToken authentication) { + @GetMapping("/assets") + public Collection 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 listVisible(@PathVariable("broadcaster") String broadcaster, - OAuth2AuthenticationToken authentication) { + @GetMapping("/assets/visible") + public Collection 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 createImage(@PathVariable("broadcaster") String broadcaster, - @Valid @RequestBody ImageRequest request, - OAuth2AuthenticationToken authentication) { + @PostMapping("/assets") + public ResponseEntity 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 transform(@PathVariable("broadcaster") String broadcaster, - @PathVariable("imageId") String imageId, - @Valid @RequestBody TransformRequest request, - OAuth2AuthenticationToken authentication) { + @PutMapping("/assets/{assetId}/transform") + public ResponseEntity 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 visibility(@PathVariable("broadcaster") String broadcaster, - @PathVariable("imageId") String imageId, - @RequestBody VisibilityRequest request, - OAuth2AuthenticationToken authentication) { + @PutMapping("/assets/{assetId}/visibility") + public ResponseEntity 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(); } diff --git a/src/main/java/com/imgfloat/app/model/ImageLayer.java b/src/main/java/com/imgfloat/app/model/Asset.java similarity index 94% rename from src/main/java/com/imgfloat/app/model/ImageLayer.java rename to src/main/java/com/imgfloat/app/model/Asset.java index 4aed97e..d476b31 100644 --- a/src/main/java/com/imgfloat/app/model/ImageLayer.java +++ b/src/main/java/com/imgfloat/app/model/Asset.java @@ -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; diff --git a/src/main/java/com/imgfloat/app/model/AssetEvent.java b/src/main/java/com/imgfloat/app/model/AssetEvent.java new file mode 100644 index 0000000..2e888b1 --- /dev/null +++ b/src/main/java/com/imgfloat/app/model/AssetEvent.java @@ -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; + } +} diff --git a/src/main/java/com/imgfloat/app/model/ImageRequest.java b/src/main/java/com/imgfloat/app/model/AssetRequest.java similarity index 95% rename from src/main/java/com/imgfloat/app/model/ImageRequest.java rename to src/main/java/com/imgfloat/app/model/AssetRequest.java index 17b8340..19b911a 100644 --- a/src/main/java/com/imgfloat/app/model/ImageRequest.java +++ b/src/main/java/com/imgfloat/app/model/AssetRequest.java @@ -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; diff --git a/src/main/java/com/imgfloat/app/model/Channel.java b/src/main/java/com/imgfloat/app/model/Channel.java index abf4dc3..b170452 100644 --- a/src/main/java/com/imgfloat/app/model/Channel.java +++ b/src/main/java/com/imgfloat/app/model/Channel.java @@ -8,12 +8,12 @@ import java.util.concurrent.ConcurrentHashMap; public class Channel { private final String broadcaster; private final Set admins; - private final Map images; + private final Map 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 getImages() { - return images; + public Map getAssets() { + return assets; } public boolean addAdmin(String username) { diff --git a/src/main/java/com/imgfloat/app/model/ImageEvent.java b/src/main/java/com/imgfloat/app/model/ImageEvent.java deleted file mode 100644 index 97ca9f3..0000000 --- a/src/main/java/com/imgfloat/app/model/ImageEvent.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java index 8f8ef48..35a21af 100644 --- a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java +++ b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java @@ -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 getImagesForAdmin(String broadcaster) { - return getOrCreateChannel(broadcaster).getImages().values(); + public Collection getAssetsForAdmin(String broadcaster) { + return getOrCreateChannel(broadcaster).getAssets().values(); } - public Collection getVisibleImages(String broadcaster) { - return getOrCreateChannel(broadcaster).getImages().values().stream() - .filter(image -> !image.isHidden()) + public Collection getVisibleAssets(String broadcaster) { + return getOrCreateChannel(broadcaster).getAssets().values().stream() + .filter(asset -> !asset.isHidden()) .toList(); } - public Optional createImage(String broadcaster, ImageRequest request) { + public Optional 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 updateTransform(String broadcaster, String imageId, TransformRequest request) { + public Optional 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 updateVisibility(String broadcaster, String imageId, VisibilityRequest request) { + public Optional 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; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0366670..f698cca 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,7 +20,7 @@ spring: twitch: client-id: ${TWITCH_CLIENT_ID} client-secret: ${TWITCH_CLIENT_SECRET} - redirect-uri: "{baseUrl}/login/oauth2/code/twitch" + redirect-uri: "${TWITCH_REDIRECT_URI:{baseUrl}/login/oauth2/code/twitch}" authorization-grant-type: authorization_code scope: ["user:read:email"] provider: diff --git a/src/main/resources/static/js/admin.js b/src/main/resources/static/js/admin.js index b25f619..6ffd880 100644 --- a/src/main/resources/static/js/admin.js +++ b/src/main/resources/static/js/admin.js @@ -3,7 +3,7 @@ const canvas = document.getElementById('admin-canvas'); const ctx = canvas.getContext('2d'); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; -const images = new Map(); +const assets = new Map(); function connect() { const socket = new SockJS('/ws'); @@ -13,13 +13,13 @@ function connect() { const body = JSON.parse(payload.body); handleEvent(body); }); - fetchImages(); + fetchAssets(); fetchAdmins(); }); } -function fetchImages() { - fetch(`/api/channels/${broadcaster}/images`).then(r => r.json()).then(renderImages); +function fetchAssets() { + fetch(`/api/channels/${broadcaster}/assets`).then(r => r.json()).then(renderAssets); } function fetchAdmins() { @@ -34,38 +34,38 @@ function fetchAdmins() { }).catch(() => {}); } -function renderImages(list) { - list.forEach(img => images.set(img.id, img)); +function renderAssets(list) { + list.forEach(asset => assets.set(asset.id, asset)); draw(); } function handleEvent(event) { if (event.type === 'DELETED') { - images.delete(event.imageId); + assets.delete(event.assetId); } else if (event.payload) { - images.set(event.payload.id, event.payload); + assets.set(event.payload.id, event.payload); } draw(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); - images.forEach(img => { + assets.forEach(asset => { ctx.save(); - ctx.globalAlpha = img.hidden ? 0.35 : 1; - ctx.translate(img.x, img.y); - ctx.rotate(img.rotation * Math.PI / 180); + ctx.globalAlpha = asset.hidden ? 0.35 : 1; + ctx.translate(asset.x, asset.y); + ctx.rotate(asset.rotation * Math.PI / 180); ctx.fillStyle = 'rgba(124, 58, 237, 0.25)'; - ctx.fillRect(0, 0, img.width, img.height); + ctx.fillRect(0, 0, asset.width, asset.height); ctx.restore(); }); } -function uploadImage() { - const url = document.getElementById('image-url').value; - const width = parseFloat(document.getElementById('image-width').value); - const height = parseFloat(document.getElementById('image-height').value); - fetch(`/api/channels/${broadcaster}/images`, { +function uploadAsset() { + const url = document.getElementById('asset-url').value; + const width = parseFloat(document.getElementById('asset-width').value); + const height = parseFloat(document.getElementById('asset-height').value); + fetch(`/api/channels/${broadcaster}/assets`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({url, width, height}) diff --git a/src/main/resources/static/js/broadcast.js b/src/main/resources/static/js/broadcast.js index 7eb35dd..a75e1af 100644 --- a/src/main/resources/static/js/broadcast.js +++ b/src/main/resources/static/js/broadcast.js @@ -2,7 +2,7 @@ const canvas = document.getElementById('broadcast-canvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; -const images = new Map(); +const assets = new Map(); function connect() { const socket = new SockJS('/ws'); @@ -12,37 +12,37 @@ function connect() { const body = JSON.parse(payload.body); handleEvent(body); }); - fetch(`/api/channels/${broadcaster}/images/visible`).then(r => r.json()).then(renderImages); + fetch(`/api/channels/${broadcaster}/assets/visible`).then(r => r.json()).then(renderAssets); }); } -function renderImages(list) { - list.forEach(img => images.set(img.id, img)); +function renderAssets(list) { + list.forEach(asset => assets.set(asset.id, asset)); draw(); } function handleEvent(event) { if (event.type === 'DELETED') { - images.delete(event.imageId); + assets.delete(event.assetId); } else if (event.payload && !event.payload.hidden) { - images.set(event.payload.id, event.payload); + assets.set(event.payload.id, event.payload); } else if (event.payload && event.payload.hidden) { - images.delete(event.payload.id); + assets.delete(event.payload.id); } draw(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); - images.forEach(img => { + assets.forEach(asset => { ctx.save(); ctx.globalAlpha = 1; - ctx.translate(img.x, img.y); - ctx.rotate(img.rotation * Math.PI / 180); + ctx.translate(asset.x, asset.y); + ctx.rotate(asset.rotation * Math.PI / 180); const image = new Image(); - image.src = img.url; + image.src = asset.url; image.onload = () => { - ctx.drawImage(image, 0, 0, img.width, img.height); + ctx.drawImage(image, 0, 0, asset.width, asset.height); }; ctx.restore(); }); diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index 5679697..96b961a 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -26,12 +26,12 @@
    -

    Images

    - - - - -
      +

      Assets

      + + + + +
        diff --git a/src/test/java/com/imgfloat/app/ChannelApiIntegrationTest.java b/src/test/java/com/imgfloat/app/ChannelApiIntegrationTest.java index fa2c66a..6d4f85a 100644 --- a/src/test/java/com/imgfloat/app/ChannelApiIntegrationTest.java +++ b/src/test/java/com/imgfloat/app/ChannelApiIntegrationTest.java @@ -1,7 +1,7 @@ package com.imgfloat.app; import com.fasterxml.jackson.databind.ObjectMapper; -import com.imgfloat.app.model.ImageRequest; +import com.imgfloat.app.model.AssetRequest; import com.imgfloat.app.model.VisibilityRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -33,7 +33,7 @@ class ChannelApiIntegrationTest { private ObjectMapper objectMapper; @Test - void broadcasterManagesAdminsAndImages() throws Exception { + void broadcasterManagesAdminsAndAssets() throws Exception { String broadcaster = "caster"; mockMvc.perform(post("/api/channels/{broadcaster}/admins", broadcaster) .contentType(MediaType.APPLICATION_JSON) @@ -41,39 +41,39 @@ class ChannelApiIntegrationTest { .with(oauth2Login().attributes(attrs -> attrs.put("preferred_username", broadcaster)))) .andExpect(status().isOk()); - ImageRequest request = new ImageRequest(); + AssetRequest request = new AssetRequest(); request.setUrl("https://example.com/image.png"); request.setWidth(300); request.setHeight(200); String body = objectMapper.writeValueAsString(request); - String imageId = objectMapper.readTree(mockMvc.perform(post("/api/channels/{broadcaster}/images", broadcaster) + String assetId = objectMapper.readTree(mockMvc.perform(post("/api/channels/{broadcaster}/assets", broadcaster) .contentType(MediaType.APPLICATION_JSON) .content(body) .with(oauth2Login().attributes(attrs -> attrs.put("preferred_username", broadcaster)))) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString()).get("id").asText(); - mockMvc.perform(get("/api/channels/{broadcaster}/images", broadcaster) + mockMvc.perform(get("/api/channels/{broadcaster}/assets", broadcaster) .with(oauth2Login().attributes(attrs -> attrs.put("preferred_username", broadcaster)))) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(1))); VisibilityRequest visibilityRequest = new VisibilityRequest(); visibilityRequest.setHidden(false); - mockMvc.perform(put("/api/channels/{broadcaster}/images/{id}/visibility", broadcaster, imageId) + mockMvc.perform(put("/api/channels/{broadcaster}/assets/{id}/visibility", broadcaster, assetId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(visibilityRequest)) .with(oauth2Login().attributes(attrs -> attrs.put("preferred_username", broadcaster)))) .andExpect(status().isOk()) .andExpect(jsonPath("$.hidden").value(false)); - mockMvc.perform(get("/api/channels/{broadcaster}/images/visible", broadcaster) + mockMvc.perform(get("/api/channels/{broadcaster}/assets/visible", broadcaster) .with(oauth2Login().attributes(attrs -> attrs.put("preferred_username", broadcaster)))) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(1))); - mockMvc.perform(delete("/api/channels/{broadcaster}/images/{id}", broadcaster, imageId) + mockMvc.perform(delete("/api/channels/{broadcaster}/assets/{id}", broadcaster, assetId) .with(oauth2Login().attributes(attrs -> attrs.put("preferred_username", broadcaster)))) .andExpect(status().isOk()); } diff --git a/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java b/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java index ae5f557..cb3636b 100644 --- a/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java +++ b/src/test/java/com/imgfloat/app/ChannelDirectoryServiceTest.java @@ -1,6 +1,6 @@ package com.imgfloat.app; -import com.imgfloat.app.model.ImageRequest; +import com.imgfloat.app.model.AssetRequest; import com.imgfloat.app.model.TransformRequest; import com.imgfloat.app.model.VisibilityRequest; import com.imgfloat.app.service.ChannelDirectoryService; @@ -26,13 +26,13 @@ class ChannelDirectoryServiceTest { } @Test - void createsImagesAndBroadcastsEvents() { - ImageRequest request = new ImageRequest(); + void createsAssetsAndBroadcastsEvents() { + AssetRequest request = new AssetRequest(); request.setUrl("https://example.com/image.png"); request.setWidth(1200); request.setHeight(800); - Optional created = service.createImage("caster", request); + Optional created = service.createAsset("caster", request); assertThat(created).isPresent(); ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); verify(messagingTemplate).convertAndSend(org.mockito.ArgumentMatchers.contains("/topic/channel/caster"), captor.capture()); @@ -40,13 +40,13 @@ class ChannelDirectoryServiceTest { @Test void updatesTransformAndVisibility() { - ImageRequest request = new ImageRequest(); + AssetRequest request = new AssetRequest(); request.setUrl("https://example.com/image.png"); request.setWidth(400); request.setHeight(300); String channel = "caster"; - String id = service.createImage(channel, request).orElseThrow().getId(); + String id = service.createAsset(channel, request).orElseThrow().getId(); TransformRequest transform = new TransformRequest(); transform.setX(10); diff --git a/src/test/resources/valid.env b/src/test/resources/valid.env index 82b3f5f..f347b4a 100644 --- a/src/test/resources/valid.env +++ b/src/test/resources/valid.env @@ -1,2 +1,3 @@ TWITCH_CLIENT_ID=test-client-id TWITCH_CLIENT_SECRET=test-client-secret +TWITCH_REDIRECT_URI=http://localhost:8080/login/oauth2/code/twitch