Skip to content

Commit 77d7839

Browse files
authored
VisibilityService integration for Public Timeline (#167)
- This PR implement VisibilityService for Public Timeline. The Public Timeline only contain status with visibility level "public". - [x] Separate the statusService.getTimeline to statusService.getHomeTimeline and statusService.getPublicTimeline. - [x] update Homefeed API (StatusController.getApiV1TimelinesHome) to use statusService.getHomeTimeline - [x] update PublicTimeline API (TimelineController.getApiV1TimelinesHome) to use statusService.getPublicTimeline - [x] implement the VisibilityService for the PublicTime - [x] integration test
1 parent 74464c5 commit 77d7839

File tree

5 files changed

+165
-7
lines changed

5 files changed

+165
-7
lines changed

server/src/main/java/edu/sjsu/moth/server/controller/StatusController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Mono<ResponseEntity<List<Status>>> getApiV1TimelinesHome(Principal user,
172172
@RequestParam(required = false) String min_id,
173173
@RequestParam(required = false, defaultValue = "20")
174174
int limit) {
175-
return statusService.getTimeline(user, max_id, since_id, min_id, limit, true).map(ResponseEntity::ok);
175+
return statusService.getHomeTimeline(user, max_id, since_id, min_id, limit, true).map(ResponseEntity::ok);
176176
}
177177

178178
// spec: https://docs.joinmastodon.org/methods/accounts/#statuses

server/src/main/java/edu/sjsu/moth/server/controller/TimelineController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,6 @@ Mono<ResponseEntity<List<Status>>> getApiV1TimelinesHome(Principal user,
5656
@RequestParam(required = false) String min_id,
5757
@RequestParam(required = false, defaultValue = "20")
5858
int limit) {
59-
return statusService.getTimeline(user, max_id, since_id, min_id, limit, false).map(ResponseEntity::ok);
59+
return statusService.getPublicTimeline(user, max_id, since_id, min_id, limit).map(ResponseEntity::ok);
6060
}
6161
}

