2026-04-28 16:55:12 +08:00
|
|
|
package com.xuqm.im.sdk;
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
|
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
2026-04-28 20:11:38 +08:00
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
2026-04-28 16:55:12 +08:00
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
|
|
|
|
import com.xuqm.common.model.ApiResponse;
|
|
|
|
|
import com.xuqm.common.security.AppRequestSignatureUtil;
|
|
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.net.URI;
|
|
|
|
|
import java.net.URLEncoder;
|
|
|
|
|
import java.net.http.HttpClient;
|
|
|
|
|
import java.net.http.HttpRequest;
|
|
|
|
|
import java.net.http.HttpResponse;
|
2026-04-28 17:43:46 +08:00
|
|
|
import java.nio.charset.Charset;
|
2026-04-28 16:55:12 +08:00
|
|
|
import java.nio.charset.StandardCharsets;
|
2026-04-28 17:43:46 +08:00
|
|
|
import java.nio.file.Files;
|
|
|
|
|
import java.nio.file.Path;
|
2026-04-28 20:11:38 +08:00
|
|
|
import java.security.MessageDigest;
|
2026-04-28 16:55:12 +08:00
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.time.ZoneOffset;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.LinkedHashMap;
|
2026-04-28 20:11:38 +08:00
|
|
|
import java.util.HexFormat;
|
2026-04-28 16:55:12 +08:00
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Objects;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
import java.util.function.Supplier;
|
2026-04-28 20:11:38 +08:00
|
|
|
import javax.crypto.Mac;
|
|
|
|
|
import javax.crypto.spec.SecretKeySpec;
|
2026-04-28 16:55:12 +08:00
|
|
|
|
|
|
|
|
public final class XuqmImServerSdk {
|
|
|
|
|
|
|
|
|
|
private final HttpClient httpClient;
|
|
|
|
|
private final ObjectMapper objectMapper;
|
|
|
|
|
private final String baseUrl;
|
2026-04-28 17:43:46 +08:00
|
|
|
private final String pushBaseUrl;
|
|
|
|
|
private final String updateBaseUrl;
|
2026-04-28 16:55:12 +08:00
|
|
|
private final String appId;
|
|
|
|
|
private final String appSecret;
|
|
|
|
|
private final Supplier<String> bearerTokenSupplier;
|
|
|
|
|
|
|
|
|
|
private XuqmImServerSdk(Builder builder) {
|
|
|
|
|
this.httpClient = HttpClient.newHttpClient();
|
|
|
|
|
this.objectMapper = new ObjectMapper()
|
|
|
|
|
.registerModule(new JavaTimeModule())
|
|
|
|
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
|
|
|
this.baseUrl = trimTrailingSlash(Objects.requireNonNull(builder.baseUrl, "baseUrl"));
|
2026-04-28 17:43:46 +08:00
|
|
|
this.pushBaseUrl = trimTrailingSlash(builder.pushBaseUrl == null ? builder.baseUrl : builder.pushBaseUrl);
|
|
|
|
|
this.updateBaseUrl = trimTrailingSlash(builder.updateBaseUrl == null ? builder.baseUrl : builder.updateBaseUrl);
|
2026-04-28 16:55:12 +08:00
|
|
|
this.appId = Objects.requireNonNull(builder.appId, "appId");
|
|
|
|
|
this.appSecret = Objects.requireNonNull(builder.appSecret, "appSecret");
|
|
|
|
|
this.bearerTokenSupplier = builder.bearerTokenSupplier;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static Builder builder() {
|
|
|
|
|
return new Builder();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LoginResponse login(String userId, String nickname, String avatar) {
|
|
|
|
|
long timestamp = System.currentTimeMillis();
|
|
|
|
|
String nonce = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
String payload = AppRequestSignatureUtil.payload(appId, userId, nickname, avatar, timestamp, nonce);
|
|
|
|
|
String signature = AppRequestSignatureUtil.sign(appSecret, payload);
|
|
|
|
|
URI uri = buildUri(
|
|
|
|
|
"/api/im/auth/login",
|
|
|
|
|
loginQuery(userId, nickname, avatar, timestamp, nonce)
|
|
|
|
|
);
|
|
|
|
|
ApiResponse<LoginResponse> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
uri,
|
|
|
|
|
null,
|
|
|
|
|
Map.of(
|
|
|
|
|
"X-App-Id", appId,
|
|
|
|
|
"X-App-Timestamp", String.valueOf(timestamp),
|
|
|
|
|
"X-App-Nonce", nonce,
|
|
|
|
|
"X-App-Signature", signature
|
|
|
|
|
),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage sendMessage(SendMessageRequest request) {
|
|
|
|
|
ApiResponse<ImMessage> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/messages/send", Map.of("appId", appId)),
|
|
|
|
|
request,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage revokeMessage(String messageId) {
|
|
|
|
|
ApiResponse<ImMessage> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/messages/" + encode(messageId) + "/revoke", Map.of("appId", appId)),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:38 +08:00
|
|
|
public ImMessage editMessage(String messageId, String content) {
|
|
|
|
|
ApiResponse<ImMessage> response = request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/messages/" + encode(messageId), Map.of("appId", appId)),
|
|
|
|
|
Map.of("content", content),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public List<ConversationView> listConversations(int size) {
|
|
|
|
|
ApiResponse<List<ConversationView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/conversations", Map.of("appId", appId, "page", "0", "size", String.valueOf(size))),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PageResult<ImMessage> fetchHistory(String toId, HistoryQuery query) {
|
|
|
|
|
HistoryQuery effective = query == null ? new HistoryQuery() : query;
|
|
|
|
|
ApiResponse<PageResult<ImMessage>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri(
|
|
|
|
|
"/api/im/messages/history/" + encode(toId),
|
|
|
|
|
queryParams(
|
|
|
|
|
effective.msgType(),
|
|
|
|
|
effective.keyword(),
|
|
|
|
|
effective.startTime(),
|
|
|
|
|
effective.endTime(),
|
|
|
|
|
effective.page() == null ? 0 : effective.page(),
|
|
|
|
|
effective.size() == null ? 20 : effective.size()
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PageResult<ImMessage> fetchGroupHistory(String groupId, HistoryQuery query) {
|
|
|
|
|
HistoryQuery effective = query == null ? new HistoryQuery() : query;
|
|
|
|
|
ApiResponse<PageResult<ImMessage>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri(
|
|
|
|
|
"/api/im/messages/group-history/" + encode(groupId),
|
|
|
|
|
queryParams(
|
|
|
|
|
effective.msgType(),
|
|
|
|
|
effective.keyword(),
|
|
|
|
|
effective.startTime(),
|
|
|
|
|
effective.endTime(),
|
|
|
|
|
effective.page() == null ? 0 : effective.page(),
|
|
|
|
|
effective.size() == null ? 20 : effective.size()
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<ConversationView> listConversations() {
|
|
|
|
|
return listConversations(20);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public AccountView getProfile(String userId) {
|
|
|
|
|
ApiResponse<AccountView> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/accounts/" + encode(userId), appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public AccountView updateProfile(String userId, String nickname, String avatar, String gender) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
if (nickname != null) {
|
|
|
|
|
query.put("nickname", nickname);
|
|
|
|
|
}
|
|
|
|
|
if (avatar != null) {
|
|
|
|
|
query.put("avatar", avatar);
|
|
|
|
|
}
|
|
|
|
|
if (gender != null) {
|
|
|
|
|
query.put("gender", gender);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<AccountView> response = request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/accounts/" + encode(userId), query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PageResult<AccountView> listUsers(int page, int size) {
|
|
|
|
|
ApiResponse<PageResult<AccountView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/users", queryWithPage(page, size)),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public AccountView registerUser(String userId, String nickname, String avatar) {
|
|
|
|
|
Map<String, String> body = new LinkedHashMap<>();
|
|
|
|
|
body.put("userId", userId);
|
|
|
|
|
if (nickname != null) {
|
|
|
|
|
body.put("nickname", nickname);
|
|
|
|
|
}
|
|
|
|
|
if (avatar != null) {
|
|
|
|
|
body.put("avatar", avatar);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<AccountView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/admin/users", appQuery()),
|
|
|
|
|
body,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public AccountView updateUserStatus(String userId, String status) {
|
|
|
|
|
ApiResponse<AccountView> response = request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/admin/users/" + encode(userId) + "/status", appQuery()),
|
|
|
|
|
Map.of("status", status),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<AccountView> searchUsers(String keyword, int size) {
|
|
|
|
|
ApiResponse<List<AccountView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/users/search", queryWithSize("keyword", keyword, size)),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public StatsView stats() {
|
|
|
|
|
ApiResponse<StatsView> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/stats", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:29:17 +08:00
|
|
|
public List<GroupView> searchGroups(String keyword, int size) {
|
|
|
|
|
ApiResponse<List<GroupView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/groups/search", queryWithSize("keyword", keyword, size)),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:38 +08:00
|
|
|
public boolean verifyCallbackSignature(String timestamp, String nonce, String body, String signature) {
|
|
|
|
|
long ts;
|
|
|
|
|
try {
|
|
|
|
|
ts = Long.parseLong(timestamp);
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
long now = System.currentTimeMillis();
|
|
|
|
|
if (Math.abs(now - ts) > 5 * 60 * 1000L) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
String payload = appId + "\n" + timestamp + "\n" + normalize(nonce) + "\n" + sha256Hex(body);
|
|
|
|
|
String expected = hmacSha256Hex(appSecret, payload);
|
|
|
|
|
return MessageDigest.isEqual(
|
|
|
|
|
expected.getBytes(StandardCharsets.UTF_8),
|
|
|
|
|
normalize(signature).getBytes(StandardCharsets.UTF_8)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public WebhookCallbackEnvelope parseCallbackEnvelope(String body) {
|
|
|
|
|
try {
|
|
|
|
|
JsonNode root = objectMapper.readTree(body);
|
|
|
|
|
return new WebhookCallbackEnvelope(
|
|
|
|
|
text(root, "callbackId"),
|
|
|
|
|
text(root, "callbackType"),
|
|
|
|
|
text(root, "callbackEvent"),
|
|
|
|
|
longValue(root, "requestTime"),
|
|
|
|
|
root.get("payload"),
|
|
|
|
|
text(root, "signature"),
|
|
|
|
|
text(root, "appId")
|
|
|
|
|
);
|
|
|
|
|
} catch (JsonProcessingException e) {
|
|
|
|
|
throw new ImSdkException("Invalid callback body", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public ImMessage parseMessageCallbackPayload(String body) {
|
2026-04-28 22:32:21 +08:00
|
|
|
return parseMessageCallbackPayload(parseCallbackEnvelope(body));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage parseMessageCallbackPayload(WebhookCallbackEnvelope envelope) {
|
2026-04-28 21:05:06 +08:00
|
|
|
if (!envelope.isMessageEvent()
|
2026-04-28 22:32:21 +08:00
|
|
|
|| !(envelope.isMessageSentEvent()
|
|
|
|
|
|| envelope.isMessageEditedEvent()
|
|
|
|
|
|| envelope.isMessageRevokedEvent())) {
|
2026-04-28 21:05:06 +08:00
|
|
|
throw new ImSdkException("Callback event is not a message event");
|
|
|
|
|
}
|
|
|
|
|
return readPayload(envelope, ImMessage.class);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 22:32:21 +08:00
|
|
|
public ImMessage parseMessageSentCallbackPayload(String body) {
|
|
|
|
|
return parseMessageSentCallbackPayload(parseCallbackEnvelope(body));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage parseMessageSentCallbackPayload(WebhookCallbackEnvelope envelope) {
|
|
|
|
|
requireMessageEvent(envelope, "message.sent");
|
|
|
|
|
return readPayload(envelope, ImMessage.class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage parseMessageEditedCallbackPayload(String body) {
|
|
|
|
|
return parseMessageEditedCallbackPayload(parseCallbackEnvelope(body));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage parseMessageEditedCallbackPayload(WebhookCallbackEnvelope envelope) {
|
|
|
|
|
requireMessageEvent(envelope, "message.edited");
|
|
|
|
|
return readPayload(envelope, ImMessage.class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage parseMessageRevokedCallbackPayload(String body) {
|
|
|
|
|
return parseMessageRevokedCallbackPayload(parseCallbackEnvelope(body));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage parseMessageRevokedCallbackPayload(WebhookCallbackEnvelope envelope) {
|
|
|
|
|
requireMessageEvent(envelope, "message.revoked");
|
|
|
|
|
return readPayload(envelope, ImMessage.class);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public MessageReadCallbackPayload parseMessageReadCallbackPayload(String body) {
|
2026-04-28 22:32:21 +08:00
|
|
|
return parseMessageReadCallbackPayload(parseCallbackEnvelope(body));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public MessageReadCallbackPayload parseMessageReadCallbackPayload(WebhookCallbackEnvelope envelope) {
|
2026-04-28 21:05:06 +08:00
|
|
|
if (!envelope.isReadReceiptEvent()) {
|
|
|
|
|
throw new ImSdkException("Callback event is not a message.read event");
|
|
|
|
|
}
|
|
|
|
|
return readPayload(envelope, MessageReadCallbackPayload.class);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:29:17 +08:00
|
|
|
public PageResult<ImMessage> searchMessages(
|
|
|
|
|
String keyword,
|
|
|
|
|
String chatType,
|
|
|
|
|
String msgType,
|
|
|
|
|
LocalDateTime startTime,
|
|
|
|
|
LocalDateTime endTime,
|
|
|
|
|
int page,
|
|
|
|
|
int size) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
if (keyword != null) {
|
|
|
|
|
query.put("keyword", keyword);
|
|
|
|
|
}
|
|
|
|
|
if (chatType != null) {
|
|
|
|
|
query.put("chatType", chatType);
|
|
|
|
|
}
|
|
|
|
|
if (msgType != null) {
|
|
|
|
|
query.put("msgType", msgType);
|
|
|
|
|
}
|
|
|
|
|
if (startTime != null) {
|
|
|
|
|
query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
|
|
|
|
|
}
|
|
|
|
|
if (endTime != null) {
|
|
|
|
|
query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
|
|
|
|
|
}
|
|
|
|
|
query.put("page", String.valueOf(page));
|
|
|
|
|
query.put("size", String.valueOf(size));
|
|
|
|
|
ApiResponse<PageResult<ImMessage>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/messages/search", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<WebhookConfigView> listWebhookConfigs() {
|
|
|
|
|
ApiResponse<List<WebhookConfigView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/webhooks", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public WebhookConfigView createWebhookConfig(String url, String secret, Boolean enabled) {
|
|
|
|
|
ApiResponse<WebhookConfigView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/admin/webhooks", appQuery()),
|
|
|
|
|
new WebhookConfigRequest(url, secret, enabled),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public WebhookConfigView updateWebhookConfig(String id, String url, String secret, Boolean enabled) {
|
|
|
|
|
ApiResponse<WebhookConfigView> response = request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/admin/webhooks/" + encode(id), appQuery()),
|
|
|
|
|
new WebhookConfigRequest(url, secret, enabled),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void deleteWebhookConfig(String id) {
|
|
|
|
|
request(
|
|
|
|
|
"DELETE",
|
|
|
|
|
buildUri("/api/im/admin/webhooks/" + encode(id), appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<KeywordFilterView> listKeywordFilters() {
|
|
|
|
|
ApiResponse<List<KeywordFilterView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/keyword-filters", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public KeywordFilterView createKeywordFilter(String pattern, String replacement, String action, Boolean enabled) {
|
|
|
|
|
ApiResponse<KeywordFilterView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/admin/keyword-filters", appQuery()),
|
|
|
|
|
new KeywordFilterRequest(pattern, replacement, action, enabled),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public KeywordFilterView updateKeywordFilter(String id, String pattern, String replacement, String action, Boolean enabled) {
|
|
|
|
|
ApiResponse<KeywordFilterView> response = request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/admin/keyword-filters/" + encode(id), appQuery()),
|
|
|
|
|
new KeywordFilterRequest(pattern, replacement, action, enabled),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void deleteKeywordFilter(String id) {
|
|
|
|
|
request(
|
|
|
|
|
"DELETE",
|
|
|
|
|
buildUri("/api/im/admin/keyword-filters/" + encode(id), appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GlobalMuteView getGlobalMute() {
|
|
|
|
|
ApiResponse<GlobalMuteView> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/global-mute", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GlobalMuteView setGlobalMute(boolean enabled) {
|
|
|
|
|
ApiResponse<GlobalMuteView> response = request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/admin/global-mute", Map.of("appId", appId, "enabled", String.valueOf(enabled))),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
public void registerPushToken(String userId, String vendor, String token) {
|
|
|
|
|
request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(pushBaseUrl, "/api/push/register", Map.of(
|
|
|
|
|
"appId", appId,
|
|
|
|
|
"userId", userId,
|
|
|
|
|
"vendor", vendor,
|
|
|
|
|
"token", token
|
|
|
|
|
)),
|
|
|
|
|
null,
|
|
|
|
|
publicHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void sendPush(String userId, String title, String body, String payload) {
|
|
|
|
|
Map<String, String> query = new LinkedHashMap<>();
|
|
|
|
|
query.put("appId", appId);
|
|
|
|
|
query.put("userId", userId);
|
|
|
|
|
query.put("title", title);
|
|
|
|
|
query.put("body", body);
|
|
|
|
|
if (payload != null) {
|
|
|
|
|
query.put("payload", payload);
|
|
|
|
|
}
|
|
|
|
|
request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(pushBaseUrl, "/api/push/send", query),
|
|
|
|
|
null,
|
|
|
|
|
publicHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Map<String, Object> checkAppUpdate(String platform, int currentVersionCode) {
|
|
|
|
|
ApiResponse<Map<String, Object>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/updates/app/check", Map.of(
|
|
|
|
|
"appId", appId,
|
|
|
|
|
"platform", platform,
|
|
|
|
|
"currentVersionCode", String.valueOf(currentVersionCode)
|
|
|
|
|
)),
|
|
|
|
|
null,
|
|
|
|
|
publicHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public AppVersionView uploadAppVersion(
|
|
|
|
|
String platform,
|
|
|
|
|
String versionName,
|
|
|
|
|
int versionCode,
|
|
|
|
|
String changeLog,
|
|
|
|
|
boolean forceUpdate,
|
|
|
|
|
Path apkFile) {
|
|
|
|
|
Map<String, String> form = new LinkedHashMap<>();
|
|
|
|
|
form.put("appId", appId);
|
|
|
|
|
form.put("platform", platform);
|
|
|
|
|
form.put("versionName", versionName);
|
|
|
|
|
form.put("versionCode", String.valueOf(versionCode));
|
|
|
|
|
if (changeLog != null) {
|
|
|
|
|
form.put("changeLog", changeLog);
|
|
|
|
|
}
|
|
|
|
|
form.put("forceUpdate", String.valueOf(forceUpdate));
|
|
|
|
|
ApiResponse<AppVersionView> response = multipartRequest(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/updates/app/upload", Map.of()),
|
|
|
|
|
form,
|
|
|
|
|
"apkFile",
|
|
|
|
|
apkFile,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public UnifiedReleaseResult uploadUnifiedRelease(UnifiedReleaseManifest manifest, Map<String, Path> files) {
|
|
|
|
|
Map<String, String> form = new LinkedHashMap<>();
|
|
|
|
|
form.put("appId", appId);
|
|
|
|
|
try {
|
|
|
|
|
form.put("manifest", objectMapper.writeValueAsString(manifest));
|
|
|
|
|
} catch (JsonProcessingException e) {
|
|
|
|
|
throw new ImSdkException("Failed to serialize unified release manifest", e);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<UnifiedReleaseResult> response = multipartRequest(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/updates/unified/upload", Map.of()),
|
|
|
|
|
form,
|
|
|
|
|
files,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
public AppVersionView publishAppVersion(String id) {
|
|
|
|
|
ApiResponse<AppVersionView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/publish", Map.of()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public AppVersionView unpublishAppVersion(String id) {
|
|
|
|
|
ApiResponse<AppVersionView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/unpublish", Map.of()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public AppVersionView grayAppVersion(String id, boolean enabled, int percent) {
|
|
|
|
|
Map<String, Object> body = new LinkedHashMap<>();
|
|
|
|
|
body.put("enabled", enabled);
|
|
|
|
|
body.put("percent", percent);
|
|
|
|
|
ApiResponse<AppVersionView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/gray", Map.of()),
|
|
|
|
|
body,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<AppVersionView> listAppVersions(String platform) {
|
|
|
|
|
ApiResponse<List<AppVersionView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/updates/app/list", Map.of("appId", appId, "platform", platform)),
|
|
|
|
|
null,
|
|
|
|
|
publicHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public RnBundleView checkRnUpdate(String moduleId, String platform, String currentVersion) {
|
|
|
|
|
ApiResponse<RnBundleView> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/rn/update/check", Map.of(
|
|
|
|
|
"appId", appId,
|
|
|
|
|
"moduleId", moduleId,
|
|
|
|
|
"platform", platform,
|
|
|
|
|
"currentVersion", currentVersion
|
|
|
|
|
)),
|
|
|
|
|
null,
|
|
|
|
|
publicHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public RnBundleView uploadRnBundle(
|
|
|
|
|
String moduleId,
|
|
|
|
|
String platform,
|
|
|
|
|
String version,
|
|
|
|
|
String minCommonVersion,
|
|
|
|
|
String note,
|
|
|
|
|
Path bundle) {
|
|
|
|
|
Map<String, String> form = new LinkedHashMap<>();
|
|
|
|
|
form.put("appId", appId);
|
|
|
|
|
form.put("moduleId", moduleId);
|
|
|
|
|
form.put("platform", platform);
|
|
|
|
|
form.put("version", version);
|
|
|
|
|
if (minCommonVersion != null) {
|
|
|
|
|
form.put("minCommonVersion", minCommonVersion);
|
|
|
|
|
}
|
|
|
|
|
if (note != null) {
|
|
|
|
|
form.put("note", note);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<RnBundleView> response = multipartRequest(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/rn/upload", Map.of()),
|
|
|
|
|
form,
|
|
|
|
|
"bundle",
|
|
|
|
|
bundle,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public RnBundleView publishRnBundle(String id) {
|
|
|
|
|
ApiResponse<RnBundleView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/rn/" + encode(id) + "/publish", Map.of()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public RnBundleView unpublishRnBundle(String id) {
|
|
|
|
|
ApiResponse<RnBundleView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/rn/" + encode(id) + "/unpublish", Map.of()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<RnBundleView> listRnBundles(String moduleId, String platform) {
|
|
|
|
|
Map<String, String> query = new LinkedHashMap<>();
|
|
|
|
|
query.put("appId", appId);
|
|
|
|
|
if (moduleId != null) {
|
|
|
|
|
query.put("moduleId", moduleId);
|
|
|
|
|
}
|
|
|
|
|
if (platform != null) {
|
|
|
|
|
query.put("platform", platform);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<List<RnBundleView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri(updateBaseUrl, "/api/v1/rn/list", query),
|
|
|
|
|
null,
|
|
|
|
|
publicHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public List<String> listFriends() {
|
|
|
|
|
ApiResponse<List<String>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/friends", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public FriendLinkView addFriend(String friendId) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("friendId", friendId);
|
|
|
|
|
ApiResponse<FriendLinkView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/friends", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public List<FriendLinkView> addFriends(List<String> friendIds) {
|
|
|
|
|
ApiResponse<List<FriendLinkView>> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/friends/batch", appQuery()),
|
|
|
|
|
new FriendBatchRequest(friendIds),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public void removeFriend(String friendId) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("friendId", friendId);
|
|
|
|
|
request(
|
|
|
|
|
"DELETE",
|
|
|
|
|
buildUri("/api/im/friends/" + encode(friendId), query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public void removeFriends(List<String> friendIds) {
|
|
|
|
|
request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/friends/batch/remove", appQuery()),
|
|
|
|
|
new FriendBatchRequest(friendIds),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public List<FriendRequestView> listFriendRequests(String direction) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
if (direction != null) {
|
|
|
|
|
query.put("direction", direction);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<List<FriendRequestView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/friend-requests", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public FriendRequestView sendFriendRequest(String toUserId, String remark) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("toUserId", toUserId);
|
|
|
|
|
if (remark != null) {
|
|
|
|
|
query.put("remark", remark);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<FriendRequestView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/friend-requests", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public FriendRequestView acceptFriendRequest(String requestId) {
|
|
|
|
|
ApiResponse<FriendRequestView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/friend-requests/" + encode(requestId) + "/accept", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public FriendRequestView rejectFriendRequest(String requestId) {
|
|
|
|
|
ApiResponse<FriendRequestView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/friend-requests/" + encode(requestId) + "/reject", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public List<FriendRequestView> acceptFriendRequests(List<String> requestIds) {
|
|
|
|
|
ApiResponse<List<FriendRequestView>> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/friend-requests/batch/accept", appQuery()),
|
|
|
|
|
new RequestBatch(requestIds),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<FriendRequestView> rejectFriendRequests(List<String> requestIds) {
|
|
|
|
|
ApiResponse<List<FriendRequestView>> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/friend-requests/batch/reject", appQuery()),
|
|
|
|
|
new RequestBatch(requestIds),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public List<BlacklistView> listBlacklist() {
|
|
|
|
|
ApiResponse<List<BlacklistView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/blacklist", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public BlacklistView addBlacklist(String blockedUserId) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("blockedUserId", blockedUserId);
|
|
|
|
|
ApiResponse<BlacklistView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/blacklist", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void removeBlacklist(String blockedUserId) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("blockedUserId", blockedUserId);
|
|
|
|
|
request(
|
|
|
|
|
"DELETE",
|
|
|
|
|
buildUri("/api/im/blacklist", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<GroupView> listGroups() {
|
|
|
|
|
ApiResponse<List<GroupView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/groups", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<GroupView> listPublicGroups(String keyword) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
if (keyword != null) {
|
|
|
|
|
query.put("keyword", keyword);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<List<GroupView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/groups/public", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GroupView getGroup(String groupId) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId), appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GroupView createGroup(String name, List<String> memberIds, String groupType) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups", appQuery()),
|
|
|
|
|
new CreateGroupRequest(name, memberIds, groupType),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GroupView updateGroup(String groupId, String name, String announcement) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId), appQuery()),
|
|
|
|
|
new UpdateGroupRequest(name, announcement),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GroupView addGroupMember(String groupId, String userId) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/members", appQuery()),
|
|
|
|
|
new MemberRequest(userId),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public GroupView addGroupMembers(String groupId, List<String> userIds) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/members/batch", appQuery()),
|
|
|
|
|
new MemberBatchRequest(userIds),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public GroupView removeGroupMember(String groupId, String targetUserId) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"DELETE",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/members/" + encode(targetUserId), appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public GroupView removeGroupMembers(String groupId, List<String> userIds) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/members/batch/remove", appQuery()),
|
|
|
|
|
new MemberBatchRequest(userIds),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public GroupView setGroupRole(String groupId, String userId, String role) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/roles", appQuery()),
|
|
|
|
|
new SetRoleRequest(userId, role),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GroupView muteGroupMember(String groupId, String userId, long minutes) {
|
|
|
|
|
ApiResponse<GroupView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/mute", appQuery()),
|
|
|
|
|
new MuteMemberRequest(userId, minutes),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void dismissGroup(String groupId) {
|
|
|
|
|
request(
|
|
|
|
|
"DELETE",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId), appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GroupJoinRequestView sendGroupJoinRequest(String groupId, String remark) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
if (remark != null) {
|
|
|
|
|
query.put("remark", remark);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<GroupJoinRequestView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<GroupJoinRequestView> listGroupJoinRequests(String groupId) {
|
|
|
|
|
ApiResponse<List<GroupJoinRequestView>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GroupJoinRequestView acceptGroupJoinRequest(String groupId, String requestId) {
|
|
|
|
|
ApiResponse<GroupJoinRequestView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/accept", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public GroupJoinRequestView rejectGroupJoinRequest(String groupId, String requestId) {
|
|
|
|
|
ApiResponse<GroupJoinRequestView> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/reject", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public List<GroupJoinRequestView> acceptGroupJoinRequests(String groupId, List<String> requestIds) {
|
|
|
|
|
ApiResponse<List<GroupJoinRequestView>> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/batch/accept", appQuery()),
|
|
|
|
|
new RequestBatch(requestIds),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<GroupJoinRequestView> rejectGroupJoinRequests(String groupId, List<String> requestIds) {
|
|
|
|
|
ApiResponse<List<GroupJoinRequestView>> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/batch/reject", appQuery()),
|
|
|
|
|
new RequestBatch(requestIds),
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public void setConversationPinned(String targetId, String chatType, boolean pinned) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("chatType", chatType);
|
|
|
|
|
query.put("pinned", String.valueOf(pinned));
|
|
|
|
|
request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/conversations/" + encode(targetId) + "/pinned", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setConversationMuted(String targetId, String chatType, boolean muted) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("chatType", chatType);
|
|
|
|
|
query.put("muted", String.valueOf(muted));
|
|
|
|
|
request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/conversations/" + encode(targetId) + "/muted", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void markRead(String targetId, String chatType) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("chatType", chatType);
|
|
|
|
|
request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/conversations/" + encode(targetId) + "/read", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setDraft(String targetId, String chatType, String draft) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("chatType", chatType);
|
|
|
|
|
if (draft != null) {
|
|
|
|
|
query.put("draft", draft);
|
|
|
|
|
}
|
|
|
|
|
request(
|
|
|
|
|
"PUT",
|
|
|
|
|
buildUri("/api/im/conversations/" + encode(targetId) + "/draft", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void deleteConversation(String targetId, String chatType) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("chatType", chatType);
|
|
|
|
|
request(
|
|
|
|
|
"DELETE",
|
|
|
|
|
buildUri("/api/im/conversations/" + encode(targetId), query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PageResult<ImMessage> queryAdminMessages(
|
|
|
|
|
String userA,
|
|
|
|
|
String userB,
|
|
|
|
|
String msgType,
|
|
|
|
|
String keyword,
|
|
|
|
|
LocalDateTime startTime,
|
|
|
|
|
LocalDateTime endTime,
|
|
|
|
|
int page,
|
|
|
|
|
int size) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("userA", userA);
|
|
|
|
|
query.put("userB", userB);
|
|
|
|
|
if (msgType != null) {
|
|
|
|
|
query.put("msgType", msgType);
|
|
|
|
|
}
|
|
|
|
|
if (keyword != null) {
|
|
|
|
|
query.put("keyword", keyword);
|
|
|
|
|
}
|
|
|
|
|
if (startTime != null) {
|
|
|
|
|
query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
|
|
|
|
|
}
|
|
|
|
|
if (endTime != null) {
|
|
|
|
|
query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
|
|
|
|
|
}
|
|
|
|
|
query.put("page", String.valueOf(page));
|
|
|
|
|
query.put("size", String.valueOf(size));
|
|
|
|
|
ApiResponse<PageResult<ImMessage>> response = request(
|
|
|
|
|
"GET",
|
|
|
|
|
buildUri("/api/im/admin/messages", query),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImMessage revokeAdminMessage(String messageId) {
|
|
|
|
|
ApiResponse<ImMessage> response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
buildUri("/api/im/admin/messages/" + encode(messageId) + "/revoke", appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<>() {}
|
|
|
|
|
);
|
|
|
|
|
return response.data();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void dismissAdminGroup(String groupId) {
|
|
|
|
|
request(
|
|
|
|
|
"DELETE",
|
|
|
|
|
buildUri("/api/im/admin/groups/" + encode(groupId), appQuery()),
|
|
|
|
|
null,
|
|
|
|
|
authorizedHeaders(),
|
|
|
|
|
new TypeReference<ApiResponse<Void>>() {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:38 +08:00
|
|
|
private static String sha256Hex(String value) {
|
|
|
|
|
try {
|
|
|
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
|
|
|
byte[] hash = digest.digest(normalize(value).getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
return HexFormat.of().formatHex(hash);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw new ImSdkException("Failed to hash callback body", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String hmacSha256Hex(String secret, String payload) {
|
|
|
|
|
try {
|
|
|
|
|
Mac mac = Mac.getInstance("HmacSHA256");
|
|
|
|
|
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
|
|
|
|
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
return HexFormat.of().formatHex(hash);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw new ImSdkException("Failed to verify callback signature", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String normalize(String value) {
|
|
|
|
|
return value == null ? "" : value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
private <T> T readPayload(WebhookCallbackEnvelope envelope, Class<T> payloadType) {
|
|
|
|
|
try {
|
|
|
|
|
return objectMapper.treeToValue(envelope.payload(), payloadType);
|
|
|
|
|
} catch (JsonProcessingException e) {
|
|
|
|
|
throw new ImSdkException("Failed to parse callback payload", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 22:32:21 +08:00
|
|
|
private void requireMessageEvent(WebhookCallbackEnvelope envelope, String event) {
|
|
|
|
|
if (!envelope.isMessageEvent() || !envelope.isEvent(event)) {
|
|
|
|
|
throw new ImSdkException("Callback event is not a " + event + " event");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:38 +08:00
|
|
|
private static String text(JsonNode node, String field) {
|
|
|
|
|
JsonNode value = node == null ? null : node.get(field);
|
|
|
|
|
return value == null || value.isNull() ? null : value.asText();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static long longValue(JsonNode node, String field) {
|
|
|
|
|
JsonNode value = node == null ? null : node.get(field);
|
|
|
|
|
if (value == null || value.isNull()) {
|
|
|
|
|
return 0L;
|
|
|
|
|
}
|
|
|
|
|
return value.asLong();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
private Map<String, String> authorizedHeaders() {
|
|
|
|
|
String token = bearerTokenSupplier == null ? null : bearerTokenSupplier.get();
|
|
|
|
|
if (token == null || token.isBlank()) {
|
|
|
|
|
throw new ImSdkException("Bearer token is required for this call");
|
|
|
|
|
}
|
|
|
|
|
return Map.of("Authorization", "Bearer " + token);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
private Map<String, String> publicHeaders() {
|
|
|
|
|
return Map.of();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
private <T> ApiResponse<T> request(
|
|
|
|
|
String method,
|
|
|
|
|
URI uri,
|
|
|
|
|
Object body,
|
|
|
|
|
Map<String, String> headers,
|
|
|
|
|
TypeReference<ApiResponse<T>> responseType
|
|
|
|
|
) {
|
|
|
|
|
try {
|
|
|
|
|
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
|
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.headers(flatten(headers));
|
|
|
|
|
if (body != null) {
|
|
|
|
|
builder.method(method, HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)));
|
|
|
|
|
} else if ("GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
|
|
|
|
|
builder.method(method, HttpRequest.BodyPublishers.noBody());
|
|
|
|
|
} else {
|
|
|
|
|
builder.method(method, HttpRequest.BodyPublishers.noBody());
|
|
|
|
|
}
|
|
|
|
|
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
|
|
|
|
if (response.statusCode() >= 400) {
|
|
|
|
|
throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body());
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<T> apiResponse = objectMapper.readValue(response.body(), responseType);
|
|
|
|
|
if (apiResponse.code() != 200) {
|
|
|
|
|
throw new ImSdkException(apiResponse.message());
|
|
|
|
|
}
|
|
|
|
|
return apiResponse;
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
throw new ImSdkException("Request failed: " + e.getMessage(), e);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
Thread.currentThread().interrupt();
|
|
|
|
|
throw new ImSdkException("Request interrupted", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
private <T> ApiResponse<T> multipartRequest(
|
|
|
|
|
String method,
|
|
|
|
|
URI uri,
|
|
|
|
|
Map<String, String> formFields,
|
|
|
|
|
String fileFieldName,
|
|
|
|
|
Path file,
|
|
|
|
|
Map<String, String> headers,
|
|
|
|
|
TypeReference<ApiResponse<T>> responseType
|
|
|
|
|
) {
|
|
|
|
|
String boundary = "----XuqmBoundary" + UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
try {
|
|
|
|
|
HttpRequest.BodyPublisher bodyPublisher = ofMultipartData(boundary, formFields, fileFieldName, file);
|
|
|
|
|
Map<String, String> mergedHeaders = new LinkedHashMap<>(headers);
|
|
|
|
|
mergedHeaders.put("Content-Type", "multipart/form-data; boundary=" + boundary);
|
|
|
|
|
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
|
|
|
|
.headers(flatten(mergedHeaders))
|
|
|
|
|
.method(method, bodyPublisher);
|
|
|
|
|
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
|
|
|
|
if (response.statusCode() >= 400) {
|
|
|
|
|
throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body());
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<T> apiResponse = objectMapper.readValue(response.body(), responseType);
|
|
|
|
|
if (apiResponse.code() != 200) {
|
|
|
|
|
throw new ImSdkException(apiResponse.message());
|
|
|
|
|
}
|
|
|
|
|
return apiResponse;
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
throw new ImSdkException("Request failed: " + e.getMessage(), e);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
Thread.currentThread().interrupt();
|
|
|
|
|
throw new ImSdkException("Request interrupted", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
private <T> ApiResponse<T> multipartRequest(
|
|
|
|
|
String method,
|
|
|
|
|
URI uri,
|
|
|
|
|
Map<String, String> formFields,
|
|
|
|
|
Map<String, Path> files,
|
|
|
|
|
Map<String, String> headers,
|
|
|
|
|
TypeReference<ApiResponse<T>> responseType
|
|
|
|
|
) {
|
|
|
|
|
String boundary = "----XuqmBoundary" + UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
try {
|
|
|
|
|
HttpRequest.BodyPublisher bodyPublisher = ofMultipartData(boundary, formFields, files);
|
|
|
|
|
Map<String, String> mergedHeaders = new LinkedHashMap<>(headers);
|
|
|
|
|
mergedHeaders.put("Content-Type", "multipart/form-data; boundary=" + boundary);
|
|
|
|
|
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
|
|
|
|
.headers(flatten(mergedHeaders))
|
|
|
|
|
.method(method, bodyPublisher);
|
|
|
|
|
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
|
|
|
|
if (response.statusCode() >= 400) {
|
|
|
|
|
throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body());
|
|
|
|
|
}
|
|
|
|
|
ApiResponse<T> apiResponse = objectMapper.readValue(response.body(), responseType);
|
|
|
|
|
if (apiResponse.code() != 200) {
|
|
|
|
|
throw new ImSdkException(apiResponse.message());
|
|
|
|
|
}
|
|
|
|
|
return apiResponse;
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
throw new ImSdkException("Request failed: " + e.getMessage(), e);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
Thread.currentThread().interrupt();
|
|
|
|
|
throw new ImSdkException("Request interrupted", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
private HttpRequest.BodyPublisher ofMultipartData(
|
|
|
|
|
String boundary,
|
|
|
|
|
Map<String, String> formFields,
|
|
|
|
|
String fileFieldName,
|
|
|
|
|
Path file
|
|
|
|
|
) throws IOException {
|
|
|
|
|
var byteArrays = new ArrayList<byte[]>();
|
|
|
|
|
Charset charset = StandardCharsets.UTF_8;
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, String> entry : formFields.entrySet()) {
|
|
|
|
|
byteArrays.add(("--" + boundary + "\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"\r\n\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(entry.getValue().getBytes(charset));
|
|
|
|
|
byteArrays.add("\r\n".getBytes(charset));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
byteArrays.add(("--" + boundary + "\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(("Content-Disposition: form-data; name=\"" + fileFieldName + "\"; filename=\"" + file.getFileName() + "\"\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(("Content-Type: application/octet-stream\r\n\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(Files.readAllBytes(file));
|
|
|
|
|
byteArrays.add("\r\n".getBytes(charset));
|
|
|
|
|
byteArrays.add(("--" + boundary + "--\r\n").getBytes(charset));
|
|
|
|
|
|
|
|
|
|
return HttpRequest.BodyPublishers.ofByteArrays(byteArrays);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
private HttpRequest.BodyPublisher ofMultipartData(
|
|
|
|
|
String boundary,
|
|
|
|
|
Map<String, String> formFields,
|
|
|
|
|
Map<String, Path> files
|
|
|
|
|
) throws IOException {
|
|
|
|
|
var byteArrays = new ArrayList<byte[]>();
|
|
|
|
|
Charset charset = StandardCharsets.UTF_8;
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, String> entry : formFields.entrySet()) {
|
|
|
|
|
byteArrays.add(("--" + boundary + "\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"\r\n\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(entry.getValue().getBytes(charset));
|
|
|
|
|
byteArrays.add("\r\n".getBytes(charset));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, Path> entry : files.entrySet()) {
|
|
|
|
|
Path file = entry.getValue();
|
|
|
|
|
byteArrays.add(("--" + boundary + "\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"; filename=\"" + file.getFileName() + "\"\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(("Content-Type: application/octet-stream\r\n\r\n").getBytes(charset));
|
|
|
|
|
byteArrays.add(Files.readAllBytes(file));
|
|
|
|
|
byteArrays.add("\r\n".getBytes(charset));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
byteArrays.add(("--" + boundary + "--\r\n").getBytes(charset));
|
|
|
|
|
return HttpRequest.BodyPublishers.ofByteArrays(byteArrays);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
private URI buildUri(String path, Map<String, String> query) {
|
|
|
|
|
StringBuilder builder = new StringBuilder(baseUrl).append(path);
|
|
|
|
|
if (query != null && !query.isEmpty()) {
|
|
|
|
|
builder.append('?');
|
|
|
|
|
boolean first = true;
|
|
|
|
|
for (Map.Entry<String, String> entry : query.entrySet()) {
|
|
|
|
|
String value = entry.getValue();
|
|
|
|
|
if (value == null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!first) {
|
|
|
|
|
builder.append('&');
|
|
|
|
|
}
|
|
|
|
|
builder.append(encode(entry.getKey())).append('=').append(encode(value));
|
|
|
|
|
first = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return URI.create(builder.toString());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
private URI buildUri(String base, String path, Map<String, String> query) {
|
|
|
|
|
StringBuilder builder = new StringBuilder(trimTrailingSlash(base)).append(path);
|
|
|
|
|
if (query != null && !query.isEmpty()) {
|
|
|
|
|
builder.append('?');
|
|
|
|
|
boolean first = true;
|
|
|
|
|
for (Map.Entry<String, String> entry : query.entrySet()) {
|
|
|
|
|
String value = entry.getValue();
|
|
|
|
|
if (value == null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!first) {
|
|
|
|
|
builder.append('&');
|
|
|
|
|
}
|
|
|
|
|
builder.append(encode(entry.getKey())).append('=').append(encode(value));
|
|
|
|
|
first = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return URI.create(builder.toString());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
private Map<String, String> loginQuery(String userId, String nickname, String avatar, long timestamp, String nonce) {
|
|
|
|
|
Map<String, String> query = new LinkedHashMap<>();
|
|
|
|
|
query.put("appId", appId);
|
|
|
|
|
query.put("userId", userId);
|
|
|
|
|
query.put("timestamp", String.valueOf(timestamp));
|
|
|
|
|
query.put("nonce", nonce);
|
|
|
|
|
if (nickname != null && !nickname.isBlank()) {
|
|
|
|
|
query.put("nickname", nickname);
|
|
|
|
|
}
|
|
|
|
|
if (avatar != null && !avatar.isBlank()) {
|
|
|
|
|
query.put("avatar", avatar);
|
|
|
|
|
}
|
|
|
|
|
return query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Map<String, String> appQuery() {
|
|
|
|
|
Map<String, String> query = new LinkedHashMap<>();
|
|
|
|
|
query.put("appId", appId);
|
|
|
|
|
return query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Map<String, String> queryWithPage(int page, int size) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put("page", String.valueOf(page));
|
|
|
|
|
query.put("size", String.valueOf(size));
|
|
|
|
|
return query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Map<String, String> queryWithSize(String key, String value, int size) {
|
|
|
|
|
Map<String, String> query = appQuery();
|
|
|
|
|
query.put(key, value);
|
|
|
|
|
query.put("size", String.valueOf(size));
|
|
|
|
|
return query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Map<String, String> queryParams(String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, int page, int size) {
|
|
|
|
|
Map<String, String> query = new LinkedHashMap<>();
|
|
|
|
|
query.put("appId", appId);
|
|
|
|
|
if (msgType != null && !msgType.isBlank()) {
|
|
|
|
|
query.put("msgType", msgType);
|
|
|
|
|
}
|
|
|
|
|
if (keyword != null && !keyword.isBlank()) {
|
|
|
|
|
query.put("keyword", keyword);
|
|
|
|
|
}
|
|
|
|
|
if (startTime != null) {
|
|
|
|
|
query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
|
|
|
|
|
}
|
|
|
|
|
if (endTime != null) {
|
|
|
|
|
query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
|
|
|
|
|
}
|
|
|
|
|
query.put("page", String.valueOf(page));
|
|
|
|
|
query.put("size", String.valueOf(size));
|
|
|
|
|
return query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String[] flatten(Map<String, String> headers) {
|
|
|
|
|
String[] pairs = new String[headers.size() * 2];
|
|
|
|
|
int index = 0;
|
|
|
|
|
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
|
|
|
|
pairs[index++] = entry.getKey();
|
|
|
|
|
pairs[index++] = entry.getValue();
|
|
|
|
|
}
|
|
|
|
|
return pairs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String encode(String value) {
|
|
|
|
|
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String trimTrailingSlash(String value) {
|
|
|
|
|
return value.endsWith("/") ? value.substring(0, value.length() - 1) : value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static final class Builder {
|
|
|
|
|
private String baseUrl;
|
2026-04-28 17:43:46 +08:00
|
|
|
private String pushBaseUrl;
|
|
|
|
|
private String updateBaseUrl;
|
2026-04-28 16:55:12 +08:00
|
|
|
private String appId;
|
|
|
|
|
private String appSecret;
|
|
|
|
|
private Supplier<String> bearerTokenSupplier;
|
|
|
|
|
|
|
|
|
|
private Builder() {}
|
|
|
|
|
|
|
|
|
|
public Builder baseUrl(String baseUrl) {
|
|
|
|
|
this.baseUrl = baseUrl;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
public Builder pushBaseUrl(String pushBaseUrl) {
|
|
|
|
|
this.pushBaseUrl = pushBaseUrl;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Builder updateBaseUrl(String updateBaseUrl) {
|
|
|
|
|
this.updateBaseUrl = updateBaseUrl;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public Builder appId(String appId) {
|
|
|
|
|
this.appId = appId;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Builder appSecret(String appSecret) {
|
|
|
|
|
this.appSecret = appSecret;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Builder bearerTokenSupplier(Supplier<String> bearerTokenSupplier) {
|
|
|
|
|
this.bearerTokenSupplier = bearerTokenSupplier;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public XuqmImServerSdk build() {
|
|
|
|
|
return new XuqmImServerSdk(this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public record LoginResponse(String token, long expiresAt) {}
|
|
|
|
|
|
|
|
|
|
public record ConversationView(
|
|
|
|
|
String targetId,
|
|
|
|
|
String chatType,
|
|
|
|
|
String lastMsgContent,
|
|
|
|
|
String lastMsgType,
|
|
|
|
|
Long lastMsgTime,
|
|
|
|
|
int unreadCount,
|
|
|
|
|
boolean isMuted,
|
|
|
|
|
boolean isPinned
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record AccountView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String userId,
|
|
|
|
|
String nickname,
|
|
|
|
|
String gender,
|
|
|
|
|
String avatar,
|
|
|
|
|
String status,
|
|
|
|
|
LocalDateTime createdAt
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record FriendLinkView(
|
|
|
|
|
Long id,
|
|
|
|
|
String appId,
|
|
|
|
|
String userId,
|
|
|
|
|
String friendId,
|
|
|
|
|
Instant createdAt
|
|
|
|
|
) {}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public record FriendBatchRequest(List<String> friendIds) {}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public record FriendRequestView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String fromUserId,
|
|
|
|
|
String toUserId,
|
|
|
|
|
String remark,
|
|
|
|
|
String status,
|
|
|
|
|
LocalDateTime createdAt,
|
|
|
|
|
LocalDateTime reviewedAt
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record BlacklistView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String userId,
|
|
|
|
|
String blockedUserId,
|
|
|
|
|
LocalDateTime createdAt
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record GroupView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String name,
|
|
|
|
|
String groupType,
|
|
|
|
|
String creatorId,
|
|
|
|
|
String memberIds,
|
|
|
|
|
String adminIds,
|
|
|
|
|
String announcement,
|
|
|
|
|
LocalDateTime createdAt
|
|
|
|
|
) {
|
|
|
|
|
public List<String> memberIdList() {
|
|
|
|
|
return parseJsonStringList(memberIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<String> adminIdList() {
|
|
|
|
|
return parseJsonStringList(adminIds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public record GroupJoinRequestView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String groupId,
|
|
|
|
|
String requesterId,
|
|
|
|
|
String remark,
|
|
|
|
|
String status,
|
|
|
|
|
LocalDateTime createdAt,
|
|
|
|
|
LocalDateTime reviewedAt
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record StatsView(
|
|
|
|
|
long totalMessages,
|
|
|
|
|
long totalUsers,
|
|
|
|
|
long totalGroups,
|
|
|
|
|
long todayMessages
|
|
|
|
|
) {}
|
|
|
|
|
|
2026-04-28 17:29:17 +08:00
|
|
|
public record WebhookConfigView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String url,
|
|
|
|
|
String secret,
|
|
|
|
|
boolean enabled,
|
|
|
|
|
Long createdAt
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record KeywordFilterView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String pattern,
|
|
|
|
|
String replacement,
|
|
|
|
|
String action,
|
|
|
|
|
boolean enabled,
|
|
|
|
|
Long createdAt
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record GlobalMuteView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
boolean enabled,
|
|
|
|
|
Long createdAt,
|
|
|
|
|
Long updatedAt
|
|
|
|
|
) {}
|
|
|
|
|
|
2026-04-28 20:11:38 +08:00
|
|
|
public record WebhookCallbackEnvelope(
|
|
|
|
|
String callbackId,
|
|
|
|
|
String callbackType,
|
|
|
|
|
String callbackEvent,
|
|
|
|
|
long requestTime,
|
|
|
|
|
JsonNode payload,
|
|
|
|
|
String signature,
|
|
|
|
|
String appId
|
2026-04-28 21:05:06 +08:00
|
|
|
) {
|
|
|
|
|
public boolean isType(String type) {
|
|
|
|
|
return type != null && callbackType != null && callbackType.equalsIgnoreCase(type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isEvent(String event) {
|
|
|
|
|
return event != null && callbackEvent != null && callbackEvent.equalsIgnoreCase(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isMessageEvent() {
|
|
|
|
|
return isType("message");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 22:32:21 +08:00
|
|
|
public boolean isMessageSentEvent() {
|
|
|
|
|
return isEvent("message.sent");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isMessageEditedEvent() {
|
|
|
|
|
return isEvent("message.edited");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isMessageRevokedEvent() {
|
|
|
|
|
return isEvent("message.revoked");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public boolean isReadReceiptEvent() {
|
|
|
|
|
return isEvent("message.read");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-28 20:11:38 +08:00
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
public record AppVersionView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String platform,
|
|
|
|
|
String versionName,
|
|
|
|
|
int versionCode,
|
|
|
|
|
String downloadUrl,
|
|
|
|
|
String changeLog,
|
|
|
|
|
boolean forceUpdate,
|
|
|
|
|
String publishStatus,
|
|
|
|
|
String appStoreUrl,
|
|
|
|
|
String marketUrl,
|
|
|
|
|
boolean grayEnabled,
|
|
|
|
|
int grayPercent,
|
|
|
|
|
Long createdAt
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record RnBundleView(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String moduleId,
|
|
|
|
|
String platform,
|
|
|
|
|
String version,
|
|
|
|
|
String bundleUrl,
|
|
|
|
|
String md5,
|
|
|
|
|
String minCommonVersion,
|
|
|
|
|
String note,
|
|
|
|
|
String publishStatus,
|
|
|
|
|
boolean grayEnabled,
|
|
|
|
|
int grayPercent,
|
|
|
|
|
Long createdAt
|
|
|
|
|
) {}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public record HistoryQuery(
|
|
|
|
|
String msgType,
|
|
|
|
|
String keyword,
|
|
|
|
|
LocalDateTime startTime,
|
|
|
|
|
LocalDateTime endTime,
|
|
|
|
|
Integer page,
|
|
|
|
|
Integer size
|
|
|
|
|
) {
|
|
|
|
|
public HistoryQuery() {
|
|
|
|
|
this(null, null, null, null, null, null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public record PageResult<T>(
|
|
|
|
|
List<T> content,
|
|
|
|
|
long totalElements,
|
|
|
|
|
int totalPages,
|
|
|
|
|
int size,
|
|
|
|
|
int number,
|
|
|
|
|
int numberOfElements,
|
|
|
|
|
boolean first,
|
|
|
|
|
boolean last,
|
|
|
|
|
boolean empty
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record ImMessage(
|
|
|
|
|
String id,
|
|
|
|
|
String appId,
|
|
|
|
|
String fromUserId,
|
|
|
|
|
String toId,
|
|
|
|
|
String chatType,
|
|
|
|
|
String msgType,
|
|
|
|
|
String content,
|
|
|
|
|
String status,
|
|
|
|
|
String mentionedUserIds,
|
|
|
|
|
Integer groupReadCount,
|
2026-04-28 20:11:38 +08:00
|
|
|
Long createdAt,
|
|
|
|
|
Long editedAt
|
2026-04-28 16:55:12 +08:00
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record SendMessageRequest(
|
|
|
|
|
String messageId,
|
|
|
|
|
String toId,
|
|
|
|
|
String chatType,
|
|
|
|
|
String msgType,
|
|
|
|
|
String content,
|
|
|
|
|
String mentionedUserIds
|
|
|
|
|
) {}
|
|
|
|
|
|
2026-04-28 17:29:17 +08:00
|
|
|
public record WebhookConfigRequest(
|
|
|
|
|
String url,
|
|
|
|
|
String secret,
|
|
|
|
|
Boolean enabled
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record KeywordFilterRequest(
|
|
|
|
|
String pattern,
|
|
|
|
|
String replacement,
|
|
|
|
|
String action,
|
|
|
|
|
Boolean enabled
|
|
|
|
|
) {}
|
|
|
|
|
|
2026-04-28 17:43:46 +08:00
|
|
|
public record AppVersionUploadRequest(
|
|
|
|
|
String appId,
|
|
|
|
|
String platform,
|
|
|
|
|
String versionName,
|
|
|
|
|
int versionCode,
|
|
|
|
|
String changeLog,
|
|
|
|
|
boolean forceUpdate
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record RnBundleUploadRequest(
|
|
|
|
|
String appId,
|
|
|
|
|
String moduleId,
|
|
|
|
|
String platform,
|
|
|
|
|
String version,
|
|
|
|
|
String minCommonVersion,
|
|
|
|
|
String note
|
|
|
|
|
) {}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public record MessageReadCallbackPayload(
|
|
|
|
|
String appId,
|
|
|
|
|
String readerId,
|
|
|
|
|
String peerId,
|
|
|
|
|
String groupId,
|
|
|
|
|
String chatType,
|
|
|
|
|
long readAt,
|
|
|
|
|
List<String> messageIds
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record UnifiedReleaseManifest(
|
|
|
|
|
List<AppUploadItem> appVersions,
|
|
|
|
|
List<RnBundleUploadItem> rnBundles
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record AppUploadItem(
|
|
|
|
|
String fileKey,
|
|
|
|
|
String platform,
|
|
|
|
|
String versionName,
|
|
|
|
|
int versionCode,
|
|
|
|
|
String changeLog,
|
|
|
|
|
boolean forceUpdate,
|
|
|
|
|
String appStoreUrl,
|
|
|
|
|
String marketUrl
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record RnBundleUploadItem(
|
|
|
|
|
String fileKey,
|
|
|
|
|
String moduleId,
|
|
|
|
|
String platform,
|
|
|
|
|
String version,
|
|
|
|
|
String minCommonVersion,
|
|
|
|
|
String note
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record UnifiedReleaseResult(
|
|
|
|
|
List<AppVersionView> appVersions,
|
|
|
|
|
List<RnBundleView> rnBundles
|
|
|
|
|
) {}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public record CreateGroupRequest(
|
|
|
|
|
String name,
|
|
|
|
|
List<String> memberIds,
|
|
|
|
|
String groupType
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record UpdateGroupRequest(
|
|
|
|
|
String name,
|
|
|
|
|
String announcement
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public record MemberRequest(String userId) {}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public record MemberBatchRequest(List<String> userIds) {}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public record SetRoleRequest(String userId, String role) {}
|
|
|
|
|
|
|
|
|
|
public record MuteMemberRequest(String userId, long minutes) {}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
public record RequestBatch(List<String> requestIds) {}
|
|
|
|
|
|
2026-04-28 16:55:12 +08:00
|
|
|
public static final class ImSdkException extends RuntimeException {
|
|
|
|
|
public ImSdkException(String message) {
|
|
|
|
|
super(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImSdkException(String message, Throwable cause) {
|
|
|
|
|
super(message, cause);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static List<String> parseJsonStringList(String value) {
|
|
|
|
|
if (value == null || value.isBlank()) {
|
|
|
|
|
return List.of();
|
|
|
|
|
}
|
|
|
|
|
String trimmed = value.trim();
|
|
|
|
|
if (!trimmed.startsWith("[")) {
|
|
|
|
|
return trimmed.isEmpty() ? List.of() : List.of(trimmed.split(","));
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
ObjectMapper mapper = new ObjectMapper();
|
|
|
|
|
return mapper.readValue(trimmed, new TypeReference<List<String>>() {});
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
return new ArrayList<>(List.of(trimmed));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|