> recyclableList = ListRecycler.obtain();
+ return conversationSettingsService
+ .querySettings(clientRequest.userId(),
+ request.getUserIdsCount() > 0
+ ? CollectionUtil.newSet(request.getUserIdsList())
+ : null,
+ request.getGroupIdsCount() > 0
+ ? CollectionUtil.newSet(request.getGroupIdsList())
+ : null,
+ request.getNamesCount() > 0
+ ? CollectionUtil.newSet(request.getNamesList())
+ : null,
+ request.hasLastUpdatedDateStart()
+ ? new Date(request.getLastUpdatedDateStart())
+ : null)
+ .collect(Collectors.toCollection(recyclableList::getValue))
+ .map(settingsList -> {
+ ConversationSettingsList.Builder builder =
+ ClientMessagePool.getConversationSettingsListBuilder();
+ for (ConversationSettings settings : settingsList) {
+ builder.addConversationSettingsList(
+ ProtoModelConvertor.conversationSettings2proto(settings));
+ }
+ return RequestHandlerResult
+ .of(ClientMessagePool.getTurmsNotificationDataBuilder()
+ .setConversationSettingsList(builder)
+ .build());
+ })
+ .doFinally(signalType -> recyclableList.recycle());
+ };
+ }
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/domain/conversation/po/ConversationSettings.java b/turms-service/src/main/java/im/turms/service/domain/conversation/po/ConversationSettings.java
new file mode 100644
index 0000000000..d90699fbd5
--- /dev/null
+++ b/turms-service/src/main/java/im/turms/service/domain/conversation/po/ConversationSettings.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2019 The Turms Project
+ * https://github.com/turms-im/turms
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.turms.service.domain.conversation.po;
+
+import java.util.Date;
+import java.util.Map;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import im.turms.server.common.domain.common.po.BaseEntity;
+import im.turms.server.common.storage.mongo.entity.IndexType;
+import im.turms.server.common.storage.mongo.entity.ShardingStrategy;
+import im.turms.server.common.storage.mongo.entity.annotation.Document;
+import im.turms.server.common.storage.mongo.entity.annotation.Field;
+import im.turms.server.common.storage.mongo.entity.annotation.Id;
+import im.turms.server.common.storage.mongo.entity.annotation.Indexed;
+import im.turms.server.common.storage.mongo.entity.annotation.Sharded;
+
+/**
+ * {@link ConversationSettings} store conversation settings for specific users.
+ *
+ * 1. Though conversation settings can be considered as the special part of the user settings, we
+ * don't put {@link ConversationSettings} under the "user" domain because:
+ *
+ * 1. Now that we have the more specific domain, "conversation", we use it to not mix the "user"
+ * domain and the "conversation" domain as it is more confusing for developers and users.
+ *
+ * 2. The effective difference for a collection to be in different domains is that they MAY use
+ * different MongoDB clients to store data in different databases. But we can also share the same
+ * MongoDB client, and the same database if users have such a need, so they can have no effective
+ * difference.
+ *
+ * 3. When we support different turms-service can be responsible for different domains, we DO want
+ * to the turms-service servers that are responsible for the "conversation" domain to manage
+ * {@link ConversationSettings}, which is why we will support assigning different domains to
+ * different turms-service.
+ *
+ * In conclusion, we use the more specific domain "conversation" for {@link ConversationSettings}.
+ *
+ * 2. Use one collection for conversation settings instead of two collections for private and group
+ * conversation settings correspondingly because it is very common use case that users want to find
+ * all conversation settings in one request, so using one collection is more efficient, while their
+ * settings are the same.
+ *
+ * @author James Chen
+ */
+@Data
+@Document(ConversationSettings.COLLECTION_NAME)
+@Sharded(
+ shardKey = ConversationSettings.Fields.ID_OWNER_ID,
+ shardingStrategy = ShardingStrategy.HASH)
+public final class ConversationSettings extends BaseEntity {
+
+ public static final String COLLECTION_NAME = "conversationSettings";
+
+ @Id
+ private final Key key;
+
+ @Field(Fields.SETTINGS)
+ private final Map settings;
+
+ @Field(Fields.LAST_UPDATED_DATE)
+ private final Date lastUpdatedDate;
+
+ @Data
+ @AllArgsConstructor
+ public static final class Key {
+
+ @Field(Key.Fields.OWNER_ID)
+ @Indexed(IndexType.HASH)
+ private Long ownerId;
+
+ /**
+ * Note that we use the positive ID as the user ID, and the negative ID as the group ID. We
+ * don't introduce a field like "isGroupId" because using a single ID is both storage and
+ * performance efficient.
+ */
+ @Field(Key.Fields.TARGET_ID)
+ private Long targetId;
+
+ public static final class Fields {
+ public static final String OWNER_ID = "oid";
+ public static final String TARGET_ID = "tid";
+
+ private Fields() {
+ }
+ }
+ }
+
+ public static final class Fields {
+
+ public static final String ID_OWNER_ID = "_id."
+ + Key.Fields.OWNER_ID;
+ public static final String SETTINGS = "s";
+ public static final String LAST_UPDATED_DATE = "lud";
+
+ private Fields() {
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/domain/conversation/repository/ConversationSettingsRepository.java b/turms-service/src/main/java/im/turms/service/domain/conversation/repository/ConversationSettingsRepository.java
new file mode 100644
index 0000000000..3e49847ad9
--- /dev/null
+++ b/turms-service/src/main/java/im/turms/service/domain/conversation/repository/ConversationSettingsRepository.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2019 The Turms Project
+ * https://github.com/turms-im/turms
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.turms.service.domain.conversation.repository;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+import jakarta.annotation.Nullable;
+
+import com.mongodb.client.result.DeleteResult;
+import com.mongodb.client.result.UpdateResult;
+import com.mongodb.reactivestreams.client.ClientSession;
+import org.bson.BsonDocument;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import im.turms.server.common.domain.common.repository.BaseRepository;
+import im.turms.server.common.storage.mongo.BsonPool;
+import im.turms.server.common.storage.mongo.DomainFieldName;
+import im.turms.server.common.storage.mongo.TurmsMongoClient;
+import im.turms.server.common.storage.mongo.operation.option.Filter;
+import im.turms.server.common.storage.mongo.operation.option.QueryOptions;
+import im.turms.server.common.storage.mongo.operation.option.Update;
+import im.turms.service.domain.conversation.po.ConversationSettings;
+
+/**
+ * @author James Chen
+ */
+@Repository
+public class ConversationSettingsRepository extends BaseRepository {
+
+ public ConversationSettingsRepository(
+ @Qualifier("conversationMongoClient") TurmsMongoClient mongoClient) {
+ super(mongoClient, ConversationSettings.class);
+ }
+
+ public Mono upsertSettings(
+ Long ownerId,
+ Long targetId,
+ Map settings) {
+ Filter filter = Filter.newBuilder(2)
+ .eq(DomainFieldName.ID, new ConversationSettings.Key(ownerId, targetId));
+ Update update = Update.newBuilder(settings.size() + 1)
+ .set(ConversationSettings.Fields.LAST_UPDATED_DATE, new Date());
+ for (Map.Entry entry : settings.entrySet()) {
+ update.set(ConversationSettings.Fields.SETTINGS
+ + "."
+ + entry.getKey(), entry.getValue());
+ }
+ return mongoClient.upsert(entityClass, filter, update);
+ }
+
+ public Mono unsetSettings(
+ Long ownerId,
+ @Nullable Collection targetIds,
+ @Nullable Collection settingNames) {
+ Filter filter;
+ if (targetIds == null || targetIds.isEmpty()) {
+ filter = Filter.newBuilder(1)
+ .eq(ConversationSettings.Fields.ID_OWNER_ID, ownerId);
+ } else {
+ filter = Filter.newBuilder(1)
+ .in(DomainFieldName.ID, targetIds);
+ }
+ Update update = Update.newBuilder(1)
+ .set(ConversationSettings.Fields.LAST_UPDATED_DATE, new Date());
+ if (settingNames == null || settingNames.isEmpty()) {
+ update = update.unset(ConversationSettings.Fields.SETTINGS);
+ } else {
+ for (String settingName : settingNames) {
+ update = update.unset(ConversationSettings.Fields.SETTINGS
+ + "."
+ + settingName);
+ }
+ }
+ return mongoClient.updateOne(entityClass, filter, update);
+ }
+
+ public Flux findByIdAndSettingNames(
+ Long ownerId,
+ @Nullable Collection settingNames,
+ @Nullable Date lastUpdatedDateStart) {
+ Filter filter = Filter.newBuilder(2)
+ .eq(ConversationSettings.Fields.ID_OWNER_ID, ownerId)
+ .gteIfNotNull(ConversationSettings.Fields.LAST_UPDATED_DATE, lastUpdatedDateStart);
+ QueryOptions queryOptions = null;
+ if (settingNames != null && !settingNames.isEmpty()) {
+ BsonDocument projection = new BsonDocument()
+ .append(ConversationSettings.Fields.LAST_UPDATED_DATE, BsonPool.BSON_INT32_1);
+ for (String settingName : settingNames) {
+ projection.append(settingName, BsonPool.BSON_INT32_1);
+ }
+ queryOptions = QueryOptions.newBuilder()
+ .projection(projection);
+ }
+ return mongoClient.findMany(entityClass, filter, queryOptions);
+ }
+
+ public Flux findByIdAndSettingNames(
+ Collection keys,
+ @Nullable Collection settingNames,
+ @Nullable Date lastUpdatedDateStart) {
+ Filter filter = Filter.newBuilder(3)
+ .eq(DomainFieldName.ID, keys)
+ .gteIfNotNull(ConversationSettings.Fields.LAST_UPDATED_DATE, lastUpdatedDateStart);
+ QueryOptions queryOptions = null;
+ if (settingNames != null && !settingNames.isEmpty()) {
+ BsonDocument projection = new BsonDocument()
+ .append(ConversationSettings.Fields.LAST_UPDATED_DATE, BsonPool.BSON_INT32_1);
+ for (String settingName : settingNames) {
+ projection.append(settingName, BsonPool.BSON_INT32_1);
+ }
+ queryOptions = QueryOptions.newBuilder()
+ .projection(projection);
+ }
+ return mongoClient.findMany(entityClass, filter, queryOptions);
+ }
+
+ public Flux findSettingFields(
+ Long ownerId,
+ Long targetId,
+ Collection includedFields) {
+ Filter filter = Filter.newBuilder(1)
+ .eq(DomainFieldName.ID, new ConversationSettings.Key(ownerId, targetId));
+ return mongoClient.findObjectFields(entityClass,
+ filter,
+ ConversationSettings.Fields.SETTINGS,
+ includedFields);
+ }
+
+ public Mono deleteByOwnerIds(
+ Collection ownerIds,
+ @Nullable ClientSession clientSession) {
+ Filter filter = Filter.newBuilder(1)
+ .in(ConversationSettings.Fields.ID_OWNER_ID, ownerIds);
+ return mongoClient.deleteMany(clientSession, entityClass, filter);
+ }
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/domain/conversation/repository/GroupConversationRepository.java b/turms-service/src/main/java/im/turms/service/domain/conversation/repository/GroupConversationRepository.java
index f1eeb6159c..193bb3007e 100644
--- a/turms-service/src/main/java/im/turms/service/domain/conversation/repository/GroupConversationRepository.java
+++ b/turms-service/src/main/java/im/turms/service/domain/conversation/repository/GroupConversationRepository.java
@@ -44,7 +44,7 @@ public GroupConversationRepository(
super(mongoClient, GroupConversation.class);
}
- public Mono upsert(
+ public Mono upsert(
Long groupId,
Long memberId,
Date readDate,
@@ -65,7 +65,7 @@ public Mono upsert(
return mongoClient.upsert(entityClass, filter, update);
}
- public Mono upsert(Long groupId, Collection memberIds, Date readDate) {
+ public Mono upsert(Long groupId, Collection memberIds, Date readDate) {
Filter filter = Filter.newBuilder(1)
.eq(DomainFieldName.ID, groupId);
Update update = Update.newBuilder(memberIds.size());
diff --git a/turms-service/src/main/java/im/turms/service/domain/conversation/repository/PrivateConversationRepository.java b/turms-service/src/main/java/im/turms/service/domain/conversation/repository/PrivateConversationRepository.java
index c8d1e3663b..ca2571cee9 100644
--- a/turms-service/src/main/java/im/turms/service/domain/conversation/repository/PrivateConversationRepository.java
+++ b/turms-service/src/main/java/im/turms/service/domain/conversation/repository/PrivateConversationRepository.java
@@ -23,6 +23,7 @@
import jakarta.annotation.Nullable;
import com.mongodb.client.result.DeleteResult;
+import com.mongodb.client.result.UpdateResult;
import com.mongodb.reactivestreams.client.ClientSession;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
@@ -48,7 +49,7 @@ public PrivateConversationRepository(
super(mongoClient, PrivateConversation.class);
}
- public Mono upsert(
+ public Mono upsert(
Set keys,
Date readDate,
boolean allowMoveReadDateForward) {
diff --git a/turms-service/src/main/java/im/turms/service/domain/conversation/service/ConversationService.java b/turms-service/src/main/java/im/turms/service/domain/conversation/service/ConversationService.java
index 0ce07a6e3d..aa9fce5664 100644
--- a/turms-service/src/main/java/im/turms/service/domain/conversation/service/ConversationService.java
+++ b/turms-service/src/main/java/im/turms/service/domain/conversation/service/ConversationService.java
@@ -189,7 +189,8 @@ public Mono upsertGroupConversationReadDate(
e -> readDate == null
? Mono.empty()
: Mono.error(ResponseException.get(
- ResponseStatusCode.MOVING_READ_DATE_FORWARD_IS_DISABLED)));
+ ResponseStatusCode.MOVING_READ_DATE_FORWARD_IS_DISABLED)))
+ .then();
}
public Mono upsertGroupConversationsReadDate(
@@ -218,7 +219,8 @@ public Mono upsertGroupConversationsReadDate(
for (Map.Entry> entry : entries) {
Long groupId = entry.getKey();
List memberIds = entry.getValue();
- upsertMonos.add(groupConversationRepository.upsert(groupId, memberIds, readDate));
+ upsertMonos.add(groupConversationRepository.upsert(groupId, memberIds, readDate)
+ .then());
}
return Mono.whenDelayError(upsertMonos);
}
@@ -262,7 +264,8 @@ public Mono upsertPrivateConversationsReadDate(
e -> readDate == null
? Mono.empty()
: Mono.error(ResponseException.get(
- ResponseStatusCode.MOVING_READ_DATE_FORWARD_IS_DISABLED)));
+ ResponseStatusCode.MOVING_READ_DATE_FORWARD_IS_DISABLED)))
+ .then();
}
public Flux queryGroupConversations(@NotNull Collection groupIds) {
diff --git a/turms-service/src/main/java/im/turms/service/domain/conversation/service/ConversationSettingsService.java b/turms-service/src/main/java/im/turms/service/domain/conversation/service/ConversationSettingsService.java
new file mode 100644
index 0000000000..0ab9405c2e
--- /dev/null
+++ b/turms-service/src/main/java/im/turms/service/domain/conversation/service/ConversationSettingsService.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2019 The Turms Project
+ * https://github.com/turms-im/turms
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.turms.service.domain.conversation.service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import jakarta.annotation.Nullable;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import org.eclipse.collections.impl.set.mutable.UnifiedSet;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import im.turms.server.common.access.client.dto.model.common.Value;
+import im.turms.server.common.access.common.ResponseStatusCode;
+import im.turms.server.common.infra.collection.CollectionUtil;
+import im.turms.server.common.infra.collection.CollectorUtil;
+import im.turms.server.common.infra.exception.ResponseException;
+import im.turms.server.common.infra.property.TurmsProperties;
+import im.turms.server.common.infra.property.TurmsPropertiesManager;
+import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties;
+import im.turms.server.common.infra.reactor.PublisherPool;
+import im.turms.server.common.infra.validation.Validator;
+import im.turms.server.common.storage.mongo.IMongoCollectionInitializer;
+import im.turms.service.domain.common.service.CustomSettingService;
+import im.turms.service.domain.conversation.po.ConversationSettings;
+import im.turms.service.domain.conversation.repository.ConversationSettingsRepository;
+import im.turms.service.domain.group.service.GroupMemberService;
+import im.turms.service.domain.user.service.UserRelationshipService;
+
+/**
+ * @author James Chen
+ */
+@Service
+@DependsOn(IMongoCollectionInitializer.BEAN_NAME)
+public class ConversationSettingsService extends CustomSettingService {
+
+ private final GroupMemberService groupMemberService;
+ private final UserRelationshipService userRelationshipService;
+
+ private final ConversationSettingsRepository conversationSettingsRepository;
+
+ public ConversationSettingsService(
+ TurmsPropertiesManager propertiesManager,
+ GroupMemberService groupMemberService,
+ UserRelationshipService userRelationshipService,
+ ConversationSettingsRepository conversationSettingsRepository) {
+ this.groupMemberService = groupMemberService;
+ this.userRelationshipService = userRelationshipService;
+ this.conversationSettingsRepository = conversationSettingsRepository;
+
+ propertiesManager.notifyAndAddGlobalPropertiesChangeListener(this::updateGlobalProperties);
+ }
+
+ private void updateGlobalProperties(TurmsProperties properties) {
+ List settings = properties.getService()
+ .getUser()
+ .getAllowedSettings();
+ super.updateGlobalProperties(settings);
+ }
+
+ public Mono upsertPrivateConversationSettings(
+ Long ownerId,
+ Long userId,
+ Map settings) {
+ try {
+ Validator.notNull(ownerId, "ownerId");
+ Validator.notNull(userId, "userId");
+ Validator.notNull(settings, "settings");
+ } catch (ResponseException e) {
+ return Mono.error(e);
+ }
+ if (settings.isEmpty()) {
+ return PublisherPool.FALSE;
+ }
+ Set immutableSettingsForUpsert = null;
+ if (!immutableSettings.isEmpty()) {
+ Set settingsForUpsert = settings.keySet();
+ for (String settingName : settingsForUpsert) {
+ if (immutableSettings.contains(settingName)) {
+ if (immutableSettingsForUpsert == null) {
+ immutableSettingsForUpsert = new UnifiedSet<>(4);
+ }
+ immutableSettingsForUpsert.add(settingName);
+ }
+ }
+ }
+ if (immutableSettingsForUpsert == null) {
+ return userRelationshipService.hasOneSidedRelationship(ownerId, userId)
+ .flatMap(hasOneSidedRelationship -> {
+ if (!hasOneSidedRelationship) {
+ return Mono.error(ResponseException.get(
+ ResponseStatusCode.NOT_RELATED_USER_TO_UPDATE_PRIVATE_CONVERSATION_SETTING));
+ }
+ Map parsedSettings =
+ parseSettings(settingPropertiesList, settings);
+ return conversationSettingsRepository
+ .upsertSettings(ownerId, userId, parsedSettings)
+ .map(updateResult -> updateResult.getModifiedCount() > 0
+ || updateResult.getUpsertedId() != null);
+ });
+ }
+ Set finalImmutableSettingsForUpsert = immutableSettingsForUpsert;
+ return userRelationshipService.hasOneSidedRelationship(ownerId, userId)
+ .flatMap(hasOneSidedRelationship -> {
+ if (!hasOneSidedRelationship) {
+ return Mono.error(ResponseException.get(
+ ResponseStatusCode.NOT_RELATED_USER_TO_UPDATE_PRIVATE_CONVERSATION_SETTING));
+ }
+ return conversationSettingsRepository
+ .findSettingFields(ownerId, userId, finalImmutableSettingsForUpsert)
+ .collect(CollectorUtil.toSet(finalImmutableSettingsForUpsert.size()))
+ .flatMap(existingSettings -> {
+ if (existingSettings.isEmpty()) {
+ Map parsedSettings =
+ parseSettings(settingPropertiesList, settings);
+ return conversationSettingsRepository
+ .upsertSettings(ownerId, userId, parsedSettings);
+ }
+ finalImmutableSettingsForUpsert.removeIf(
+ settingName -> !existingSettings.contains(settingName));
+ if (finalImmutableSettingsForUpsert.isEmpty()) {
+ Map parsedSettings =
+ parseSettings(settingPropertiesList, settings);
+ return conversationSettingsRepository
+ .upsertSettings(ownerId, userId, parsedSettings);
+ }
+ List sortedConflictedSettings =
+ new ArrayList<>(finalImmutableSettingsForUpsert);
+ sortedConflictedSettings.sort(null);
+ return Mono.error(
+ ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
+ "Cannot update existing immutable settings: "
+ + sortedConflictedSettings));
+ })
+ .map(updateResult -> updateResult.getModifiedCount() > 0
+ || updateResult.getUpsertedId() != null);
+ });
+ }
+
+ public Mono upsertGroupConversationSettings(
+ Long ownerId,
+ Long groupId,
+ Map settings) {
+ try {
+ Validator.notNull(ownerId, "ownerId");
+ Validator.notNull(groupId, "groupId");
+ Validator.notNull(settings, "settings");
+ } catch (ResponseException e) {
+ return Mono.error(e);
+ }
+ if (settings.isEmpty()) {
+ return PublisherPool.FALSE;
+ }
+ Set immutableSettingsForUpsert = null;
+ if (!immutableSettings.isEmpty()) {
+ Set settingsForUpsert = settings.keySet();
+ for (String settingName : settingsForUpsert) {
+ if (immutableSettings.contains(settingName)) {
+ if (immutableSettingsForUpsert == null) {
+ immutableSettingsForUpsert = new UnifiedSet<>(4);
+ }
+ immutableSettingsForUpsert.add(settingName);
+ }
+ }
+ }
+ if (immutableSettingsForUpsert == null) {
+ return groupMemberService.isGroupMember(groupId, ownerId, false)
+ .flatMap(isGroupMember -> {
+ if (!isGroupMember) {
+ return Mono.error(ResponseException.get(
+ ResponseStatusCode.NOT_GROUP_MEMBER_TO_UPDATE_GROUP_CONVERSATION_SETTING));
+ }
+ Map parsedSettings =
+ parseSettings(settingPropertiesList, settings);
+ return conversationSettingsRepository
+ .upsertSettings(ownerId,
+ getTargetIdFromGroupId(groupId),
+ parsedSettings)
+ .map(updateResult -> updateResult.getModifiedCount() > 0
+ || updateResult.getUpsertedId() != null);
+ });
+ }
+ Set finalImmutableSettingsForUpsert = immutableSettingsForUpsert;
+ return groupMemberService.isGroupMember(groupId, ownerId, false)
+ .flatMap(hasOneSidedRelationship -> {
+ if (!hasOneSidedRelationship) {
+ return Mono.error(ResponseException.get(
+ ResponseStatusCode.NOT_GROUP_MEMBER_TO_UPDATE_GROUP_CONVERSATION_SETTING));
+ }
+ return conversationSettingsRepository
+ .findSettingFields(ownerId,
+ getTargetIdFromGroupId(groupId),
+ finalImmutableSettingsForUpsert)
+ .collect(CollectorUtil.toSet(finalImmutableSettingsForUpsert.size()))
+ .flatMap(existingSettings -> {
+ if (existingSettings.isEmpty()) {
+ Map parsedSettings =
+ parseSettings(settingPropertiesList, settings);
+ return conversationSettingsRepository.upsertSettings(ownerId,
+ getTargetIdFromGroupId(groupId),
+ parsedSettings);
+ }
+ finalImmutableSettingsForUpsert.removeIf(
+ settingName -> !existingSettings.contains(settingName));
+ if (finalImmutableSettingsForUpsert.isEmpty()) {
+ Map parsedSettings =
+ parseSettings(settingPropertiesList, settings);
+ return conversationSettingsRepository.upsertSettings(ownerId,
+ getTargetIdFromGroupId(groupId),
+ parsedSettings);
+ }
+ List sortedConflictedSettings =
+ new ArrayList<>(finalImmutableSettingsForUpsert);
+ sortedConflictedSettings.sort(null);
+ return Mono.error(
+ ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
+ "Cannot update existing immutable settings: "
+ + sortedConflictedSettings));
+ })
+ .map(updateResult -> updateResult.getModifiedCount() > 0
+ || updateResult.getUpsertedId() != null);
+ });
+ }
+
+ public Mono deleteSettings(
+ Collection ownerIds,
+ @Nullable ClientSession clientSession) {
+ try {
+ Validator.notNull(ownerIds, "ownerIds");
+ } catch (ResponseException e) {
+ return Mono.error(e);
+ }
+ return conversationSettingsRepository.deleteByOwnerIds(ownerIds, clientSession)
+ .map(deleteResult -> deleteResult.getDeletedCount() > 0);
+ }
+
+ public Mono unsetSettings(
+ Long ownerId,
+ @Nullable Set userIds,
+ @Nullable Set groupIds,
+ @Nullable Set settingNames) {
+ if (!deletableSettings.isEmpty()) {
+ if (settingNames == null) {
+ return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
+ "Cannot delete non-deletable settings: "
+ + deletableSettings));
+ }
+ for (String settingName : settingNames) {
+ if (!deletableSettings.contains(settingName)) {
+ return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
+ "Cannot delete non-deletable settings: "
+ + deletableSettings));
+ }
+ }
+ }
+ int userIdCount = CollectionUtil.getSize(userIds);
+ int groupIdCount = CollectionUtil.getSize(groupIds);
+ if (userIdCount > 0) {
+ List targetIds;
+ if (groupIdCount > 0) {
+ targetIds = new ArrayList<>(userIdCount + groupIdCount);
+ for (Long groupId : groupIds) {
+ targetIds.add(getTargetIdFromGroupId(groupId));
+ }
+ } else {
+ targetIds = new ArrayList<>(userIdCount);
+ }
+ targetIds.addAll(userIds);
+ return conversationSettingsRepository.unsetSettings(ownerId, targetIds, settingNames)
+ .map(updateResult -> updateResult.getModifiedCount() > 0);
+ }
+ return conversationSettingsRepository.unsetSettings(ownerId, null, settingNames)
+ .map(updateResult -> updateResult.getModifiedCount() > 0);
+ }
+
+ public Flux querySettings(
+ Long ownerId,
+ @Nullable Collection userIds,
+ @Nullable Collection groupIds,
+ @Nullable Set settingNames,
+ @Nullable Date lastUpdatedDateStart) {
+ try {
+ Validator.notNull(ownerId, "ownerId");
+ } catch (ResponseException e) {
+ return Flux.error(e);
+ }
+ Collection keys;
+ int groupIdCount = CollectionUtil.getSize(groupIds);
+ int userIdCount = CollectionUtil.getSize(userIds);
+ if (userIdCount > 0) {
+ if (groupIdCount > 0) {
+ keys = new ArrayList<>(groupIdCount + userIdCount);
+ for (Long groupId : groupIds) {
+ keys.add(
+ new ConversationSettings.Key(ownerId, getTargetIdFromGroupId(groupId)));
+ }
+ } else {
+ keys = new ArrayList<>(userIdCount);
+ }
+ for (Long userId : userIds) {
+ keys.add(new ConversationSettings.Key(ownerId, userId));
+ }
+ return conversationSettingsRepository
+ .findByIdAndSettingNames(keys, settingNames, lastUpdatedDateStart);
+ } else if (groupIdCount > 0) {
+ keys = new ArrayList<>(groupIdCount);
+ for (Long groupId : groupIds) {
+ keys.add(new ConversationSettings.Key(ownerId, getTargetIdFromGroupId(groupId)));
+ }
+ return conversationSettingsRepository
+ .findByIdAndSettingNames(keys, settingNames, lastUpdatedDateStart);
+ } else {
+ return conversationSettingsRepository
+ .findByIdAndSettingNames(ownerId, settingNames, lastUpdatedDateStart);
+ }
+ }
+
+ private Long getTargetIdFromGroupId(Long groupId) {
+ return -groupId;
+ }
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/domain/user/access/servicerequest/controller/UserSettingsServiceController.java b/turms-service/src/main/java/im/turms/service/domain/user/access/servicerequest/controller/UserSettingsServiceController.java
new file mode 100644
index 0000000000..ccebbc0622
--- /dev/null
+++ b/turms-service/src/main/java/im/turms/service/domain/user/access/servicerequest/controller/UserSettingsServiceController.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 The Turms Project
+ * https://github.com/turms-im/turms
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.turms.service.domain.user.access.servicerequest.controller;
+
+import java.util.Date;
+
+import org.springframework.stereotype.Controller;
+
+import im.turms.server.common.access.client.dto.ClientMessagePool;
+import im.turms.server.common.access.client.dto.request.TurmsRequest;
+import im.turms.server.common.access.client.dto.request.user.DeleteUserSettingsRequest;
+import im.turms.server.common.access.client.dto.request.user.QueryUserSettingsRequest;
+import im.turms.server.common.access.client.dto.request.user.UpdateUserSettingsRequest;
+import im.turms.server.common.infra.collection.CollectionUtil;
+import im.turms.server.common.infra.property.TurmsProperties;
+import im.turms.server.common.infra.property.TurmsPropertiesManager;
+import im.turms.server.common.infra.property.env.service.business.notification.NotificationProperties;
+import im.turms.server.common.infra.property.env.service.business.notification.user.NotificationUserSettingDeletedProperties;
+import im.turms.server.common.infra.property.env.service.business.notification.user.NotificationUserSettingUpdatedProperties;
+import im.turms.service.access.servicerequest.dispatcher.ClientRequestHandler;
+import im.turms.service.access.servicerequest.dispatcher.ServiceRequestMapping;
+import im.turms.service.access.servicerequest.dto.RequestHandlerResult;
+import im.turms.service.domain.common.access.servicerequest.controller.BaseServiceController;
+import im.turms.service.domain.user.service.UserSettingsService;
+import im.turms.service.infra.proto.ProtoModelConvertor;
+
+import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_USER_SETTINGS_REQUEST;
+import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_USER_SETTINGS_REQUEST;
+import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_USER_SETTINGS_REQUEST;
+
+/**
+ * @author James Chen
+ */
+@Controller
+public class UserSettingsServiceController extends BaseServiceController {
+
+ private final UserSettingsService userSettingsService;
+
+ private boolean notifyRequesterOtherOnlineSessionsOfUserSettingDeleted;
+ private boolean notifyRequesterOtherOnlineSessionsOfUserSettingUpdated;
+
+ public UserSettingsServiceController(
+ TurmsPropertiesManager propertiesManager,
+ UserSettingsService userSettingsService) {
+ this.userSettingsService = userSettingsService;
+
+ propertiesManager.notifyAndAddGlobalPropertiesChangeListener(this::updateProperties);
+ }
+
+ private void updateProperties(TurmsProperties properties) {
+ NotificationProperties notificationProperties = properties.getService()
+ .getNotification();
+
+ NotificationUserSettingDeletedProperties notificationUserSettingDeletedProperties =
+ notificationProperties.getUserSettingDeleted();
+ notifyRequesterOtherOnlineSessionsOfUserSettingDeleted =
+ notificationUserSettingDeletedProperties.isNotifyRequesterOtherOnlineSessions();
+
+ NotificationUserSettingUpdatedProperties notificationUserSettingUpdatedProperties =
+ notificationProperties.getUserSettingUpdated();
+ notifyRequesterOtherOnlineSessionsOfUserSettingUpdated =
+ notificationUserSettingUpdatedProperties.isNotifyRequesterOtherOnlineSessions();
+ }
+
+ @ServiceRequestMapping(DELETE_USER_SETTINGS_REQUEST)
+ public ClientRequestHandler handleDeleteUserSettingsRequest() {
+ return clientRequest -> {
+ TurmsRequest turmsRequest = clientRequest.turmsRequest();
+ DeleteUserSettingsRequest request = turmsRequest.getDeleteUserSettingsRequest();
+ return userSettingsService
+ .unsetSettings(clientRequest.userId(),
+ request.getNamesCount() > 0
+ ? CollectionUtil.newSet(request.getNamesList())
+ : null)
+ .map(deleted -> RequestHandlerResult.of(
+ notifyRequesterOtherOnlineSessionsOfUserSettingDeleted && deleted,
+ turmsRequest));
+ };
+ }
+
+ @ServiceRequestMapping(UPDATE_USER_SETTINGS_REQUEST)
+ public ClientRequestHandler handleUpdateUserSettingsRequest() {
+ return clientRequest -> {
+ UpdateUserSettingsRequest request = clientRequest.turmsRequest()
+ .getUpdateUserSettingsRequest();
+ return userSettingsService
+ .upsertSettings(clientRequest.userId(), request.getSettingsMap())
+ .map(updated -> updated
+ ? RequestHandlerResult
+ .of(notifyRequesterOtherOnlineSessionsOfUserSettingUpdated
+ && updated, clientRequest.turmsRequest())
+ : RequestHandlerResult.OK);
+ };
+ }
+
+ @ServiceRequestMapping(QUERY_USER_SETTINGS_REQUEST)
+ public ClientRequestHandler handleQueryUserSettingsRequest() {
+ return clientRequest -> {
+ QueryUserSettingsRequest request = clientRequest.turmsRequest()
+ .getQueryUserSettingsRequest();
+ return userSettingsService
+ .querySettings(clientRequest.userId(),
+ request.getNamesCount() > 0
+ ? CollectionUtil.newSet(request.getNamesList())
+ : null,
+ request.hasLastUpdatedDateStart()
+ ? new Date(request.getLastUpdatedDateStart())
+ : null)
+ .map(settings -> RequestHandlerResult
+ .of(ClientMessagePool.getTurmsNotificationDataBuilder()
+ .setUserSettings(
+ ProtoModelConvertor.userSettings2proto(settings))
+ .build()));
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/domain/user/po/UserSettings.java b/turms-service/src/main/java/im/turms/service/domain/user/po/UserSettings.java
new file mode 100644
index 0000000000..b944861e56
--- /dev/null
+++ b/turms-service/src/main/java/im/turms/service/domain/user/po/UserSettings.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 The Turms Project
+ * https://github.com/turms-im/turms
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.turms.service.domain.user.po;
+
+import java.util.Date;
+import java.util.Map;
+
+import lombok.Data;
+
+import im.turms.server.common.domain.common.po.BaseEntity;
+import im.turms.server.common.storage.mongo.entity.annotation.Document;
+import im.turms.server.common.storage.mongo.entity.annotation.Field;
+import im.turms.server.common.storage.mongo.entity.annotation.Id;
+import im.turms.server.common.storage.mongo.entity.annotation.Sharded;
+
+/**
+ * For most applications, {@link UserSettings} represents the application-level settings related to
+ * the user. For example, "language", "theme", etc. So it is more accurate to call
+ * "UserApplicationSetting", but we still call it "UserSetting" because we don't want to limit its
+ * usage and scope to have to be application-level.
+ *
+ * @author James Chen
+ */
+@Data
+@Document(UserSettings.COLLECTION_NAME)
+@Sharded
+public final class UserSettings extends BaseEntity {
+
+ public static final String COLLECTION_NAME = "userSettings";
+
+ @Id
+ private final Long userId;
+
+ @Field(Fields.SETTINGS)
+ private final Map settings;
+
+ @Field(Fields.LAST_UPDATED_DATE)
+ private final Date lastUpdatedDate;
+
+ public static final class Fields {
+
+ public static final String SETTINGS = "s";
+ public static final String LAST_UPDATED_DATE = "lud";
+
+ private Fields() {
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/domain/user/repository/UserSettingsRepository.java b/turms-service/src/main/java/im/turms/service/domain/user/repository/UserSettingsRepository.java
new file mode 100644
index 0000000000..deb9c4ba92
--- /dev/null
+++ b/turms-service/src/main/java/im/turms/service/domain/user/repository/UserSettingsRepository.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Turms Project
+ * https://github.com/turms-im/turms
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.turms.service.domain.user.repository;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+import jakarta.annotation.Nullable;
+
+import com.mongodb.client.result.UpdateResult;
+import org.bson.BsonDocument;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Mono;
+
+import im.turms.server.common.domain.common.repository.BaseRepository;
+import im.turms.server.common.storage.mongo.BsonPool;
+import im.turms.server.common.storage.mongo.DomainFieldName;
+import im.turms.server.common.storage.mongo.TurmsMongoClient;
+import im.turms.server.common.storage.mongo.operation.option.Filter;
+import im.turms.server.common.storage.mongo.operation.option.QueryOptions;
+import im.turms.server.common.storage.mongo.operation.option.Update;
+import im.turms.service.domain.user.po.UserSettings;
+
+/**
+ * @author James Chen
+ */
+@Repository
+public class UserSettingsRepository extends BaseRepository {
+
+ public UserSettingsRepository(@Qualifier("userMongoClient") TurmsMongoClient mongoClient) {
+ super(mongoClient, UserSettings.class);
+ }
+
+ public Mono upsertSettings(Long userId, Map settings) {
+ Filter filter = Filter.newBuilder(1)
+ .eq(DomainFieldName.ID, userId);
+ Update update = Update.newBuilder(settings.size() + 1)
+ .set(UserSettings.Fields.LAST_UPDATED_DATE, new Date());
+ for (Map.Entry entry : settings.entrySet()) {
+ update.set(UserSettings.Fields.SETTINGS
+ + "."
+ + entry.getKey(), entry.getValue());
+ }
+ return mongoClient.upsert(entityClass, filter, update);
+ }
+
+ public Mono unsetSettings(
+ Long userId,
+ @Nullable Collection settingNames) {
+ Filter filter = Filter.newBuilder(1)
+ .eq(DomainFieldName.ID, userId);
+ Update update = Update.newBuilder(1)
+ .set(UserSettings.Fields.LAST_UPDATED_DATE, new Date());
+ if (settingNames == null || settingNames.isEmpty()) {
+ update = update.unset(UserSettings.Fields.SETTINGS);
+ } else {
+ for (String settingName : settingNames) {
+ update = update.unset(UserSettings.Fields.SETTINGS
+ + "."
+ + settingName);
+ }
+ }
+ return mongoClient.updateOne(entityClass, filter, update);
+ }
+
+ public Mono findByIdAndSettingNames(
+ Long userId,
+ @Nullable Collection settingNames,
+ @Nullable Date lastUpdatedDateStart) {
+ Filter filter = lastUpdatedDateStart == null
+ ? Filter.newBuilder(1)
+ .eq(DomainFieldName.ID, userId)
+ : Filter.newBuilder(2)
+ .eq(DomainFieldName.ID, userId)
+ .gte(UserSettings.Fields.LAST_UPDATED_DATE, lastUpdatedDateStart);
+ QueryOptions queryOptions = null;
+ if (settingNames != null && !settingNames.isEmpty()) {
+ BsonDocument projection = new BsonDocument()
+ .append(UserSettings.Fields.LAST_UPDATED_DATE, BsonPool.BSON_INT32_1);
+ for (String settingName : settingNames) {
+ projection.append(settingName, BsonPool.BSON_INT32_1);
+ }
+ queryOptions = QueryOptions.newBuilder()
+ .projection(projection);
+ }
+ return mongoClient.findOne(entityClass, filter, queryOptions);
+ }
+
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/domain/user/service/UserService.java b/turms-service/src/main/java/im/turms/service/domain/user/service/UserService.java
index bfdc52a51c..4c7781b48b 100644
--- a/turms-service/src/main/java/im/turms/service/domain/user/service/UserService.java
+++ b/turms-service/src/main/java/im/turms/service/domain/user/service/UserService.java
@@ -69,6 +69,7 @@
import im.turms.service.domain.common.permission.ServicePermission;
import im.turms.service.domain.common.validation.DataValidator;
import im.turms.service.domain.conversation.service.ConversationService;
+import im.turms.service.domain.conversation.service.ConversationSettingsService;
import im.turms.service.domain.group.service.GroupMemberService;
import im.turms.service.domain.message.service.MessageService;
import im.turms.service.domain.observation.service.MetricsService;
@@ -98,6 +99,7 @@ public class UserService implements RpcUserService {
private final UserVersionService userVersionService;
private final SessionService sessionService;
private final ConversationService conversationService;
+ private final ConversationSettingsService conversationSettingsService;
private final MessageService messageService;
private final Node node;
@@ -107,6 +109,7 @@ public class UserService implements RpcUserService {
private final Counter registeredUsersCounter;
private final Counter deletedUsersCounter;
+ private final UserSettingsService userSettingsService;
private boolean activateUserWhenAdded;
private boolean deleteUserLogically;
@@ -134,8 +137,10 @@ public UserService(
GroupMemberService groupMemberService,
UserVersionService userVersionService,
UserRelationshipGroupService userRelationshipGroupService,
+ UserSettingsService userSettingsService,
SessionService sessionService,
ConversationService conversationService,
+ ConversationSettingsService conversationSettingsService,
@Lazy MessageService messageService,
MetricsService metricsService) {
this.node = node;
@@ -147,8 +152,10 @@ public UserService(
this.groupMemberService = groupMemberService;
this.userVersionService = userVersionService;
this.userRelationshipGroupService = userRelationshipGroupService;
+ this.userSettingsService = userSettingsService;
this.sessionService = sessionService;
this.conversationService = conversationService;
+ this.conversationSettingsService = conversationSettingsService;
this.messageService = messageService;
registeredUsersCounter = metricsService.getRegistry()
@@ -511,10 +518,13 @@ public Mono deleteUsers(
.deleteAllRelationshipGroups(userIds,
session,
false))
+ .then(userSettingsService.deleteSettings(userIds, session))
.then(conversationService
.deletePrivateConversations(userIds, session))
.then(conversationService
.deleteGroupMemberConversations(userIds, session))
+ .then(conversationSettingsService.deleteSettings(userIds,
+ session))
.then(userVersionService.delete(userIds, session)
.onErrorResume(t -> {
LOGGER.error(
diff --git a/turms-service/src/main/java/im/turms/service/domain/user/service/UserSettingsService.java b/turms-service/src/main/java/im/turms/service/domain/user/service/UserSettingsService.java
new file mode 100644
index 0000000000..dbf09d384a
--- /dev/null
+++ b/turms-service/src/main/java/im/turms/service/domain/user/service/UserSettingsService.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 The Turms Project
+ * https://github.com/turms-im/turms
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.turms.service.domain.user.service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import jakarta.annotation.Nullable;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import org.eclipse.collections.impl.set.mutable.UnifiedSet;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import im.turms.server.common.access.client.dto.model.common.Value;
+import im.turms.server.common.access.common.ResponseStatusCode;
+import im.turms.server.common.infra.collection.CollectorUtil;
+import im.turms.server.common.infra.exception.ResponseException;
+import im.turms.server.common.infra.property.TurmsProperties;
+import im.turms.server.common.infra.property.TurmsPropertiesManager;
+import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties;
+import im.turms.server.common.infra.reactor.PublisherPool;
+import im.turms.server.common.infra.validation.Validator;
+import im.turms.server.common.storage.mongo.IMongoCollectionInitializer;
+import im.turms.service.domain.common.service.CustomSettingService;
+import im.turms.service.domain.user.po.UserSettings;
+import im.turms.service.domain.user.repository.UserSettingsRepository;
+
+/**
+ * @author James Chen
+ */
+@Service
+@DependsOn(IMongoCollectionInitializer.BEAN_NAME)
+public class UserSettingsService extends CustomSettingService {
+
+ private final UserSettingsRepository userSettingsRepository;
+
+ public UserSettingsService(
+ TurmsPropertiesManager propertiesManager,
+ UserSettingsRepository userSettingsRepository) {
+ this.userSettingsRepository = userSettingsRepository;
+
+ propertiesManager.notifyAndAddGlobalPropertiesChangeListener(this::updateGlobalProperties);
+ }
+
+ private void updateGlobalProperties(TurmsProperties properties) {
+ List settings = properties.getService()
+ .getUser()
+ .getAllowedSettings();
+ super.updateGlobalProperties(settings);
+ }
+
+ public Mono upsertSettings(Long userId, Map settings) {
+ try {
+ Validator.notNull(userId, "userId");
+ Validator.notNull(settings, "settings");
+ } catch (ResponseException e) {
+ return Mono.error(e);
+ }
+ if (settings.isEmpty()) {
+ return PublisherPool.FALSE;
+ }
+ Set immutableSettingsForUpsert = null;
+ if (!immutableSettings.isEmpty()) {
+ Set settingsForUpsert = settings.keySet();
+ for (String settingName : settingsForUpsert) {
+ if (immutableSettings.contains(settingName)) {
+ if (immutableSettingsForUpsert == null) {
+ immutableSettingsForUpsert = new UnifiedSet<>(4);
+ }
+ immutableSettingsForUpsert.add(settingName);
+ }
+ }
+ }
+ if (immutableSettingsForUpsert == null) {
+ Map parsedSettings = parseSettings(settingPropertiesList, settings);
+ return userSettingsRepository.upsertSettings(userId, parsedSettings)
+ .map(updateResult -> updateResult.getModifiedCount() > 0
+ || updateResult.getUpsertedId() != null);
+ }
+ Set finalImmutableSettingsForUpsert = immutableSettingsForUpsert;
+ return userSettingsRepository
+ .findObjectFieldsById(userId,
+ UserSettings.Fields.SETTINGS,
+ immutableSettingsForUpsert)
+ .collect(CollectorUtil.toSet(immutableSettingsForUpsert.size()))
+ .flatMap(existingSettings -> {
+ if (existingSettings.isEmpty()) {
+ Map parsedSettings =
+ parseSettings(settingPropertiesList, settings);
+ return userSettingsRepository.upsertSettings(userId, parsedSettings);
+ }
+ finalImmutableSettingsForUpsert
+ .removeIf(settingName -> !existingSettings.contains(settingName));
+ if (finalImmutableSettingsForUpsert.isEmpty()) {
+ Map parsedSettings =
+ parseSettings(settingPropertiesList, settings);
+ return userSettingsRepository.upsertSettings(userId, parsedSettings);
+ }
+ List sortedConflictedSettings =
+ new ArrayList<>(finalImmutableSettingsForUpsert);
+ sortedConflictedSettings.sort(null);
+ return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
+ "Cannot update existing immutable settings: "
+ + sortedConflictedSettings));
+ })
+ .map(updateResult -> updateResult.getModifiedCount() > 0
+ || updateResult.getUpsertedId() != null);
+ }
+
+ public Mono deleteSettings(
+ Collection userIds,
+ @Nullable ClientSession clientSession) {
+ try {
+ Validator.notNull(userIds, "userIds");
+ } catch (ResponseException e) {
+ return Mono.error(e);
+ }
+ return userSettingsRepository.deleteByIds(userIds, clientSession)
+ .map(deleteResult -> deleteResult.getDeletedCount() > 0);
+ }
+
+ public Mono unsetSettings(Long userId, @Nullable Set settingNames) {
+ try {
+ Validator.notNull(userId, "userId");
+ } catch (ResponseException e) {
+ return Mono.error(e);
+ }
+ if (!deletableSettings.isEmpty()) {
+ if (settingNames == null) {
+ return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
+ "Cannot delete non-deletable settings: "
+ + deletableSettings));
+ }
+ for (String settingName : settingNames) {
+ if (!deletableSettings.contains(settingName)) {
+ return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
+ "Cannot delete non-deletable settings: "
+ + deletableSettings));
+ }
+ }
+ }
+ return userSettingsRepository.unsetSettings(userId, settingNames)
+ .map(updateResult -> updateResult.getModifiedCount() > 0);
+ }
+
+ public Mono querySettings(
+ Long userId,
+ @Nullable Set settingNames,
+ @Nullable Date lastUpdatedDateStart) {
+ try {
+ Validator.notNull(userId, "userId");
+ } catch (ResponseException e) {
+ return Mono.error(e);
+ }
+ return userSettingsRepository
+ .findByIdAndSettingNames(userId, settingNames, lastUpdatedDateStart);
+ }
+
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/infra/locale/LocaleUtils.java b/turms-service/src/main/java/im/turms/service/infra/locale/LocaleUtils.java
new file mode 100644
index 0000000000..9d653bbd25
--- /dev/null
+++ b/turms-service/src/main/java/im/turms/service/infra/locale/LocaleUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 The Turms Project
+ * https://github.com/turms-im/turms
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.turms.service.infra.locale;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * @author James Chen
+ */
+public final class LocaleUtils {
+
+ private static final Map ID_TO_LOCALE;
+
+ static {
+ ID_TO_LOCALE = new HashMap<>(2048);
+ // Use "availableLocales()" to avoid unnecessary copy.
+ Locale.availableLocales()
+ .forEach(locale -> ID_TO_LOCALE.put(locale.toLanguageTag(), locale));
+ }
+
+ private LocaleUtils() {
+ }
+
+ public static boolean isAvailableLanguage(String languageId) {
+ return ID_TO_LOCALE.containsKey(languageId);
+ }
+
+}
\ No newline at end of file
diff --git a/turms-service/src/main/java/im/turms/service/infra/proto/ProtoModelConvertor.java b/turms-service/src/main/java/im/turms/service/infra/proto/ProtoModelConvertor.java
index 574367a294..516dc101c5 100644
--- a/turms-service/src/main/java/im/turms/service/infra/proto/ProtoModelConvertor.java
+++ b/turms-service/src/main/java/im/turms/service/infra/proto/ProtoModelConvertor.java
@@ -34,6 +34,7 @@
import im.turms.server.common.access.client.dto.constant.ProfileAccessStrategy;
import im.turms.server.common.access.client.dto.constant.RequestStatus;
import im.turms.server.common.access.client.dto.constant.UserStatus;
+import im.turms.server.common.access.client.dto.model.common.Value;
import im.turms.server.common.access.client.dto.model.conversation.GroupConversation;
import im.turms.server.common.access.client.dto.model.conversation.PrivateConversation;
import im.turms.server.common.access.client.dto.model.group.Group;
@@ -52,8 +53,10 @@
import im.turms.server.common.domain.user.po.User;
import im.turms.server.common.infra.collection.CollectionUtil;
import im.turms.service.domain.conference.po.Meeting;
+import im.turms.service.domain.conversation.po.ConversationSettings;
import im.turms.service.domain.message.po.Message;
import im.turms.service.domain.storage.bo.StorageResourceInfo;
+import im.turms.service.domain.user.po.UserSettings;
/**
* @author James Chen
@@ -232,6 +235,24 @@ public static UserOnlineStatus.Builder userSessionsStatus2proto(
return builder;
}
+ public static im.turms.server.common.access.client.dto.model.user.UserSettings.Builder userSettings2proto(
+ UserSettings userSettings) {
+ var builder = ClientMessagePool.getUserSettingsBuilder();
+ Map settings = userSettings.getSettings();
+ Date lastUpdatedDate = userSettings.getLastUpdatedDate();
+ if (settings != null && !settings.isEmpty()) {
+ Value.Builder valueBuilder = ClientMessagePool.getValueBuilder();
+ for (Map.Entry entry : settings.entrySet()) {
+ builder.putSettings(entry.getKey(), value2proto(valueBuilder, entry.getValue()));
+ valueBuilder.clear();
+ }
+ }
+ if (lastUpdatedDate != null) {
+ builder.setLastUpdatedDate(lastUpdatedDate.getTime());
+ }
+ return builder;
+ }
+
public static GroupMember.Builder userOnlineInfo2groupMember(
@NotNull Long userId,
@Nullable UserSessionsStatus userSessionsStatus,
@@ -581,6 +602,33 @@ public static GroupConversation.Builder groupConversations2proto(
return builder;
}
+ public static im.turms.server.common.access.client.dto.model.conversation.ConversationSettings.Builder conversationSettings2proto(
+ ConversationSettings conversationSettings) {
+ var builder = ClientMessagePool.getConversationSettingsBuilder();
+ ConversationSettings.Key key = conversationSettings.getKey();
+ Long targetId = key.getTargetId();
+ Map settings = conversationSettings.getSettings();
+ Date lastUpdatedDate = conversationSettings.getLastUpdatedDate();
+ if (targetId != null) {
+ if (targetId < 0) {
+ builder.setGroupId(-targetId);
+ } else {
+ builder.setUserId(targetId);
+ }
+ }
+ if (settings != null && !settings.isEmpty()) {
+ Value.Builder valueBuilder = ClientMessagePool.getValueBuilder();
+ for (Map.Entry entry : settings.entrySet()) {
+ builder.putSettings(entry.getKey(), value2proto(valueBuilder, entry.getValue()));
+ valueBuilder.clear();
+ }
+ }
+ if (lastUpdatedDate != null) {
+ builder.setLastUpdatedDate(lastUpdatedDate.getTime());
+ }
+ return builder;
+ }
+
public static CreateMessageRequest.Builder message2createMessageRequest(Message message) {
CreateMessageRequest.Builder builder = ClientMessagePool.getCreateMessageRequestBuilder();
Long messageId = message.getId();
@@ -675,4 +723,22 @@ public static im.turms.server.common.access.client.dto.model.storage.StorageReso
.build();
}
+ public static Value value2proto(Value.Builder builder, Object value) {
+ switch (value) {
+ case Integer val -> builder.setInt32Value(val);
+ case Long val -> builder.setInt64Value(val);
+ case Float val -> builder.setFloatValue(val);
+ case Double val -> builder.setDoubleValue(val);
+ case Boolean val -> builder.setBoolValue(val);
+ case byte[] val -> builder.setBytesValue(ByteStringUtil.wrap(val));
+ case String val -> builder.setStringValue(val);
+ case null -> {
+ }
+ default -> throw new IllegalArgumentException(
+ "Unsupported type: "
+ + value.getClass());
+ }
+ return builder.build();
+ }
+
}
\ No newline at end of file
diff --git a/turms-service/src/test/java/system/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationServiceControllerST.java b/turms-service/src/test/java/system/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationServiceControllerST.java
index 4ec4ac4bc2..506462ba75 100644
--- a/turms-service/src/test/java/system/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationServiceControllerST.java
+++ b/turms-service/src/test/java/system/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationServiceControllerST.java
@@ -57,7 +57,7 @@ void handleUpdateConversationRequest_updatePrivateConversationReadDate_shouldSuc
TurmsRequest request = TurmsRequest.newBuilder()
.setUpdateConversationRequest(UpdateConversationRequest.newBuilder()
.setReadDate(System.currentTimeMillis())
- .setTargetId(RELATED_USER_ID))
+ .setUserId(RELATED_USER_ID))
.build();
ClientRequest clientRequest =
new ClientRequest(USER_ID, USER_DEVICE, USER_IP, REQUEST_ID, request);
@@ -118,7 +118,7 @@ void handleUpdateTypingStatusRequest_updateGroupConversationTypingStatus_shouldS
void handleQueryConversationsRequest_queryPrivateConversations_shouldReturnNotEmptyConversations() {
TurmsRequest request = TurmsRequest.newBuilder()
.setQueryConversationsRequest(QueryConversationsRequest.newBuilder()
- .addTargetIds(RELATED_USER_ID))
+ .addUserIds(RELATED_USER_ID))
.build();
ClientRequest clientRequest =
new ClientRequest(USER_ID, USER_DEVICE, USER_IP, REQUEST_ID, request);
@@ -131,7 +131,7 @@ void handleQueryConversationsRequest_queryPrivateConversations_shouldReturnNotEm
request = TurmsRequest.newBuilder()
.setQueryConversationsRequest(QueryConversationsRequest.newBuilder()
- .addTargetIds(USER_ID))
+ .addUserIds(USER_ID))
.build();
clientRequest =
new ClientRequest(RELATED_USER_ID, USER_DEVICE, USER_IP, REQUEST_ID, request);