server/src/main/java/edu/sjsu/moth/server/service/StatusService.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ public class StatusService {
5454
@Autowired
5555
StatusHistoryRepository statusHistoryRepository;
5656

57+
@Autowired
58+
VisibilityService visibilityService;
59+
5760
Logger LOG = Logger.getLogger(StatusService.class.getName());
5861

5962
public Mono<ArrayList<StatusEdit>> findHistory(String id) {
@@ -158,18 +161,28 @@ public Mono<Status> findStatusById(String id) {
158161
return statusRepository.findById(id);
159162
}
160163

161-
public Mono<List<Status>> getTimeline(Principal user, String max_id, String since_id, String min_id, int limit,
164+
public Mono<List<Status>> getHomeTimeline(Principal user, String max_id, String since_id, String min_id, int limit,
162165
boolean isFollowingTimeline) {
163166
var qStatus = QStatus.status;
164167
var predicate = qStatus.content.isNotNull();
165168
predicate = addRangeQueries(predicate, max_id, since_id, max_id);
166169
var external = externalStatusRepository.findAll(predicate, Sort.by(Sort.Direction.DESC, "id"))
167170
.flatMap(statuses -> filterStatusByViewable(user, statuses, isFollowingTimeline)).take(limit);
168171
var internal = statusRepository.findAll(predicate, Sort.by(Sort.Direction.DESC, "id"))
169-
//.switchIfEmpty(Mono.fromRunnable(() -> System.out.println("No status found")))
170-
//.doOnNext(x -> System.out.println("before filter: " + x))
171-
.flatMap(statuses -> filterStatusByViewable(user, statuses, isFollowingTimeline));
172-
//.doOnNext(x -> System.out.println("after filter: " + x)).take(limit);
172+
.flatMap(statuses -> filterStatusByViewable(user, statuses, isFollowingTimeline)).take(limit);
173+
174+
//TODO: we may want to merge sort them, unsure if merge does that
175+
return Flux.merge(external, internal).collectList();
176+
}
177+
178+
public Mono<List<Status>> getPublicTimeline(Principal user, String max_id, String since_id, String min_id, int limit) {
179+
var qStatus = QStatus.status;
180+
var predicate = qStatus.content.isNotNull();
181+
predicate = addRangeQueries(predicate, max_id, since_id, max_id);
182+
var external = externalStatusRepository.findAll(predicate, Sort.by(Sort.Direction.DESC, "id"))
183+
.flatMap(status -> visibilityService.publicTimelinesViewable(status)).take(limit);
184+
var internal = statusRepository.findAll(predicate, Sort.by(Sort.Direction.DESC, "id"))
185+
.flatMap(status -> visibilityService.publicTimelinesViewable(status)).take(limit);
173186

174187
//TODO: we may want to merge sort them, unsure if merge does that
175188
return Flux.merge(external, internal).collectList();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package edu.sjsu.moth.server.service;
2+
3+
import edu.sjsu.moth.generated.Status;
4+
import edu.sjsu.moth.server.controller.MothController;
5+
import edu.sjsu.moth.server.db.FollowRepository;
6+
import lombok.extern.apachecommons.CommonsLog;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.security.core.userdetails.UsernameNotFoundException;
9+
import org.springframework.stereotype.Service;
10+
import reactor.core.publisher.Flux;
11+
import reactor.core.publisher.Mono;
12+
13+
import java.security.Principal;
14+
import java.util.logging.Logger;
15+
16+
@Service
17+
@CommonsLog
18+
public class VisibilityService {
19+
final String PUBLIC_VISIBILITY = "public";
20+
final String QUITE_PUBLIC = "unlisted";
21+
final String DIRECT_VISIBILITY = "direct";
22+
23+
24+
public Flux<Status> publicTimelinesViewable(Status status) {
25+
if (status.visibility.equals(PUBLIC_VISIBILITY)) return Flux.just(status);
26+
return Flux.empty();
27+
}
28+
29+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package edu.sjsu.moth.controllers;
2+
3+
import de.flapdoodle.embed.mongo.config.Net;
4+
import de.flapdoodle.embed.mongo.distribution.Version;
5+
import de.flapdoodle.embed.mongo.transitions.Mongod;
6+
import de.flapdoodle.embed.mongo.transitions.RunningMongodProcess;
7+
import de.flapdoodle.embed.process.io.ProcessOutput;
8+
import de.flapdoodle.reverse.TransitionWalker;
9+
import de.flapdoodle.reverse.transitions.Start;
10+
import edu.sjsu.moth.IntegrationTest;
11+
import edu.sjsu.moth.server.MothServerMain;
12+
import edu.sjsu.moth.server.controller.StatusController;
13+
import edu.sjsu.moth.server.db.Account;
14+
import edu.sjsu.moth.server.db.AccountRepository;
15+
import edu.sjsu.moth.server.util.MothConfiguration;
16+
import org.junit.jupiter.api.AfterAll;
17+
import org.junit.jupiter.api.Assertions;
18+
import org.junit.jupiter.api.BeforeAll;
19+
import org.junit.jupiter.api.Test;
20+
import org.springframework.beans.factory.annotation.Autowired;
21+
import org.springframework.boot.test.autoconfigure.data.mongo.AutoConfigureDataMongo;
22+
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
23+
import org.springframework.boot.test.context.SpringBootTest;
24+
import org.springframework.context.annotation.ComponentScan;
25+
import org.springframework.http.MediaType;
26+
import org.springframework.test.web.reactive.server.WebTestClient;
27+
28+
import java.io.File;
29+
import java.util.Random;
30+
31+
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt;
32+
33+
@SpringBootTest(classes = { TimelineControllerTest.class }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
34+
@AutoConfigureDataMongo
35+
@ComponentScan(basePackageClasses = MothServerMain.class)
36+
@AutoConfigureWebTestClient
37+
public class TimelineControllerTest {
38+
public static final String TIMELINE_END_POINTS = "/api/v1/timelines/public";
39+
public static final String POST_STATUS_ENDPOINT = "/api/v1/statuses";
40+
static private final int RAND_MONGO_PORT = 27017 + new Random().nextInt(17, 37);
41+
// https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo/blob/main/docs/Howto.md documents how to startup
42+
// embedded mongodb
43+
static private TransitionWalker.ReachedState<RunningMongodProcess> eMongod;
44+
45+
final WebTestClient webTestClient;
46+
final AccountRepository accountRepository;
47+
48+
static {
49+
try {
50+
var fullname = IntegrationTest.class.getResource("/test.cfg").getFile();
51+
// we need to fake out MothConfiguration
52+
System.out.println(new MothConfiguration(new File(fullname)).properties);
53+
} catch (Exception e) {
54+
System.err.println(e.getMessage());
55+
System.exit(2);
56+
}
57+
58+
}
59+
60+
@Autowired
61+
public TimelineControllerTest(WebTestClient webTestClient, AccountRepository accountRepository) {
62+
this.webTestClient = webTestClient;
63+
this.accountRepository = accountRepository;
64+
}
65+
66+
@AfterAll
67+
static void clean() {
68+
eMongod.close();
69+
}
70+
71+
@BeforeAll
72+
static void setup() {
73+
eMongod = Mongod.builder().processOutput(Start.to(ProcessOutput.class).initializedWith(ProcessOutput.silent()))
74+
.net(Start.to(Net.class).initializedWith(Net.defaults().withPort(RAND_MONGO_PORT))).build()
75+
.start(Version.Main.V6_0);
76+
System.setProperty("spring.data.mongodb.port", Integer.toString(RAND_MONGO_PORT));
77+
}
78+
79+
@Test
80+
public void checkAutoWires() {
81+
Assertions.assertNotNull(webTestClient);
82+
Assertions.assertNotNull(accountRepository);
83+
}
84+
85+
private void prepareStatus() {
86+
String statusCreator = "test-user";
87+
accountRepository.save(new Account(statusCreator)).block();
88+
89+
StatusController.V1PostStatus request;
90+
String[] visibilities = { "public", "unlisted", "follower", "direct" };
91+
for (String visibility : visibilities) {
92+
request = new StatusController.V1PostStatus();
93+
request.status = String.format("This is a %s status", visibility);
94+
request.visibility = visibility;
95+
96+
webTestClient.mutateWith(mockJwt().jwt(jwt -> jwt.claim("sub", statusCreator))).post()
97+
.uri(POST_STATUS_ENDPOINT).contentType(MediaType.APPLICATION_JSON).bodyValue(request).exchange()
98+
.expectStatus().isOk();
99+
}
100+
}
101+
102+
@Test
103+
public void testTimelineGetAllPublicStatus() {
104+
prepareStatus();
105+
accountRepository.save(new Account("test-fetch")).block();
106+
// Mock the authentication
107+
webTestClient
108+
.mutateWith(mockJwt().jwt(jwt -> jwt.claim("sub", "test-fetch")))
109+
.get()
110+
.uri(TIMELINE_END_POINTS)
111+
.exchange().expectStatus().isOk()
112+
.expectBody()
113+
.jsonPath("$.length()").isEqualTo(1)
114+
.jsonPath("$[0].visibility").isEqualTo("public");
115+
}
116+
}

0 commit comments

Comments
 (0)