Compare commits

...

6 Commits

Author SHA1 Message Date
ivmiku
17a52ed471 fix: 依赖改名 2024-09-14 18:06:46 +08:00
ivmiku
b987c11cd1 fix: 依赖改名 2024-09-14 14:57:39 +08:00
ivmiku
62a521388d feat: 修改网关配置 2024-09-14 13:57:21 +08:00
ivmiku
bda83a1bfe feat: 完善相关功能 2024-09-14 13:54:30 +08:00
ivmiku
1dc7681ac6 Merge remote-tracking branch 'origin/main' 2024-09-14 13:53:32 +08:00
ivmiku
262103e034 feat: 完善相关功能 2024-09-13 22:13:21 +08:00
23 changed files with 465 additions and 17 deletions

View File

@@ -0,0 +1,14 @@
package com.ivmiku.tutorial.response;
import lombok.Data;
/**
* 提醒用户实体
* @author Aurora
*/
@Data
public class AtNotifier {
private String fromId;
private String toId;
private String postId;
}

View File

@@ -190,6 +190,14 @@
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -13,6 +13,7 @@ public class SaTokenConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
// 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**");
.addPathPatterns("/**")
.excludePathPatterns("/swagger-ui/**");
}
}

View File

@@ -0,0 +1,34 @@
package com.ivmiku.tutorial.config;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
import org.springframework.web.util.WebAppRootListener;
@Configuration
public class WebSocketConfig implements ServletContextInitializer {
@Bean
public ServerEndpointExporter serverEndpointExporter (){
return new ServerEndpointExporter();
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addListener(WebAppRootListener.class);
servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize","102400000");
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
// 在此处设置bufferSize
container.setMaxTextMessageBufferSize(50*1024*1024);
container.setMaxBinaryMessageBufferSize(50*1024*1024);
container.setMaxSessionIdleTimeout(15 * 60000L);
return container;
}
}

View File

