mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Add audit log
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package dev.kruhlmann.imgfloat.controller;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.AuditLogEntryView;
|
||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||
import dev.kruhlmann.imgfloat.service.AuditLogService;
|
||||
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
||||
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/channels/{broadcaster}/audit")
|
||||
@SecurityRequirement(name = "twitchOAuth")
|
||||
public class AuditLogApiController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AuditLogApiController.class);
|
||||
private final AuditLogService auditLogService;
|
||||
private final AuthorizationService authorizationService;
|
||||
|
||||
public AuditLogApiController(AuditLogService auditLogService, AuthorizationService authorizationService) {
|
||||
this.auditLogService = auditLogService;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<AuditLogEntryView> listAuditEntries(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||
LOG.info(
|
||||
"Listing audit log entries for {} by {}",
|
||||
LogSanitizer.sanitize(broadcaster),
|
||||
LogSanitizer.sanitize(sessionUsername)
|
||||
);
|
||||
return auditLogService.listEntries(broadcaster);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public class ChannelApiController {
|
||||
String logRequestUsername = LogSanitizer.sanitize(request.getUsername());
|
||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||
LOG.info("User {} adding admin {} to {}", logSessionUsername, logRequestUsername, logBroadcaster);
|
||||
boolean added = channelDirectoryService.addAdmin(broadcaster, request.getUsername());
|
||||
boolean added = channelDirectoryService.addAdmin(broadcaster, request.getUsername(), sessionUsername);
|
||||
if (!added) {
|
||||
LOG.info("User {} already admin for {} or could not be added", logRequestUsername, logBroadcaster);
|
||||
}
|
||||
@@ -171,7 +171,7 @@ public class ChannelApiController {
|
||||
String logUsername = LogSanitizer.sanitize(username);
|
||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||
LOG.info("User {} removing admin {} from {}", logSessionUsername, logUsername, logBroadcaster);
|
||||
boolean removed = channelDirectoryService.removeAdmin(broadcaster, username);
|
||||
boolean removed = channelDirectoryService.removeAdmin(broadcaster, username, sessionUsername);
|
||||
return ResponseEntity.ok(removed);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ public class ChannelApiController {
|
||||
request.getWidth(),
|
||||
request.getHeight()
|
||||
);
|
||||
return channelDirectoryService.updateCanvasSettings(broadcaster, request);
|
||||
return channelDirectoryService.updateCanvasSettings(broadcaster, request, sessionUsername);
|
||||
}
|
||||
|
||||
@GetMapping("/settings")
|
||||
@@ -226,7 +226,7 @@ public class ChannelApiController {
|
||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||
LOG.info("Updating script settings for {} by {}", logBroadcaster, logSessionUsername);
|
||||
return channelDirectoryService.updateChannelScriptSettings(broadcaster, request);
|
||||
return channelDirectoryService.updateChannelScriptSettings(broadcaster, request, sessionUsername);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/assets", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@@ -250,7 +250,7 @@ public class ChannelApiController {
|
||||
String logOriginalFilename = LogSanitizer.sanitize(file.getOriginalFilename());
|
||||
LOG.info("User {} uploading asset {} to {}", logSessionUsername, logOriginalFilename, logBroadcaster);
|
||||
return channelDirectoryService
|
||||
.createAsset(broadcaster, file)
|
||||
.createAsset(broadcaster, file, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to read image"));
|
||||
} catch (IOException e) {
|
||||
@@ -274,7 +274,7 @@ public class ChannelApiController {
|
||||
);
|
||||
LOG.info("Creating custom script for {} by {}", logBroadcaster, logSessionUsername);
|
||||
return channelDirectoryService
|
||||
.createCodeAsset(broadcaster, request)
|
||||
.createCodeAsset(broadcaster, request, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to save custom script"));
|
||||
}
|
||||
@@ -296,7 +296,7 @@ public class ChannelApiController {
|
||||
);
|
||||
LOG.info("Updating custom script {} for {} by {}", logAssetId, logBroadcaster, logSessionUsername);
|
||||
return channelDirectoryService
|
||||
.updateCodeAsset(broadcaster, assetId, request)
|
||||
.updateCodeAsset(broadcaster, assetId, request, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
|
||||
}
|
||||
@@ -318,7 +318,7 @@ public class ChannelApiController {
|
||||
);
|
||||
LOG.debug("Applying transform to asset {} on {} by {}", logAssetId, logBroadcaster, logSessionUsername);
|
||||
return channelDirectoryService
|
||||
.updateTransform(broadcaster, assetId, request)
|
||||
.updateTransform(broadcaster, assetId, request, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> {
|
||||
LOG.warn(
|
||||
@@ -348,7 +348,7 @@ public class ChannelApiController {
|
||||
);
|
||||
LOG.info("Triggering playback for asset {} on {} by {}", logAssetId, logBroadcaster, logSessionUsername);
|
||||
return channelDirectoryService
|
||||
.triggerPlayback(broadcaster, assetId, request)
|
||||
.triggerPlayback(broadcaster, assetId, request, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Asset not found"));
|
||||
}
|
||||
@@ -376,7 +376,7 @@ public class ChannelApiController {
|
||||
request.isHidden()
|
||||
);
|
||||
return channelDirectoryService
|
||||
.updateVisibility(broadcaster, assetId, request)
|
||||
.updateVisibility(broadcaster, assetId, request, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> {
|
||||
LOG.warn(
|
||||
@@ -502,7 +502,7 @@ public class ChannelApiController {
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
boolean removed = channelDirectoryService.deleteAsset(assetId);
|
||||
boolean removed = channelDirectoryService.deleteAsset(assetId, sessionUsername);
|
||||
if (!removed) {
|
||||
LOG.warn("Attempt to delete missing asset {} on {} by {}", logAssetId, logBroadcaster, logSessionUsername);
|
||||
throw createAsset404();
|
||||
@@ -542,7 +542,7 @@ public class ChannelApiController {
|
||||
}
|
||||
try {
|
||||
return channelDirectoryService
|
||||
.createScriptAttachment(broadcaster, assetId, file)
|
||||
.createScriptAttachment(broadcaster, assetId, file, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to save attachment"));
|
||||
} catch (IOException e) {
|
||||
@@ -568,7 +568,7 @@ public class ChannelApiController {
|
||||
}
|
||||
try {
|
||||
return channelDirectoryService
|
||||
.updateScriptLogo(broadcaster, assetId, file)
|
||||
.updateScriptLogo(broadcaster, assetId, file, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to save logo"));
|
||||
} catch (IOException e) {
|
||||
@@ -588,7 +588,7 @@ public class ChannelApiController {
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
channelDirectoryService.clearScriptLogo(broadcaster, assetId);
|
||||
channelDirectoryService.clearScriptLogo(broadcaster, assetId, sessionUsername);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -604,7 +604,7 @@ public class ChannelApiController {
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
boolean removed = channelDirectoryService.deleteScriptAttachment(broadcaster, assetId, attachmentId);
|
||||
boolean removed = channelDirectoryService.deleteScriptAttachment(broadcaster, assetId, attachmentId, sessionUsername);
|
||||
if (!removed) {
|
||||
throw createAsset404();
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public class ScriptMarketplaceController {
|
||||
String logTarget = LogSanitizer.sanitize(request.getTargetBroadcaster());
|
||||
LOG.info("Importing marketplace script {} into {}", logScriptId, logTarget);
|
||||
return channelDirectoryService
|
||||
.importMarketplaceScript(request.getTargetBroadcaster(), scriptId)
|
||||
.importMarketplaceScript(request.getTargetBroadcaster(), scriptId, sessionUsername)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to import script"));
|
||||
}
|
||||
|
||||
@@ -160,6 +160,24 @@ public class ViewController {
|
||||
return "admin";
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.GetMapping("/view/{broadcaster}/audit")
|
||||
public String auditLogView(
|
||||
@org.springframework.web.bind.annotation.PathVariable("broadcaster") String broadcaster,
|
||||
OAuth2AuthenticationToken oauthToken,
|
||||
Model model
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||
LOG.info("Rendering audit log for {} by {}", logBroadcaster, logSessionUsername);
|
||||
model.addAttribute("broadcaster", broadcaster.toLowerCase());
|
||||
model.addAttribute("username", sessionUsername);
|
||||
addStagingAttribute(model);
|
||||
addVersionAttributes(model);
|
||||
return "audit-log";
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.GetMapping("/view/{broadcaster}/broadcast")
|
||||
public String broadcastView(
|
||||
@org.springframework.web.bind.annotation.PathVariable("broadcaster") String broadcaster,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "channel_audit_log")
|
||||
public class AuditLogEntry {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String broadcaster;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String action;
|
||||
|
||||
@Column
|
||||
private String actor;
|
||||
|
||||
@Column
|
||||
private String details;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
public AuditLogEntry() {}
|
||||
|
||||
public AuditLogEntry(String broadcaster, String actor, String action, String details) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.broadcaster = normalize(broadcaster);
|
||||
this.actor = normalize(actor);
|
||||
this.action = action == null || action.isBlank() ? "UNKNOWN" : action;
|
||||
this.details = details;
|
||||
this.createdAt = Instant.now();
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
public void prepare() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID().toString();
|
||||
}
|
||||
if (createdAt == null) {
|
||||
createdAt = Instant.now();
|
||||
}
|
||||
broadcaster = normalize(broadcaster);
|
||||
actor = normalize(actor);
|
||||
if (action == null || action.isBlank()) {
|
||||
action = "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getBroadcaster() {
|
||||
return broadcaster;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public String getActor() {
|
||||
return actor;
|
||||
}
|
||||
|
||||
public String getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record AuditLogEntryView(String id, String actor, String action, String details, Instant createdAt) {
|
||||
public static AuditLogEntryView fromEntry(AuditLogEntry entry) {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
return new AuditLogEntryView(
|
||||
entry.getId(),
|
||||
entry.getActor(),
|
||||
entry.getAction(),
|
||||
entry.getDetails(),
|
||||
entry.getCreatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package dev.kruhlmann.imgfloat.repository;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.AuditLogEntry;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface AuditLogRepository extends JpaRepository<AuditLogEntry, String> {
|
||||
List<AuditLogEntry> findTop200ByBroadcasterOrderByCreatedAtDesc(String broadcaster);
|
||||
|
||||
void deleteByBroadcaster(String broadcaster);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public class AccountService {
|
||||
private final SystemAdministratorRepository systemAdministratorRepository;
|
||||
private final AssetStorageService assetStorageService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
public AccountService(
|
||||
ChannelDirectoryService channelDirectoryService,
|
||||
@@ -37,7 +38,8 @@ public class AccountService {
|
||||
MarketplaceScriptHeartRepository marketplaceScriptHeartRepository,
|
||||
SystemAdministratorRepository systemAdministratorRepository,
|
||||
AssetStorageService assetStorageService,
|
||||
JdbcTemplate jdbcTemplate
|
||||
JdbcTemplate jdbcTemplate,
|
||||
AuditLogService auditLogService
|
||||
) {
|
||||
this.channelDirectoryService = channelDirectoryService;
|
||||
this.assetRepository = assetRepository;
|
||||
@@ -47,6 +49,7 @@ public class AccountService {
|
||||
this.systemAdministratorRepository = systemAdministratorRepository;
|
||||
this.assetStorageService = assetStorageService;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -61,7 +64,7 @@ public class AccountService {
|
||||
.stream()
|
||||
.map(Asset::getId)
|
||||
.toList();
|
||||
assetIds.forEach(channelDirectoryService::deleteAsset);
|
||||
assetIds.forEach((assetId) -> channelDirectoryService.deleteAsset(assetId, normalized));
|
||||
|
||||
List<ScriptAssetFile> scriptFiles = scriptAssetFileRepository.findByBroadcaster(normalized);
|
||||
scriptFiles.forEach(this::deleteScriptAssetFile);
|
||||
@@ -69,6 +72,7 @@ public class AccountService {
|
||||
marketplaceScriptHeartRepository.deleteByUsername(normalized);
|
||||
systemAdministratorRepository.deleteByTwitchUsername(normalized);
|
||||
channelRepository.deleteById(normalized);
|
||||
auditLogService.deleteEntriesForBroadcaster(normalized);
|
||||
|
||||
deleteSessionsForUser(normalized);
|
||||
LOG.info("Account data deleted for {}", normalized);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package dev.kruhlmann.imgfloat.service;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.AuditLogEntry;
|
||||
import dev.kruhlmann.imgfloat.model.AuditLogEntryView;
|
||||
import dev.kruhlmann.imgfloat.repository.AuditLogRepository;
|
||||
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AuditLogService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AuditLogService.class);
|
||||
private static final String DEFAULT_ACTOR = "system";
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
|
||||
public AuditLogService(AuditLogRepository auditLogRepository) {
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
}
|
||||
|
||||
public void recordEntry(String broadcaster, String actor, String action, String details) {
|
||||
String normalizedBroadcaster = normalize(broadcaster);
|
||||
if (normalizedBroadcaster == null || normalizedBroadcaster.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String normalizedActor = normalize(actor);
|
||||
if (normalizedActor == null || normalizedActor.isBlank()) {
|
||||
normalizedActor = DEFAULT_ACTOR;
|
||||
}
|
||||
try {
|
||||
AuditLogEntry entry = new AuditLogEntry(normalizedBroadcaster, normalizedActor, action, details);
|
||||
auditLogRepository.save(entry);
|
||||
LOG.info(
|
||||
"Audit log entry created for broadcaster {} by {}: {}",
|
||||
LogSanitizer.sanitize(normalizedBroadcaster),
|
||||
LogSanitizer.sanitize(normalizedActor),
|
||||
action
|
||||
);
|
||||
} catch (DataAccessException ex) {
|
||||
LOG.warn(
|
||||
"Unable to save audit log entry for broadcaster {} by {}",
|
||||
LogSanitizer.sanitize(normalizedBroadcaster),
|
||||
LogSanitizer.sanitize(normalizedActor),
|
||||
ex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public List<AuditLogEntryView> listEntries(String broadcaster) {
|
||||
String normalizedBroadcaster = normalize(broadcaster);
|
||||
if (normalizedBroadcaster == null || normalizedBroadcaster.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return auditLogRepository
|
||||
.findTop200ByBroadcasterOrderByCreatedAtDesc(normalizedBroadcaster)
|
||||
.stream()
|
||||
.map(AuditLogEntryView::fromEntry)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void deleteEntriesForBroadcaster(String broadcaster) {
|
||||
String normalizedBroadcaster = normalize(broadcaster);
|
||||
if (normalizedBroadcaster == null || normalizedBroadcaster.isBlank()) {
|
||||
return;
|
||||
}
|
||||
auditLogRepository.deleteByBroadcaster(normalizedBroadcaster);
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ public class ChannelDirectoryService {
|
||||
private final SettingsService settingsService;
|
||||
private final long uploadLimitBytes;
|
||||
private final MarketplaceScriptSeedLoader marketplaceScriptSeedLoader;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
@Autowired
|
||||
public ChannelDirectoryService(
|
||||
@@ -94,7 +95,8 @@ public class ChannelDirectoryService {
|
||||
MediaOptimizationService mediaOptimizationService,
|
||||
SettingsService settingsService,
|
||||
long uploadLimitBytes,
|
||||
MarketplaceScriptSeedLoader marketplaceScriptSeedLoader
|
||||
MarketplaceScriptSeedLoader marketplaceScriptSeedLoader,
|
||||
AuditLogService auditLogService
|
||||
) {
|
||||
this.channelRepository = channelRepository;
|
||||
this.assetRepository = assetRepository;
|
||||
@@ -111,6 +113,7 @@ public class ChannelDirectoryService {
|
||||
this.settingsService = settingsService;
|
||||
this.uploadLimitBytes = uploadLimitBytes;
|
||||
this.marketplaceScriptSeedLoader = marketplaceScriptSeedLoader;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
public Channel getOrCreateChannel(String broadcaster) {
|
||||
@@ -127,22 +130,36 @@ public class ChannelDirectoryService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
public boolean addAdmin(String broadcaster, String username) {
|
||||
public boolean addAdmin(String broadcaster, String username, String actor) {
|
||||
Channel channel = getOrCreateChannel(broadcaster);
|
||||
boolean added = channel.addAdmin(username);
|
||||
String normalizedUsername = normalize(username);
|
||||
boolean added = channel.addAdmin(normalizedUsername);
|
||||
if (added) {
|
||||
channelRepository.saveAndFlush(channel);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), "Admin added: " + username);
|
||||
auditLogService.recordEntry(
|
||||
channel.getBroadcaster(),
|
||||
actor,
|
||||
"ADMIN_ADDED",
|
||||
"Added admin " + normalizedUsername
|
||||
);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
public boolean removeAdmin(String broadcaster, String username) {
|
||||
public boolean removeAdmin(String broadcaster, String username, String actor) {
|
||||
Channel channel = getOrCreateChannel(broadcaster);
|
||||
boolean removed = channel.removeAdmin(username);
|
||||
String normalizedUsername = normalize(username);
|
||||
boolean removed = channel.removeAdmin(normalizedUsername);
|
||||
if (removed) {
|
||||
channelRepository.saveAndFlush(channel);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), "Admin removed: " + username);
|
||||
auditLogService.recordEntry(
|
||||
channel.getBroadcaster(),
|
||||
actor,
|
||||
"ADMIN_REMOVED",
|
||||
"Removed admin " + normalizedUsername
|
||||
);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
@@ -184,13 +201,30 @@ public class ChannelDirectoryService {
|
||||
return new CanvasSettingsRequest(channel.getCanvasWidth(), channel.getCanvasHeight());
|
||||
}
|
||||
|
||||
public CanvasSettingsRequest updateCanvasSettings(String broadcaster, CanvasSettingsRequest req) {
|
||||
public CanvasSettingsRequest updateCanvasSettings(String broadcaster, CanvasSettingsRequest req, String actor) {
|
||||
Channel channel = getOrCreateChannel(broadcaster);
|
||||
double beforeWidth = channel.getCanvasWidth();
|
||||
double beforeHeight = channel.getCanvasHeight();
|
||||
channel.setCanvasWidth(req.getWidth());
|
||||
channel.setCanvasHeight(req.getHeight());
|
||||
channelRepository.save(channel);
|
||||
CanvasSettingsRequest response = new CanvasSettingsRequest(channel.getCanvasWidth(), channel.getCanvasHeight());
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), CanvasEvent.updated(broadcaster, response));
|
||||
if (beforeWidth != channel.getCanvasWidth() || beforeHeight != channel.getCanvasHeight()) {
|
||||
auditLogService.recordEntry(
|
||||
channel.getBroadcaster(),
|
||||
actor,
|
||||
"CANVAS_UPDATED",
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"Canvas updated to %.0fx%.0f (was %.0fx%.0f)",
|
||||
channel.getCanvasWidth(),
|
||||
channel.getCanvasHeight(),
|
||||
beforeWidth,
|
||||
beforeHeight
|
||||
)
|
||||
);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -205,13 +239,46 @@ public class ChannelDirectoryService {
|
||||
|
||||
public ChannelScriptSettingsRequest updateChannelScriptSettings(
|
||||
String broadcaster,
|
||||
ChannelScriptSettingsRequest request
|
||||
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(),
|
||||
@@ -219,7 +286,7 @@ public class ChannelDirectoryService {
|
||||
);
|
||||
}
|
||||
|
||||
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file) throws IOException {
|
||||
public Optional<AssetView> createAsset(String broadcaster, MultipartFile file, String actor) throws IOException {
|
||||
long fileSize = file.getSize();
|
||||
if (fileSize > uploadLimitBytes) {
|
||||
throw new ResponseStatusException(
|
||||
@@ -299,11 +366,17 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, view));
|
||||
auditLogService.recordEntry(
|
||||
channel.getBroadcaster(),
|
||||
actor,
|
||||
"ASSET_CREATED",
|
||||
"Created asset " + view.name() + " (" + view.assetType() + ")"
|
||||
);
|
||||
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public Optional<AssetView> createCodeAsset(String broadcaster, CodeAssetRequest request) {
|
||||
public Optional<AssetView> createCodeAsset(String broadcaster, CodeAssetRequest request, String actor) {
|
||||
validateCodeAssetSource(request.getSource());
|
||||
Channel channel = getOrCreateChannel(broadcaster);
|
||||
byte[] bytes = request.getSource().getBytes(StandardCharsets.UTF_8);
|
||||
@@ -339,10 +412,16 @@ public class ChannelDirectoryService {
|
||||
scriptAssetRepository.save(script);
|
||||
AssetView view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, view));
|
||||
auditLogService.recordEntry(
|
||||
channel.getBroadcaster(),
|
||||
actor,
|
||||
"SCRIPT_CREATED",
|
||||
"Created script " + view.name() + " (" + view.id() + ")"
|
||||
);
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public Optional<AssetView> updateCodeAsset(String broadcaster, String assetId, CodeAssetRequest request) {
|
||||
public Optional<AssetView> updateCodeAsset(String broadcaster, String assetId, CodeAssetRequest request, String actor) {
|
||||
validateCodeAssetSource(request.getSource());
|
||||
String normalized = normalize(broadcaster);
|
||||
byte[] bytes = request.getSource().getBytes(StandardCharsets.UTF_8);
|
||||
@@ -397,11 +476,17 @@ public class ChannelDirectoryService {
|
||||
scriptAssetRepository.save(script);
|
||||
AssetView view = AssetView.fromScript(normalized, asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, view));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"SCRIPT_UPDATED",
|
||||
"Updated script " + script.getName() + " (" + asset.getId() + ")"
|
||||
);
|
||||
return view;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<AssetView> updateScriptLogo(String broadcaster, String assetId, MultipartFile file)
|
||||
public Optional<AssetView> updateScriptLogo(String broadcaster, String assetId, MultipartFile file, String actor)
|
||||
throws IOException {
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, assetId);
|
||||
byte[] bytes = file.getBytes();
|
||||
@@ -442,10 +527,16 @@ public class ChannelDirectoryService {
|
||||
|
||||
AssetView view = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, view));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"SCRIPT_LOGO_UPDATED",
|
||||
"Updated script logo for " + script.getName() + " (" + asset.getId() + ")"
|
||||
);
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public Optional<AssetView> clearScriptLogo(String broadcaster, String assetId) {
|
||||
public Optional<AssetView> clearScriptLogo(String broadcaster, String assetId, String actor) {
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, assetId);
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
@@ -460,6 +551,12 @@ public class ChannelDirectoryService {
|
||||
removeScriptAssetFileIfOrphaned(previousLogoFileId);
|
||||
AssetView view = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, view));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"SCRIPT_LOGO_CLEARED",
|
||||
"Cleared script logo for " + script.getName() + " (" + asset.getId() + ")"
|
||||
);
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
@@ -667,77 +764,88 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<AssetView> importMarketplaceScript(String targetBroadcaster, String scriptId) {
|
||||
public Optional<AssetView> importMarketplaceScript(String targetBroadcaster, String scriptId, String actor) {
|
||||
Optional<MarketplaceScriptSeedLoader.SeedScript> seedScript = marketplaceScriptSeedLoader.findById(scriptId);
|
||||
Optional<AssetView> imported;
|
||||
if (seedScript.isPresent()) {
|
||||
return importSeedMarketplaceScript(targetBroadcaster, seedScript.get());
|
||||
}
|
||||
ScriptAsset sourceScript;
|
||||
try {
|
||||
sourceScript = scriptAssetRepository.findById(scriptId).filter(ScriptAsset::isPublic).orElse(null);
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to import marketplace script {}", scriptId, ex);
|
||||
return Optional.empty();
|
||||
}
|
||||
Asset sourceAsset = sourceScript == null ? null : assetRepository.findById(scriptId).orElse(null);
|
||||
if (sourceScript == null || sourceAsset == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
AssetContent sourceContent = loadScriptSourceContent(sourceAsset, sourceScript).orElse(null);
|
||||
if (sourceContent == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
imported = importSeedMarketplaceScript(targetBroadcaster, seedScript.get());
|
||||
} else {
|
||||
ScriptAsset sourceScript;
|
||||
try {
|
||||
sourceScript = scriptAssetRepository.findById(scriptId).filter(ScriptAsset::isPublic).orElse(null);
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to import marketplace script {}", scriptId, ex);
|
||||
return Optional.empty();
|
||||
}
|
||||
Asset sourceAsset = sourceScript == null ? null : assetRepository.findById(scriptId).orElse(null);
|
||||
if (sourceScript == null || sourceAsset == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
AssetContent sourceContent = loadScriptSourceContent(sourceAsset, sourceScript).orElse(null);
|
||||
if (sourceContent == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Asset asset = new Asset(targetBroadcaster, AssetType.SCRIPT);
|
||||
ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT);
|
||||
sourceFile.setId(asset.getId());
|
||||
sourceFile.setMediaType(sourceContent.mediaType());
|
||||
sourceFile.setOriginalMediaType(sourceContent.mediaType());
|
||||
try {
|
||||
assetStorageService.storeAsset(
|
||||
sourceFile.getBroadcaster(),
|
||||
sourceFile.getId(),
|
||||
sourceContent.bytes(),
|
||||
sourceContent.mediaType()
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
|
||||
}
|
||||
assetRepository.save(asset);
|
||||
scriptAssetFileRepository.save(sourceFile);
|
||||
Asset asset = new Asset(targetBroadcaster, AssetType.SCRIPT);
|
||||
ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT);
|
||||
sourceFile.setId(asset.getId());
|
||||
sourceFile.setMediaType(sourceContent.mediaType());
|
||||
sourceFile.setOriginalMediaType(sourceContent.mediaType());
|
||||
try {
|
||||
assetStorageService.storeAsset(
|
||||
sourceFile.getBroadcaster(),
|
||||
sourceFile.getId(),
|
||||
sourceContent.bytes(),
|
||||
sourceContent.mediaType()
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
|
||||
}
|
||||
assetRepository.save(asset);
|
||||
scriptAssetFileRepository.save(sourceFile);
|
||||
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), sourceScript.getName());
|
||||
script.setDescription(sourceScript.getDescription());
|
||||
script.setPublic(false);
|
||||
script.setMediaType(sourceContent.mediaType());
|
||||
script.setOriginalMediaType(sourceContent.mediaType());
|
||||
script.setSourceFileId(sourceFile.getId());
|
||||
script.setLogoFileId(sourceScript.getLogoFileId());
|
||||
script.setZIndex(nextScriptZIndex(targetBroadcaster));
|
||||
script.setAttachments(List.of());
|
||||
scriptAssetRepository.save(script);
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), sourceScript.getName());
|
||||
script.setDescription(sourceScript.getDescription());
|
||||
script.setPublic(false);
|
||||
script.setMediaType(sourceContent.mediaType());
|
||||
script.setOriginalMediaType(sourceContent.mediaType());
|
||||
script.setSourceFileId(sourceFile.getId());
|
||||
script.setLogoFileId(sourceScript.getLogoFileId());
|
||||
script.setZIndex(nextScriptZIndex(targetBroadcaster));
|
||||
script.setAttachments(List.of());
|
||||
scriptAssetRepository.save(script);
|
||||
|
||||
List<ScriptAssetAttachment> sourceAttachments = scriptAssetAttachmentRepository
|
||||
.findByScriptAssetId(sourceScript.getId());
|
||||
List<ScriptAssetAttachment> newAttachments = sourceAttachments
|
||||
.stream()
|
||||
.map((attachment) -> {
|
||||
ScriptAssetAttachment copy = new ScriptAssetAttachment(asset.getId(), attachment.getName());
|
||||
String fileId = attachment.getFileId() != null ? attachment.getFileId() : attachment.getId();
|
||||
copy.setFileId(fileId);
|
||||
copy.setMediaType(attachment.getMediaType());
|
||||
copy.setOriginalMediaType(attachment.getOriginalMediaType());
|
||||
copy.setAssetType(attachment.getAssetType());
|
||||
return copy;
|
||||
})
|
||||
.toList();
|
||||
if (!newAttachments.isEmpty()) {
|
||||
scriptAssetAttachmentRepository.saveAll(newAttachments);
|
||||
List<ScriptAssetAttachment> sourceAttachments = scriptAssetAttachmentRepository
|
||||
.findByScriptAssetId(sourceScript.getId());
|
||||
List<ScriptAssetAttachment> newAttachments = sourceAttachments
|
||||
.stream()
|
||||
.map((attachment) -> {
|
||||
ScriptAssetAttachment copy = new ScriptAssetAttachment(asset.getId(), attachment.getName());
|
||||
String fileId = attachment.getFileId() != null ? attachment.getFileId() : attachment.getId();
|
||||
copy.setFileId(fileId);
|
||||
copy.setMediaType(attachment.getMediaType());
|
||||
copy.setOriginalMediaType(attachment.getOriginalMediaType());
|
||||
copy.setAssetType(attachment.getAssetType());
|
||||
return copy;
|
||||
})
|
||||
.toList();
|
||||
if (!newAttachments.isEmpty()) {
|
||||
scriptAssetAttachmentRepository.saveAll(newAttachments);
|
||||
}
|
||||
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
|
||||
AssetView view = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(targetBroadcaster), AssetEvent.created(targetBroadcaster, view));
|
||||
imported = Optional.of(view);
|
||||
}
|
||||
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
|
||||
AssetView view = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(targetBroadcaster), AssetEvent.created(targetBroadcaster, view));
|
||||
return Optional.of(view);
|
||||
imported.ifPresent((view) ->
|
||||
auditLogService.recordEntry(
|
||||
view.broadcaster(),
|
||||
actor,
|
||||
"MARKETPLACE_SCRIPT_IMPORTED",
|
||||
"Imported marketplace script " + scriptId + " as " + view.name() + " (" + view.id() + ")"
|
||||
)
|
||||
);
|
||||
return imported;
|
||||
}
|
||||
|
||||
private Optional<AssetView> importSeedMarketplaceScript(
|
||||
@@ -858,7 +966,7 @@ public class ChannelDirectoryService {
|
||||
return SAFE_FILENAME.matcher(stripped).replaceAll("_");
|
||||
}
|
||||
|
||||
public Optional<AssetView> updateTransform(String broadcaster, String assetId, TransformRequest req) {
|
||||
public Optional<AssetView> updateTransform(String broadcaster, String assetId, TransformRequest req, String actor) {
|
||||
String normalized = normalize(broadcaster);
|
||||
|
||||
return assetRepository
|
||||
@@ -887,6 +995,12 @@ public class ChannelDirectoryService {
|
||||
AssetPatch patch = AssetPatch.fromAudioTransform(before, audio, req);
|
||||
if (hasPatchChanges(patch)) {
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, patch));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"AUDIO_UPDATED",
|
||||
formatAudioTransformDetails(asset.getId(), req)
|
||||
);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
@@ -921,6 +1035,12 @@ public class ChannelDirectoryService {
|
||||
null
|
||||
);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, patch));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"SCRIPT_LAYER_UPDATED",
|
||||
formatScriptTransformDetails(asset.getId(), script.getZIndex())
|
||||
);
|
||||
}
|
||||
}
|
||||
script.setAttachments(loadScriptAttachments(normalized, asset.getId(), null));
|
||||
@@ -959,6 +1079,12 @@ public class ChannelDirectoryService {
|
||||
AssetPatch patch = AssetPatch.fromVisualTransform(before, visual, req);
|
||||
if (hasPatchChanges(patch)) {
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, patch));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"VISUAL_UPDATED",
|
||||
formatVisualTransformDetails(asset.getId(), req)
|
||||
);
|
||||
}
|
||||
return view;
|
||||
});
|
||||
@@ -1026,7 +1152,7 @@ public class ChannelDirectoryService {
|
||||
);
|
||||
}
|
||||
|
||||
public Optional<AssetView> triggerPlayback(String broadcaster, String assetId, PlaybackRequest req) {
|
||||
public Optional<AssetView> triggerPlayback(String broadcaster, String assetId, PlaybackRequest req, String actor) {
|
||||
String normalized = normalize(broadcaster);
|
||||
return assetRepository
|
||||
.findById(assetId)
|
||||
@@ -1038,11 +1164,22 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
boolean play = req == null || req.getPlay();
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.play(broadcaster, view, play));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"ASSET_PLAYBACK_TRIGGERED",
|
||||
"Playback " + (play ? "started" : "stopped") + " for asset " + asset.getId()
|
||||
);
|
||||
return view;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<AssetView> updateVisibility(String broadcaster, String assetId, VisibilityRequest request) {
|
||||
public Optional<AssetView> updateVisibility(
|
||||
String broadcaster,
|
||||
String assetId,
|
||||
VisibilityRequest request,
|
||||
String actor
|
||||
) {
|
||||
String normalized = normalize(broadcaster);
|
||||
return assetRepository
|
||||
.findById(assetId)
|
||||
@@ -1065,6 +1202,12 @@ public class ChannelDirectoryService {
|
||||
topicFor(broadcaster),
|
||||
AssetEvent.visibility(broadcaster, patch, payload)
|
||||
);
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"AUDIO_VISIBILITY_UPDATED",
|
||||
"Audio asset " + asset.getId() + " hidden=" + hidden
|
||||
);
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -1091,12 +1234,18 @@ public class ChannelDirectoryService {
|
||||
topicFor(broadcaster),
|
||||
AssetEvent.visibility(broadcaster, patch, payload)
|
||||
);
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"VISUAL_VISIBILITY_UPDATED",
|
||||
"Visual asset " + asset.getId() + " hidden=" + hidden
|
||||
);
|
||||
return view;
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean deleteAsset(String assetId) {
|
||||
public boolean deleteAsset(String assetId, String actor) {
|
||||
return assetRepository
|
||||
.findById(assetId)
|
||||
.map((asset) -> {
|
||||
@@ -1128,6 +1277,12 @@ public class ChannelDirectoryService {
|
||||
topicFor(asset.getBroadcaster()),
|
||||
AssetEvent.deleted(asset.getBroadcaster(), assetId)
|
||||
);
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"ASSET_DELETED",
|
||||
"Deleted asset " + asset.getId() + " (" + asset.getAssetType() + ")"
|
||||
);
|
||||
return true;
|
||||
})
|
||||
.orElse(false);
|
||||
@@ -1145,7 +1300,8 @@ public class ChannelDirectoryService {
|
||||
public Optional<ScriptAssetAttachmentView> createScriptAttachment(
|
||||
String broadcaster,
|
||||
String scriptAssetId,
|
||||
MultipartFile file
|
||||
MultipartFile file,
|
||||
String actor
|
||||
) throws IOException {
|
||||
long fileSize = file.getSize();
|
||||
if (fileSize > uploadLimitBytes) {
|
||||
@@ -1214,11 +1370,22 @@ public class ChannelDirectoryService {
|
||||
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
|
||||
AssetView scriptView = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, scriptView));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"SCRIPT_ATTACHMENT_ADDED",
|
||||
"Added attachment " + attachment.getName() + " to script " + asset.getId()
|
||||
);
|
||||
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public boolean deleteScriptAttachment(String broadcaster, String scriptAssetId, String attachmentId) {
|
||||
public boolean deleteScriptAttachment(
|
||||
String broadcaster,
|
||||
String scriptAssetId,
|
||||
String attachmentId,
|
||||
String actor
|
||||
) {
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, scriptAssetId);
|
||||
ScriptAssetAttachment attachment = scriptAssetAttachmentRepository
|
||||
.findById(attachmentId)
|
||||
@@ -1227,6 +1394,7 @@ public class ChannelDirectoryService {
|
||||
if (attachment == null) {
|
||||
return false;
|
||||
}
|
||||
String attachmentName = attachment.getName();
|
||||
String fileId = attachment.getFileId() != null ? attachment.getFileId() : attachment.getId();
|
||||
scriptAssetAttachmentRepository.deleteById(attachment.getId());
|
||||
removeScriptAssetFileIfOrphaned(fileId);
|
||||
@@ -1237,6 +1405,12 @@ public class ChannelDirectoryService {
|
||||
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
|
||||
AssetView scriptView = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, scriptView));
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"SCRIPT_ATTACHMENT_REMOVED",
|
||||
"Removed attachment " + attachmentName + " from script " + asset.getId()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1637,6 +1811,45 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
}
|
||||
|
||||
private String formatVisualTransformDetails(String assetId, TransformRequest req) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (req.getX() != null) parts.add("x=" + req.getX());
|
||||
if (req.getY() != null) parts.add("y=" + req.getY());
|
||||
if (req.getWidth() != null) parts.add("width=" + req.getWidth());
|
||||
if (req.getHeight() != null) parts.add("height=" + req.getHeight());
|
||||
if (req.getRotation() != null) parts.add("rotation=" + req.getRotation());
|
||||
if (req.getZIndex() != null) parts.add("zIndex=" + req.getZIndex());
|
||||
if (req.getSpeed() != null) parts.add("speed=" + req.getSpeed());
|
||||
if (req.getMuted() != null) parts.add("muted=" + req.getMuted());
|
||||
if (req.getAudioVolume() != null) parts.add("audioVolume=" + req.getAudioVolume());
|
||||
return formatTransformDetails("Updated visual asset " + assetId, parts);
|
||||
}
|
||||
|
||||
private String formatAudioTransformDetails(String assetId, TransformRequest req) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (req.getAudioLoop() != null) parts.add("loop=" + req.getAudioLoop());
|
||||
if (req.getAudioDelayMillis() != null) parts.add("delayMs=" + req.getAudioDelayMillis());
|
||||
if (req.getAudioSpeed() != null) parts.add("speed=" + req.getAudioSpeed());
|
||||
if (req.getAudioPitch() != null) parts.add("pitch=" + req.getAudioPitch());
|
||||
if (req.getAudioVolume() != null) parts.add("volume=" + req.getAudioVolume());
|
||||
return formatTransformDetails("Updated audio asset " + assetId, parts);
|
||||
}
|
||||
|
||||
private String formatScriptTransformDetails(String assetId, Integer zIndex) {
|
||||
String detail = "Updated script asset " + assetId;
|
||||
if (zIndex != null) {
|
||||
return detail + " (zIndex=" + zIndex + ")";
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
private String formatTransformDetails(String summary, List<String> parts) {
|
||||
if (parts == null || parts.isEmpty()) {
|
||||
return summary;
|
||||
}
|
||||
return summary + " (" + String.join(", ", parts) + ")";
|
||||
}
|
||||
|
||||
private boolean hasPatchChanges(AssetPatch patch) {
|
||||
return (
|
||||
patch.x() != null ||
|
||||
|
||||
Reference in New Issue
Block a user