2026-04-21 22:07:29 +08:00
|
|
|
package com.xuqm.im.service;
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import com.xuqm.common.exception.BusinessException;
|
|
|
|
|
import com.xuqm.im.entity.ImMessageEntity;
|
|
|
|
|
import com.xuqm.im.entity.WebhookConfigEntity;
|
|
|
|
|
import com.xuqm.im.model.SendMessageRequest;
|
|
|
|
|
import com.xuqm.im.repository.ImMessageRepository;
|
|
|
|
|
import com.xuqm.im.repository.WebhookConfigRepository;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
import org.springframework.data.domain.Page;
|
|
|
|
|
import org.springframework.data.domain.PageRequest;
|
|
|
|
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
|
|
|
|
import org.springframework.scheduling.annotation.Async;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
import java.net.URI;
|
|
|
|
|
import java.net.http.HttpClient;
|
|
|
|
|
import java.net.http.HttpRequest;
|
|
|
|
|
import java.net.http.HttpResponse;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
@Service
|
|
|
|
|
public class MessageService {
|
|
|
|
|
|
|
|
|
|
private final ImMessageRepository messageRepository;
|
|
|
|
|
private final WebhookConfigRepository webhookRepository;
|
|
|
|
|
private final KeywordFilterService keywordFilterService;
|
|
|
|
|
private final SimpMessagingTemplate messagingTemplate;
|
|
|
|
|
private final ObjectMapper objectMapper;
|
|
|
|
|
|
|
|
|
|
@Value("${im.webhook-timeout-ms:3000}")
|
|
|
|
|
private int webhookTimeoutMs;
|
|
|
|
|
|
|
|
|
|
public MessageService(ImMessageRepository messageRepository,
|
|
|
|
|
WebhookConfigRepository webhookRepository,
|
|
|
|
|
KeywordFilterService keywordFilterService,
|
|
|
|
|
SimpMessagingTemplate messagingTemplate,
|
|
|
|
|
ObjectMapper objectMapper) {
|
|
|
|
|
this.messageRepository = messageRepository;
|
|
|
|
|
this.webhookRepository = webhookRepository;
|
|
|
|
|
this.keywordFilterService = keywordFilterService;
|
|
|
|
|
this.messagingTemplate = messagingTemplate;
|
|
|
|
|
this.objectMapper = objectMapper;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessageEntity send(String appId, String fromUserId, SendMessageRequest req) {
|
|
|
|
|
String content = req.content();
|
|
|
|
|
if (req.msgType() == ImMessageEntity.MsgType.TEXT) {
|
|
|
|
|
content = keywordFilterService.filter(appId, content);
|
|
|
|
|
if (content == null) {
|
|
|
|
|
throw new BusinessException("消息包含违禁内容");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImMessageEntity message = new ImMessageEntity();
|
|
|
|
|
message.setId(UUID.randomUUID().toString());
|
|
|
|
|
message.setAppId(appId);
|
|
|
|
|
message.setFromUserId(fromUserId);
|
|
|
|
|
message.setToId(req.toId());
|
|
|
|
|
message.setChatType(req.chatType());
|
|
|
|
|
message.setMsgType(req.msgType());
|
|
|
|
|
message.setContent(content);
|
|
|
|
|
message.setStatus(ImMessageEntity.MsgStatus.SENT);
|
|
|
|
|
message.setMentionedUserIds(req.mentionedUserIds());
|
|
|
|
|
message.setCreatedAt(LocalDateTime.now());
|
|
|
|
|
messageRepository.save(message);
|
|
|
|
|
|
|
|
|
|
String destination = req.chatType() == ImMessageEntity.ChatType.SINGLE
|
|
|
|
|
? "/user/" + req.toId() + "/queue/messages"
|
|
|
|
|
: "/topic/group/" + req.toId();
|
|
|
|
|
messagingTemplate.convertAndSend(destination, message);
|
2026-04-24 10:42:11 +08:00
|
|
|
if (req.chatType() == ImMessageEntity.ChatType.SINGLE && !fromUserId.equals(req.toId())) {
|
|
|
|
|
messagingTemplate.convertAndSend("/user/" + fromUserId + "/queue/messages", message);
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
dispatchWebhooks(appId, message);
|
|
|
|
|
|
|
|
|
|
return message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessageEntity revoke(String appId, String messageId, String requestUserId) {
|
|
|
|
|
ImMessageEntity message = messageRepository.findById(messageId)
|
|
|
|
|
.orElseThrow(() -> new BusinessException(404, "消息不存在"));
|
|
|
|
|
if (!message.getAppId().equals(appId)) {
|
|
|
|
|
throw new BusinessException(403, "无权操作");
|
|
|
|
|
}
|
|
|
|
|
if (!message.getFromUserId().equals(requestUserId)) {
|
|
|
|
|
throw new BusinessException(403, "只能撤回自己发送的消息");
|
|
|
|
|
}
|
|
|
|
|
message.setStatus(ImMessageEntity.MsgStatus.REVOKED);
|
|
|
|
|
message.setMsgType(ImMessageEntity.MsgType.REVOKED);
|
2026-04-24 10:42:11 +08:00
|
|
|
ImMessageEntity saved = messageRepository.save(message);
|
|
|
|
|
if (saved.getChatType() == ImMessageEntity.ChatType.SINGLE) {
|
|
|
|
|
messagingTemplate.convertAndSend("/user/" + saved.getToId() + "/queue/messages", saved);
|
|
|
|
|
if (!saved.getFromUserId().equals(saved.getToId())) {
|
|
|
|
|
messagingTemplate.convertAndSend("/user/" + saved.getFromUserId() + "/queue/messages", saved);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
messagingTemplate.convertAndSend("/topic/group/" + saved.getToId(), saved);
|
|
|
|
|
}
|
|
|
|
|
return saved;
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 10:42:11 +08:00
|
|
|
public Page<ImMessageEntity> history(String appId, String userId, String toId, int page, int size) {
|
|
|
|
|
return messageRepository.findSingleConversation(
|
|
|
|
|
appId, userId, toId, PageRequest.of(page, size));
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Async
|
|
|
|
|
protected void dispatchWebhooks(String appId, ImMessageEntity message) {
|
|
|
|
|
List<WebhookConfigEntity> webhooks = webhookRepository.findByAppIdAndEnabledTrue(appId);
|
|
|
|
|
if (webhooks.isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
String body = objectMapper.writeValueAsString(message);
|
|
|
|
|
HttpClient client = HttpClient.newHttpClient();
|
|
|
|
|
for (WebhookConfigEntity webhook : webhooks) {
|
|
|
|
|
try {
|
|
|
|
|
HttpRequest request = HttpRequest.newBuilder()
|
|
|
|
|
.uri(URI.create(webhook.getUrl()))
|
|
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.POST(HttpRequest.BodyPublishers.ofString(body))
|
|
|
|
|
.build();
|
|
|
|
|
client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|