@@ -2,22 +2,23 @@ package com.ivmiku.tutorial.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.pagehelper.PageInfo;
import com.ivmiku.tutorial.entity.Comment;
import com.ivmiku.tutorial.entity.Pages;
import com.ivmiku.tutorial.response.AtNotifier;
import com.ivmiku.tutorial.response.Result;
import com.ivmiku.tutorial.service.CommentService;
import com.ivmiku.tutorial.service.FileService;
import com.ivmiku.tutorial.util.MyRedisUtil;
import io.swagger.v3.oas.annotations.Operation;
import org.apiguardian.api.API;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
/**
* 评论控制器用于处理评论相关的HTTP请求。
@@ -36,6 +37,13 @@ public class CommentController {
@Resource
private FileService fileService;
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private MyRedisUtil myRedisUtil;
/**
* 创建评论接口。
*/
@@ -66,6 +74,14 @@ public class CommentController {
commentService.createComment(comment);
logger.info("评论创建成功评论ID{}", comment.getCommentId());
if (!mentionedUserId.isEmpty()) {
AtNotifier notifier = new AtNotifier();
notifier.setFromId(comment.getUserOpenid());
notifier.setToId(mentionedUserId);
notifier.setPostId(String.valueOf(comment.getCommentId()));
rabbitTemplate.convertAndSend("exchange2", "", JSON.toJSONString(notifier));
myRedisUtil.listAdd("at:" + notifier.getToId(), JSON.toJSONString(notifier));
}
return Result.ok(comment);
}
@@ -106,6 +122,14 @@ public class CommentController {
commentService.createComment(reply);
logger.info("评论回复成功评论ID{}", reply.getCommentId());
if (!mentionedUserId.isEmpty()) {
AtNotifier notifier = new AtNotifier();
notifier.setFromId(reply.getUserOpenid());
notifier.setToId(mentionedUserId);
notifier.setPostId(String.valueOf(reply.getCommentId()));
rabbitTemplate.convertAndSend("exchange2", "", JSON.toJSONString(notifier));
myRedisUtil.listAdd("at:" + notifier.getToId(), JSON.toJSONString(notifier));
}
return Result.ok(reply);
}
@@ -115,7 +139,6 @@ public class CommentController {
comment.setUserOpenid(StpUtil.getLoginIdAsString());
comment.setContent(content);
comment.setMentionedUserId(mentionedUserId);
if ("post".equals(type)) {
comment.setPostId(postId);
} else if ("tutorial".equals(type)) {

View File

@@ -0,0 +1,80 @@
package com.ivmiku.tutorial.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.fastjson.JSON;
import com.ivmiku.tutorial.entity.UserDTO;
import com.ivmiku.tutorial.response.Result;
import com.ivmiku.tutorial.service.impl.SearchService;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Controller;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Controller
@ServerEndpoint(value = "/search/{satoken}")
public class WebSocketServer implements ApplicationContextAware {
public static Map<String, Session> sessionMap = new ConcurrentHashMap<>();
private static ApplicationContext applicationContext;
private SearchService searchService;
@OnOpen
public void onOpen(Session session, EndpointConfig endpointConfig, @PathParam("satoken") String satoken) throws IOException {
String userId = (String) StpUtil.getLoginIdByToken(satoken);
if (userId == null) {
session.getBasicRemote().sendText("Invalid Token");
session.close();
}
sessionMap.put(userId, session);
this.searchService = WebSocketServer.applicationContext.getBean(SearchService.class);
}
@OnClose
public void onClose(CloseReason closeReason, Session session){
sessionMap.remove(session.getId());
}
@OnError
public void onError(Throwable throwable) throws IOException {
throwable.printStackTrace();
}
@OnMessage
public void onMessage(String message, Session session) throws IOException {
char first = message.charAt(0);
String userId = null;
for (Map.Entry<String, Session> entry : sessionMap.entrySet()) {
if (entry.getValue().equals(session)) {
userId = entry.getKey();
}
}
switch (first) {
case '@' -> {
String sub = message.substring(1);
List<UserDTO> result = searchService.getUserList(sub, userId);
session.getBasicRemote().sendText(JSON.toJSONString(Result.ok(result)));
}
case '#' -> {
String sub = message.substring(1);
List<String> list = searchService.getTag(sub);
session.getBasicRemote().sendText(JSON.toJSONString(Result.ok(list)));
}
//预留给社区搜索
default -> {
}
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
WebSocketServer.applicationContext = applicationContext;
}
}

View File

@@ -0,0 +1,13 @@
package com.ivmiku.tutorial.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* @author Aurora
*/
@TableName("subscribe")
@Data
public class Subscribe {
private Long id;
private Long subId;
}

View File

@@ -4,11 +4,15 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@TableName("user")
@Data
@TableName("User")
public class User {
@TableId
private String openid;
private String nickname;
private String avatarUrl;
private boolean isAdmin;
private String phoneNum;
private String birthday;
private String gender;
}

View File

@@ -0,0 +1,12 @@
package com.ivmiku.tutorial.entity;
import lombok.Data;
@Data
public class UserDTO {
private String nickname;
private String avatarUrl;
private String phoneNum;
private String birthday;
private String gender;
}

View File

@@ -0,0 +1,8 @@
package com.ivmiku.tutorial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ivmiku.tutorial.entity.Subscribe;
public interface SubscribeMapper extends BaseMapper<Subscribe> {
}

View File

@@ -0,0 +1,7 @@
package com.ivmiku.tutorial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ivmiku.tutorial.entity.User;
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -1,6 +1,7 @@
package com.ivmiku.tutorial.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -8,10 +9,13 @@ import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ivmiku.tutorial.entity.Comment;
import com.ivmiku.tutorial.mapper.CommentMapper;
import com.ivmiku.tutorial.response.AtNotifier;
import com.ivmiku.tutorial.service.CommentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -30,6 +34,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
@Autowired
protected CommentMapper commentMapper;
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 保存评论。
* 将评论数据保存到数据库。
@@ -186,6 +193,13 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
reply.setMentionedUserId(mentionedUserId);
}
createComment(reply);
if (mentionedUserId != null) {
AtNotifier notifier = new AtNotifier();
notifier.setFromId(reply.getUserOpenid());
notifier.setToId(mentionedUserId);
notifier.setPostId(String.valueOf(reply.getCommentId()));
rabbitTemplate.convertAndSend("exchange2", "", JSON.toJSONString(notifier));
}
logger.info("评论回复成功评论ID{}", reply.getCommentId());
}

View File

@@ -0,0 +1,58 @@
package com.ivmiku.tutorial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ivmiku.tutorial.entity.Subscribe;
import com.ivmiku.tutorial.entity.Tag;
import com.ivmiku.tutorial.entity.User;
import com.ivmiku.tutorial.entity.UserDTO;
import com.ivmiku.tutorial.mapper.PostTagMapper;
import com.ivmiku.tutorial.mapper.SubscribeMapper;
import com.ivmiku.tutorial.mapper.TagMapper;
import com.ivmiku.tutorial.mapper.UserMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Service
public class SearchService {
@Resource
private UserMapper userMapper;
@Resource
private TagMapper tagMapper;
@Resource
private SubscribeMapper subscribeMapper;
public List<UserDTO> getUserList(String text, String userId) {
int len = text.length();
QueryWrapper<Subscribe> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user1_id", userId);
List<Subscribe> list = subscribeMapper.selectList(queryWrapper);
List<UserDTO> result = new ArrayList<>();
for (Subscribe subscribe : list) {
String user2Id = String.valueOf(subscribe.getSubId());
User user = userMapper.selectById(user2Id);
if (user.getNickname().substring(0, len).equals(text)) {
UserDTO userDTO = new UserDTO();
userDTO.setNickname(user.getNickname());
userDTO.setAvatarUrl(user.getAvatarUrl());
result.add(userDTO);
}
}
return result;
}
public List<String> getTag(String text) {
QueryWrapper<Tag> queryWrapper = new QueryWrapper<>();
queryWrapper.likeRight("name", text);
List<Tag> list = tagMapper.selectList(queryWrapper);
List<String> result = new ArrayList<>();
for (Tag tag : list) {
result.add(tag.getName());
}
return result;
}
}

View File

@@ -0,0 +1,94 @@
package com.ivmiku.tutorial.util;
import com.alibaba.fastjson2.JSON;
import com.ivmiku.tutorial.response.AtNotifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author Aurora
*/
@Component
public class MyRedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void listAdd(String key, Object value) {
redisTemplate.opsForList().leftPush(key, value);
}
public List<AtNotifier> listGet(String key, int s, int e) {
List<Object> list = redisTemplate.opsForList().range(key, s, e);
List<AtNotifier> result = new ArrayList<>();
if (list != null) {
for (Object json : list) {
result.add(JSON.parseObject(JSON.toJSONString(json), AtNotifier.class));
}
}
return result;
}
public void listClear(String key) {
redisTemplate.opsForList().trim(key, 1, 0);
}
public Long getListSize(String key) {
return redisTemplate.opsForList().size(key);
}
public Set<String> getKey() {
return redisTemplate.keys("history:*");
}
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
public void setExpireTime(String key) {
if (redisTemplate.opsForValue().getOperations().getExpire(key) > 0) {
redisTemplate.expire(key, 3, TimeUnit.DAYS);
}
}
public void refreshExpire(String key) {
redisTemplate.persist(key);
redisTemplate.expire(key, 3, TimeUnit.DAYS);
}
public boolean ifExist(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public List<String> getStringList(String key, int s, int e) {
List<Object> list = redisTemplate.opsForList().range(key, s, e);
List<String> result = new ArrayList<>();
assert list != null;
for (Object object : list) {
result.add((String) object);
}
return result;
}
public void insertKey(String openid, String sessionKey) {
redisTemplate.opsForValue().set("sessionkey:" + openid, sessionKey);
}
public String getKey(String userId) {
return (String) redisTemplate.opsForValue().get("sessionkey:" + userId);
}
public void deleteFromList(AtNotifier message) {
String id = message.getToId();
redisTemplate.opsForList().remove("unread:" + id, 0, message);
}
}

View File

@@ -13,15 +13,12 @@ server.port=8073
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.password=Shuodedaoli114514
spring.datasource.url=jdbc:mysql://mysql:3306/tutorial?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.cloud.nacos.discovery.server-addr=nacos:8848
spring.cloud.nacos.discovery.enabled=true
management.zipkin.tracing.endpoint=http://127.0.0.1:9411/api/v2/spans
management.tracing.sampling.probability=1.0
dubbo.application.qos-enable=false
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
@@ -36,5 +33,5 @@ spring.data.redis.lettuce.pool.max-active=32
spring.data.redis.lettuce.pool.max-idle=16
spring.data.redis.lettuce.pool.min-idle=8
spring.rabbitmq.host=rabbitmq

View File

@@ -47,7 +47,8 @@ public class SaTokenConfigure {
"/user/register",
"/swagger-resources/**",
"/v3/**",
"/swagger-ui/**")
"/swagger-ui/**",
"/ws/**")
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由,并排除指定路由

