Skip to content

Commit

Permalink
feat: support configuring notifier for different notification reason …
Browse files Browse the repository at this point in the history
…types (#4680)

#### What type of PR is this?
/kind feature
/area core
/area console
/milestone 2.10.x

#### What this PR does / why we need it:

用户支持为不同的通知事件配置通知方式

<img width="872" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/dad85ba3-96bc-4580-9dae-2b9e66e877fe">


#### Does this PR introduce a user-facing change?

```release-note
用户支持为不同的通知事件配置通知方式
```
  • Loading branch information
guqing authored Oct 8, 2023
1 parent da02165 commit 6411cef
Show file tree
Hide file tree
Showing 20 changed files with 1,020 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Set;
import lombok.Data;
import lombok.Getter;
import org.springframework.lang.NonNull;

/**
* Notification preference of user.
Expand All @@ -28,6 +29,7 @@ public static class ReasonTypeNotifier extends HashMap<String, NotifierSetting>
* @return if key of reasonType not exists, return default notifier, otherwise return the
* notifiers
*/
@NonNull
public Set<String> getNotifiers(String reasonType) {
var result = this.get(reasonType);
return result == null ? Set.of(DEFAULT_NOTIFIER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@
public interface UserNotificationPreferenceService {

Mono<UserNotificationPreference> getByUser(String username);

Mono<Void> saveByUser(String username,
UserNotificationPreference userNotificationPreference);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package run.halo.app.notification;

import java.util.HashMap;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.utils.JsonUtils;

Expand Down Expand Up @@ -39,6 +41,28 @@ public Mono<UserNotificationPreference> getByUser(String username) {
.defaultIfEmpty(new UserNotificationPreference());
}

@Override
public Mono<Void> saveByUser(String username,
UserNotificationPreference userNotificationPreference) {
var configName = buildUserPreferenceConfigMapName(username);
return client.fetch(ConfigMap.class, configName)
.switchIfEmpty(Mono.defer(() -> {
var configMap = new ConfigMap();
configMap.setMetadata(new Metadata());
configMap.getMetadata().setName(configName);
return client.create(configMap);
}))
.flatMap(config -> {
if (config.getData() == null) {
config.setData(new HashMap<>());
}
config.getData().put(NOTIFICATION_PREFERENCE,
JsonUtils.objectToJson(userNotificationPreference));
return client.update(config);
})
.then();
}

static String buildUserPreferenceConfigMapName(String username) {
return "user-preferences-" + username;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package run.halo.app.notification.endpoint;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;

import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuples;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.core.extension.notification.NotifierDescriptor;
import run.halo.app.core.extension.notification.ReasonType;
import run.halo.app.extension.Comparators;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.notification.UserNotificationPreference;
import run.halo.app.notification.UserNotificationPreferenceService;

/**
* Endpoint for user notification preferences.
*
* @author guqing
* @since 2.10.0
*/
@Component
@RequiredArgsConstructor
public class UserNotificationPreferencesEndpoint implements CustomEndpoint {

private final ReactiveExtensionClient client;
private final UserNotificationPreferenceService userNotificationPreferenceService;

@Override
public RouterFunction<ServerResponse> endpoint() {
return SpringdocRouteBuilder.route()
.nest(RequestPredicates.path("/userspaces/{username}"), userspaceScopedApis(),
builder -> {
})
.build();
}

Supplier<RouterFunction<ServerResponse>> userspaceScopedApis() {
var tag = "api.notification.halo.run/v1alpha1/Notification";
return () -> SpringdocRouteBuilder.route()
.GET("/notification-preferences", this::listNotificationPreferences,
builder -> builder.operationId("ListUserNotificationPreferences")
.description("List notification preferences for the authenticated user.")
.tag(tag)
.parameter(parameterBuilder()
.in(ParameterIn.PATH)
.name("username")
.description("Username")
.required(true)
)
.response(responseBuilder()
.implementation(ReasonTypeNotifierMatrix.class)
)
)
.POST("/notification-preferences", this::saveNotificationPreferences,
builder -> builder.operationId("SaveUserNotificationPreferences")
.description("Save notification preferences for the authenticated user.")
.tag(tag)
.parameter(parameterBuilder()
.in(ParameterIn.PATH)
.name("username")
.description("Username")
.required(true)
)
.requestBody(requestBodyBuilder()
.implementation(ReasonTypeNotifierCollectionRequest.class)
)
.response(responseBuilder().implementation(ReasonTypeNotifierMatrix.class))
)
.build();
}

private Mono<ServerResponse> saveNotificationPreferences(ServerRequest request) {
var username = request.pathVariable("username");
return request.bodyToMono(ReasonTypeNotifierCollectionRequest.class)
.flatMap(requestBody -> {
var reasonTypNotifiers = requestBody.reasonTypeNotifiers();
return userNotificationPreferenceService.getByUser(username)
.flatMap(preference -> {
var reasonTypeNotifierMap = preference.getReasonTypeNotifier();
reasonTypeNotifierMap.clear();
reasonTypNotifiers.forEach(reasonTypeNotifierRequest -> {
var reasonType = reasonTypeNotifierRequest.getReasonType();
var notifiers = reasonTypeNotifierRequest.getNotifiers();
var notifierSetting = new UserNotificationPreference.NotifierSetting();
notifierSetting.setNotifiers(
notifiers == null ? Set.of() : Set.copyOf(notifiers));
reasonTypeNotifierMap.put(reasonType, notifierSetting);
});
return userNotificationPreferenceService.saveByUser(username, preference);
});
})
.then(Mono.defer(() -> listReasonTypeNotifierMatrix(username)
.flatMap(result -> ServerResponse.ok().bodyValue(result)))
);
}

private Mono<ServerResponse> listNotificationPreferences(ServerRequest request) {
var username = request.pathVariable("username");
return listReasonTypeNotifierMatrix(username)
.flatMap(matrix -> ServerResponse.ok().bodyValue(matrix));
}

@NonNull
private static <T> Map<String, Integer> toNameIndexMap(List<T> collection,
Function<T, String> nameGetter) {
Map<String, Integer> indexMap = new HashMap<>();
for (int i = 0; i < collection.size(); i++) {
var item = collection.get(i);
indexMap.put(nameGetter.apply(item), i);
}
return indexMap;
}

Mono<ReasonTypeNotifierMatrix> listReasonTypeNotifierMatrix(String username) {
return client.list(ReasonType.class, null, Comparators.defaultComparator())
.map(reasonType -> new ReasonTypeInfo(reasonType.getMetadata().getName(),
reasonType.getSpec().getDisplayName(),
reasonType.getSpec().getDescription())
)
.collectList()
.flatMap(reasonTypes -> client.list(NotifierDescriptor.class, null,
Comparators.defaultComparator())
.map(notifier -> new NotifierInfo(notifier.getMetadata().getName(),
notifier.getSpec().getDisplayName(),
notifier.getSpec().getDescription())
)
.collectList()
.map(notifiers -> {
var matrix = new ReasonTypeNotifierMatrix()
.setReasonTypes(reasonTypes)
.setNotifiers(notifiers)
.setStateMatrix(new boolean[reasonTypes.size()][notifiers.size()]);
return Tuples.of(reasonTypes, matrix);
})
)
.flatMap(tuple2 -> {
var reasonTypes = tuple2.getT1();
var matrix = tuple2.getT2();

var reasonTypeIndexMap = toNameIndexMap(reasonTypes, ReasonTypeInfo::name);
var notifierIndexMap = toNameIndexMap(matrix.getNotifiers(), NotifierInfo::name);
var stateMatrix = matrix.getStateMatrix();

return userNotificationPreferenceService.getByUser(username)
.doOnNext(preference -> {
var reasonTypeNotifierMap = preference.getReasonTypeNotifier();
for (ReasonTypeInfo reasonType : reasonTypes) {
var reasonTypeIndex = reasonTypeIndexMap.get(reasonType.name());
var notifierNames =
reasonTypeNotifierMap.getNotifiers(reasonType.name());
for (String notifierName : notifierNames) {
var notifierIndex = notifierIndexMap.get(notifierName);
stateMatrix[reasonTypeIndex][notifierIndex] = true;
}
}
})
.thenReturn(matrix);
});
}

@Data
@Accessors(chain = true)
static class ReasonTypeNotifierMatrix {
private List<ReasonTypeInfo> reasonTypes;
private List<NotifierInfo> notifiers;
private boolean[][] stateMatrix;
}

record ReasonTypeInfo(String name, String displayName, String description) {
}

record NotifierInfo(String name, String displayName, String description) {
}

record ReasonTypeNotifierCollectionRequest(
@Schema(requiredMode = REQUIRED) List<ReasonTypeNotifierRequest> reasonTypeNotifiers) {
}

@Data
static class ReasonTypeNotifierRequest {
private String reasonType;
private List<String> notifiers;
}

@Override
public GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("api.notification.halo.run/v1alpha1");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,6 @@ rules:
- apiGroups: [ "api.notification.halo.run" ]
resources: [ "notifiers/receiver-config" ]
verbs: [ "get", "update" ]
- apiGroups: [ "api.notification.halo.run" ]
resources: [ "notification-preferences" ]
verbs: [ "create", "list" ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package run.halo.app.notification.endpoint;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.notification.NotifierDescriptor;
import run.halo.app.core.extension.notification.ReasonType;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.notification.UserNotificationPreferenceService;

/**
* Tests for {@link UserNotificationPreferencesEndpoint}.
*
* @author guqing
* @since 2.10.0
*/
@ExtendWith(MockitoExtension.class)
class UserNotificationPreferencesEndpointTest {

@Mock
private ReactiveExtensionClient client;

@Mock
private UserNotificationPreferenceService userNotificationPreferenceService;

@InjectMocks
private UserNotificationPreferencesEndpoint userNotificationPreferencesEndpoint;

private WebTestClient webTestClient;

@BeforeEach
void setUp() {
webTestClient = WebTestClient
.bindToRouterFunction(userNotificationPreferencesEndpoint.endpoint())
.build();
}

@Test
void listNotificationPreferences() {
when(client.list(eq(ReasonType.class), eq(null), any())).thenReturn(Flux.empty());
when(client.list(eq(NotifierDescriptor.class), eq(null), any())).thenReturn(Flux.empty());
when(userNotificationPreferenceService.getByUser(any())).thenReturn(Mono.empty());
webTestClient.post()
.uri("/userspaces/{username}/notification-preferences", "guqing")
.exchange()
.expectStatus()
.isOk();
}

@Test
void saveNotificationPreferences() {
when(client.list(eq(ReasonType.class), eq(null), any())).thenReturn(Flux.empty());
when(client.list(eq(NotifierDescriptor.class), eq(null), any())).thenReturn(Flux.empty());
when(userNotificationPreferenceService.getByUser(any())).thenReturn(Mono.empty());
webTestClient.post()
.uri("/userspaces/{username}/notification-preferences", "guqing")
.exchange()
.expectStatus()
.isOk();
}
}
5 changes: 5 additions & 0 deletions console/packages/api-client/src/.openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ models/notification.ts
models/notifier-descriptor-list.ts
models/notifier-descriptor-spec.ts
models/notifier-descriptor.ts
models/notifier-info.ts
models/notifier-setting-ref.ts
models/owner-info.ts
models/pat-spec.ts
Expand Down Expand Up @@ -211,7 +212,11 @@ models/reason-selector.ts
models/reason-spec-attributes.ts
models/reason-spec.ts
models/reason-subject.ts
models/reason-type-info.ts
models/reason-type-list.ts
models/reason-type-notifier-collection-request.ts
models/reason-type-notifier-matrix.ts
models/reason-type-notifier-request.ts
models/reason-type-spec.ts
models/reason-type.ts
models/reason.ts
Expand Down
Loading

0 comments on commit 6411cef

Please sign in to comment.