mirror of
https://github.com/imgfloat/server.git
synced 2026-05-08 10:19:35 +00:00
refactor: extract ChannelAdminApiController and clean up ChannelDirectoryService
- Extract admin endpoint group (add/remove/list admins, suggestions) into dedicated ChannelAdminApiController - ChannelApiController reduced from 665 to 520 lines; removes 3 dependency injections - ChannelDirectoryService: extract resolveOrderForSort() helper from bulk-reorder comparator lambda - Remove TODO comment from ChannelApiController (partially addressed by this split)
This commit is contained in:
@@ -0,0 +1,188 @@
|
|||||||
|
package dev.kruhlmann.imgfloat.controller;
|
||||||
|
|
||||||
|
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.response.TwitchUserProfile;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.request.AdminRequest;
|
||||||
|
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
||||||
|
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
|
||||||
|
import dev.kruhlmann.imgfloat.service.TwitchUserLookupService;
|
||||||
|
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||||
|
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages channel admin membership (add, remove, list) and Twitch moderator suggestions.
|
||||||
|
* Extracted from {@link ChannelApiController} to reduce its surface area.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/channels/{broadcaster}/admins")
|
||||||
|
@SecurityRequirement(name = "twitchOAuth")
|
||||||
|
public class ChannelAdminApiController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ChannelAdminApiController.class);
|
||||||
|
|
||||||
|
private final ChannelDirectoryService channelDirectoryService;
|
||||||
|
private final TwitchUserLookupService twitchUserLookupService;
|
||||||
|
private final AuthorizationService authorizationService;
|
||||||
|
private final OAuth2AuthorizedClientService authorizedClientService;
|
||||||
|
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||||
|
|
||||||
|
public ChannelAdminApiController(
|
||||||
|
ChannelDirectoryService channelDirectoryService,
|
||||||
|
TwitchUserLookupService twitchUserLookupService,
|
||||||
|
AuthorizationService authorizationService,
|
||||||
|
OAuth2AuthorizedClientService authorizedClientService,
|
||||||
|
OAuth2AuthorizedClientRepository authorizedClientRepository
|
||||||
|
) {
|
||||||
|
this.channelDirectoryService = channelDirectoryService;
|
||||||
|
this.twitchUserLookupService = twitchUserLookupService;
|
||||||
|
this.authorizationService = authorizationService;
|
||||||
|
this.authorizedClientService = authorizedClientService;
|
||||||
|
this.authorizedClientRepository = authorizedClientRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Boolean> addAdmin(
|
||||||
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
|
@Valid @RequestBody AdminRequest request,
|
||||||
|
OAuth2AuthenticationToken oauthToken
|
||||||
|
) {
|
||||||
|
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||||
|
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||||
|
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||||
|
String logRequestUsername = LogSanitizer.sanitize(request.username());
|
||||||
|
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||||
|
LOG.info("User {} adding admin {} to {}", logSessionUsername, logRequestUsername, logBroadcaster);
|
||||||
|
boolean added = channelDirectoryService.addAdmin(broadcaster, request.username(), sessionUsername);
|
||||||
|
if (!added) {
|
||||||
|
LOG.info("User {} already admin for {} or could not be added", logRequestUsername, logBroadcaster);
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(added);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Collection<TwitchUserProfile> listAdmins(
|
||||||
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
|
OAuth2AuthenticationToken oauthToken,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||||
|
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||||
|
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||||
|
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||||
|
LOG.debug("Listing admins for {} by {}", logBroadcaster, logSessionUsername);
|
||||||
|
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
|
||||||
|
List<String> admins = channel.getAdmins().stream().sorted(Comparator.naturalOrder()).toList();
|
||||||
|
OAuth2AuthorizedClient authorizedClient = resolveAuthorizedClient(oauthToken, request);
|
||||||
|
String accessToken = Optional.ofNullable(authorizedClient)
|
||||||
|
.map(OAuth2AuthorizedClient::getAccessToken)
|
||||||
|
.map(AbstractOAuth2Token::getTokenValue)
|
||||||
|
.orElse(null);
|
||||||
|
String clientId = Optional.ofNullable(authorizedClient)
|
||||||
|
.map(OAuth2AuthorizedClient::getClientRegistration)
|
||||||
|
.map(ClientRegistration::getClientId)
|
||||||
|
.orElse(null);
|
||||||
|
return twitchUserLookupService.fetchProfiles(admins, accessToken, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/suggestions")
|
||||||
|
public Collection<TwitchUserProfile> listAdminSuggestions(
|
||||||
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
|
OAuth2AuthenticationToken oauthToken,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||||
|
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||||
|
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||||
|
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||||
|
LOG.debug("Listing admin suggestions for {} by {}", logBroadcaster, logSessionUsername);
|
||||||
|
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
|
||||||
|
OAuth2AuthorizedClient authorizedClient = resolveAuthorizedClient(oauthToken, request);
|
||||||
|
|
||||||
|
if (authorizedClient == null) {
|
||||||
|
LOG.warn(
|
||||||
|
"No authorized Twitch client found for {} while fetching admin suggestions for {}",
|
||||||
|
logSessionUsername,
|
||||||
|
logBroadcaster
|
||||||
|
);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
String accessToken = Optional.of(authorizedClient)
|
||||||
|
.map(OAuth2AuthorizedClient::getAccessToken)
|
||||||
|
.map(AbstractOAuth2Token::getTokenValue)
|
||||||
|
.orElse(null);
|
||||||
|
String clientId = Optional.of(authorizedClient)
|
||||||
|
.map(OAuth2AuthorizedClient::getClientRegistration)
|
||||||
|
.map(ClientRegistration::getClientId)
|
||||||
|
.orElse(null);
|
||||||
|
if (accessToken == null || accessToken.isBlank() || clientId == null || clientId.isBlank()) {
|
||||||
|
LOG.warn(
|
||||||
|
"Missing Twitch credentials for {} while fetching admin suggestions for {}",
|
||||||
|
logSessionUsername,
|
||||||
|
logBroadcaster
|
||||||
|
);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return twitchUserLookupService.fetchModerators(broadcaster, channel.getAdmins(), accessToken, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{username}")
|
||||||
|
public ResponseEntity<Boolean> removeAdmin(
|
||||||
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
|
@PathVariable("username") String username,
|
||||||
|
OAuth2AuthenticationToken oauthToken
|
||||||
|
) {
|
||||||
|
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||||
|
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||||
|
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||||
|
String logUsername = LogSanitizer.sanitize(username);
|
||||||
|
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
||||||
|
LOG.info("User {} removing admin {} from {}", logSessionUsername, logUsername, logBroadcaster);
|
||||||
|
boolean removed = channelDirectoryService.removeAdmin(broadcaster, username, sessionUsername);
|
||||||
|
return ResponseEntity.ok(removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2AuthorizedClient resolveAuthorizedClient(
|
||||||
|
@Nullable OAuth2AuthenticationToken oauthToken,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
if (oauthToken == null) {
|
||||||
|
LOG.error("Attempt to resolve authorized client without oauth token");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
OAuth2AuthorizedClient sessionClient = authorizedClientRepository.loadAuthorizedClient(
|
||||||
|
oauthToken.getAuthorizedClientRegistrationId(),
|
||||||
|
oauthToken,
|
||||||
|
request
|
||||||
|
);
|
||||||
|
if (sessionClient != null) {
|
||||||
|
return sessionClient;
|
||||||
|
}
|
||||||
|
return authorizedClientService.loadAuthorizedClient(
|
||||||
|
oauthToken.getAuthorizedClientRegistrationId(),
|
||||||
|
oauthToken.getName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,187 +3,66 @@ package dev.kruhlmann.imgfloat.controller;
|
|||||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||||
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.AdminRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.AssetView;
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
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.OauthSessionUser;
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
||||||
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
|
|
||||||
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
||||||
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.model.api.response.AssetView;
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
|
||||||
|
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||||
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.ChannelSettingsService;
|
||||||
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;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.lang.Nullable;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
||||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
|
||||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestPart;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages assets, canvas settings, and script attachments for a specific broadcaster channel.
|
||||||
|
* Admin management has been extracted to {@link ChannelAdminApiController}.
|
||||||
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/channels/{broadcaster}")
|
@RequestMapping("/api/channels/{broadcaster}")
|
||||||
@SecurityRequirement(name = "twitchOAuth")
|
@SecurityRequirement(name = "twitchOAuth")
|
||||||
public class ChannelApiController {
|
public class ChannelApiController {
|
||||||
|
|
||||||
// TODO: Code smell Controller surface area is very large, suggesting too many endpoint responsibilities in one type.
|
|
||||||
|
|
||||||
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 ChannelSettingsService channelSettingsService;
|
||||||
private final OAuth2AuthorizedClientService authorizedClientService;
|
|
||||||
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
|
|
||||||
private final TwitchUserLookupService twitchUserLookupService;
|
|
||||||
private final AuthorizationService authorizationService;
|
private final AuthorizationService authorizationService;
|
||||||
|
|
||||||
public ChannelApiController(
|
public ChannelApiController(
|
||||||
ChannelDirectoryService channelDirectoryService,
|
ChannelDirectoryService channelDirectoryService,
|
||||||
ChannelSettingsService channelSettingsService,
|
ChannelSettingsService channelSettingsService,
|
||||||
OAuth2AuthorizedClientService authorizedClientService,
|
|
||||||
OAuth2AuthorizedClientRepository authorizedClientRepository,
|
|
||||||
TwitchUserLookupService twitchUserLookupService,
|
|
||||||
AuthorizationService authorizationService
|
AuthorizationService authorizationService
|
||||||
) {
|
) {
|
||||||
this.channelDirectoryService = channelDirectoryService;
|
this.channelDirectoryService = channelDirectoryService;
|
||||||
this.channelSettingsService = channelSettingsService;
|
this.channelSettingsService = channelSettingsService;
|
||||||
this.authorizedClientService = authorizedClientService;
|
|
||||||
this.authorizedClientRepository = authorizedClientRepository;
|
|
||||||
this.twitchUserLookupService = twitchUserLookupService;
|
|
||||||
this.authorizationService = authorizationService;
|
this.authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/admins")
|
|
||||||
public ResponseEntity<Boolean> addAdmin(
|
|
||||||
@PathVariable("broadcaster") String broadcaster,
|
|
||||||
@Valid @RequestBody AdminRequest request,
|
|
||||||
OAuth2AuthenticationToken oauthToken
|
|
||||||
) {
|
|
||||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
|
||||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
|
||||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
|
||||||
String logRequestUsername = LogSanitizer.sanitize(request.username());
|
|
||||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
|
||||||
LOG.info("User {} adding admin {} to {}", logSessionUsername, logRequestUsername, logBroadcaster);
|
|
||||||
boolean added = channelDirectoryService.addAdmin(broadcaster, request.username(), sessionUsername);
|
|
||||||
if (!added) {
|
|
||||||
LOG.info("User {} already admin for {} or could not be added", logRequestUsername, logBroadcaster);
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(added);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/admins")
|
|
||||||
public Collection<TwitchUserProfile> listAdmins(
|
|
||||||
@PathVariable("broadcaster") String broadcaster,
|
|
||||||
OAuth2AuthenticationToken oauthToken,
|
|
||||||
HttpServletRequest request
|
|
||||||
) {
|
|
||||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
|
||||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
|
||||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
|
||||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
|
||||||
LOG.debug("Listing admins for {} by {}", logBroadcaster, logSessionUsername);
|
|
||||||
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
|
|
||||||
List<String> admins = channel.getAdmins().stream().sorted(Comparator.naturalOrder()).toList();
|
|
||||||
OAuth2AuthorizedClient authorizedClient = resolveAuthorizedClient(oauthToken, request);
|
|
||||||
String accessToken = Optional.ofNullable(authorizedClient)
|
|
||||||
.map(OAuth2AuthorizedClient::getAccessToken)
|
|
||||||
.map(AbstractOAuth2Token::getTokenValue)
|
|
||||||
.orElse(null);
|
|
||||||
String clientId = Optional.ofNullable(authorizedClient)
|
|
||||||
.map(OAuth2AuthorizedClient::getClientRegistration)
|
|
||||||
.map(ClientRegistration::getClientId)
|
|
||||||
.orElse(null);
|
|
||||||
return twitchUserLookupService.fetchProfiles(admins, accessToken, clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/admins/suggestions")
|
|
||||||
public Collection<TwitchUserProfile> listAdminSuggestions(
|
|
||||||
@PathVariable("broadcaster") String broadcaster,
|
|
||||||
OAuth2AuthenticationToken oauthToken,
|
|
||||||
HttpServletRequest request
|
|
||||||
) {
|
|
||||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
|
||||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
|
||||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
|
||||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
|
||||||
LOG.debug("Listing admin suggestions for {} by {}", logBroadcaster, logSessionUsername);
|
|
||||||
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
|
|
||||||
OAuth2AuthorizedClient authorizedClient = resolveAuthorizedClient(oauthToken, request);
|
|
||||||
|
|
||||||
if (authorizedClient == null) {
|
|
||||||
LOG.warn(
|
|
||||||
"No authorized Twitch client found for {} while fetching admin suggestions for {}",
|
|
||||||
logSessionUsername,
|
|
||||||
logBroadcaster
|
|
||||||
);
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
String accessToken = Optional.of(authorizedClient)
|
|
||||||
.map(OAuth2AuthorizedClient::getAccessToken)
|
|
||||||
.map(AbstractOAuth2Token::getTokenValue)
|
|
||||||
.orElse(null);
|
|
||||||
String clientId = Optional.of(authorizedClient)
|
|
||||||
.map(OAuth2AuthorizedClient::getClientRegistration)
|
|
||||||
.map(ClientRegistration::getClientId)
|
|
||||||
.orElse(null);
|
|
||||||
if (accessToken == null || accessToken.isBlank() || clientId == null || clientId.isBlank()) {
|
|
||||||
LOG.warn(
|
|
||||||
"Missing Twitch credentials for {} while fetching admin suggestions for {}",
|
|
||||||
logSessionUsername,
|
|
||||||
logBroadcaster
|
|
||||||
);
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return twitchUserLookupService.fetchModerators(broadcaster, channel.getAdmins(), accessToken, clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/admins/{username}")
|
|
||||||
public ResponseEntity<Boolean> removeAdmin(
|
|
||||||
@PathVariable("broadcaster") String broadcaster,
|
|
||||||
@PathVariable("username") String username,
|
|
||||||
OAuth2AuthenticationToken oauthToken
|
|
||||||
) {
|
|
||||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
|
||||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
|
||||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
|
||||||
String logUsername = LogSanitizer.sanitize(username);
|
|
||||||
authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername);
|
|
||||||
LOG.info("User {} removing admin {} from {}", logSessionUsername, logUsername, logBroadcaster);
|
|
||||||
boolean removed = channelDirectoryService.removeAdmin(broadcaster, username, sessionUsername);
|
|
||||||
return ResponseEntity.ok(removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/assets")
|
@GetMapping("/assets")
|
||||||
public Collection<AssetView> listAssets(@PathVariable("broadcaster") String broadcaster) {
|
public Collection<AssetView> listAssets(@PathVariable("broadcaster") String broadcaster) {
|
||||||
return channelDirectoryService.getAssetsForAdmin(broadcaster);
|
return channelDirectoryService.getAssetsForAdmin(broadcaster);
|
||||||
@@ -505,15 +384,6 @@ public class ChannelApiController {
|
|||||||
.orElseThrow(this::createAsset404);
|
.orElseThrow(this::createAsset404);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String contentDispositionFor(String mediaType) {
|
|
||||||
if (
|
|
||||||
dev.kruhlmann.imgfloat.service.media.MediaDetectionService.isInlineDisplayType(mediaType)
|
|
||||||
) {
|
|
||||||
return "inline";
|
|
||||||
}
|
|
||||||
return "attachment";
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/assets/{assetId}")
|
@DeleteMapping("/assets/{assetId}")
|
||||||
public ResponseEntity<Void> delete(
|
public ResponseEntity<Void> delete(
|
||||||
@PathVariable("broadcaster") String broadcaster,
|
@PathVariable("broadcaster") String broadcaster,
|
||||||
@@ -637,29 +507,14 @@ public class ChannelApiController {
|
|||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String contentDispositionFor(String mediaType) {
|
||||||
|
if (dev.kruhlmann.imgfloat.service.media.MediaDetectionService.isInlineDisplayType(mediaType)) {
|
||||||
|
return "inline";
|
||||||
|
}
|
||||||
|
return "attachment";
|
||||||
|
}
|
||||||
|
|
||||||
private ResponseStatusException createAsset404() {
|
private ResponseStatusException createAsset404() {
|
||||||
return new ResponseStatusException(NOT_FOUND, "Asset not found");
|
return new ResponseStatusException(NOT_FOUND, "Asset not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
private OAuth2AuthorizedClient resolveAuthorizedClient(
|
|
||||||
@Nullable OAuth2AuthenticationToken oauthToken,
|
|
||||||
HttpServletRequest request
|
|
||||||
) {
|
|
||||||
if (oauthToken == null) {
|
|
||||||
LOG.error("Attempt to resolve authorized client without oauth token");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
OAuth2AuthorizedClient sessionClient = authorizedClientRepository.loadAuthorizedClient(
|
|
||||||
oauthToken.getAuthorizedClientRegistrationId(),
|
|
||||||
oauthToken,
|
|
||||||
request
|
|
||||||
);
|
|
||||||
if (sessionClient != null) {
|
|
||||||
return sessionClient;
|
|
||||||
}
|
|
||||||
return authorizedClientService.loadAuthorizedClient(
|
|
||||||
oauthToken.getAuthorizedClientRegistrationId(),
|
|
||||||
oauthToken.getName()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -957,8 +957,8 @@ public class ChannelDirectoryService {
|
|||||||
}
|
}
|
||||||
List<Asset> ordered = new ArrayList<>(bucket);
|
List<Asset> ordered = new ArrayList<>(bucket);
|
||||||
ordered.sort((a, b) -> {
|
ordered.sort((a, b) -> {
|
||||||
int orderA = desiredOrder.getOrDefault(a.getId(), a.getDisplayOrder() != null ? a.getDisplayOrder() : bucket.size() - originalIndex.getOrDefault(a.getId(), bucket.size()));
|
int orderA = resolveOrderForSort(a, desiredOrder, originalIndex, bucket.size());
|
||||||
int orderB = desiredOrder.getOrDefault(b.getId(), b.getDisplayOrder() != null ? b.getDisplayOrder() : bucket.size() - originalIndex.getOrDefault(b.getId(), bucket.size()));
|
int orderB = resolveOrderForSort(b, desiredOrder, originalIndex, bucket.size());
|
||||||
int cmp = Integer.compare(orderB, orderA);
|
int cmp = Integer.compare(orderB, orderA);
|
||||||
if (cmp != 0) {
|
if (cmp != 0) {
|
||||||
return cmp;
|
return cmp;
|
||||||
@@ -1561,6 +1561,27 @@ public class ChannelDirectoryService {
|
|||||||
return value == null ? Integer.MIN_VALUE : value;
|
return value == null ? Integer.MIN_VALUE : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the effective sort key for an asset during a bulk reorder operation.
|
||||||
|
* Assets with an explicit desired order use that value; others fall back to their
|
||||||
|
* current display order, or their inverse original-index position as a last resort.
|
||||||
|
*/
|
||||||
|
private int resolveOrderForSort(
|
||||||
|
Asset asset,
|
||||||
|
Map<String, Integer> desiredOrder,
|
||||||
|
Map<String, Integer> originalIndex,
|
||||||
|
int bucketSize
|
||||||
|
) {
|
||||||
|
String id = asset.getId();
|
||||||
|
if (desiredOrder.containsKey(id)) {
|
||||||
|
return desiredOrder.get(id);
|
||||||
|
}
|
||||||
|
if (asset.getDisplayOrder() != null) {
|
||||||
|
return asset.getDisplayOrder();
|
||||||
|
}
|
||||||
|
return bucketSize - originalIndex.getOrDefault(id, bucketSize);
|
||||||
|
}
|
||||||
|
|
||||||
private List<Asset> updateDisplayOrder(String broadcaster, Asset target, int desiredOrder, Set<AssetType> types) {
|
private List<Asset> updateDisplayOrder(String broadcaster, Asset target, int desiredOrder, Set<AssetType> types) {
|
||||||
if (target == null || types == null || types.isEmpty()) {
|
if (target == null || types == null || types.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
|
|||||||
Reference in New Issue
Block a user