package cn.iocoder.yudao.module.im.util; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum; import cn.iocoder.yudao.module.im.dal.dataobject.message.content.QuoteMessage; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_CONTENT_INVALID; /** * IM 消息内容相关工具类 */ public class ImMessageUtils { /** * 文本消息最大长度 */ private static final int TEXT_MAX_LENGTH = 4096; /** * URL 最大长度 */ private static final int URL_MAX_LENGTH = 2048; /** * 合并消息最大条数 */ private static final int MERGE_MESSAGE_MAX_COUNT = 100; /** * 校验用户发送的消息内容 * * @param type 消息类型 * @param content 消息内容 */ public static void validateUserMessageContent(Integer type, String content) { ImContentTypeEnum messageType; try { messageType = ImContentTypeEnum.validate(type); } catch (IllegalArgumentException e) { throw exception(MESSAGE_CONTENT_INVALID); } if (!messageType.isNormal()) { throw exception(MESSAGE_CONTENT_INVALID); } Map map = JsonUtils.parseMap(content); if (map == null) { throw exception(MESSAGE_CONTENT_INVALID); } if (ImContentTypeEnum.TEXT == messageType) { validateTextContent(map); } else if (ImContentTypeEnum.IMAGE == messageType || ImContentTypeEnum.FACE == messageType) { validateUrl(getString(map, "url")); } else if (ImContentTypeEnum.VOICE == messageType) { validateUrl(getString(map, "url")); validatePositiveNumber(map.get("duration")); } else if (ImContentTypeEnum.VIDEO == messageType) { validateUrl(getString(map, "url")); } else if (ImContentTypeEnum.FILE == messageType) { validateUrl(getString(map, "url")); validateNotBlank(getString(map, "name")); } else if (ImContentTypeEnum.CARD == messageType) { validateCardContent(map); } else if (ImContentTypeEnum.MERGE == messageType) { validateMergeContent(map); } else if (ImContentTypeEnum.MATERIAL == messageType) { validateMaterialContent(map); } } private static void validateTextContent(Map map) { String text = getString(map, "content"); validateNotBlank(text); if (text.length() > TEXT_MAX_LENGTH) { throw exception(MESSAGE_CONTENT_INVALID); } } private static void validateCardContent(Map map) { validateNumber(map.get("targetType")); validateNumber(map.get("targetId")); validateNotBlank(getString(map, "name")); } @SuppressWarnings("PatternVariableCanBeUsed") private static void validateMergeContent(Map map) { validateNotBlank(getString(map, "title")); Object messages = map.get("messages"); if (!(messages instanceof Collection)) { throw exception(MESSAGE_CONTENT_INVALID); } Collection messageList = (Collection) messages; if (CollUtil.isEmpty(messageList) || messageList.size() > MERGE_MESSAGE_MAX_COUNT) { throw exception(MESSAGE_CONTENT_INVALID); } } private static void validateMaterialContent(Map map) { if (map.get("materialId") == null && StrUtil.isBlank(getString(map, "title"))) { throw exception(MESSAGE_CONTENT_INVALID); } validateUrlIfPresent(getString(map, "url")); validateUrlIfPresent(getString(map, "coverUrl")); } private static String getString(Map map, String field) { return Convert.toStr(map.get(field), null); } private static void validateNotBlank(String value) { if (StrUtil.isBlank(value)) { throw exception(MESSAGE_CONTENT_INVALID); } } private static void validateNumber(Object value) { if (Convert.toLong(value, null) == null) { throw exception(MESSAGE_CONTENT_INVALID); } } private static void validatePositiveNumber(Object value) { Integer number = Convert.toInt(value, null); if (number == null || number < 0) { throw exception(MESSAGE_CONTENT_INVALID); } } private static void validateUrlIfPresent(String url) { if (StrUtil.isBlank(url)) { return; } validateUrl(url); } private static void validateUrl(String url) { validateNotBlank(url); if (url.length() > URL_MAX_LENGTH) { throw exception(MESSAGE_CONTENT_INVALID); } String lowerUrl = StrUtil.trim(url).toLowerCase(); if (StrUtil.startWithAny(lowerUrl, "javascript:", "data:", "vbscript:", "file:")) { throw exception(MESSAGE_CONTENT_INVALID); } } /** * 从 content 解析客户端写入的 quote.messageId * * @param content 原 content(JSON) * @return messageId,不存在 / 非法返回 null */ public static Long parseQuoteMessageId(String content) { Map map = JsonUtils.parseMap(content); if (map == null) { return null; } Object quoteRaw = map.get(QuoteMessage.FIELD_NAME); if (!(quoteRaw instanceof Map)) { return null; } return Convert.toLong(((Map) quoteRaw).get(QuoteMessage.FIELD_MESSAGE_ID)); } /** * 把 quote 写入 content 的 quote 字段 * * @param content 原 content(JSON) * @param quote QuoteMessage 对象 * @return 注入 quote 后的 content(JSON) */ public static String appendQuote(String content, QuoteMessage quote) { Map map = JsonUtils.parseMap(content); if (map == null) { map = new LinkedHashMap<>(); } map.put(QuoteMessage.FIELD_NAME, quote); return JsonUtils.toJsonString(map); } /** * 移除 content 里的 quote 字段 * * @param content 原 content(JSON) * @return remove quote 后的 content(JSON) */ public static String removeQuote(String content) { if (StrUtil.isBlank(content) || !StrUtil.contains(content, "\"" + QuoteMessage.FIELD_NAME + "\"")) { return content; } Map map = JsonUtils.parseMap(content); if (map == null) { return content; } map.remove(QuoteMessage.FIELD_NAME); return JsonUtils.toJsonString(map); } /** * 构建 QuoteMessage 对象 * * @param messageId 被引用消息编号 * @param senderId 被引用消息发送人编号 * @param type 被引用消息类型 * @param originalContent 被引用消息原 content(JSON),服务端会 removeQuote 防止嵌套 * @return QuoteMessage 对象 */ public static QuoteMessage buildQuote(Long messageId, Long senderId, Integer type, String originalContent) { return new QuoteMessage().setMessageId(messageId).setSenderId(senderId).setType(type) .setContent(removeQuote(originalContent)); } }