Add signup block display

This commit is contained in:
2025-12-09 10:53:37 +01:00
parent e8a0261ca7
commit f234790aeb
6 changed files with 218 additions and 5 deletions

View File

@@ -4,11 +4,15 @@ import com.imgfloat.app.model.AdminRequest;
import com.imgfloat.app.model.Asset;
import com.imgfloat.app.model.CanvasSettingsRequest;
import com.imgfloat.app.model.TransformRequest;
import com.imgfloat.app.model.TwitchUserProfile;
import com.imgfloat.app.model.VisibilityRequest;
import com.imgfloat.app.service.ChannelDirectoryService;
import com.imgfloat.app.service.TwitchUserLookupService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.http.MediaType;
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.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -23,6 +27,9 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.Collection;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;
@@ -32,9 +39,15 @@ import static org.springframework.http.HttpStatus.BAD_REQUEST;
@RequestMapping("/api/channels/{broadcaster}")
public class ChannelApiController {
private final ChannelDirectoryService channelDirectoryService;
private final OAuth2AuthorizedClientService authorizedClientService;
private final TwitchUserLookupService twitchUserLookupService;
public ChannelApiController(ChannelDirectoryService channelDirectoryService) {
public ChannelApiController(ChannelDirectoryService channelDirectoryService,
OAuth2AuthorizedClientService authorizedClientService,
TwitchUserLookupService twitchUserLookupService) {
this.channelDirectoryService = channelDirectoryService;
this.authorizedClientService = authorizedClientService;
this.twitchUserLookupService = twitchUserLookupService;
}
@PostMapping("/admins")
@@ -48,11 +61,26 @@ public class ChannelApiController {
}
@GetMapping("/admins")
public Collection<String> listAdmins(@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken authentication) {
public Collection<TwitchUserProfile> listAdmins(@PathVariable("broadcaster") String broadcaster,
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureBroadcaster(broadcaster, login);
return channelDirectoryService.getOrCreateChannel(broadcaster).getAdmins();
var channel = channelDirectoryService.getOrCreateChannel(broadcaster);
List<String> admins = channel.getAdmins().stream()
.sorted(Comparator.naturalOrder())
.toList();
OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(),
authentication.getName());
String accessToken = Optional.ofNullable(authorizedClient)
.map(OAuth2AuthorizedClient::getAccessToken)
.map(token -> token.getTokenValue())
.orElse(null);
String clientId = Optional.ofNullable(authorizedClient)
.map(OAuth2AuthorizedClient::getClientRegistration)
.map(registration -> registration.getClientId())
.orElse(null);
return twitchUserLookupService.fetchProfiles(admins, accessToken, clientId);
}
@DeleteMapping("/admins/{username}")

View File

@@ -0,0 +1,12 @@
package com.imgfloat.app.model;
/**
* Minimal Twitch user details used for rendering avatars and display names.
*/
public record TwitchUserProfile(String login, String displayName, String avatarUrl) {
public TwitchUserProfile {
if (displayName == null || displayName.isBlank()) {
displayName = login;
}
}
}

View File

@@ -0,0 +1,110 @@
package com.imgfloat.app.service;
import com.imgfloat.app.model.TwitchUserProfile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class TwitchUserLookupService {
private static final Logger LOG = LoggerFactory.getLogger(TwitchUserLookupService.class);
private final RestTemplate restTemplate;
public TwitchUserLookupService(RestTemplateBuilder builder) {
this.restTemplate = builder
.setConnectTimeout(Duration.ofSeconds(15))
.setReadTimeout(Duration.ofSeconds(15))
.build();
}
public List<TwitchUserProfile> fetchProfiles(Collection<String> logins, String accessToken, String clientId) {
if (logins == null || logins.isEmpty()) {
return List.of();
}
List<String> normalizedLogins = logins.stream()
.filter(Objects::nonNull)
.map(login -> login.toLowerCase(Locale.ROOT))
.distinct()
.toList();
if (accessToken == null || accessToken.isBlank() || clientId == null || clientId.isBlank()) {
return normalizedLogins.stream()
.map(login -> new TwitchUserProfile(login, login, null))
.toList();
}
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
headers.add("Client-ID", clientId);
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromHttpUrl("https://api.twitch.tv/helix/users");
normalizedLogins.forEach(login -> uriBuilder.queryParam("login", login));
HttpEntity<Void> entity = new HttpEntity<>(headers);
try {
ResponseEntity<TwitchUsersResponse> response = restTemplate.exchange(
uriBuilder.build(true).toUri(),
HttpMethod.GET,
entity,
TwitchUsersResponse.class);
Map<String, TwitchUserData> byLogin = response.getBody() == null
? Collections.emptyMap()
: response.getBody().data().stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(
user -> user.login().toLowerCase(Locale.ROOT),
Function.identity(),
(a, b) -> a));
return normalizedLogins.stream()
.map(login -> toProfile(login, byLogin.get(login)))
.toList();
} catch (RestClientException ex) {
LOG.warn("Unable to fetch Twitch user profiles", ex);
return normalizedLogins.stream()
.map(login -> new TwitchUserProfile(login, login, null))
.toList();
}
}
private TwitchUserProfile toProfile(String login, TwitchUserData data) {
if (data == null) {
return new TwitchUserProfile(login, login, null);
}
return new TwitchUserProfile(login, data.displayName(), data.profileImageUrl());
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record TwitchUsersResponse(List<TwitchUserData> data) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record TwitchUserData(
String login,
@JsonProperty("display_name") String displayName,
@JsonProperty("profile_image_url") String profileImageUrl) {
}
}