mirror of
https://github.com/imgfloat/server.git
synced 2026-05-08 10:19:35 +00:00
Extract ChannelSettingsService for canvas and script-overlay settings; add ChannelSettingsServiceTest
This commit is contained in:
@@ -17,6 +17,7 @@ import dev.kruhlmann.imgfloat.model.api.response.TwitchUserProfile;
|
|||||||
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
|
||||||
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
||||||
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
|
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
|
||||||
|
import dev.kruhlmann.imgfloat.service.ChannelSettingsService;
|
||||||
import dev.kruhlmann.imgfloat.service.TwitchUserLookupService;
|
import dev.kruhlmann.imgfloat.service.TwitchUserLookupService;
|
||||||
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
@@ -60,6 +61,7 @@ public class ChannelApiController {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ChannelApiController.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ChannelApiController.class);
|
||||||
private final ChannelDirectoryService channelDirectoryService;
|
private final ChannelDirectoryService channelDirectoryService;
|
||||||
|
private final ChannelSettingsService channelSettingsService;
|
||||||
private final OAuth2AuthorizedClientService authorizedClientService;
|
private final OAuth2AuthorizedClientService authorizedClientService;
|
||||||
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
|
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||||
private final TwitchUserLookupService twitchUserLookupService;
|
private final TwitchUserLookupService twitchUserLookupService;
|
||||||
@@ -67,12 +69,14 @@ public class ChannelApiController {
|
|||||||
|
|
||||||
public ChannelApiController(
|
public ChannelApiController(
|
||||||
ChannelDirectoryService channelDirectoryService,
|
ChannelDirectoryService channelDirectoryService,
|
||||||
|
ChannelSettingsService channelSettingsService,
|
||||||
OAuth2AuthorizedClientService authorizedClientService,
|
OAuth2AuthorizedClientService authorizedClientService,
|
||||||
OAuth2AuthorizedClientRepository authorizedClientRepository,
|
OAuth2AuthorizedClientRepository authorizedClientRepository,
|
||||||
TwitchUserLookupService twitchUserLookupService,
|
TwitchUserLookupService twitchUserLookupService,
|
||||||
AuthorizationService authorizationService
|
AuthorizationService authorizationService
|
||||||
) {
|
) {
|
||||||
this.channelDirectoryService = channelDirectoryService;
|
this.channelDirectoryService = channelDirectoryService;
|
||||||
|
this.channelSettingsService = channelSettingsService;
|
||||||
this.authorizedClientService = authorizedClientService;
|
this.authorizedClientService = authorizedClientService;
|
||||||
this.authorizedClientRepository = authorizedClientRepository;
|
this.authorizedClientRepository = authorizedClientRepository;
|
||||||
this.twitchUserLookupService = twitchUserLookupService;
|
this.twitchUserLookupService = twitchUserLookupService;
|
||||||
@@ -192,7 +196,7 @@ public class ChannelApiController {
|
|||||||
|
|
||||||
@GetMapping("/canvas")
|
@GetMapping("/canvas")
|
||||||
public CanvasSettingsRequest getCanvas(@PathVariable("broadcaster") String broadcaster) {
|
public CanvasSettingsRequest getCanvas(@PathVariable("broadcaster") String broadcaster) {
|
||||||
return channelDirectoryService.getCanvasSettings(broadcaster);
|
return channelSettingsService.getCanvasSettings(broadcaster);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/canvas")
|
@PutMapping("/canvas")
|
||||||
@@ -212,12 +216,12 @@ public class ChannelApiController {
|
|||||||
request.getWidth(),
|
request.getWidth(),
|
||||||
request.getHeight()
|
request.getHeight()
|
||||||
);
|
);
|
||||||
return channelDirectoryService.updateCanvasSettings(broadcaster, request, sessionUsername);
|
return channelSettingsService.updateCanvasSettings(broadcaster, request, sessionUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/settings")
|
@GetMapping("/settings")
|
||||||
public ChannelScriptSettingsRequest getScriptSettings(@PathVariable("broadcaster") String broadcaster) {
|
public ChannelScriptSettingsRequest getScriptSettings(@PathVariable("broadcaster") String broadcaster) {
|
||||||
return channelDirectoryService.getChannelScriptSettings(broadcaster);
|
return channelSettingsService.getChannelScriptSettings(broadcaster);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/settings")
|
@PutMapping("/settings")
|
||||||
@@ -231,7 +235,7 @@ public class ChannelApiController {
|
|||||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||||
LOG.info("Updating script settings for {} by {}", logBroadcaster, logSessionUsername);
|
LOG.info("Updating script settings for {} by {}", logBroadcaster, logSessionUsername);
|
||||||
return channelDirectoryService.updateChannelScriptSettings(broadcaster, request, sessionUsername);
|
return channelSettingsService.updateChannelScriptSettings(broadcaster, request, sessionUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/assets", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(value = "/assets", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;
|
|||||||
|
|
||||||
import dev.kruhlmann.imgfloat.model.AssetType;
|
import dev.kruhlmann.imgfloat.model.AssetType;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
||||||
@@ -14,7 +12,6 @@ import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
|
|||||||
import dev.kruhlmann.imgfloat.model.api.response.AssetEvent;
|
import dev.kruhlmann.imgfloat.model.api.response.AssetEvent;
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.AssetPatch;
|
import dev.kruhlmann.imgfloat.model.api.response.AssetPatch;
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
|
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.CanvasEvent;
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
|
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry;
|
import dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry;
|
||||||
import dev.kruhlmann.imgfloat.model.db.imgfloat.Asset;
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.Asset;
|
||||||
@@ -198,162 +195,6 @@ public class ChannelDirectoryService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CanvasSettingsRequest getCanvasSettings(String broadcaster) {
|
|
||||||
Channel channel = getOrCreateChannel(broadcaster);
|
|
||||||
return new CanvasSettingsRequest(
|
|
||||||
channel.getCanvasWidth(),
|
|
||||||
channel.getCanvasHeight(),
|
|
||||||
channel.getMaxVolumeDb()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CanvasSettingsRequest updateCanvasSettings(String broadcaster, CanvasSettingsRequest req, String actor) {
|
|
||||||
validateCanvasSettings(req);
|
|
||||||
Channel channel = getOrCreateChannel(broadcaster);
|
|
||||||
double beforeWidth = channel.getCanvasWidth();
|
|
||||||
double beforeHeight = channel.getCanvasHeight();
|
|
||||||
Double beforeMaxVolumeDb = channel.getMaxVolumeDb();
|
|
||||||
channel.setCanvasWidth(req.getWidth());
|
|
||||||
channel.setCanvasHeight(req.getHeight());
|
|
||||||
if (req.getMaxVolumeDb() != null) {
|
|
||||||
channel.setMaxVolumeDb(req.getMaxVolumeDb());
|
|
||||||
}
|
|
||||||
channelRepository.save(channel);
|
|
||||||
CanvasSettingsRequest response = new CanvasSettingsRequest(
|
|
||||||
channel.getCanvasWidth(),
|
|
||||||
channel.getCanvasHeight(),
|
|
||||||
channel.getMaxVolumeDb()
|
|
||||||
);
|
|
||||||
messagingTemplate.convertAndSend(topicFor(broadcaster), CanvasEvent.updated(broadcaster, response));
|
|
||||||
if (
|
|
||||||
beforeWidth != channel.getCanvasWidth() ||
|
|
||||||
beforeHeight != channel.getCanvasHeight() ||
|
|
||||||
!Objects.equals(beforeMaxVolumeDb, channel.getMaxVolumeDb())
|
|
||||||
) {
|
|
||||||
List<String> changes = new ArrayList<>();
|
|
||||||
if (beforeWidth != channel.getCanvasWidth() || beforeHeight != channel.getCanvasHeight()) {
|
|
||||||
changes.add(
|
|
||||||
String.format(
|
|
||||||
Locale.ROOT,
|
|
||||||
"canvas %.0fx%.0f -> %.0fx%.0f",
|
|
||||||
beforeWidth,
|
|
||||||
beforeHeight,
|
|
||||||
channel.getCanvasWidth(),
|
|
||||||
channel.getCanvasHeight()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!Objects.equals(beforeMaxVolumeDb, channel.getMaxVolumeDb())) {
|
|
||||||
changes.add(
|
|
||||||
String.format(
|
|
||||||
Locale.ROOT,
|
|
||||||
"max volume %.0f dB -> %.0f dB",
|
|
||||||
beforeMaxVolumeDb == null ? 0.0 : beforeMaxVolumeDb,
|
|
||||||
channel.getMaxVolumeDb() == null ? 0.0 : channel.getMaxVolumeDb()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
auditLogService.recordEntry(
|
|
||||||
channel.getBroadcaster(),
|
|
||||||
actor,
|
|
||||||
"CANVAS_UPDATED",
|
|
||||||
"Canvas settings updated" + (changes.isEmpty() ? "" : " (" + String.join(", ", changes) + ")")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateCanvasSettings(CanvasSettingsRequest req) {
|
|
||||||
Settings settings = settingsService.get();
|
|
||||||
int canvasMaxSizePixels = settings.getMaxCanvasSideLengthPixels();
|
|
||||||
|
|
||||||
if (
|
|
||||||
req.getWidth() <= 0 ||
|
|
||||||
req.getWidth() > canvasMaxSizePixels ||
|
|
||||||
!Double.isFinite(req.getWidth()) ||
|
|
||||||
req.getWidth() % 1 != 0
|
|
||||||
) throw new ResponseStatusException(
|
|
||||||
BAD_REQUEST,
|
|
||||||
"Canvas width must be a whole number within [1 to " + canvasMaxSizePixels + "]"
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
req.getHeight() <= 0 ||
|
|
||||||
req.getHeight() > canvasMaxSizePixels ||
|
|
||||||
!Double.isFinite(req.getHeight()) ||
|
|
||||||
req.getHeight() % 1 != 0
|
|
||||||
) throw new ResponseStatusException(
|
|
||||||
BAD_REQUEST,
|
|
||||||
"Canvas height must be a whole number within [1 to " + canvasMaxSizePixels + "]"
|
|
||||||
);
|
|
||||||
if (req.getMaxVolumeDb() != null) {
|
|
||||||
double maxVolumeDb = req.getMaxVolumeDb();
|
|
||||||
if (!Double.isFinite(maxVolumeDb) || maxVolumeDb < -60 || maxVolumeDb > 0) {
|
|
||||||
throw new ResponseStatusException(
|
|
||||||
BAD_REQUEST,
|
|
||||||
"Max volume must be within [-60 to 0] dB"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelScriptSettingsRequest getChannelScriptSettings(String broadcaster) {
|
|
||||||
Channel channel = getOrCreateChannel(broadcaster);
|
|
||||||
return new ChannelScriptSettingsRequest(
|
|
||||||
channel.isAllowChannelEmotesForAssets(),
|
|
||||||
channel.isAllowSevenTvEmotesForAssets(),
|
|
||||||
channel.isAllowScriptChatAccess()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelScriptSettingsRequest updateChannelScriptSettings(
|
|
||||||
String broadcaster,
|
|
||||||
ChannelScriptSettingsRequest request,
|
|
||||||
String actor
|
|
||||||
) {
|
|
||||||
Channel channel = getOrCreateChannel(broadcaster);
|
|
||||||
boolean beforeChannelEmotes = channel.isAllowChannelEmotesForAssets();
|
|
||||||
boolean beforeSevenTv = channel.isAllowSevenTvEmotesForAssets();
|
|
||||||
boolean beforeChatAccess = channel.isAllowScriptChatAccess();
|
|
||||||
channel.setAllowChannelEmotesForAssets(request.isAllowChannelEmotesForAssets());
|
|
||||||
channel.setAllowSevenTvEmotesForAssets(request.isAllowSevenTvEmotesForAssets());
|
|
||||||
channel.setAllowScriptChatAccess(request.isAllowScriptChatAccess());
|
|
||||||
channelRepository.save(channel);
|
|
||||||
if (
|
|
||||||
beforeChannelEmotes != channel.isAllowChannelEmotesForAssets() ||
|
|
||||||
beforeSevenTv != channel.isAllowSevenTvEmotesForAssets() ||
|
|
||||||
beforeChatAccess != channel.isAllowScriptChatAccess()
|
|
||||||
) {
|
|
||||||
List<String> changes = new ArrayList<>();
|
|
||||||
if (beforeChannelEmotes != channel.isAllowChannelEmotesForAssets()) {
|
|
||||||
changes.add(
|
|
||||||
"channelEmotes: " + beforeChannelEmotes + " -> " + channel.isAllowChannelEmotesForAssets()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (beforeSevenTv != channel.isAllowSevenTvEmotesForAssets()) {
|
|
||||||
changes.add(
|
|
||||||
"sevenTvEmotes: " + beforeSevenTv + " -> " + channel.isAllowSevenTvEmotesForAssets()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (beforeChatAccess != channel.isAllowScriptChatAccess()) {
|
|
||||||
changes.add(
|
|
||||||
"scriptChatAccess: " + beforeChatAccess + " -> " + channel.isAllowScriptChatAccess()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
String detailSuffix = changes.isEmpty() ? "" : " (" + String.join(", ", changes) + ")";
|
|
||||||
auditLogService.recordEntry(
|
|
||||||
channel.getBroadcaster(),
|
|
||||||
actor,
|
|
||||||
"SCRIPT_SETTINGS_UPDATED",
|
|
||||||
"Script settings updated" + detailSuffix
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new ChannelScriptSettingsRequest(
|
|
||||||
channel.isAllowChannelEmotesForAssets(),
|
|
||||||
channel.isAllowSevenTvEmotesForAssets(),
|
|
||||||
channel.isAllowScriptChatAccess()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = IOException.class)
|
@Transactional(rollbackFor = IOException.class)
|
||||||
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file, String actor) throws IOException {
|
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file, String actor) throws IOException {
|
||||||
long fileSize = file.getSize();
|
long fileSize = file.getSize();
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package dev.kruhlmann.imgfloat.service;
|
||||||
|
|
||||||
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
|
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.response.CanvasEvent;
|
||||||
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.Channel;
|
||||||
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.Settings;
|
||||||
|
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages per-channel canvas and script-overlay settings.
|
||||||
|
* Extracted from {@link ChannelDirectoryService} to give it a focused responsibility.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ChannelSettingsService {
|
||||||
|
|
||||||
|
private final ChannelRepository channelRepository;
|
||||||
|
private final SettingsService settingsService;
|
||||||
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
|
private final AuditLogService auditLogService;
|
||||||
|
|
||||||
|
public ChannelSettingsService(
|
||||||
|
ChannelRepository channelRepository,
|
||||||
|
SettingsService settingsService,
|
||||||
|
SimpMessagingTemplate messagingTemplate,
|
||||||
|
AuditLogService auditLogService
|
||||||
|
) {
|
||||||
|
this.channelRepository = channelRepository;
|
||||||
|
this.settingsService = settingsService;
|
||||||
|
this.messagingTemplate = messagingTemplate;
|
||||||
|
this.auditLogService = auditLogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CanvasSettingsRequest getCanvasSettings(String broadcaster) {
|
||||||
|
Channel channel = getOrCreateChannel(broadcaster);
|
||||||
|
return new CanvasSettingsRequest(
|
||||||
|
channel.getCanvasWidth(),
|
||||||
|
channel.getCanvasHeight(),
|
||||||
|
channel.getMaxVolumeDb()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CanvasSettingsRequest updateCanvasSettings(String broadcaster, CanvasSettingsRequest req, String actor) {
|
||||||
|
validateCanvasSettings(req);
|
||||||
|
Channel channel = getOrCreateChannel(broadcaster);
|
||||||
|
double beforeWidth = channel.getCanvasWidth();
|
||||||
|
double beforeHeight = channel.getCanvasHeight();
|
||||||
|
Double beforeMaxVolumeDb = channel.getMaxVolumeDb();
|
||||||
|
|
||||||
|
channel.setCanvasWidth(req.getWidth());
|
||||||
|
channel.setCanvasHeight(req.getHeight());
|
||||||
|
if (req.getMaxVolumeDb() != null) {
|
||||||
|
channel.setMaxVolumeDb(req.getMaxVolumeDb());
|
||||||
|
}
|
||||||
|
channelRepository.save(channel);
|
||||||
|
|
||||||
|
CanvasSettingsRequest response = new CanvasSettingsRequest(
|
||||||
|
channel.getCanvasWidth(),
|
||||||
|
channel.getCanvasHeight(),
|
||||||
|
channel.getMaxVolumeDb()
|
||||||
|
);
|
||||||
|
messagingTemplate.convertAndSend(topicFor(broadcaster), CanvasEvent.updated(broadcaster, response));
|
||||||
|
|
||||||
|
boolean changed =
|
||||||
|
beforeWidth != channel.getCanvasWidth() ||
|
||||||
|
beforeHeight != channel.getCanvasHeight() ||
|
||||||
|
!Objects.equals(beforeMaxVolumeDb, channel.getMaxVolumeDb());
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
List<String> changes = new ArrayList<>();
|
||||||
|
if (beforeWidth != channel.getCanvasWidth() || beforeHeight != channel.getCanvasHeight()) {
|
||||||
|
changes.add(
|
||||||
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
|
"canvas %.0fx%.0f -> %.0fx%.0f",
|
||||||
|
beforeWidth,
|
||||||
|
beforeHeight,
|
||||||
|
channel.getCanvasWidth(),
|
||||||
|
channel.getCanvasHeight()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!Objects.equals(beforeMaxVolumeDb, channel.getMaxVolumeDb())) {
|
||||||
|
changes.add(
|
||||||
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
|
"max volume %.0f dB -> %.0f dB",
|
||||||
|
beforeMaxVolumeDb == null ? 0.0 : beforeMaxVolumeDb,
|
||||||
|
channel.getMaxVolumeDb() == null ? 0.0 : channel.getMaxVolumeDb()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
auditLogService.recordEntry(
|
||||||
|
channel.getBroadcaster(),
|
||||||
|
actor,
|
||||||
|
"CANVAS_UPDATED",
|
||||||
|
"Canvas settings updated" + (changes.isEmpty() ? "" : " (" + String.join(", ", changes) + ")")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelScriptSettingsRequest getChannelScriptSettings(String broadcaster) {
|
||||||
|
Channel channel = getOrCreateChannel(broadcaster);
|
||||||
|
return new ChannelScriptSettingsRequest(
|
||||||
|
channel.isAllowChannelEmotesForAssets(),
|
||||||
|
channel.isAllowSevenTvEmotesForAssets(),
|
||||||
|
channel.isAllowScriptChatAccess()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelScriptSettingsRequest updateChannelScriptSettings(
|
||||||
|
String broadcaster,
|
||||||
|
ChannelScriptSettingsRequest request,
|
||||||
|
String actor
|
||||||
|
) {
|
||||||
|
Channel channel = getOrCreateChannel(broadcaster);
|
||||||
|
boolean beforeChannelEmotes = channel.isAllowChannelEmotesForAssets();
|
||||||
|
boolean beforeSevenTv = channel.isAllowSevenTvEmotesForAssets();
|
||||||
|
boolean beforeChatAccess = channel.isAllowScriptChatAccess();
|
||||||
|
|
||||||
|
channel.setAllowChannelEmotesForAssets(request.isAllowChannelEmotesForAssets());
|
||||||
|
channel.setAllowSevenTvEmotesForAssets(request.isAllowSevenTvEmotesForAssets());
|
||||||
|
channel.setAllowScriptChatAccess(request.isAllowScriptChatAccess());
|
||||||
|
channelRepository.save(channel);
|
||||||
|
|
||||||
|
boolean changed =
|
||||||
|
beforeChannelEmotes != channel.isAllowChannelEmotesForAssets() ||
|
||||||
|
beforeSevenTv != channel.isAllowSevenTvEmotesForAssets() ||
|
||||||
|
beforeChatAccess != channel.isAllowScriptChatAccess();
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
List<String> changes = new ArrayList<>();
|
||||||
|
if (beforeChannelEmotes != channel.isAllowChannelEmotesForAssets()) {
|
||||||
|
changes.add("channelEmotes: " + beforeChannelEmotes + " -> " + channel.isAllowChannelEmotesForAssets());
|
||||||
|
}
|
||||||
|
if (beforeSevenTv != channel.isAllowSevenTvEmotesForAssets()) {
|
||||||
|
changes.add("sevenTvEmotes: " + beforeSevenTv + " -> " + channel.isAllowSevenTvEmotesForAssets());
|
||||||
|
}
|
||||||
|
if (beforeChatAccess != channel.isAllowScriptChatAccess()) {
|
||||||
|
changes.add("scriptChatAccess: " + beforeChatAccess + " -> " + channel.isAllowScriptChatAccess());
|
||||||
|
}
|
||||||
|
auditLogService.recordEntry(
|
||||||
|
channel.getBroadcaster(),
|
||||||
|
actor,
|
||||||
|
"SCRIPT_SETTINGS_UPDATED",
|
||||||
|
"Script settings updated" + (changes.isEmpty() ? "" : " (" + String.join(", ", changes) + ")")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new ChannelScriptSettingsRequest(
|
||||||
|
channel.isAllowChannelEmotesForAssets(),
|
||||||
|
channel.isAllowSevenTvEmotesForAssets(),
|
||||||
|
channel.isAllowScriptChatAccess()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateCanvasSettings(CanvasSettingsRequest req) {
|
||||||
|
Settings settings = settingsService.get();
|
||||||
|
int max = settings.getMaxCanvasSideLengthPixels();
|
||||||
|
|
||||||
|
if (req.getWidth() <= 0 || req.getWidth() > max || !Double.isFinite(req.getWidth()) || req.getWidth() % 1 != 0) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
|
BAD_REQUEST,
|
||||||
|
"Canvas width must be a whole number within [1 to " + max + "]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (req.getHeight() <= 0 || req.getHeight() > max || !Double.isFinite(req.getHeight()) || req.getHeight() % 1 != 0) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
|
BAD_REQUEST,
|
||||||
|
"Canvas height must be a whole number within [1 to " + max + "]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (req.getMaxVolumeDb() != null) {
|
||||||
|
double db = req.getMaxVolumeDb();
|
||||||
|
if (!Double.isFinite(db) || db < -60 || db > 0) {
|
||||||
|
throw new ResponseStatusException(BAD_REQUEST, "Max volume must be within [-60 to 0] dB");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Channel getOrCreateChannel(String broadcaster) {
|
||||||
|
String normalized = normalize(broadcaster);
|
||||||
|
return channelRepository.findById(normalized)
|
||||||
|
.orElseGet(() -> channelRepository.save(new Channel(normalized)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String topicFor(String broadcaster) {
|
||||||
|
return "/topic/channel/" + normalize(broadcaster);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String value) {
|
||||||
|
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
import dev.kruhlmann.imgfloat.model.AssetType;
|
import dev.kruhlmann.imgfloat.model.AssetType;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
|
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
|
||||||
@@ -247,17 +246,6 @@ class ChannelDirectoryServiceTest {
|
|||||||
assertThat(saved.allowedDomains()).isEmpty();
|
assertThat(saved.allowedDomains()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void updatesCanvasMaxVolumeDb() {
|
|
||||||
CanvasSettingsRequest request = new CanvasSettingsRequest(1920, 1080, -12.0);
|
|
||||||
|
|
||||||
CanvasSettingsRequest saved = service.updateCanvasSettings("caster", request, "caster");
|
|
||||||
|
|
||||||
assertThat(saved.getMaxVolumeDb()).isEqualTo(-12.0);
|
|
||||||
Channel channel = channelRepository.findById("caster").orElseThrow();
|
|
||||||
assertThat(channel.getMaxVolumeDb()).isEqualTo(-12.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] samplePng() throws IOException {
|
private byte[] samplePng() throws IOException {
|
||||||
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
|
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package dev.kruhlmann.imgfloat.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
||||||
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.Channel;
|
||||||
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.Settings;
|
||||||
|
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
class ChannelSettingsServiceTest {
|
||||||
|
|
||||||
|
private ChannelRepository channelRepository;
|
||||||
|
private SettingsService settingsService;
|
||||||
|
private SimpMessagingTemplate messagingTemplate;
|
||||||
|
private AuditLogService auditLogService;
|
||||||
|
private ChannelSettingsService service;
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, Channel> channels = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
channelRepository = mock(ChannelRepository.class);
|
||||||
|
settingsService = mock(SettingsService.class);
|
||||||
|
messagingTemplate = mock(SimpMessagingTemplate.class);
|
||||||
|
auditLogService = mock(AuditLogService.class);
|
||||||
|
|
||||||
|
when(settingsService.get()).thenReturn(Settings.defaults());
|
||||||
|
when(channelRepository.findById(anyString())).thenAnswer(inv ->
|
||||||
|
Optional.ofNullable(channels.get(inv.getArgument(0))));
|
||||||
|
when(channelRepository.save(any(Channel.class))).thenAnswer(inv -> {
|
||||||
|
Channel ch = inv.getArgument(0);
|
||||||
|
channels.put(ch.getBroadcaster(), ch);
|
||||||
|
return ch;
|
||||||
|
});
|
||||||
|
|
||||||
|
service = new ChannelSettingsService(channelRepository, settingsService, messagingTemplate, auditLogService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- canvas settings ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCanvasSettingsReturnsDefaults() {
|
||||||
|
CanvasSettingsRequest result = service.getCanvasSettings("caster");
|
||||||
|
assertThat(result.getWidth()).isGreaterThan(0);
|
||||||
|
assertThat(result.getHeight()).isGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateCanvasPersistsAndBroadcasts() {
|
||||||
|
CanvasSettingsRequest req = new CanvasSettingsRequest(1920, 1080, -12.0);
|
||||||
|
CanvasSettingsRequest result = service.updateCanvasSettings("caster", req, "caster");
|
||||||
|
|
||||||
|
assertThat(result.getWidth()).isEqualTo(1920);
|
||||||
|
assertThat(result.getHeight()).isEqualTo(1080);
|
||||||
|
assertThat(result.getMaxVolumeDb()).isEqualTo(-12.0);
|
||||||
|
verify(messagingTemplate).convertAndSend(anyString(), any(Object.class));
|
||||||
|
Channel channel = channels.get("caster");
|
||||||
|
assertThat(channel.getMaxVolumeDb()).isEqualTo(-12.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateCanvasRecordsAuditEntry() {
|
||||||
|
service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, null), "admin");
|
||||||
|
verify(auditLogService).recordEntry(eq("caster"), eq("admin"), eq("CANVAS_UPDATED"), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateCanvasNoAuditEntryWhenNothingChanged() {
|
||||||
|
service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, null), "admin");
|
||||||
|
clearInvocations(auditLogService);
|
||||||
|
// Second call with same values — nothing changed, no audit
|
||||||
|
service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, null), "admin");
|
||||||
|
verify(auditLogService, never()).recordEntry(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateCanvasRejectsZeroWidth() {
|
||||||
|
assertThatThrownBy(() -> service.updateCanvasSettings("caster", new CanvasSettingsRequest(0, 720, null), "admin"))
|
||||||
|
.isInstanceOf(ResponseStatusException.class)
|
||||||
|
.hasMessageContaining("width");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateCanvasRejectsInvalidVolumeDb() {
|
||||||
|
assertThatThrownBy(() -> service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, 5.0), "admin"))
|
||||||
|
.isInstanceOf(ResponseStatusException.class)
|
||||||
|
.hasMessageContaining("volume");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateCanvasAcceptsBoundaryVolumeValues() {
|
||||||
|
// -60 and 0 are the inclusive bounds
|
||||||
|
assertThat(service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, -60.0), "admin")
|
||||||
|
.getMaxVolumeDb()).isEqualTo(-60.0);
|
||||||
|
assertThat(service.updateCanvasSettings("caster", new CanvasSettingsRequest(1280, 720, 0.0), "admin")
|
||||||
|
.getMaxVolumeDb()).isEqualTo(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- script settings ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getScriptSettingsReturnsDefaults() {
|
||||||
|
ChannelScriptSettingsRequest result = service.getChannelScriptSettings("caster");
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateScriptSettingsPersistsChanges() {
|
||||||
|
ChannelScriptSettingsRequest req = new ChannelScriptSettingsRequest(true, false, true);
|
||||||
|
ChannelScriptSettingsRequest result = service.updateChannelScriptSettings("caster", req, "admin");
|
||||||
|
|
||||||
|
assertThat(result.isAllowChannelEmotesForAssets()).isTrue();
|
||||||
|
assertThat(result.isAllowSevenTvEmotesForAssets()).isFalse();
|
||||||
|
assertThat(result.isAllowScriptChatAccess()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateScriptSettingsRecordsAuditEntry() {
|
||||||
|
service.updateChannelScriptSettings("caster", new ChannelScriptSettingsRequest(true, false, true), "admin");
|
||||||
|
verify(auditLogService).recordEntry(eq("caster"), eq("admin"), eq("SCRIPT_SETTINGS_UPDATED"), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateScriptSettingsNoAuditWhenNothingChanged() {
|
||||||
|
service.updateChannelScriptSettings("caster", new ChannelScriptSettingsRequest(false, false, false), "admin");
|
||||||
|
clearInvocations(auditLogService);
|
||||||
|
service.updateChannelScriptSettings("caster", new ChannelScriptSettingsRequest(false, false, false), "admin");
|
||||||
|
verify(auditLogService, never()).recordEntry(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user