Compare commits

..
6 Commits
17 changed files with 186 additions and 37 deletions
+5
View File
@@ -158,6 +158,11 @@ mvn clean package
## 服务对象
因为野火的使用范围比较广,难免被一些不法分子利用,国内反诈反赌力度很大,所以国内多地警方都跟我们都有过联系。***为了预防我们的产品被用于非法目的,试用及购买只能支持境内法人单位,不接受个人或者境外团体。如果我们发现产品被用于非法目的,我们会立即停止技术支持服务并且报警和配合警方调查。请用于非法目的用户绕行!***
## 群的性能问题
群的大小和性能不是线性相关的,而是O(N²)的关系,这是群的特性决定的。所以社区版IM服务很难支持很大的群人数,我们建议最大群不要超过500人,像微信的群大小一样。修改方法是修改数据库中```t_settings```表中的群最大人数。
如果是专业版IM服务,普通群性能会有些加强,但也不会提高太多,所以我们就开发了超级群,可以支持上万群成员。关于超级群,请参考[野火超级群组](https://mp.weixin.qq.com/s/ov5ZiaEIUZuvgFBXOrcjbg)。现有客户有接近10W的群组成员能够流畅稳定地运行。
## 常见问题
请参考文档[常见问题](https://docs.wildfirechat.cn/faq/server.html)
+16
View File
@@ -211,6 +211,12 @@ friend.request_expiration_duration 604800000
##好友请求限制频率,一个用户24小时之内允许请求好友的次数,默认不限频。
friend.request_rate_limit 30
##如果搜索电话号码,24小时内搜索错误号码几次就不再允许电话号码搜素。防止有人遍历搜索电话号码
friend.search_mobile_empty_rate_limit 5
##如果搜索用户,24小时内搜索次数。防止有人遍历用户名或者用户ID乱加好友。另外可以禁止ID搜索friend.disable_user_id_search
friend.search_rate_limit 100
##请求添加机器人为好友时,机器人是否自动接受,默认为true
friend.robot_auto_accept true
@@ -267,6 +273,16 @@ group.enable_mark_deletion true
## 销毁用户时,用户信息会被清空,deleted字段设置为1,这个开关控制是否保留用户昵称,默认为true。
user.keep_display_name_when_destroy true
## 销毁用户时deleted字段设置为1,这个开关控制是否保留其他信息,默认是false。
user.keep_full_info_when_destroy false
## 销毁用户时,如果user.keep_full_info_when_destroy为true,会保留所有信息,这个开关控制是否保留这个用户信息的电话号码,默认为false。
## 当保留所有信息且保留电话号码时,用户如果再注册会用到原来的UID,能够看到原来用户的消息,需要考虑一下是否让已删除用户再次注册登录看到之前的消息。
user.keep_mobile_when_destroy false
## 销毁用户时是否保留归属于这个用户的所有消息记录,默认为false。
user.keep_messages_when_destroy false
# 好友/好友请求/用户设置 这三类数据同步的最大条目数。之前是客户第一次登录时,会把所有的对应数据一次下载到客户端,但如果有特别多的信息,会导致无法同步到客户端,
# 因此需要分段多次同步,此开关定义了每次同步的数据的条数。此需要客户端支持,客户端SDK在2021.5.25日后支持此功能。如果确认所有客户端都在这个日志之后才可以打开此设置。
#sync.data_part_size 1000
+1
View File
@@ -0,0 +1 @@
CREATE INDEX `user_gid_type_index` ON `t_group_member` (`_gid`, `_type`);
+1
View File
@@ -0,0 +1 @@
CREATE INDEX `user_gid_type_index` ON `t_group_member` (`_gid`, `_type`);
+1 -1
View File
@@ -5,7 +5,7 @@
<relativePath>../</relativePath>
<artifactId>wildfirechat-parent</artifactId>
<groupId>cn.wildfirechat</groupId>
<version>1.4.0</version>
<version>1.4.1</version>
</parent>
<artifactId>moquette-broker</artifactId>
@@ -190,6 +190,8 @@ public final class BrokerConstants {
public static final String FRIEND_Reject_Request_Duration = "friend.reject_request_duration";
public static final String FRIEND_Request_Expiration_Duration = "friend.request_expiration_duration";
public static final String FRIEND_Request_Rate_Limit = "friend.request_rate_limit";
public static final String FRIEND_Search_Mobile_Empty_Rate_Limit = "friend.search_mobile_empty_rate_limit";
public static final String FRIEND_Search_Rate_Limit = "friend.search_rate_limit";
public static final String FRIEND_Request_Robot_Auto_Accept = "friend.robot_auto_accept";
@@ -213,6 +215,9 @@ public final class BrokerConstants {
public static final String USER_HIDE_PROPERTIES = "user.hide_properties";
public static final String USER_KEEP_DISPLAY_NAME_WHEN_DESTROY = "user.keep_display_name_when_destroy";
public static final String USER_KEEP_FULL_INFO_WHEN_DESTROY = "user.keep_full_info_when_destroy";
public static final String USER_KEEP_MOBILE_WHEN_DESTROY = "user.keep_mobile_when_destroy";
public static final String USER_KEEP_MESSAGES_WHEN_DESTROY = "user.keep_messages_when_destroy";
public static final String SYNC_Data_Part_Size = "sync.data_part_size";
@@ -15,13 +15,18 @@ import io.netty.buffer.ByteBuf;
import cn.wildfirechat.common.ErrorCode;
import win.liyufan.im.IMTopic;
import java.util.ArrayList;
import java.util.List;
@Handler(IMTopic.UserSearchTopic)
public class UserSearchHandler extends IMHandler<WFCMessage.SearchUserRequest> {
@Override
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, ProtoConstants.RequestSourceType requestSourceType, WFCMessage.SearchUserRequest request, Qos1PublishHandler.IMCallback callback) {
List<WFCMessage.User> users = m_messagesStore.searchUser(request.getKeyword(), request.getFuzzy(), request.getType(), request.getPage());
List<WFCMessage.User> users = new ArrayList<>();
ErrorCode errorCode = m_messagesStore.searchUser(fromUser, request.getKeyword(), request.getFuzzy(), request.getType(), request.getPage(), users);
if(errorCode != ErrorCode.ERROR_CODE_SUCCESS) {
return errorCode;
}
WFCMessage.SearchUserResult.Builder builder = WFCMessage.SearchUserResult.newBuilder();
builder.addAllEntry(users);
byte[] data = builder.build().toByteArray();
@@ -58,6 +58,8 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cn.wildfirechat.common.IMExceptionEvent.EventType.EVENT_CALLBACK_Exception;
import static cn.wildfirechat.proto.ProtoConstants.ChannelState.*;
@@ -157,6 +159,8 @@ public class MemoryMessagesStore implements IMessagesStore {
private long mFriendRejectDuration = 30 * 24 * 60 * 60 * 1000;
private long mFriendRequestExpiration = 7 * 24 * 60 * 60 * 1000;
private RateLimiter mFriendRateLimiter;
private RateLimiter mFriendSearchEmpthMobileRateLimiter;
private RateLimiter mFriendSearchRateLimiter;
private boolean mFriendRobotAutoAccept = true;
private long mPushExpiredTimes = 604800000L;
@@ -202,6 +206,10 @@ public class MemoryMessagesStore implements IMessagesStore {
private int mGroupForbiddenClientOperation;
private int mSyncDataPartSize = 0;
private boolean keepDisplayNameWhenDestroyUser = true;
private boolean keepFullInfoWhenDestroyUser = false;
private boolean keepMobileWhenDestroyUser = false;
private boolean keepMessagesWhenDestroyUser = false;
private String mRecallForwardUrl = null;
private boolean mBroadcastTargetFromUserTable = false;
@@ -376,6 +384,21 @@ public class MemoryMessagesStore implements IMessagesStore {
}
} catch (Exception e) {
}
try {
int rateLimit = Integer.parseInt(m_Server.getConfig().getProperty(FRIEND_Search_Mobile_Empty_Rate_Limit, "0"));
if(rateLimit > 0) {
mFriendSearchEmpthMobileRateLimiter = new RateLimiter(86400, rateLimit);
}
} catch (Exception e) {
}
try {
int rateLimit = Integer.parseInt(m_Server.getConfig().getProperty(FRIEND_Search_Rate_Limit, "0"));
if(rateLimit > 0) {
mFriendSearchRateLimiter = new RateLimiter(86400, rateLimit);
}
} catch (Exception e) {
}
try {
mFriendRobotAutoAccept = Boolean.parseBoolean(m_Server.getConfig().getProperty(BrokerConstants.FRIEND_Request_Robot_Auto_Accept, "true"));
@@ -600,6 +623,17 @@ public class MemoryMessagesStore implements IMessagesStore {
} catch (Exception e) {
}
try {
keepFullInfoWhenDestroyUser = "true".equals(server.getConfig().getProperty(USER_KEEP_FULL_INFO_WHEN_DESTROY));
} catch (Exception e) {}
try {
keepMobileWhenDestroyUser = "true".equals(server.getConfig().getProperty(USER_KEEP_MOBILE_WHEN_DESTROY));
} catch (Exception e) {}
try {
keepMessagesWhenDestroyUser = "true".equals(server.getConfig().getProperty(USER_KEEP_MESSAGES_WHEN_DESTROY));
} catch (Exception e) {}
try {
@@ -2758,8 +2792,9 @@ public class MemoryMessagesStore implements IMessagesStore {
} finally {
mWriteLock.unlock();
}
databaseStore.clearUserMessage(userId);
if(!keepMessagesWhenDestroyUser) {
databaseStore.clearUserMessage(userId);
}
}
@Override
@@ -3137,37 +3172,29 @@ public class MemoryMessagesStore implements IMessagesStore {
IMap<String, WFCMessage.User> mUserMap = hzInstance.getMap(USERS);
WFCMessage.User us = mUserMap.get(userId);
if (us != null) {
if (keepDisplayNameWhenDestroyUser) {
us = WFCMessage.User.newBuilder()
.setUid(userId)
.setUpdateDt(System.currentTimeMillis())
.setName(userId)
.setDeleted(1)
.clearAddress()
.clearCompany()
.clearEmail()
.clearExtra()
.clearMobile()
.clearPortrait()
.clearSocial()
.build();
WFCMessage.User.Builder builder = WFCMessage.User.newBuilder().setUpdateDt(System.currentTimeMillis()).setDeleted(1);
if(keepFullInfoWhenDestroyUser) {
if(!keepMobileWhenDestroyUser) {
builder.clearMobile();
}
} else {
us = WFCMessage.User.newBuilder()
.setUid(userId)
.setUpdateDt(System.currentTimeMillis())
builder.setUid(userId)
.setName(userId)
.setDeleted(1)
.clearDisplayName()
.clearAddress()
.clearCompany()
.clearEmail()
.clearExtra()
.clearMobile()
.clearPortrait()
.clearSocial()
.build();
.clearGender()
.clearSocial();
if (!keepDisplayNameWhenDestroyUser) {
builder.clearDisplayName();
}
}
us = builder.build();
try {
databaseStore.updateUser(us);
} catch (Exception e) {
@@ -3176,8 +3203,8 @@ public class MemoryMessagesStore implements IMessagesStore {
}
mUserMap.put(userId, us);
}
}
@Override
public void updateUserInfo(WFCMessage.User user) throws Exception {
HazelcastInstance hzInstance = m_Server.getHazelcastInstance();
@@ -3222,10 +3249,42 @@ public class MemoryMessagesStore implements IMessagesStore {
return databaseStore.getAllUsers(count, offset);
}
// 电话号码校验正则表达式(支持中国大陆手机号和固话)
private static final String PHONE_REGEX = "^\\s*" + // 允许开头空白
"(?:" +
"(1[0-9])\\d{9}" +
"|" +
"((0\\d{2,3})|(\\(0\\d{2,3}\\)))" +
"[-\\s.]?" +
"\\d{7,8}" +
"|" +
"\\d{7,8}" +
")\\s*$";
private static final Pattern PHONE_PATTERN = Pattern.compile(PHONE_REGEX);
private static final Pattern PHONE_PART_PATTERN = Pattern.compile("^1\\d{0,10}$");
public static boolean isPhoneNumber(String phone) {
if (phone == null || phone.trim().isEmpty()) {
return false;
}
Matcher matcher = PHONE_PATTERN.matcher(phone);
return matcher.matches();
}
public static boolean isPhoneNumberPart(String str) {
if (str == null || str.trim().isEmpty()) {
return false;
}
Matcher matcher = PHONE_PART_PATTERN.matcher(str);
return matcher.matches();
}
@Override
public List<WFCMessage.User> searchUser(String keyword, int searchType, int userType, int page) {
public ErrorCode searchUser(String userId, String keyword, int searchType, int userType, int page, List<WFCMessage.User> users) {
if (mDisableSearch) {
return new ArrayList<>();
return ErrorCode.ERROR_CODE_NOT_IMPLEMENT;
}
if (mDisableNicknameSearch && searchType == ProtoConstants.SearchUserType.SearchUserType_General) {
@@ -3241,10 +3300,29 @@ public class MemoryMessagesStore implements IMessagesStore {
}
if(mDisableUserIdSearch && searchType == ProtoConstants.SearchUserType.SearchUserType_UserId) {
return new ArrayList<>();
return ErrorCode.ERROR_CODE_NOT_IMPLEMENT;
}
return databaseStore.searchUserFromDB(keyword, searchType, userType, page);
if(mFriendSearchRateLimiter != null && !isPhoneNumberPart(keyword)) {
//非电话号码,允许多少次搜索。
if(!mFriendSearchRateLimiter.isGranted(userId)) {
LOG.error("User {} search user mobile over frequency, keyword {}, searchType {}", userId, keyword, searchType);
return ErrorCode.ERROR_CODE_OVER_FREQUENCY;
}
}
List<WFCMessage.User> matchedUsers = databaseStore.searchUserFromDB(keyword, searchType, userType, page);
if(mFriendSearchEmpthMobileRateLimiter != null && matchedUsers.isEmpty() && isPhoneNumber(keyword)) {
//查电话号码为空
if(!mFriendSearchEmpthMobileRateLimiter.isGranted(userId)) {
LOG.error("User {} search user over frequency, keyword {}, searchType {}", userId, keyword, searchType);
return ErrorCode.ERROR_CODE_OVER_FREQUENCY;
}
}
users.addAll(matchedUsers);
return ErrorCode.ERROR_CODE_SUCCESS;
}
@Override
@@ -154,7 +154,7 @@ public interface IMessagesStore {
WFCMessage.User getUserInfoByName(String name);
WFCMessage.User getUserInfoByMobile(String mobile);
List<WFCMessage.User> getUserInfosByEmail(String email);
List<WFCMessage.User> searchUser(String keyword, int searchType, int userType, int page);
ErrorCode searchUser(String userId, String keyword, int searchType, int userType, int page, List<WFCMessage.User> users);
List<WFCMessage.User> getUserInfoList(int count, int offset);
@@ -230,6 +230,7 @@ public class MessagesPublisher {
}
}
boolean unreadCountIncreased = false;
for (Session targetSession : sessions) {
//超过7天不活跃的用户忽略
if(System.currentTimeMillis() - targetSession.getLastActiveTime() > m_messagesStore.getPushExpiredTimes()) {
@@ -314,8 +315,9 @@ public class MessagesPublisher {
}
if (!StringUtil.isNullOrEmpty(pushContent) || messageContentType == 400) {
if (!isConvSilent && (persistFlag & 0x02) > 0) {
if (!isConvSilent && (persistFlag & 0x02) > 0 && !unreadCountIncreased) {
m_messagesStore.increaseUnreceivedMsgCount(user);
unreadCountIncreased = true;
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>wildfirechat-parent</artifactId>
<groupId>cn.wildfirechat</groupId>
<version>1.4.0</version>
<version>1.4.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -25,6 +25,7 @@ public class InputOutputUserInfo {
private String social;
private String extra;
private int type;
private int deleted;
private long updateDt;
public static InputOutputUserInfo fromPbUser(WFCMessage.User pbUser) {
@@ -42,6 +43,7 @@ public class InputOutputUserInfo {
inputCreateUser.extra = pbUser.getExtra();
inputCreateUser.type = pbUser.getType();
inputCreateUser.updateDt = pbUser.getUpdateDt();
inputCreateUser.deleted = pbUser.getDeleted();
return inputCreateUser;
}
@@ -77,6 +79,7 @@ public class InputOutputUserInfo {
newUserBuilder.setExtra(getExtra());
newUserBuilder.setGender(gender);
newUserBuilder.setType(type);
newUserBuilder.setDeleted(deleted);
newUserBuilder.setUpdateDt(System.currentTimeMillis());
return newUserBuilder.build();
@@ -185,4 +188,12 @@ public class InputOutputUserInfo {
public void setType(int type) {
this.type = type;
}
public int getDeleted() {
return deleted;
}
public void setDeleted(int deleted) {
this.deleted = deleted;
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<!--<relativePath>../pom.xml</relativePath>-->
<artifactId>wildfirechat-parent</artifactId>
<groupId>cn.wildfirechat</groupId>
<version>1.4.0</version>
<version>1.4.1</version>
</parent>
<artifactId>distribution</artifactId>
@@ -206,7 +206,13 @@ friend.reject_request_duration 2592000000
friend.request_expiration_duration 604800000
##好友请求限制频率,一个用户24小时之内允许请求好友的次数,默认不限频。
#friend.request_rate_limit 30
friend.request_rate_limit 30
##如果搜索电话号码,24小时内搜索错误号码几次就不再允许电话号码搜素。防止有人遍历搜索电话号码
friend.search_mobile_empty_rate_limit 5
##如果搜索用户,24小时内搜索次数。防止有人遍历用户名或者用户ID乱加好友。另外可以禁止ID搜索friend.disable_user_id_search
friend.search_rate_limit 100
##请求添加机器人为好友时,机器人是否自动接受,默认为true
friend.robot_auto_accept true
@@ -262,6 +268,19 @@ group.enable_mark_deletion true
## 注意,修改本配置不会对已经同步的信息产生影响。
#user.hide_properties 3,4
## 销毁用户时,用户信息会被清空,deleted字段设置为1,这个开关控制是否保留用户昵称,默认为true。
user.keep_display_name_when_destroy true
## 销毁用户时deleted字段设置为1,这个开关控制是否保留其他信息,默认是false。
user.keep_full_info_when_destroy false
## 销毁用户时,如果user.keep_full_info_when_destroy为true,会保留所有信息,这个开关控制是否保留这个用户信息的电话号码,默认为false。
## 当保留所有信息且保留电话号码时,用户如果再注册会用到原来的UID,能够看到原来用户的消息,需要考虑一下是否让已删除用户再次注册登录看到之前的消息。
user.keep_mobile_when_destroy false
## 销毁用户时是否保留归属于这个用户的所有消息记录,默认为false。
user.keep_messages_when_destroy false
# 好友/好友请求/用户设置 这三类数据同步的最大条目数。之前是客户第一次登录时,会把所有的对应数据一次下载到客户端,但如果有特别多的信息,会导致无法同步到客户端,
# 因此需要分段多次同步,此开关定义了每次同步的数据的条数。此需要客户端支持,客户端SDK在2021.5.25日后支持此功能。如果确认所有客户端都在这个日志之后才可以打开此设置。
#sync.data_part_size 1000
+1 -1
View File
@@ -12,7 +12,7 @@
<artifactId>wildfirechat-parent</artifactId>
<packaging>pom</packaging>
<version>1.4.0</version>
<version>1.4.1</version>
<name>Wildfire Chat</name>
<description>Wildfire Chat Server</description>
<inceptionYear>2019</inceptionYear>
+6
View File
@@ -12,6 +12,12 @@
11. *** 1.3.8版本添加了server api发送消息的限制,限制消息体最大内容不能超过64KB,如果升级请注意业务系统发送消息大小。***
# 更新记录
--------------
Release note 1.4.1:
1. 解决移动端和PAD同时存在时推送计数错误问题。
2. 添加配置,当销毁用户时是否销毁用户信息和用户的消息。
3. 添加配置,现在搜索用户的次数。
--------------
Release note 1.4.0:
1. 添加对用户IP来源的记录。
+1 -1
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>wildfirechat-parent</artifactId>
<groupId>cn.wildfirechat</groupId>
<version>1.4.0</version>
<version>1.4.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>