diff --git a/pom.xml b/pom.xml index a4df87c497..63c38fbfa7 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 74.2 4.0.3 2.23.1 - 4.26.1 + 4.27.0 2.2.20 5.11.10 diff --git a/turms-gateway/src/main/java/im/turms/gateway/access/client/common/authorization/policy/PolicyStatementResource.java b/turms-gateway/src/main/java/im/turms/gateway/access/client/common/authorization/policy/PolicyStatementResource.java index f4a3848f8c..ae7a708ed5 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/access/client/common/authorization/policy/PolicyStatementResource.java +++ b/turms-gateway/src/main/java/im/turms/gateway/access/client/common/authorization/policy/PolicyStatementResource.java @@ -40,6 +40,7 @@ import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.CREATE_MESSAGE_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.CREATE_RELATIONSHIP_GROUP_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.CREATE_RELATIONSHIP_REQUEST; +import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_CONVERSATION_SETTINGS_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_GROUP_BLOCKED_USER_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_GROUP_INVITATION_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_GROUP_JOIN_QUESTIONS_REQUEST; @@ -50,7 +51,9 @@ import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_RELATIONSHIP_GROUP_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_RELATIONSHIP_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_RESOURCE_REQUEST; +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_CONVERSATIONS_REQUEST; +import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_CONVERSATION_SETTINGS_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_FRIEND_REQUESTS_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_GROUPS_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_GROUP_BLOCKED_USER_IDS_REQUEST; @@ -72,7 +75,9 @@ import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_RESOURCE_UPLOAD_INFO_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_USER_ONLINE_STATUSES_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_USER_PROFILES_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_CONVERSATION_REQUEST; +import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_CONVERSATION_SETTINGS_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_FRIEND_REQUEST_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_GROUP_INVITATION_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_GROUP_JOIN_QUESTION_REQUEST; @@ -89,6 +94,7 @@ import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_USER_LOCATION_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_USER_ONLINE_STATUS_REQUEST; import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_USER_REQUEST; +import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_USER_SETTINGS_REQUEST; /** * @author James Chen @@ -104,6 +110,8 @@ public enum PolicyStatementResource { Set.of(UPDATE_USER_ONLINE_STATUS_REQUEST), Set.of(QUERY_USER_ONLINE_STATUSES_REQUEST)), USER_PROFILE(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Set.of(QUERY_USER_PROFILES_REQUEST)), + USER_SETTING(Collections.emptySet(), Set.of(DELETE_USER_SETTINGS_REQUEST), + Set.of(UPDATE_USER_SETTINGS_REQUEST), Set.of(QUERY_USER_SETTINGS_REQUEST)), NEARBY_USER(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Set.of(QUERY_NEARBY_USERS_REQUEST)), // endregion @@ -147,6 +155,9 @@ public enum PolicyStatementResource { // region conversation CONVERSATION(Collections.emptySet(), Collections.emptySet(), Set.of(UPDATE_CONVERSATION_REQUEST), Set.of(QUERY_CONVERSATIONS_REQUEST)), + CONVERSATION_SETTING(Collections.emptySet(), Set.of(DELETE_CONVERSATION_SETTINGS_REQUEST), + Set.of(UPDATE_CONVERSATION_SETTINGS_REQUEST), + Set.of(QUERY_CONVERSATION_SETTINGS_REQUEST)), // endregion // region typing status TYPING_STATUS(Collections.emptySet(), Collections.emptySet(), diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/PasswordSessionIdentityAccessManager.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/PasswordSessionIdentityAccessManager.java index fb1708080f..211505263a 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/PasswordSessionIdentityAccessManager.java +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/PasswordSessionIdentityAccessManager.java @@ -70,7 +70,7 @@ public boolean updateGlobalProperties(TurmsProperties properties) { "Refused an illegal operation that tried to enable the previously disabled password-based identity and access management, " + "because " + "\"turms.gateway.session.identity-access-management.enabled\" is false, or " - + "\"turms.gateway.session.identity-access-management.type\" is not \"password\" at startup. " + + "\"turms.gateway.session.identity-access-management.type\" is not \"password\" on startup. " + "To enable it, you need to update the \"turms.gateway.session.identity-access-management.enabled\" setting to true and restart the server"); return false; } else { diff --git a/turms-server-common/src/main/java/im/turms/server/common/access/common/ResponseStatusCode.java b/turms-server-common/src/main/java/im/turms/server/common/access/common/ResponseStatusCode.java index db1d863a6e..c0fcddde73 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/access/common/ResponseStatusCode.java +++ b/turms-server-common/src/main/java/im/turms/server/common/access/common/ResponseStatusCode.java @@ -327,6 +327,12 @@ public enum ResponseStatusCode { 403), NOT_FRIEND_TO_SEND_TYPING_STATUS(4102, "Only friends can send their typing status", 403), + // Conversation - Setting + NOT_RELATED_USER_TO_UPDATE_PRIVATE_CONVERSATION_SETTING(4200, + "Only the related user can update the private conversation setting", 403), + NOT_GROUP_MEMBER_TO_UPDATE_GROUP_CONVERSATION_SETTING(4201, + "Only the group member can update the group conversation setting", 403), + // Message // Message - Send diff --git a/turms-server-common/src/main/java/im/turms/server/common/domain/common/repository/BaseRepository.java b/turms-server-common/src/main/java/im/turms/server/common/domain/common/repository/BaseRepository.java index ce2b39a3e8..d126ebba7d 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/domain/common/repository/BaseRepository.java +++ b/turms-server-common/src/main/java/im/turms/server/common/domain/common/repository/BaseRepository.java @@ -150,6 +150,15 @@ public Flux findByIds(Collection ids) { return mongoClient.findMany(entityClass, filter); } + public Flux findObjectFieldsById( + K id, + String parentField, + Collection includedFields) { + Filter filter = Filter.newBuilder(1) + .eq(DomainFieldName.ID, id); + return mongoClient.findObjectFields(entityClass, filter, parentField, includedFields); + } + public Flux> watch(FullDocument fullDocument) { return mongoClient.watch(entityClass, fullDocument); } diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/collection/CollectionUtil.java b/turms-server-common/src/main/java/im/turms/server/common/infra/collection/CollectionUtil.java index 88bc0b8584..ca95b83779 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/collection/CollectionUtil.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/collection/CollectionUtil.java @@ -18,6 +18,7 @@ package im.turms.server.common.infra.collection; import java.util.ArrayList; +import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -662,6 +663,14 @@ public static Set union(Collection> collections) { return set; } + public static BitSet newBitSet(byte[] bytes) { + BitSet set = new BitSet(bytes.length); + for (byte b : bytes) { + set.set(b); + } + return set; + } + // endregion } \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/PasswordType.java b/turms-server-common/src/main/java/im/turms/server/common/infra/lang/StringPattern.java similarity index 89% rename from turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/PasswordType.java rename to turms-server-common/src/main/java/im/turms/server/common/infra/lang/StringPattern.java index 4375c25501..34c1f50fe2 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/PasswordType.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/lang/StringPattern.java @@ -15,12 +15,12 @@ * limitations under the License. */ -package im.turms.server.common.infra.property.constant; +package im.turms.server.common.infra.lang; /** * @author James Chen */ -public enum PasswordType { +public enum StringPattern { NUMERIC, ALPHABETIC, ALPHANUMERIC diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/lang/StringUtil.java b/turms-server-common/src/main/java/im/turms/server/common/infra/lang/StringUtil.java index 031e027842..5756b02030 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/lang/StringUtil.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/lang/StringUtil.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -750,4 +751,23 @@ public static float findSimilarity(String x, String y) { return 1.0F - distance / (float) maxLength; } + public static boolean allCharsInSet(String string, BitSet set) { + byte coder = getCoder(string); + if (LATIN1 == coder) { + byte[] bytes = getBytes(string); + for (byte b : bytes) { + if (!set.get(b)) { + return false; + } + } + return true; + } + int length = string.length(); + for (int i = 0; i < length; i++) { + if (!set.get(string.charAt(i))) { + return false; + } + } + return true; + } } \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/PropertyConstraints.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/PropertyConstraints.java index 29d5466ca2..d22a178332 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/PropertyConstraints.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/PropertyConstraints.java @@ -23,7 +23,9 @@ import jakarta.validation.constraints.Size; import im.turms.server.common.infra.validation.LessThanOrEqualTo; +import im.turms.server.common.infra.validation.MatchesStringPattern; import im.turms.server.common.infra.validation.ValidCron; +import im.turms.server.common.infra.validation.ValidRegex; /** * @author James Chen @@ -33,23 +35,29 @@ public record PropertyConstraints( long max, @Nullable String lessThanOrEqualTo, @Nullable Size size, - @Nullable ValidCron validCron + @Nullable ValidCron validCron, + @Nullable ValidRegex validRegex, + @Nullable MatchesStringPattern matchesStringPattern ) { public static final PropertyConstraints NULL = - new PropertyConstraints(Long.MIN_VALUE, Long.MAX_VALUE, null, null, null); + new PropertyConstraints(Long.MIN_VALUE, Long.MAX_VALUE, null, null, null, null, null); public static PropertyConstraints of( @Nullable Min min, @Nullable Max max, @Nullable LessThanOrEqualTo lessThanOrEqualTo, @Nullable Size size, - @Nullable ValidCron validCron) { + @Nullable ValidCron validCron, + @Nullable ValidRegex validRegex, + @Nullable MatchesStringPattern matchesStringPattern) { if (min == null && max == null && lessThanOrEqualTo == null && size == null - && validCron == null) { + && validCron == null + && validRegex == null + && matchesStringPattern == null) { return NULL; } return new PropertyConstraints( @@ -63,7 +71,9 @@ public static PropertyConstraints of( ? null : lessThanOrEqualTo.value(), size, - validCron); + validCron, + validRegex, + matchesStringPattern); } -} +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/TurmsPropertiesInspector.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/TurmsPropertiesInspector.java index b5c804719b..3a2dfc9190 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/TurmsPropertiesInspector.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/TurmsPropertiesInspector.java @@ -51,7 +51,9 @@ import im.turms.server.common.infra.security.SensitiveProperty; import im.turms.server.common.infra.serialization.SerializationException; import im.turms.server.common.infra.validation.LessThanOrEqualTo; +import im.turms.server.common.infra.validation.MatchesStringPattern; import im.turms.server.common.infra.validation.ValidCron; +import im.turms.server.common.infra.validation.ValidRegex; import static im.turms.server.common.infra.json.JsonCodecPool.MAPPER; @@ -163,7 +165,9 @@ private static void collectFieldInfos( field.getDeclaredAnnotation(Max.class), field.getDeclaredAnnotation(LessThanOrEqualTo.class), field.getDeclaredAnnotation(Size.class), - field.getDeclaredAnnotation(ValidCron.class)); + field.getDeclaredAnnotation(ValidCron.class), + field.getDeclaredAnnotation(ValidRegex.class), + field.getDeclaredAnnotation(MatchesStringPattern.class)); propertyFieldInfos.add(new PropertyFieldInfo( field, VarAccessorFactory.get(field), @@ -232,4 +236,4 @@ private static String getTypeName(Class type) { return type.getTypeName(); } -} +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/TurmsPropertiesValidator.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/TurmsPropertiesValidator.java index 18a0e8f8ec..8248aa964b 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/TurmsPropertiesValidator.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/TurmsPropertiesValidator.java @@ -18,17 +18,25 @@ package im.turms.server.common.infra.property; import java.lang.reflect.Field; +import java.util.BitSet; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import jakarta.annotation.Nullable; import jakarta.validation.constraints.Size; import org.springframework.scheduling.support.CronExpression; +import im.turms.server.common.infra.collection.CollectionUtil; import im.turms.server.common.infra.lang.ClassUtil; +import im.turms.server.common.infra.lang.StringPattern; +import im.turms.server.common.infra.lang.StringUtil; +import im.turms.server.common.infra.validation.MatchesStringPattern; import im.turms.server.common.infra.validation.ValidCron; +import im.turms.server.common.infra.validation.ValidRegex; import static im.turms.server.common.infra.property.TurmsPropertiesInspector.getFieldInfo; import static im.turms.server.common.infra.property.TurmsPropertiesInspector.getFieldInfos; @@ -38,6 +46,13 @@ */ public class TurmsPropertiesValidator { + private static final BitSet NUMERIC_CHAR_SET = + CollectionUtil.newBitSet(StringUtil.NUMERIC_TABLE); + private static final BitSet ALPHABETIC_CHAR_SET = + CollectionUtil.newBitSet(StringUtil.ALPHABETIC_TABLE); + private static final BitSet ALPHANUMERIC_CHAR_SET = + CollectionUtil.newBitSet(StringUtil.ALPHANUMERIC_TABLE); + private TurmsPropertiesValidator() { } @@ -85,12 +100,16 @@ private static void validateProperty( long max = constraints.max(); String lessThanOrEqualTo = constraints.lessThanOrEqualTo(); Size size = constraints.size(); - ValidCron cron = constraints.validCron(); + ValidCron validCron = constraints.validCron(); + ValidRegex validRegex = constraints.validRegex(); + MatchesStringPattern matchesStringPattern = constraints.matchesStringPattern(); if (min == Long.MIN_VALUE && max == Long.MAX_VALUE && lessThanOrEqualTo == null && size == null - && cron == null) { + && validCron == null + && validRegex == null + && matchesStringPattern == null) { return; } Object value = fieldInfo.get(properties); @@ -104,9 +123,63 @@ private static void validateProperty( if (size != null) { validateSizeProperty(size, value, field, errorMessages); } - if (cron != null) { + if (validCron != null) { validateCronProperty(value, field, errorMessages); } + if (validRegex != null) { + validateRegexProperty(value, field, errorMessages); + } + if (matchesStringPattern != null) { + validateStringPatternProperty(matchesStringPattern.value(), + value, + field, + errorMessages); + } + } + + private static void validateStringPatternProperty( + StringPattern stringPattern, + Object value, + Field field, + List errorMessages) { + if (!(value instanceof String str)) { + throw new IllegalArgumentException( + "The value of the field (" + + ClassUtil.getReference(field) + + ") must be a string for string pattern validation"); + } + switch (stringPattern) { + case NUMERIC -> { + if (StringUtil.allCharsInSet(str, NUMERIC_CHAR_SET)) { + String message = "The property \"" + + field.getName() + + "\" has an invalid string pattern \"" + + str + + "\". Expected a numeric string"; + errorMessages.add(message); + } + } + case ALPHABETIC -> { + if (StringUtil.allCharsInSet(str, ALPHABETIC_CHAR_SET)) { + String message = "The property \"" + + field.getName() + + "\" has an invalid string pattern \"" + + str + + "\". Expected an alphabetic string"; + errorMessages.add(message); + } + } + case ALPHANUMERIC -> { + if (StringUtil.allCharsInSet(str, ALPHANUMERIC_CHAR_SET)) { + String message = "The property \"" + + field.getName() + + "\" has an invalid string pattern \"" + + str + + "\". Expected an alphanumeric string"; + errorMessages.add(message); + } + } + } } private static void validateMinMaxProperty( @@ -243,4 +316,32 @@ private static void validateCronProperty( } } + private static void validateRegexProperty( + Object value, + Field field, + List errorMessages) { + if (!(value instanceof String str)) { + if (!(value instanceof Collection collection)) { + throw new IllegalArgumentException( + "The value of the field (" + + ClassUtil.getReference(field) + + ") must be a string or string list for regex validation"); + } + for (Object val : collection) { + validateRegexProperty(val, field, errorMessages); + } + return; + } + try { + Pattern.compile(str); + } catch (PatternSyntaxException exception) { + String message = "The property \"" + + field.getName() + + "\" has an invalid regex \"" + + str + + "\""; + errorMessages.add(message); + } + } + } \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/CustomSettingType.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/CustomSettingType.java new file mode 100644 index 0000000000..e71d97ad62 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/CustomSettingType.java @@ -0,0 +1,30 @@ +/* + * 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.server.common.infra.property.constant; + +/** + * @author James Chen + */ +public enum CustomSettingType { + INT, + LONG, + DOUBLE, + STRING, + BOOL, + LANGUAGE +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/common/plugin/NetworkPluginProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/common/plugin/NetworkPluginProperties.java index 02e21ff04b..5dcba07999 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/common/plugin/NetworkPluginProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/common/plugin/NetworkPluginProperties.java @@ -46,7 +46,7 @@ public class NetworkPluginProperties { + "or \".js\" for JavaScript plugins") private PluginType type = PluginType.AUTO; - @Description("Whether to use the local cache. If false, turms will download the plugin every time at startup " + @Description("Whether to use the local cache. If false, turms will download the plugin every time on startup " + "even if the plugin has been downloaded on the local machine") private boolean useLocalCache; diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingDoubleValueProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingDoubleValueProperties.java new file mode 100644 index 0000000000..ee2af71c05 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingDoubleValueProperties.java @@ -0,0 +1,48 @@ +/* + * 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.server.common.infra.property.env.service.business.common.setting; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class CustomSettingDoubleValueProperties { + + @Description("The minimum allowed value") + @GlobalProperty + @MutableProperty + private double min = Double.MIN_VALUE; + + @Description("The maximum allowed value") + @GlobalProperty + @MutableProperty + private double max = Double.MAX_VALUE; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingIntValueProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingIntValueProperties.java new file mode 100644 index 0000000000..543c217e17 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingIntValueProperties.java @@ -0,0 +1,48 @@ +/* + * 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.server.common.infra.property.env.service.business.common.setting; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class CustomSettingIntValueProperties { + + @Description("The minimum allowed value") + @GlobalProperty + @MutableProperty + private int min = Integer.MIN_VALUE; + + @Description("The maximum allowed value") + @GlobalProperty + @MutableProperty + private int max = Integer.MAX_VALUE; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingLongValueProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingLongValueProperties.java new file mode 100644 index 0000000000..2d003663c0 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingLongValueProperties.java @@ -0,0 +1,48 @@ +/* + * 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.server.common.infra.property.env.service.business.common.setting; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class CustomSettingLongValueProperties { + + @Description("The minimum allowed value") + @GlobalProperty + @MutableProperty + private long min = Long.MIN_VALUE; + + @Description("The maximum allowed value") + @GlobalProperty + @MutableProperty + private long max = Long.MAX_VALUE; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingProperties.java new file mode 100644 index 0000000000..99fa4fbcc1 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingProperties.java @@ -0,0 +1,69 @@ +/* + * 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.server.common.infra.property.env.service.business.common.setting; + +import jakarta.validation.constraints.Size; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import im.turms.server.common.infra.lang.StringPattern; +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; +import im.turms.server.common.infra.validation.MatchesStringPattern; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class CustomSettingProperties { + + @Description("The source name of the setting") + @GlobalProperty + @MutableProperty + @Size(min = 1) + @MatchesStringPattern(StringPattern.ALPHANUMERIC) + private String sourceName = ""; + + @Description("The stored name of the setting. If empty, the source name will be used") + @GlobalProperty + @MutableProperty + @MatchesStringPattern(StringPattern.ALPHANUMERIC) + private String storedName = ""; + + @Description("Whether the setting is immutable") + @GlobalProperty + @MutableProperty + private boolean immutable; + + @Description("Whether the setting is deletable") + @GlobalProperty + @MutableProperty + private boolean deletable = true; + + @NestedConfigurationProperty + private CustomSettingValueProperties value = new CustomSettingValueProperties(); + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingStringValueProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingStringValueProperties.java new file mode 100644 index 0000000000..3a460801b4 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingStringValueProperties.java @@ -0,0 +1,83 @@ +/* + * 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.server.common.infra.property.env.service.business.common.setting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; +import jakarta.validation.constraints.Min; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; +import im.turms.server.common.infra.validation.ValidRegex; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class CustomSettingStringValueProperties { + + @Description("The minimum allowed length") + @GlobalProperty + @MutableProperty + @Min(0) + private int minLength = 0; + + @Description("The maximum allowed length") + @GlobalProperty + @MutableProperty + private int maxLength = 100; + + @Description("The regular expressions that the value must match") + @GlobalProperty + @MutableProperty + @ValidRegex + private List regexes = Collections.emptyList(); + + private transient List parsedRegexes; + + @JsonIgnore + public List getParsedRegexes() { + if (parsedRegexes != null) { + return parsedRegexes; + } + if (regexes == null) { + parsedRegexes = Collections.emptyList(); + return parsedRegexes; + } + List list = new ArrayList<>(regexes.size()); + for (String regex : regexes) { + Pattern compile = Pattern.compile(regex); + list.add(compile); + } + parsedRegexes = list; + return parsedRegexes; + } + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingValueProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingValueProperties.java new file mode 100644 index 0000000000..5e62e2d5d6 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/common/setting/CustomSettingValueProperties.java @@ -0,0 +1,59 @@ +/* + * 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.server.common.infra.property.env.service.business.common.setting; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import im.turms.server.common.infra.property.constant.CustomSettingType; +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class CustomSettingValueProperties { + + @Description("The setting value type") + @GlobalProperty + @MutableProperty + private CustomSettingType type = CustomSettingType.STRING; + + @NestedConfigurationProperty + private CustomSettingIntValueProperties intValue = new CustomSettingIntValueProperties(); + + @NestedConfigurationProperty + private CustomSettingLongValueProperties longValue = new CustomSettingLongValueProperties(); + + @NestedConfigurationProperty + private CustomSettingDoubleValueProperties doubleValue = + new CustomSettingDoubleValueProperties(); + + @NestedConfigurationProperty + private CustomSettingStringValueProperties stringValue = + new CustomSettingStringValueProperties(); + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/conference/meeting/PasswordProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/conference/meeting/PasswordProperties.java index 5500661041..01a986c0c2 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/conference/meeting/PasswordProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/conference/meeting/PasswordProperties.java @@ -22,8 +22,8 @@ import lombok.Data; import lombok.NoArgsConstructor; +import im.turms.server.common.infra.lang.StringPattern; import im.turms.server.common.infra.property.constant.PasswordPolicy; -import im.turms.server.common.infra.property.constant.PasswordType; import im.turms.server.common.infra.property.metadata.Description; import im.turms.server.common.infra.property.metadata.GlobalProperty; import im.turms.server.common.infra.property.metadata.MutableProperty; @@ -45,7 +45,7 @@ public class PasswordProperties { @Description("The password type") @GlobalProperty @MutableProperty - private PasswordType type = PasswordType.NUMERIC; + private StringPattern type = StringPattern.NUMERIC; /** * @implNote Use 6 by default because it is easy for mobile users to input. diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/conversation/ConversationProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/conversation/ConversationProperties.java index 1f44ec3d8d..092f2155d6 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/conversation/ConversationProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/conversation/ConversationProperties.java @@ -17,13 +17,20 @@ package im.turms.server.common.infra.property.env.service.business.conversation; +import java.util.Collections; +import java.util.List; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties; import im.turms.server.common.infra.property.env.service.business.message.TypingStatusProperties; +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; /** * @author James Chen @@ -40,4 +47,8 @@ public class ConversationProperties { @NestedConfigurationProperty private TypingStatusProperties typingStatus = new TypingStatusProperties(); + @Description("The list of allowed conversation settings") + @GlobalProperty + @MutableProperty + private List allowedSettings = Collections.emptyList(); } \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/NotificationProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/NotificationProperties.java index fdd0e7fb6e..b5bc69b3de 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/NotificationProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/NotificationProperties.java @@ -24,7 +24,11 @@ import org.springframework.boot.context.properties.NestedConfigurationProperty; import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationGroupConversationReadDateUpdatedProperties; +import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationGroupConversationSettingDeletedProperties; +import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationGroupConversationSettingUpdatedProperties; import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationPrivateConversationReadDateUpdatedProperties; +import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationPrivateConversationSettingDeletedProperties; +import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationPrivateConversationSettingUpdatedProperties; import im.turms.server.common.infra.property.env.service.business.notification.group.NotificationGroupBlockedUserAddedProperties; import im.turms.server.common.infra.property.env.service.business.notification.group.NotificationGroupBlockedUserRemovedProperties; import im.turms.server.common.infra.property.env.service.business.notification.group.NotificationGroupCreatedProperties; @@ -55,6 +59,8 @@ import im.turms.server.common.infra.property.env.service.business.notification.user.NotificationOneSidedRelationshipUpdatedProperties; import im.turms.server.common.infra.property.env.service.business.notification.user.NotificationUserInfoUpdatedProperties; import im.turms.server.common.infra.property.env.service.business.notification.user.NotificationUserOnlineStatusUpdatedProperties; +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; /** * @author James Chen @@ -75,6 +81,14 @@ public class NotificationProperties { private NotificationUserOnlineStatusUpdatedProperties userOnlineStatusUpdated = new NotificationUserOnlineStatusUpdatedProperties(); + @NestedConfigurationProperty + private NotificationUserSettingDeletedProperties userSettingDeleted = + new NotificationUserSettingDeletedProperties(); + + @NestedConfigurationProperty + private NotificationUserSettingUpdatedProperties userSettingUpdated = + new NotificationUserSettingUpdatedProperties(); + @NestedConfigurationProperty private NotificationFriendRequestCreatedProperties friendRequestCreated = new NotificationFriendRequestCreatedProperties(); @@ -178,10 +192,26 @@ public class NotificationProperties { private NotificationPrivateConversationReadDateUpdatedProperties privateConversationReadDateUpdated = new NotificationPrivateConversationReadDateUpdatedProperties(); + @NestedConfigurationProperty + private NotificationPrivateConversationSettingDeletedProperties privateConversationSettingDeleted = + new NotificationPrivateConversationSettingDeletedProperties(); + + @NestedConfigurationProperty + private NotificationPrivateConversationSettingUpdatedProperties privateConversationSettingUpdated = + new NotificationPrivateConversationSettingUpdatedProperties(); + @NestedConfigurationProperty private NotificationGroupConversationReadDateUpdatedProperties groupConversationReadDateUpdated = new NotificationGroupConversationReadDateUpdatedProperties(); + @NestedConfigurationProperty + private NotificationGroupConversationSettingDeletedProperties groupConversationSettingDeleted = + new NotificationGroupConversationSettingDeletedProperties(); + + @NestedConfigurationProperty + private NotificationGroupConversationSettingUpdatedProperties groupConversationSettingUpdated = + new NotificationGroupConversationSettingUpdatedProperties(); + // endregion // region Message diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationGroupConversationSettingDeletedProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationGroupConversationSettingDeletedProperties.java new file mode 100644 index 0000000000..0f9768175e --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationGroupConversationSettingDeletedProperties.java @@ -0,0 +1,43 @@ +/* + * 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.server.common.infra.property.env.service.business.notification.conversation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class NotificationGroupConversationSettingDeletedProperties { + + @Description("Whether to notify the requester's other online sessions when they have deleted their group conversation settings") + @GlobalProperty + @MutableProperty + private boolean notifyRequesterOtherOnlineSessions = true; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationGroupConversationSettingUpdatedProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationGroupConversationSettingUpdatedProperties.java new file mode 100644 index 0000000000..dad5ed1793 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationGroupConversationSettingUpdatedProperties.java @@ -0,0 +1,43 @@ +/* + * 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.server.common.infra.property.env.service.business.notification.conversation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class NotificationGroupConversationSettingUpdatedProperties { + + @Description("Whether to notify the requester's other online sessions when they have updated their group conversation settings") + @GlobalProperty + @MutableProperty + private boolean notifyRequesterOtherOnlineSessions = true; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationPrivateConversationSettingDeletedProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationPrivateConversationSettingDeletedProperties.java new file mode 100644 index 0000000000..3325e90836 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationPrivateConversationSettingDeletedProperties.java @@ -0,0 +1,43 @@ +/* + * 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.server.common.infra.property.env.service.business.notification.conversation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class NotificationPrivateConversationSettingDeletedProperties { + + @Description("Whether to notify the requester's other online sessions when they have deleted their private conversation settings") + @GlobalProperty + @MutableProperty + private boolean notifyRequesterOtherOnlineSessions = true; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationPrivateConversationSettingUpdatedProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationPrivateConversationSettingUpdatedProperties.java new file mode 100644 index 0000000000..16ba121eba --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/conversation/NotificationPrivateConversationSettingUpdatedProperties.java @@ -0,0 +1,43 @@ +/* + * 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.server.common.infra.property.env.service.business.notification.conversation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class NotificationPrivateConversationSettingUpdatedProperties { + + @Description("Whether to notify the requester's other online sessions when they have updated their private conversation settings") + @GlobalProperty + @MutableProperty + private boolean notifyRequesterOtherOnlineSessions = true; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/user/NotificationUserSettingDeletedProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/user/NotificationUserSettingDeletedProperties.java new file mode 100644 index 0000000000..aa5d3c33b0 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/user/NotificationUserSettingDeletedProperties.java @@ -0,0 +1,43 @@ +/* + * 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.server.common.infra.property.env.service.business.notification.user; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class NotificationUserSettingDeletedProperties { + + @Description("Whether to notify the requester's other online sessions when they have deleted their settings") + @GlobalProperty + @MutableProperty + private boolean notifyRequesterOtherOnlineSessions = true; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/user/NotificationUserSettingUpdatedProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/user/NotificationUserSettingUpdatedProperties.java new file mode 100644 index 0000000000..c88990f063 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/notification/user/NotificationUserSettingUpdatedProperties.java @@ -0,0 +1,43 @@ +/* + * 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.server.common.infra.property.env.service.business.notification.user; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.property.metadata.GlobalProperty; +import im.turms.server.common.infra.property.metadata.MutableProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class NotificationUserSettingUpdatedProperties { + + @Description("Whether to notify the requester's other online sessions when they have updated their settings") + @GlobalProperty + @MutableProperty + private boolean notifyRequesterOtherOnlineSessions = true; + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/user/UserProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/user/UserProperties.java index 3da7010af4..024020bb76 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/user/UserProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/business/user/UserProperties.java @@ -17,6 +17,8 @@ package im.turms.server.common.infra.property.env.service.business.user; +import java.util.Collections; +import java.util.List; import jakarta.validation.constraints.Min; import lombok.AllArgsConstructor; @@ -25,6 +27,7 @@ import lombok.NoArgsConstructor; import org.springframework.boot.context.properties.NestedConfigurationProperty; +import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties; import im.turms.server.common.infra.property.metadata.Description; import im.turms.server.common.infra.property.metadata.GlobalProperty; import im.turms.server.common.infra.property.metadata.MutableProperty; @@ -95,4 +98,9 @@ public class UserProperties { @Min(0) private int maxProfilePictureLength = 100; + @Description("The list of allowed user settings") + @GlobalProperty + @MutableProperty + private List allowedSettings = Collections.emptyList(); + } \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/FakeProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/FakeProperties.java index f0308475f9..ed8a6b7c0d 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/FakeProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/FakeProperties.java @@ -42,7 +42,7 @@ public class FakeProperties { @Min(0) private int userCount = 1000; - @Description("Whether to clear all collections before faking at startup") + @Description("Whether to clear all collections before faking on startup") private boolean clearAllCollectionsBeforeFaking; @Description("Whether to fake data even if the collection has already existed") diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/elasticsearch/ElasticsearchLanguageDetectProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/elasticsearch/ElasticsearchLanguageDetectProperties.java index 9d2ef9e1d7..6e2b2ef504 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/elasticsearch/ElasticsearchLanguageDetectProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/elasticsearch/ElasticsearchLanguageDetectProperties.java @@ -38,7 +38,7 @@ public class ElasticsearchLanguageDetectProperties { @Description("Whether to enable language detection. " - + "If true, a pipeline for language detection will be created at startup, " + + "If true, a pipeline for language detection will be created on startup, " + "and will be used as the default pipeline of new indexes") private boolean enabled; diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/elasticsearch/ElasticsearchSyncProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/elasticsearch/ElasticsearchSyncProperties.java index 9b6cc1421e..0b9373e5bd 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/elasticsearch/ElasticsearchSyncProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/service/env/elasticsearch/ElasticsearchSyncProperties.java @@ -34,7 +34,7 @@ public class ElasticsearchSyncProperties { @Description("Whether to sync existing data from MongoDB to Elasticsearch. " - + "If true and the current node is the leader, turms will run a full sync at startup if the data has not been synced yet") + + "If true and the current node is the leader, turms will run a full sync on startup if the data has not been synced yet") private boolean performFullSyncAtStartup = true; } \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/validation/MatchesStringPattern.java b/turms-server-common/src/main/java/im/turms/server/common/infra/validation/MatchesStringPattern.java new file mode 100644 index 0000000000..80633fd0a4 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/validation/MatchesStringPattern.java @@ -0,0 +1,36 @@ +/* + * 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.server.common.infra.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import im.turms.server.common.infra.lang.StringPattern; + +/** + * @author James Chen + */ +@Target({ElementType.PARAMETER, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MatchesStringPattern { + + StringPattern value(); + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/validation/ValidRegex.java b/turms-server-common/src/main/java/im/turms/server/common/infra/validation/ValidRegex.java new file mode 100644 index 0000000000..4e8a0afce7 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/validation/ValidRegex.java @@ -0,0 +1,31 @@ +/* + * 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.server.common.infra.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author James Chen + */ +@Target({ElementType.PARAMETER, ElementType.TYPE_USE, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidRegex { +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/MongoOperationsSupport.java b/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/MongoOperationsSupport.java index 4232ed4339..c1043f0be5 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/MongoOperationsSupport.java +++ b/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/MongoOperationsSupport.java @@ -64,6 +64,12 @@ public interface MongoOperationsSupport { Flux findIds(Class clazz, Filter filter); + Flux findObjectFields( + Class clazz, + Filter filter, + String parentFieldName, + Collection includedFields); + Mono exists(Class clazz, Filter filter); Mono count(Class clazz, Filter filter); @@ -74,7 +80,7 @@ public interface MongoOperationsSupport { Mono upsert(ClientSession session, T o); - Mono upsert(Class clazz, Filter filter, Update update); + Mono upsert(Class clazz, Filter filter, Update update); Mono insert(T value); diff --git a/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/TurmsMongoOperations.java b/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/TurmsMongoOperations.java index 4dea97f3e6..ea950d5ff8 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/TurmsMongoOperations.java +++ b/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/TurmsMongoOperations.java @@ -57,6 +57,7 @@ import com.mongodb.reactivestreams.client.internal.MongoOperationPublisher; import com.mongodb.reactivestreams.client.internal.TurmsFindPublisherImpl; import org.apache.commons.lang3.StringUtils; +import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; import org.bson.BsonDocumentWriter; @@ -65,6 +66,7 @@ import org.bson.Document; import org.bson.codecs.Codec; import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.jctools.maps.NonBlockingIdentityHashMap; import org.reactivestreams.Publisher; @@ -131,6 +133,10 @@ public class TurmsMongoOperations implements MongoOperationsSupport { private static final InsertOneOptions DEFAULT_INSERT_ONE_OPTIONS = new InsertOneOptions(); private static final UpdateOptions DEFAULT_UPDATE_OPTIONS = new UpdateOptions(); private static final UpdateOptions DEFAULT_UPSERT_OPTIONS = new UpdateOptions().upsert(true); + public static final BsonDocument FIND_OBJECT_KEYS_GET_FIELD_OPERATOR = + new BsonDocument().append("$getField", + new BsonDocument().append("field", new BsonString("k")) + .append("input", new BsonString("$$field"))); private final MongoContext context; private final Map, MongoOperationPublisher> publisherMap = @@ -204,6 +210,43 @@ public Flux findIds(Class clazz, Filter filter) { return Flux.from(publisher); } + @Override + public Flux findObjectFields( + Class clazz, + Filter filter, + String parentField, + Collection includedFields) { + MongoCollection collection = context.getCollection(clazz); + BsonArray includedFieldsArray = new BsonArray(includedFields.size()); + for (String includedField : includedFields) { + includedFieldsArray.add(new BsonString(includedField)); + } + List pipeline = List.of(Aggregates.match(filter), Aggregates.project(new Bson() { + @Override + public BsonDocument toBsonDocument( + Class tDocumentClass, + CodecRegistry codecRegistry) { + return new BsonDocument().append("_id", + new BsonDocument().append("$map", + new BsonDocument() + .append("input", + new BsonDocument().append("$objectToArray", + new BsonString( + "$" + + parentField))) + .append("as", new BsonString("field")) + .append("in", FIND_OBJECT_KEYS_GET_FIELD_OPERATOR))); + } + }), + Aggregates.unwind("$_id"), + Aggregates.match(new BsonDocument().append("_id", + new BsonDocument().append("$in", includedFieldsArray)))); + AggregatePublisher names = collection.aggregate(pipeline, Document.class); + return Flux.from(names) + .map(document -> document.get("_id") + .toString()); + } + @Override public Mono exists(Class clazz, Filter filter) { MongoCollection collection = context.getCollection(clazz); @@ -306,13 +349,12 @@ private void applyShardKey(BsonDocument filter, ShardKey shardKey, BsonDocument } @Override - public Mono upsert(Class clazz, Filter filter, Update update) { + public Mono upsert(Class clazz, Filter filter, Update update) { MongoCollection collection = context.getCollection(clazz); Publisher source = collection.updateOne(filter, update, DEFAULT_UPSERT_OPTIONS); return Mono.from(source) - .onErrorMap(MongoExceptionUtil::translate) - .then(); + .onErrorMap(MongoExceptionUtil::translate); } // Insert @@ -495,7 +537,7 @@ public Mono countDistinct(Class clazz, Filter filter, String groupB + groupByFieldName, Accumulators.sum("count", 1))); AggregatePublisher count = collection.aggregate(pipeline, Document.class); return Mono.from(count) - .map(document -> Long.valueOf((Integer) document.get("count"))) + .map(document -> ((Number) document.get("count")).longValue()) .defaultIfEmpty(0L); } diff --git a/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/option/Filter.java b/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/option/Filter.java index f5abef140e..8bcffa2d38 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/option/Filter.java +++ b/turms-server-common/src/main/java/im/turms/server/common/storage/mongo/operation/option/Filter.java @@ -121,6 +121,14 @@ public Filter gte(String key, Object value) { return this; } + public Filter gteIfNotNull(String key, @Nullable Object value) { + if (value != null) { + document.append(key, + new BsonDocument("$gte", BsonValueEncoder.encodeSingleValue(value))); + } + return this; + } + public Filter gteOrNull(String key, Object value) { or(Filter.newBuilder(1) .eq(key, null), diff --git a/turms-server-common/src/test/resources/turms-properties-metadata-with-property-value.json b/turms-server-common/src/test/resources/turms-properties-metadata-with-property-value.json index 232cec377b..1b400c356e 100644 --- a/turms-server-common/src/test/resources/turms-properties-metadata-with-property-value.json +++ b/turms-server-common/src/test/resources/turms-properties-metadata-with-property-value.json @@ -577,6 +577,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -623,6 +626,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -681,6 +687,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -727,6 +736,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -2951,6 +2963,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -2997,6 +3012,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -3056,6 +3074,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -3102,6 +3123,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -3160,6 +3184,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -3206,6 +3233,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -3400,6 +3430,16 @@ } }, "conversation": { + "allowedSettings": { + "deprecated": false, + "description": "The list of allowed conversation settings", + "elementType": "im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties", + "global": true, + "mutable": true, + "sensitive": false, + "type": "java.util.List", + "value": [] + }, "readReceipt": { "allowMoveReadDateForward": { "deprecated": false, @@ -3559,7 +3599,7 @@ "sync": { "performFullSyncAtStartup": { "deprecated": false, - "description": "Whether to sync existing data from MongoDB to Elasticsearch. If true and the current node is the leader, turms will run a full sync at startup if the data has not been synced yet", + "description": "Whether to sync existing data from MongoDB to Elasticsearch. If true and the current node is the leader, turms will run a full sync on startup if the data has not been synced yet", "global": false, "mutable": false, "sensitive": false, @@ -3657,7 +3697,7 @@ "sync": { "performFullSyncAtStartup": { "deprecated": false, - "description": "Whether to sync existing data from MongoDB to Elasticsearch. If true and the current node is the leader, turms will run a full sync at startup if the data has not been synced yet", + "description": "Whether to sync existing data from MongoDB to Elasticsearch. If true and the current node is the leader, turms will run a full sync on startup if the data has not been synced yet", "global": false, "mutable": false, "sensitive": false, @@ -3671,7 +3711,7 @@ "fake": { "clearAllCollectionsBeforeFaking": { "deprecated": false, - "description": "Whether to clear all collections before faking at startup", + "description": "Whether to clear all collections before faking on startup", "global": false, "mutable": false, "sensitive": false, @@ -4644,6 +4684,28 @@ "value": true } }, + "groupConversationSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their group conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean", + "value": true + } + }, + "groupConversationSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their group conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean", + "value": true + } + }, "groupCreated": { "notifyRequesterOtherOnlineSessions": { "deprecated": false, @@ -5223,6 +5285,28 @@ "value": true } }, + "privateConversationSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their private conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean", + "value": true + } + }, + "privateConversationSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their private conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean", + "value": true + } + }, "userInfoUpdated": { "notifyNonBlockedRelatedUsers": { "deprecated": false, @@ -5262,6 +5346,28 @@ "type": "boolean", "value": true } + }, + "userSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean", + "value": true + } + }, + "userSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean", + "value": true + } } }, "pushNotification": { @@ -5567,6 +5673,16 @@ "type": "boolean", "value": true }, + "allowedSettings": { + "deprecated": false, + "description": "The list of allowed user settings", + "elementType": "im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties", + "global": true, + "mutable": true, + "sensitive": false, + "type": "java.util.List", + "value": [] + }, "deleteTwoSidedRelationships": { "deprecated": false, "description": "Whether to delete the two-sided relationships when a user requests to delete a relationship", diff --git a/turms-server-common/src/test/resources/turms-properties-metadata.json b/turms-server-common/src/test/resources/turms-properties-metadata.json index 60c47e6b54..1be346eb97 100644 --- a/turms-server-common/src/test/resources/turms-properties-metadata.json +++ b/turms-server-common/src/test/resources/turms-properties-metadata.json @@ -515,6 +515,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -561,6 +564,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -617,6 +623,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -663,6 +672,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -2562,6 +2574,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -2608,6 +2623,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -2665,6 +2683,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -2711,6 +2732,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -2767,6 +2791,9 @@ "UPDATE_USER_LOCATION_REQUEST", "UPDATE_USER_ONLINE_STATUS_REQUEST", "UPDATE_USER_REQUEST", + "UPDATE_USER_SETTINGS_REQUEST", + "DELETE_USER_SETTINGS_REQUEST", + "QUERY_USER_SETTINGS_REQUEST", "CREATE_FRIEND_REQUEST_REQUEST", "CREATE_RELATIONSHIP_GROUP_REQUEST", "CREATE_RELATIONSHIP_REQUEST", @@ -2813,6 +2840,9 @@ "QUERY_RESOURCE_UPLOAD_INFO_REQUEST", "QUERY_MESSAGE_ATTACHMENT_INFOS_REQUEST", "UPDATE_MESSAGE_ATTACHMENT_INFO_REQUEST", + "DELETE_CONVERSATION_SETTINGS_REQUEST", + "QUERY_CONVERSATION_SETTINGS_REQUEST", + "UPDATE_CONVERSATION_SETTINGS_REQUEST", "KIND_NOT_SET" ], "sensitive": false, @@ -2985,6 +3015,15 @@ } }, "conversation": { + "allowedSettings": { + "deprecated": false, + "description": "The list of allowed conversation settings", + "elementType": "im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties", + "global": true, + "mutable": true, + "sensitive": false, + "type": "java.util.List" + }, "readReceipt": { "allowMoveReadDateForward": { "deprecated": false, @@ -3113,7 +3152,7 @@ "sync": { "performFullSyncAtStartup": { "deprecated": false, - "description": "Whether to sync existing data from MongoDB to Elasticsearch. If true and the current node is the leader, turms will run a full sync at startup if the data has not been synced yet", + "description": "Whether to sync existing data from MongoDB to Elasticsearch. If true and the current node is the leader, turms will run a full sync on startup if the data has not been synced yet", "global": false, "mutable": false, "sensitive": false, @@ -3186,7 +3225,7 @@ "sync": { "performFullSyncAtStartup": { "deprecated": false, - "description": "Whether to sync existing data from MongoDB to Elasticsearch. If true and the current node is the leader, turms will run a full sync at startup if the data has not been synced yet", + "description": "Whether to sync existing data from MongoDB to Elasticsearch. If true and the current node is the leader, turms will run a full sync on startup if the data has not been synced yet", "global": false, "mutable": false, "sensitive": false, @@ -3199,7 +3238,7 @@ "fake": { "clearAllCollectionsBeforeFaking": { "deprecated": false, - "description": "Whether to clear all collections before faking at startup", + "description": "Whether to clear all collections before faking on startup", "global": false, "mutable": false, "sensitive": false, @@ -4043,6 +4082,26 @@ "type": "boolean" } }, + "groupConversationSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their group conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, + "groupConversationSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their group conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, "groupCreated": { "notifyRequesterOtherOnlineSessions": { "deprecated": false, @@ -4563,6 +4622,26 @@ "type": "boolean" } }, + "privateConversationSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their private conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, + "privateConversationSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their private conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, "userInfoUpdated": { "notifyNonBlockedRelatedUsers": { "deprecated": false, @@ -4598,6 +4677,26 @@ "sensitive": false, "type": "boolean" } + }, + "userSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, + "userSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } } }, "pushNotification": { @@ -4871,6 +4970,15 @@ "sensitive": false, "type": "boolean" }, + "allowedSettings": { + "deprecated": false, + "description": "The list of allowed user settings", + "elementType": "im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties", + "global": true, + "mutable": true, + "sensitive": false, + "type": "java.util.List" + }, "deleteTwoSidedRelationships": { "deprecated": false, "description": "Whether to delete the two-sided relationships when a user requests to delete a relationship", diff --git a/turms-server-common/src/test/resources/turms-properties-only-mutable-metadata.json b/turms-server-common/src/test/resources/turms-properties-only-mutable-metadata.json index d01fba1d46..5105fa3c3d 100644 --- a/turms-server-common/src/test/resources/turms-properties-only-mutable-metadata.json +++ b/turms-server-common/src/test/resources/turms-properties-only-mutable-metadata.json @@ -866,6 +866,15 @@ } }, "conversation": { + "allowedSettings": { + "deprecated": false, + "description": "The list of allowed conversation settings", + "elementType": "im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties", + "global": true, + "mutable": true, + "sensitive": false, + "type": "java.util.List" + }, "readReceipt": { "allowMoveReadDateForward": { "deprecated": false, @@ -1416,6 +1425,26 @@ "type": "boolean" } }, + "groupConversationSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their group conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, + "groupConversationSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their group conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, "groupCreated": { "notifyRequesterOtherOnlineSessions": { "deprecated": false, @@ -1936,6 +1965,26 @@ "type": "boolean" } }, + "privateConversationSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their private conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, + "privateConversationSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their private conversation settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, "userInfoUpdated": { "notifyNonBlockedRelatedUsers": { "deprecated": false, @@ -1971,6 +2020,26 @@ "sensitive": false, "type": "boolean" } + }, + "userSettingDeleted": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have deleted their settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } + }, + "userSettingUpdated": { + "notifyRequesterOtherOnlineSessions": { + "deprecated": false, + "description": "Whether to notify the requester's other online sessions when they have updated their settings", + "global": true, + "mutable": true, + "sensitive": false, + "type": "boolean" + } } }, "pushNotification": { @@ -2004,6 +2073,15 @@ "sensitive": false, "type": "boolean" }, + "allowedSettings": { + "deprecated": false, + "description": "The list of allowed user settings", + "elementType": "im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties", + "global": true, + "mutable": true, + "sensitive": false, + "type": "java.util.List" + }, "deleteTwoSidedRelationships": { "deprecated": false, "description": "Whether to delete the two-sided relationships when a user requests to delete a relationship", diff --git a/turms-service/src/main/java/im/turms/service/domain/common/service/CustomSettingService.java b/turms-service/src/main/java/im/turms/service/domain/common/service/CustomSettingService.java new file mode 100644 index 0000000000..d88e29c5be --- /dev/null +++ b/turms-service/src/main/java/im/turms/service/domain/common/service/CustomSettingService.java @@ -0,0 +1,274 @@ +/* + * 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.common.service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.eclipse.collections.impl.set.mutable.UnifiedSet; + +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.exception.ResponseException; +import im.turms.server.common.infra.lang.StringUtil; +import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingDoubleValueProperties; +import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingIntValueProperties; +import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingLongValueProperties; +import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingProperties; +import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingStringValueProperties; +import im.turms.server.common.infra.property.env.service.business.common.setting.CustomSettingValueProperties; +import im.turms.service.infra.locale.LocaleUtils; + +/** + * @author James Chen + */ +public abstract class CustomSettingService { + + protected List settingPropertiesList; + protected Set immutableSettings; + protected Set deletableSettings; + + protected void updateGlobalProperties(List settingPropertiesList) { + int size = settingPropertiesList.size(); + if (0 == size) { + this.settingPropertiesList = Collections.emptyList(); + immutableSettings = Collections.emptySet(); + deletableSettings = Collections.emptySet(); + return; + } + List newSettingsPropertiesList = null; + Set newImmutableSettings = null; + Set newDeletableSettings = null; + for (int i = 0; i < size; i++) { + CustomSettingProperties settingProperties = settingPropertiesList.get(i); + String sourceName = settingProperties.getSourceName(); + String storedName = settingProperties.getStoredName(); + if (StringUtil.isEmpty(storedName)) { + if (newSettingsPropertiesList == null) { + newSettingsPropertiesList = new ArrayList<>(settingPropertiesList); + } + newSettingsPropertiesList.set(i, + settingProperties.toBuilder() + .storedName(sourceName) + .build()); + } + if (settingProperties.isImmutable()) { + if (newImmutableSettings == null) { + newImmutableSettings = new UnifiedSet<>(4); + } + newImmutableSettings.add(sourceName); + } + if (settingProperties.isDeletable()) { + if (newDeletableSettings == null) { + newDeletableSettings = new UnifiedSet<>(4); + } + newDeletableSettings.add(sourceName); + } + } + + this.settingPropertiesList = newSettingsPropertiesList == null + ? settingPropertiesList + : newSettingsPropertiesList; + this.immutableSettings = newImmutableSettings == null + ? Collections.emptySet() + : newImmutableSettings; + this.deletableSettings = newDeletableSettings == null + ? Collections.emptySet() + : newDeletableSettings; + } + + /** + * @param propertiesList we assume the properties are valid (i.e. both source name and stored + * name are not blank). + */ + protected Map parseSettings( + List propertiesList, + Map inputSettings) { + if (propertiesList.isEmpty() || inputSettings.isEmpty()) { + return Collections.emptyMap(); + } + Map outputSettings = + CollectionUtil.newMapWithExpectedSize(propertiesList.size()); + for (CustomSettingProperties properties : propertiesList) { + String sourceName = properties.getSourceName(); + Value value = inputSettings.get(sourceName); + if (value == null) { + continue; + } + Object parsedValue = parseValue(properties.getValue(), sourceName, value); + outputSettings.put(properties.getStoredName(), parsedValue); + } + return outputSettings; + } + + private Object parseValue( + CustomSettingValueProperties valueProperties, + String sourceName, + Value value) { + return switch (valueProperties.getType()) { + case INT -> { + CustomSettingIntValueProperties properties = valueProperties.getIntValue(); + int min = properties.getMin(); + int max = properties.getMax(); + int val = switch (value.getKindCase()) { + case INT32_VALUE -> value.getInt32Value(); + case INT64_VALUE -> (int) value.getInt64Value(); + default -> throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be in [" + + min + + ", " + + max + + "]"); + }; + if (val > max || val < min) { + throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be in [" + + min + + ", " + + max + + "]"); + } + yield val; + } + case LONG -> { + CustomSettingLongValueProperties properties = valueProperties.getLongValue(); + long min = properties.getMin(); + long max = properties.getMax(); + long val = switch (value.getKindCase()) { + case INT32_VALUE -> value.getInt32Value(); + case INT64_VALUE -> value.getInt64Value(); + default -> throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be in [" + + min + + ", " + + max + + "]"); + }; + if (val > max || val < min) { + throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be in [" + + min + + ", " + + max + + "]"); + } + yield val; + } + case DOUBLE -> { + CustomSettingDoubleValueProperties properties = valueProperties.getDoubleValue(); + double min = properties.getMin(); + double max = properties.getMax(); + double val = switch (value.getKindCase()) { + case FLOAT_VALUE -> value.getFloatValue(); + case DOUBLE_VALUE -> value.getDoubleValue(); + default -> throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be in [" + + min + + ", " + + max + + "]"); + }; + if (val > max || val < min) { + throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be in [" + + min + + ", " + + max + + "]"); + } + yield val; + } + case STRING -> { + CustomSettingStringValueProperties properties = valueProperties.getStringValue(); + int minLength = properties.getMinLength(); + int maxLength = properties.getMaxLength(); + List parsedRegexes = properties.getParsedRegexes(); + String val = switch (value.getKindCase()) { + case INT32_VALUE -> String.valueOf(value.getInt32Value()); + case INT64_VALUE -> String.valueOf(value.getInt64Value()); + case FLOAT_VALUE -> String.valueOf(value.getFloatValue()); + case DOUBLE_VALUE -> String.valueOf(value.getDoubleValue()); + case BOOL_VALUE -> String.valueOf(value.getBoolValue()); + case STRING_VALUE -> value.getStringValue(); + default -> throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be an int32, int64, float, double, bool or string"); + }; + int length = val.length(); + if (length < minLength || length > maxLength) { + throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The string value length of the setting \"" + + sourceName + + "\" must be in [" + + minLength + + ", " + + maxLength + + "]"); + } + if (!parsedRegexes.isEmpty()) { + for (Pattern regex : parsedRegexes) { + if (!regex.matcher(val) + .matches()) { + throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The string value of the setting \"" + + sourceName + + "\" must match: " + + regex.pattern()); + } + } + } + yield val; + } + case BOOL -> switch (value.getKindCase()) { + case BOOL_VALUE -> value.getBoolValue(); + default -> throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be a bool"); + }; + case LANGUAGE -> { + String languageId = value.getStringValue(); + if (!LocaleUtils.isAvailableLanguage(languageId)) { + throw ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The value of the setting \"" + + sourceName + + "\" must be a valid language ID"); + } + yield languageId; + } + }; + } +} \ No newline at end of file diff --git a/turms-service/src/main/java/im/turms/service/domain/conference/po/Meeting.java b/turms-service/src/main/java/im/turms/service/domain/conference/po/Meeting.java index b1afb0cfcd..88e0ddeb68 100644 --- a/turms-service/src/main/java/im/turms/service/domain/conference/po/Meeting.java +++ b/turms-service/src/main/java/im/turms/service/domain/conference/po/Meeting.java @@ -33,8 +33,6 @@ import static im.turms.server.common.storage.mongo.entity.annotation.IndexedReason.EXTENDED_FEATURE; /** - * No need to shard because there should be only a few user permission groups. - * * @author James Chen */ @Data diff --git a/turms-service/src/main/java/im/turms/service/domain/conference/service/ConferenceService.java b/turms-service/src/main/java/im/turms/service/domain/conference/service/ConferenceService.java index 703f3071e5..a233a231b2 100644 --- a/turms-service/src/main/java/im/turms/service/domain/conference/service/ConferenceService.java +++ b/turms-service/src/main/java/im/turms/service/domain/conference/service/ConferenceService.java @@ -43,6 +43,7 @@ import im.turms.server.common.infra.exception.IncompatibleJvmException; import im.turms.server.common.infra.exception.ResponseException; import im.turms.server.common.infra.exception.ResponseExceptionPublisherPool; +import im.turms.server.common.infra.lang.StringPattern; import im.turms.server.common.infra.logging.core.logger.Logger; import im.turms.server.common.infra.logging.core.logger.LoggerFactory; import im.turms.server.common.infra.plugin.ExtensionPointEventListener; @@ -51,7 +52,6 @@ import im.turms.server.common.infra.property.TurmsPropertiesManager; import im.turms.server.common.infra.property.constant.MeetingIdType; import im.turms.server.common.infra.property.constant.PasswordPolicy; -import im.turms.server.common.infra.property.constant.PasswordType; import im.turms.server.common.infra.property.env.service.business.conference.meeting.IdProperties; import im.turms.server.common.infra.property.env.service.business.conference.meeting.IntroProperties; import im.turms.server.common.infra.property.env.service.business.conference.meeting.MeetingProperties; @@ -137,7 +137,7 @@ public class ConferenceService { private boolean isMaxActiveMeetingCountPerUserEnabled; private int maxActiveMeetingCountPerUser; private PasswordPolicy passwordPolicy; - private PasswordType passwordType; + private StringPattern passwordPattern; private int passwordMinLength; private int passwordMaxLength; @@ -218,7 +218,7 @@ private void updateGlobalProperties(TurmsProperties properties) { introMinLength = introProperties.getMinLength(); introMaxLength = introProperties.getMaxLength(); passwordPolicy = passwordProperties.getPolicy(); - passwordType = passwordProperties.getType(); + passwordPattern = passwordProperties.getType(); passwordMinLength = passwordProperties.getMinLength(); passwordMaxLength = passwordProperties.getMaxLength(); maxAllowedStartDateOffsetMillis = @@ -886,7 +886,7 @@ private void validatePassword(String password) { } private String generatePassword() { - return switch (passwordType) { + return switch (passwordPattern) { case ALPHANUMERIC -> RandomUtil.nextAlphanumericString(passwordMinLength, passwordMaxLength); case NUMERIC -> RandomUtil.nextNumericString(passwordMinLength, passwordMaxLength); diff --git a/turms-service/src/main/java/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationServiceController.java b/turms-service/src/main/java/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationServiceController.java index 06b3d03dbe..1528cd9699 100644 --- a/turms-service/src/main/java/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationServiceController.java +++ b/turms-service/src/main/java/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationServiceController.java @@ -98,7 +98,7 @@ public ClientRequestHandler handleQueryConversationsRequest() { return clientRequest -> { QueryConversationsRequest request = clientRequest.turmsRequest() .getQueryConversationsRequest(); - List targetIds = request.getTargetIdsList(); + List targetIds = request.getUserIdsList(); Mono dataFlux; if (targetIds.isEmpty()) { List groupIds = request.getGroupIdsList(); @@ -148,18 +148,20 @@ public ClientRequestHandler handleUpdateConversationRequest() { return clientRequest -> { UpdateConversationRequest request = clientRequest.turmsRequest() .getUpdateConversationRequest(); - if (!request.hasTargetId() && !request.hasGroupId()) { + boolean hasUserId = request.hasUserId(); + if (!hasUserId && !request.hasGroupId()) { return Mono.just(RequestHandlerResult.of(ResponseStatusCode.ILLEGAL_ARGUMENT, - "The targetId and groupId must not both be null")); + "The userId and groupId must not both be null")); } Long requesterId = clientRequest.userId(); + // todo: limit read date range + Date readDate = new Date(request.getReadDate()); - boolean isUpdatePrivateConversationRequest = request.hasTargetId(); long targetId; Mono mono; - if (isUpdatePrivateConversationRequest) { - targetId = request.getTargetId(); + if (hasUserId) { + targetId = request.getUserId(); mono = conversationService .authAndUpsertPrivateConversationReadDate(requesterId, targetId, readDate); } else { @@ -168,7 +170,7 @@ public ClientRequestHandler handleUpdateConversationRequest() { .authAndUpsertGroupConversationReadDate(targetId, requesterId, readDate); } return mono.then(Mono.defer(() -> { - if (isUpdatePrivateConversationRequest) { + if (hasUserId) { if (notifyContactOfPrivateConversationReadDateUpdated) { return Mono.just(RequestHandlerResult.of( notifyRequesterOtherOnlineSessionsOfPrivateConversationReadDateUpdated, @@ -191,5 +193,4 @@ public ClientRequestHandler handleUpdateConversationRequest() { })); }; } - } \ No newline at end of file diff --git a/turms-service/src/main/java/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationSettingsServiceController.java b/turms-service/src/main/java/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationSettingsServiceController.java new file mode 100644 index 0000000000..8b5c052909 --- /dev/null +++ b/turms-service/src/main/java/im/turms/service/domain/conversation/access/servicerequest/controller/ConversationSettingsServiceController.java @@ -0,0 +1,203 @@ +/* + * 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.access.servicerequest.controller; + +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Mono; + +import im.turms.server.common.access.client.dto.ClientMessagePool; +import im.turms.server.common.access.client.dto.model.conversation.ConversationSettingsList; +import im.turms.server.common.access.client.dto.request.TurmsRequest; +import im.turms.server.common.access.client.dto.request.conversation.DeleteConversationSettingsRequest; +import im.turms.server.common.access.client.dto.request.conversation.QueryConversationSettingsRequest; +import im.turms.server.common.access.client.dto.request.conversation.UpdateConversationSettingsRequest; +import im.turms.server.common.access.common.ResponseStatusCode; +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.ServiceProperties; +import im.turms.server.common.infra.property.env.service.business.notification.NotificationProperties; +import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationGroupConversationSettingDeletedProperties; +import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationGroupConversationSettingUpdatedProperties; +import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationPrivateConversationSettingDeletedProperties; +import im.turms.server.common.infra.property.env.service.business.notification.conversation.NotificationPrivateConversationSettingUpdatedProperties; +import im.turms.server.common.infra.recycler.ListRecycler; +import im.turms.server.common.infra.recycler.Recyclable; +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.conversation.po.ConversationSettings; +import im.turms.service.domain.conversation.service.ConversationSettingsService; +import im.turms.service.infra.proto.ProtoModelConvertor; + +import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.DELETE_CONVERSATION_SETTINGS_REQUEST; +import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.QUERY_CONVERSATION_SETTINGS_REQUEST; +import static im.turms.server.common.access.client.dto.request.TurmsRequest.KindCase.UPDATE_CONVERSATION_SETTINGS_REQUEST; + +/** + * @author James Chen + */ +@Controller +public class ConversationSettingsServiceController extends BaseServiceController { + + private final ConversationSettingsService conversationSettingsService; + + private boolean notifyRequesterOtherOnlineSessionsOfPrivateConversationSettingDeleted; + private boolean notifyRequesterOtherOnlineSessionsOfPrivateConversationSettingUpdated; + + private boolean notifyRequesterOtherOnlineSessionsOfGroupConversationSettingDeleted; + private boolean notifyRequesterOtherOnlineSessionsOfGroupConversationSettingUpdated; + + public ConversationSettingsServiceController( + TurmsPropertiesManager propertiesManager, + ConversationSettingsService conversationSettingsService) { + this.conversationSettingsService = conversationSettingsService; + + propertiesManager.notifyAndAddGlobalPropertiesChangeListener(this::updateProperties); + } + + private void updateProperties(TurmsProperties properties) { + ServiceProperties serviceProperties = properties.getService(); + NotificationProperties notificationProperties = serviceProperties.getNotification(); + + NotificationPrivateConversationSettingDeletedProperties privateConversationSettingDeletedProperties = + notificationProperties.getPrivateConversationSettingDeleted(); + notifyRequesterOtherOnlineSessionsOfPrivateConversationSettingDeleted = + privateConversationSettingDeletedProperties.isNotifyRequesterOtherOnlineSessions(); + + NotificationPrivateConversationSettingUpdatedProperties privateConversationSettingUpdatedProperties = + notificationProperties.getPrivateConversationSettingUpdated(); + notifyRequesterOtherOnlineSessionsOfPrivateConversationSettingUpdated = + privateConversationSettingUpdatedProperties.isNotifyRequesterOtherOnlineSessions(); + + NotificationGroupConversationSettingUpdatedProperties groupConversationSettingUpdatedProperties = + notificationProperties.getGroupConversationSettingUpdated(); + notifyRequesterOtherOnlineSessionsOfGroupConversationSettingUpdated = + groupConversationSettingUpdatedProperties.isNotifyRequesterOtherOnlineSessions(); + + NotificationGroupConversationSettingDeletedProperties groupConversationSettingDeletedProperties = + notificationProperties.getGroupConversationSettingDeleted(); + notifyRequesterOtherOnlineSessionsOfGroupConversationSettingDeleted = + groupConversationSettingDeletedProperties.isNotifyRequesterOtherOnlineSessions(); + } + + @ServiceRequestMapping(UPDATE_CONVERSATION_SETTINGS_REQUEST) + public ClientRequestHandler handleUpdateConversationSettingsRequest() { + return clientRequest -> { + UpdateConversationSettingsRequest request = clientRequest.turmsRequest() + .getUpdateConversationSettingsRequest(); + boolean hasUserId = request.hasUserId(); + if (!hasUserId && !request.hasGroupId()) { + return Mono.just(RequestHandlerResult.of(ResponseStatusCode.ILLEGAL_ARGUMENT, + "The userId and groupId must not both be null")); + } + if (hasUserId) { + return conversationSettingsService + .upsertPrivateConversationSettings(clientRequest.userId(), + request.getUserId(), + request.getSettingsMap()) + .map(updated -> updated + ? RequestHandlerResult.of( + notifyRequesterOtherOnlineSessionsOfPrivateConversationSettingUpdated + && updated, + clientRequest.turmsRequest()) + : RequestHandlerResult.OK); + } else { + return conversationSettingsService + .upsertGroupConversationSettings(clientRequest.userId(), + request.getGroupId(), + request.getSettingsMap()) + .map(updated -> updated + ? RequestHandlerResult.of( + notifyRequesterOtherOnlineSessionsOfGroupConversationSettingUpdated + && updated, + clientRequest.turmsRequest()) + : RequestHandlerResult.OK); + } + }; + } + + @ServiceRequestMapping(DELETE_CONVERSATION_SETTINGS_REQUEST) + public ClientRequestHandler handleDeleteConversationSettingsRequest() { + return clientRequest -> { + TurmsRequest turmsRequest = clientRequest.turmsRequest(); + DeleteConversationSettingsRequest request = + turmsRequest.getDeleteConversationSettingsRequest(); + boolean hasUserId = request.getUserIdsCount() > 0; + boolean hasGroupId = request.getGroupIdsCount() > 0; + return conversationSettingsService.unsetSettings(clientRequest.userId(), + hasUserId + ? CollectionUtil.newSet(request.getUserIdsList()) + : null, + hasGroupId + ? CollectionUtil.newSet(request.getGroupIdsList()) + : null, + request.getNamesCount() > 0 + ? CollectionUtil.newSet(request.getNamesList()) + : null) + .map(deleted -> RequestHandlerResult.of(deleted + && ((hasUserId + && notifyRequesterOtherOnlineSessionsOfPrivateConversationSettingDeleted) + || (hasGroupId + && notifyRequesterOtherOnlineSessionsOfGroupConversationSettingDeleted)), + turmsRequest)); + }; + } + + @ServiceRequestMapping(QUERY_CONVERSATION_SETTINGS_REQUEST) + public ClientRequestHandler handleQueryConversationSettingsRequest() { + return clientRequest -> { + QueryConversationSettingsRequest request = clientRequest.turmsRequest() + .getQueryConversationSettingsRequest(); + Recyclable> 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);