View File

@@ -26,3 +26,7 @@ spring.cloud.gateway.routes[2].filters[0]=StripPrefix=1
spring.cloud.gateway.routes[3].id=websocket
spring.cloud.gateway.routes[3].uri=lb:ws://user
spring.cloud.gateway.routes[3].predicates[0]=Path=/chat/**
spring.cloud.gateway.routes[4].id=search
spring.cloud.gateway.routes[4].uri=lb:ws://community
spring.cloud.gateway.routes[4].predicates[0]=Path=/search/**

View File

@@ -24,4 +24,8 @@ spring.cloud.gateway.routes[2].filters[0]=StripPrefix=1
spring.cloud.gateway.routes[3].id=websocket
spring.cloud.gateway.routes[3].uri=lb:ws://user
spring.cloud.gateway.routes[3].predicates[0]=Path=/chat/**
spring.cloud.gateway.routes[3].predicates[0]=Path=/chat/**
spring.cloud.gateway.routes[4].id=search
spring.cloud.gateway.routes[4].uri=lb:ws://community
spring.cloud.gateway.routes[4].predicates[0]=Path=/search/**

View File

@@ -10,5 +10,5 @@ dubbo.application.qos-enable=false
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=12345abcde
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tutorial?useUnicode=true&characterEncoding=utf8&useSSL=false&ServerTimezone=Asia/Shanghai
spring.datasource.password=Shuodedaoli114514
spring.datasource.url=jdbc:mysql://154.40.44.199:4514/tutorial?useUnicode=true&characterEncoding=utf8&useSSL=false&ServerTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

View File

@@ -14,14 +14,29 @@ public class RabbitMqConfig {
return new Queue("queue1",true);
}
@Bean
public Queue queue2() {
return new Queue("queue2",true);
}
@Bean
public FanoutExchange exchange1() {
return new FanoutExchange("exchange1",true, false);
}
@Bean
public FanoutExchange exchange2() {
return new FanoutExchange("exchange2",true, false);
}
@Bean
public Binding binding1() {
return BindingBuilder.bind(queue1()).to(exchange1());
}
@Bean
public Binding binding2() {
return BindingBuilder.bind(queue2()).to(exchange2());
}
}

View File

@@ -2,7 +2,9 @@ package com.ivmiku.tutorial.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ivmiku.tutorial.entity.Message;
import com.ivmiku.tutorial.response.AtNotifier;
import com.ivmiku.tutorial.service.MessageService;
import com.ivmiku.tutorial.service.RelationService;
import com.ivmiku.tutorial.utils.DateUtil;
@@ -84,7 +86,15 @@ public class WebSocketServer implements ApplicationContextAware {
sessionMap.put(userId, session);
List<Message> unreadList = messageService.getUnreadMsg(userId);
for(Message msg : unreadList) {
session.getBasicRemote().sendText(JSON.toJSONString(msg));
JSONObject object = JSONObject.from(msg);
object.put("type", 1);
session.getBasicRemote().sendText(object.toJSONString());
}
List<AtNotifier> atList = messageService.getAtList(userId);
for (AtNotifier notifier : atList) {
JSONObject object = JSONObject.from(notifier);
object.put("type", 2);
session.getBasicRemote().sendText(object.toJSONString());
}
}
@@ -132,8 +142,25 @@ public class WebSocketServer implements ApplicationContextAware {
public void sendMsg(String message) throws IOException {
Message msg = JSON.parseObject(message, Message.class);
if (sessionMap.containsKey(msg.getToId())) {
sessionMap.get(msg.getToId()).getBasicRemote().sendText(JSON.toJSONString(msg));
JSONObject jsonObject = JSONObject.from(msg);
jsonObject.put("type", 1);
sessionMap.get(msg.getToId()).getBasicRemote().sendText(jsonObject.toJSONString());
messageService.deleteMessage(msg);
}
}
@RabbitHandler
@RabbitListener(bindings = @QueueBinding(
value = @Queue(),
exchange = @Exchange(value = "exchange2",type = ExchangeTypes.FANOUT)
))
public void sendAt(String message) throws IOException {
AtNotifier notifier = JSON.parseObject(message, AtNotifier.class);
if (sessionMap.containsKey(notifier.getToId())) {
JSONObject jsonObject = JSONObject.from(notifier);
jsonObject.put("type", 2);
sessionMap.get(notifier.getToId()).getBasicRemote().sendText(jsonObject.toJSONString());
messageService.deleteNotifier(notifier);
}
}
}

View File

@@ -6,6 +6,7 @@ import com.ivmiku.tutorial.entity.ChatId;
import com.ivmiku.tutorial.entity.Message;
import com.ivmiku.tutorial.mapper.ChatIdMapper;
import com.ivmiku.tutorial.mapper.MessageMapper;
import com.ivmiku.tutorial.response.AtNotifier;
import com.ivmiku.tutorial.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
@@ -245,4 +246,12 @@ public class MessageService {
public void deleteMessage(Message message) {
redisUtil.deleteFromList(message);
}
public void deleteNotifier(AtNotifier notifier) {
redisUtil.deleteFromList(notifier);
}
public List<AtNotifier> getAtList(String userId) {
return redisUtil.listGetN("at:" + userId, 0, -1);
}
}

View File

@@ -2,6 +2,7 @@ package com.ivmiku.tutorial.utils;
import com.alibaba.fastjson2.JSON;
import com.ivmiku.tutorial.entity.Message;
import com.ivmiku.tutorial.response.AtNotifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@@ -127,4 +128,24 @@ public class RedisUtil {
String id = message.getToId();
redisTemplate.opsForList().remove("unread:" + id, 0, message);
}
public void listAddN(String key, Object value) {
redisTemplate.opsForList().leftPush(key, value);
}
public List<AtNotifier> listGetN(String key, int s, int e) {
List<Object> list = redisTemplate.opsForList().range(key, s, e);
List<AtNotifier> result = new ArrayList<>();
if (list != null) {
for (Object json : list) {
result.add(JSON.parseObject(JSON.toJSONString(json), AtNotifier.class));
}
}
return result;
}
public void deleteFromList(AtNotifier message) {
String id = message.getToId();
redisTemplate.opsForList().remove("unread:" + id, 0, message);
}
}