aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2023-01-06 09:42:13 +0300
committerGravatar Vitaly Takmazov2023-01-06 09:42:13 +0300
commita5686d4be2a0e82deeaedcd4194a732759832578 (patch)
treefbbeb810abf8c06f2d3e9323dd7fb89cbcea1540 /src/main/java
parent67235435df5f7c8153696c4d9200d82da5d4325f (diff)
Mastodon API: timelines and minor fixes
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/juick/config/SecurityConfig.java17
-rw-r--r--src/main/java/com/juick/service/ActivityPubService.java3
-rw-r--r--src/main/java/com/juick/www/api/Mastodon.java161
3 files changed, 123 insertions, 58 deletions
diff --git a/src/main/java/com/juick/config/SecurityConfig.java b/src/main/java/com/juick/config/SecurityConfig.java
index 7fada80b..47033c11 100644
--- a/src/main/java/com/juick/config/SecurityConfig.java
+++ b/src/main/java/com/juick/config/SecurityConfig.java
@@ -52,7 +52,10 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.authentication.*;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.RememberMeServices;
+import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
@@ -81,6 +84,8 @@ public class SecurityConfig {
@Inject
private JdbcTemplate jdbcTemplate;
private static final String COOKIE_NAME = "juick-remember-me";
+ @Value("${ap_base_uri:http://localhost:8080/}")
+ private String baseUri;
@Bean
UserDetailsService userDetailsService() {
return new JuickUserDetailsService(userService);
@@ -148,6 +153,8 @@ public class SecurityConfig {
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
+ var loginUrlAuthenticationEntryPoint = new LoginUrlAuthenticationEntryPoint("/login");
+ loginUrlAuthenticationEntryPoint.setForceHttps(true);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.authorizationServerSettings(AuthorizationServerSettings.builder()
.authorizationEndpoint("/oauth/authorize")
@@ -158,7 +165,7 @@ public class SecurityConfig {
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
- .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
+ .authenticationEntryPoint(loginUrlAuthenticationEntryPoint)
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
@@ -169,7 +176,6 @@ public class SecurityConfig {
public RegisteredClientRepository registeredClientRepository() {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
-
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAPublicKey publicKey = (RSAPublicKey) keystoreManager.getPublicKey();
@@ -186,11 +192,6 @@ public class SecurityConfig {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
- public AuthorizationServerSettings authorizationServerSettings() {
- return AuthorizationServerSettings.builder().build();
- }
-
- @Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**", "/u/**", "/n/**")
diff --git a/src/main/java/com/juick/service/ActivityPubService.java b/src/main/java/com/juick/service/ActivityPubService.java
index f161be7b..295910bd 100644
--- a/src/main/java/com/juick/service/ActivityPubService.java
+++ b/src/main/java/com/juick/service/ActivityPubService.java
@@ -38,6 +38,7 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.*;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -152,7 +153,7 @@ public class ActivityPubService extends BaseJdbcService implements SocialService
}
return Optional.of(context);
}
- } catch (IOException e) {
+ } catch (IOException | RestClientException e) {
logger.warn("HTTP Signature exception: {}", e.getMessage());
}
return Optional.empty();
diff --git a/src/main/java/com/juick/www/api/Mastodon.java b/src/main/java/com/juick/www/api/Mastodon.java
index 9ea937d1..7591ec92 100644
--- a/src/main/java/com/juick/www/api/Mastodon.java
+++ b/src/main/java/com/juick/www/api/Mastodon.java
@@ -17,16 +17,20 @@
package com.juick.www.api;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.juick.model.Chat;
import com.juick.model.Message;
import com.juick.model.User;
import com.juick.service.ChatService;
+import com.juick.service.MessagesService;
import com.juick.service.TagService;
import com.juick.service.UserService;
+import com.juick.util.HttpBadRequestException;
+import com.juick.util.MessageUtils;
import com.juick.www.WebApp;
-import com.juick.www.api.webfinger.model.Account;
+import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -36,16 +40,13 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.web.bind.annotation.*;
import javax.inject.Inject;
-
+import java.time.Duration;
import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -63,27 +64,31 @@ public class Mastodon {
TagService tagService;
@Inject
ChatService chatService;
+ @Inject
+ MessagesService messagesService;
+ @Inject
+ User serviceUser;
- @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
- public record ApplicationRequest(String clientName,
- String redirectUris,
- String scopes) {
+ public record ApplicationRequest(
+ @JsonProperty("client_name") String clientName,
+ @JsonProperty("redirect_uris") String redirectUris,
+ @JsonProperty("scopes") String scopes) {
}
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record ApplicationResponse(String id,
- String name,
- String redirectUri,
- String clientId,
- String clientSecret) {
+ String name,
+ String redirectUri,
+ String clientId,
+ String clientSecret) {
}
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record CredentialAccount(String id, String username, String acct,
- String displayName,
- Integer followersCount,
- Integer followingCount,
- String avatar) {
+ String displayName,
+ Integer followersCount,
+ Integer followingCount,
+ String avatar) {
}
@@ -91,16 +96,7 @@ public class Mastodon {
return s != null ? Arrays.asList(s.split(" ")) : Collections.emptyList();
}
- @PostMapping(value = "/api/v1/apps", consumes = { MediaType.APPLICATION_JSON_VALUE })
- public ApplicationResponse apps(@RequestBody ApplicationRequest application) {
- return apps(application.clientName(), application.redirectUris(), application.scopes());
- }
-
- @PostMapping(value = "/api/v1/apps", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE,
- MediaType.MULTIPART_FORM_DATA_VALUE })
- public ApplicationResponse apps(@RequestParam("client_name") String clientName,
- @RequestParam("redirect_uris") String redirectUris,
- @RequestParam("scopes") String scopes) {
+ private ApplicationResponse registerApp(String clientName, String redirectUri, String scopes) {
var secret = UUID.randomUUID().toString();
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(UUID.randomUUID().toString())
@@ -110,8 +106,9 @@ public class Mastodon {
ClientAuthenticationMethod.CLIENT_SECRET_POST,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC)))
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
- .redirectUri(redirectUris)
+ .redirectUri(redirectUri)
.scopes((coll) -> coll.addAll(parseScopes(scopes)))
+ .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(365)).build())
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();
registeredClientRepository.save(registeredClient);
return new ApplicationResponse(
@@ -122,12 +119,38 @@ public class Mastodon {
secret);
}
- public record Instance(String domain) {
+ @PostMapping(value = "/api/v1/apps", consumes = {MediaType.APPLICATION_JSON_VALUE})
+ public ApplicationResponse apps(@RequestBody @Valid ApplicationRequest application) {
+ return registerApp(application.clientName(), application.redirectUris(), application.scopes());
+ }
+
+ @PostMapping(value = "/api/v1/apps", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE,
+ MediaType.MULTIPART_FORM_DATA_VALUE})
+ public ApplicationResponse apps(@RequestParam("client_name") String clientName,
+ @RequestParam("redirect_uris") String redirectUris,
+ @RequestParam("scopes") String scopes) {
+ return registerApp(clientName, redirectUris, scopes);
+ }
+
+ public record Contact(
+ String email,
+ CredentialAccount account
+ ) {
+
+ }
+
+ public record Instance(
+ String uri,
+ String title,
+ String description,
+ String version,
+ String email
+ ) {
}
- @GetMapping({ "/api/v1/instance", "/api/v2/instance" })
+ @GetMapping({"/api/v1/instance", "/api/v2/instance"})
public Instance getInstance() {
- return new Instance(domain);
+ return new Instance(domain, "Microblogging service", "Juick", "2.x","support@juick.com");
}
@GetMapping("/api/v1/accounts/verify_credentials")
@@ -135,13 +158,15 @@ public class Mastodon {
return toAccount(visitor);
}
- @GetMapping({ "/api/v1/announcements", "/api/v1/lists", "/api/v1/custom_emojis" })
+ @GetMapping({"/api/v1/announcements", "/api/v1/lists", "/api/v1/custom_emojis", "/api/v1/notifications"})
public ResponseEntity<List<Void>> arrayStubs() {
return new ResponseEntity<>(List.of(), HttpStatus.OK);
}
public record Preferences() {
- };
+ }
+
+ ;
@GetMapping("/api/v1/preferences")
public Preferences preferences() {
@@ -150,8 +175,8 @@ public class Mastodon {
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Filter(String id, String phrase, List<String> context,
- Instant expiresAt, Boolean irreversible,
- Boolean wholeWord) {
+ Instant expiresAt, Boolean irreversible,
+ Boolean wholeWord) {
}
@@ -162,17 +187,17 @@ public class Mastodon {
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Relationship(String id, Boolean following, Boolean showingReblogs,
- Boolean notyfing, Boolean followingBy, Boolean blocked, Boolean blockedBy,
- Boolean muting, Boolean mutingNotifications, Boolean requested, Boolean domainBlocking,
- Boolean endorsed, String note) {
+ Boolean notyfing, Boolean followingBy, Boolean blocked, Boolean blockedBy,
+ Boolean muting, Boolean mutingNotifications, Boolean requested, Boolean domainBlocking,
+ Boolean endorsed, String note) {
}
private Relationship findRelationships(String visitorId, String userId) {
- User user = userService.getUserByUID(Integer.valueOf(userId)).orElseThrow();
- var readers = userService.getUserReaders(Integer.valueOf(visitorId));
- var friends = userService.getUserFriends(Integer.valueOf(visitorId));
- var bl = userService.getUserBLUsers(Integer.valueOf(visitorId));
+ User user = userService.getUserByUID(Integer.parseInt(userId)).orElseThrow(HttpBadRequestException::new);
+ var readers = userService.getUserReaders(Integer.parseInt(visitorId));
+ var friends = userService.getUserFriends(Integer.parseInt(visitorId));
+ var bl = userService.getUserBLUsers(Integer.parseInt(visitorId));
var isFriend = friends.contains(user);
var isReader = readers.contains(user);
var isMuting = bl.contains(user);
@@ -183,21 +208,26 @@ public class Mastodon {
}
@GetMapping("/api/v1/accounts/relationships")
- public List<Relationship> relationships(@ModelAttribute User visitor, @RequestParam(value="id[]") String[] ids) {
+ public List<Relationship> relationships(@ModelAttribute User visitor, @RequestParam(value = "id[]") String[] ids) {
return Stream.of(ids).map(
- id -> findRelationships(String.valueOf(visitor.getUid()), id)
+ id -> findRelationships(String.valueOf(visitor.getUid()), id)
).collect(Collectors.toList());
}
+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Status(String id, Instant createdAt, String inReplyToId, String inReplyToAccountId,
- Boolean sensitive, String spoilerText, String visibility, String content) {}
+ Boolean sensitive, String spoilerText, String visibility, String content) {
+ }
+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Conversation(
String id,
Boolean unread,
List<CredentialAccount> accounts,
Status lastStatus
- ) {}
+ ) {
+ }
+
public CredentialAccount toAccount(User user) {
return new CredentialAccount(
String.valueOf(user.getUid()),
@@ -212,7 +242,7 @@ public class Mastodon {
public Status mapLastMessage(Chat chat) {
return new Status(
String.valueOf(chat.getLastMessageTimestamp().toEpochMilli()),
- chat.getLastMessageTimestamp(),
+ chat.getLastMessageTimestamp(),
null,
null,
false,
@@ -236,4 +266,37 @@ public class Mastodon {
this::toConversation
).collect(Collectors.toList());
}
+
+ public Status toStatus(Message message) {
+ return new Status(
+ String.valueOf(message.getMid()),
+ message.getCreated(),
+ null,
+ null,
+ MessageUtils.isSensitive(message),
+ "",
+ "public",
+ MessageUtils.formatMessage(message.getText())
+ );
+ }
+
+ @GetMapping("/api/v1/timelines/{timeline}")
+ public List<Status> publicTimeline(@ModelAttribute User visitor,
+ @PathVariable String timeline,
+ @RequestParam(value = "max_id", required = false, defaultValue = "0") int before,
+ @RequestParam(value = "only_media", required = false, defaultValue = "false") Boolean media) {
+ List<Integer> mids = List.of();
+ if (timeline.equals("public")) {
+ if (media) {
+ mids = messagesService.getPhotos(visitor.getUid(), before);
+ } else {
+ mids = messagesService.getAll(visitor.getUid(), before);
+ }
+ } else if (timeline.equals("home")) {
+ mids = messagesService.getMyFeed(visitor.getUid(), before, true);
+ }
+ return messagesService.getMessages(visitor, mids).stream()
+ .map(this::toStatus)
+ .collect(Collectors.toList());
+ }
}