From fcde250ec6a519173c53f195ffed1aa139b333f0 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 6 Jan 2023 04:14:38 +0300 Subject: Mastodon API: conversations --- src/main/java/com/juick/www/api/Mastodon.java | 175 +++++++++++++++++---- .../java/com/juick/www/api/activity/Profile.java | 2 +- 2 files changed, 147 insertions(+), 30 deletions(-) (limited to 'src/main/java/com/juick/www') diff --git a/src/main/java/com/juick/www/api/Mastodon.java b/src/main/java/com/juick/www/api/Mastodon.java index ff7f2e8c..b23ec9c1 100644 --- a/src/main/java/com/juick/www/api/Mastodon.java +++ b/src/main/java/com/juick/www/api/Mastodon.java @@ -17,12 +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.TagService; import com.juick.service.UserService; import com.juick.www.WebApp; +import com.juick.www.api.webfinger.model.Account; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; @@ -31,11 +39,15 @@ import org.springframework.security.oauth2.server.authorization.settings.ClientS import org.springframework.web.bind.annotation.*; import javax.inject.Inject; + +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.stream.Collectors; +import java.util.stream.Stream; @RestController public class Mastodon { @@ -47,26 +59,34 @@ public class Mastodon { UserService userService; @Inject RegisteredClientRepository registeredClientRepository; + @Inject + TagService tagService; + @Inject + ChatService chatService; - public record ApplicationRequest(@JsonProperty("client_name") String clientName, - @JsonProperty("redirect_uris") String redirectUris, - @JsonProperty("scopes") String scopes) { + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record ApplicationRequest(String clientName, + String redirectUris, + String scopes) { } + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record ApplicationResponse(String id, - @JsonProperty("client_name") String name, - @JsonProperty("redirect_uri") String redirectUri, - @JsonProperty("client_id") String clientId, - @JsonProperty("client_secret") String clientSecret) { + String name, + String redirectUri, + String clientId, + String clientSecret) { } + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record CredentialAccount(String id, String username, String acct, - @JsonProperty("display_name") String displayName, - @JsonProperty("followers_count") Integer followersCount, - @JsonProperty("following_count") Integer followingCount, - String avatar) { + String displayName, + Integer followersCount, + Integer followingCount, + String avatar) { } + private Collection parseScopes(String s) { return s != null ? Arrays.asList(s.split(" ")) : Collections.emptyList(); } @@ -76,19 +96,19 @@ public class Mastodon { return apps(application.clientName(), application.redirectUris(), application.scopes()); } - @PostMapping(value = "/api/v1/apps", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE }) + @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) { + @RequestParam("redirect_uris") String redirectUris, + @RequestParam("scopes") String scopes) { var secret = UUID.randomUUID().toString(); RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(UUID.randomUUID().toString()) .clientSecret("{noop}" + secret) .clientName(clientName) .clientAuthenticationMethods(coll -> coll.addAll(List.of( - ClientAuthenticationMethod.CLIENT_SECRET_POST, - ClientAuthenticationMethod.CLIENT_SECRET_BASIC - ))) + ClientAuthenticationMethod.CLIENT_SECRET_POST, + ClientAuthenticationMethod.CLIENT_SECRET_BASIC))) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri(redirectUris) .scopes((coll) -> coll.addAll(parseScopes(scopes))) @@ -99,24 +119,121 @@ public class Mastodon { registeredClient.getClientName(), String.join(",", registeredClient.getRedirectUris()), registeredClient.getClientId(), - secret - ); + secret); } - public record Instance(String domain) {} - @GetMapping("/api/v1/instance") + + public record Instance(String domain) { + } + + @GetMapping({ "/api/v1/instance", "/api/v2/instance" }) public Instance getInstance() { return new Instance(domain); } + @GetMapping("/api/v1/accounts/verify_credentials") public CredentialAccount account(@ModelAttribute User visitor) { + return toAccount(visitor); + } + + @GetMapping({ "/api/v1/announcements", "/api/v1/lists", "/api/v1/custom_emojis" }) + public ResponseEntity> arrayStubs() { + return new ResponseEntity<>(List.of(), HttpStatus.OK); + } + + public record Preferences() { + }; + + @GetMapping("/api/v1/preferences") + public Preferences preferences() { + return new Preferences(); + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record Filter(String id, String phrase, List context, + Instant expiresAt, Boolean irreversible, + Boolean wholeWord) { + + } + + @GetMapping("/api/v1/filters") + public List filters() { + return List.of(); + } + + @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) { + + } + + 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)); + var isFriend = friends.contains(user); + var isReader = readers.contains(user); + var isMuting = bl.contains(user); + return new Relationship(userId, + isFriend, isFriend, isFriend, isReader, false, false, + isMuting, isMuting, false, + false, false, ""); + } + + @GetMapping("/api/v1/accounts/relationships") + public List relationships(@ModelAttribute User visitor, @RequestParam(value="id[]") String[] ids) { + return Stream.of(ids).map( + 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) {} + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record Conversation( + String id, + Boolean unread, + List accounts, + Status lastStatus + ) {} + public CredentialAccount toAccount(User user) { return new CredentialAccount( - String.valueOf(visitor.getUid()), - visitor.getName(), - visitor.getName(), - visitor.getFullName(), - userService.getUserReaders(visitor.getUid()).size(), - userService.getUserFriends(visitor.getUid()).size(), - webApp.getAvatarUrl(visitor) + String.valueOf(user.getUid()), + user.getName(), + user.getName(), + user.getFullName(), + userService.getUserReaders(user.getUid()).size(), + userService.getUserFriends(user.getUid()).size(), + webApp.getAvatarUrl(user)); + } + + public Status mapLastMessage(Chat chat) { + return new Status( + String.valueOf(chat.getLastMessageTimestamp().toEpochMilli()), + chat.getLastMessageTimestamp(), + null, + null, + false, + "", + "direct", + chat.getLastMessageText() ); } + + public Conversation toConversation(User user, Chat chat) { + return new Conversation( + String.valueOf(chat.getUid()), + chat.getUnread() != null && chat.getUnread().size() > 0, + List.of(toAccount(user), toAccount(chat)), + mapLastMessage(chat)); + } + + @GetMapping("/api/v1/conversations") + public List conversations(@ModelAttribute User visitor) { + return chatService.getLastChats(visitor).stream().map( + chat -> toConversation(visitor, chat) + ).collect(Collectors.toList()); + } } diff --git a/src/main/java/com/juick/www/api/activity/Profile.java b/src/main/java/com/juick/www/api/activity/Profile.java index fba39954..b0a39a76 100644 --- a/src/main/java/com/juick/www/api/activity/Profile.java +++ b/src/main/java/com/juick/www/api/activity/Profile.java @@ -422,7 +422,7 @@ public class Profile { return new ResponseEntity<>(CommandResult.fromString("Can not authenticate"), HttpStatus.UNAUTHORIZED); } - @PostMapping(value = { "/u/", "/api/u/" }, produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/api/u/", produces = MediaType.APPLICATION_JSON_VALUE) public User fetchUser(@RequestParam URI uri) throws JsonProcessingException, HttpBadRequestException { return activityPubManager.actorToUser(uri); } -- cgit v1.2.3