|
@@ -0,0 +1,185 @@
|
|
|
|
|
+package com.jeesite.modules.mes.dingtalk;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
+import com.jeesite.common.lang.StringUtils;
|
|
|
|
|
+import okhttp3.HttpUrl;
|
|
|
|
|
+import okhttp3.MediaType;
|
|
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
|
|
+import okhttp3.Request;
|
|
|
|
|
+import okhttp3.RequestBody;
|
|
|
|
|
+import okhttp3.Response;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+import java.util.stream.Stream;
|
|
|
|
|
+
|
|
|
|
|
+@Component
|
|
|
|
|
+public class DingTalkAppClient {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger logger = LoggerFactory.getLogger(DingTalkAppClient.class);
|
|
|
|
|
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
|
|
|
|
+ private static final String TOKEN_URL = "https://oapi.dingtalk.com/gettoken";
|
|
|
|
|
+ private static final String USER_BY_MOBILE_URL = "https://oapi.dingtalk.com/topapi/v2/user/getbymobile";
|
|
|
|
|
+ private static final String ASYNC_SEND_URL = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2";
|
|
|
|
|
+
|
|
|
|
|
+ private final DingTalkRobotProperties properties;
|
|
|
|
|
+ private final OkHttpClient httpClient = new OkHttpClient.Builder()
|
|
|
|
|
+ .connectTimeout(5, TimeUnit.SECONDS)
|
|
|
|
|
+ .readTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ private volatile String accessToken;
|
|
|
|
|
+ private volatile long tokenExpireAt;
|
|
|
|
|
+
|
|
|
|
|
+ public DingTalkAppClient(DingTalkRobotProperties properties) {
|
|
|
|
|
+ this.properties = properties;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public boolean sendMarkdown(String title, String markdownText) {
|
|
|
|
|
+ if (!isConfigured()) {
|
|
|
|
|
+ logger.warn("DingTalk app is disabled or app config is incomplete.");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ String token = getAccessToken();
|
|
|
|
|
+ List<String> userIds = getReceiverUserIds(token);
|
|
|
|
|
+ List<String> deptIds = split(properties.getDeptIds());
|
|
|
|
|
+ if (!properties.isAtAll() && userIds.isEmpty() && deptIds.isEmpty()) {
|
|
|
|
|
+ logger.warn("DingTalk app receiver is empty, set ding.user-ids, ding.dept-ids, ding.at-mobiles, or ding.at-all.");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
|
|
+ body.put("agent_id", Long.parseLong(properties.getAgentId().trim()));
|
|
|
|
|
+ body.put("to_all_user", properties.isAtAll());
|
|
|
|
|
+ if (!userIds.isEmpty()) {
|
|
|
|
|
+ body.put("userid_list", String.join(",", userIds));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!deptIds.isEmpty()) {
|
|
|
|
|
+ body.put("dept_id_list", String.join(",", deptIds));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject markdown = new JSONObject();
|
|
|
|
|
+ markdown.put("title", title);
|
|
|
|
|
+ markdown.put("text", markdownText);
|
|
|
|
|
+ JSONObject msg = new JSONObject();
|
|
|
|
|
+ msg.put("msgtype", "markdown");
|
|
|
|
|
+ msg.put("markdown", markdown);
|
|
|
|
|
+ body.put("msg", msg);
|
|
|
|
|
+ return postApi(ASYNC_SEND_URL, token, body, "DingTalk app message send");
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ logger.warn("DingTalk app send exception: {}", ex.getMessage(), ex);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isConfigured() {
|
|
|
|
|
+ return properties.isAppEnabled()
|
|
|
|
|
+ && StringUtils.isNotBlank(properties.getAgentId())
|
|
|
|
|
+ && StringUtils.isNotBlank(properties.getClientId())
|
|
|
|
|
+ && StringUtils.isNotBlank(properties.getClientSecret());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getAccessToken() {
|
|
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
|
|
+ if (StringUtils.isNotBlank(accessToken) && now < tokenExpireAt) {
|
|
|
|
|
+ return accessToken;
|
|
|
|
|
+ }
|
|
|
|
|
+ synchronized (this) {
|
|
|
|
|
+ now = System.currentTimeMillis();
|
|
|
|
|
+ if (StringUtils.isNotBlank(accessToken) && now < tokenExpireAt) {
|
|
|
|
|
+ return accessToken;
|
|
|
|
|
+ }
|
|
|
|
|
+ HttpUrl url = HttpUrl.parse(TOKEN_URL).newBuilder()
|
|
|
|
|
+ .addQueryParameter("appkey", properties.getClientId())
|
|
|
|
|
+ .addQueryParameter("appsecret", properties.getClientSecret())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ Request request = new Request.Builder().url(url).get().build();
|
|
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
|
|
+ String responseBody = response.body() == null ? "" : response.body().string();
|
|
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
|
|
+ throw new IllegalStateException("DingTalk token httpStatus=" + response.code() + ", body=" + responseBody);
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject result = JSONObject.parseObject(responseBody);
|
|
|
|
|
+ if (result == null || result.getIntValue("errcode") != 0) {
|
|
|
|
|
+ throw new IllegalStateException("DingTalk token failed: " + responseBody);
|
|
|
|
|
+ }
|
|
|
|
|
+ accessToken = result.getString("access_token");
|
|
|
|
|
+ long expiresIn = result.getLongValue("expires_in");
|
|
|
|
|
+ tokenExpireAt = System.currentTimeMillis() + Math.max(60L, expiresIn - 300L) * 1000L;
|
|
|
|
|
+ return accessToken;
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ accessToken = null;
|
|
|
|
|
+ tokenExpireAt = 0;
|
|
|
|
|
+ throw new IllegalStateException("Get DingTalk access token failed", ex);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<String> getReceiverUserIds(String token) {
|
|
|
|
|
+ List<String> userIds = split(properties.getUserIds());
|
|
|
|
|
+ if (!userIds.isEmpty()) {
|
|
|
|
|
+ return userIds;
|
|
|
|
|
+ }
|
|
|
|
|
+ return split(properties.getAtMobiles()).stream()
|
|
|
|
|
+ .map(mobile -> getUserIdByMobile(token, mobile))
|
|
|
|
|
+ .filter(StringUtils::isNotBlank)
|
|
|
|
|
+ .distinct()
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getUserIdByMobile(String token, String mobile) {
|
|
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
|
|
+ body.put("mobile", mobile);
|
|
|
|
|
+ JSONObject result = postApiForResult(USER_BY_MOBILE_URL, token, body, "DingTalk user lookup");
|
|
|
|
|
+ if (result == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject user = result.getJSONObject("result");
|
|
|
|
|
+ return user == null ? null : user.getString("userid");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean postApi(String apiUrl, String token, JSONObject body, String action) {
|
|
|
|
|
+ JSONObject result = postApiForResult(apiUrl, token, body, action);
|
|
|
|
|
+ return result != null && result.getIntValue("errcode") == 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private JSONObject postApiForResult(String apiUrl, String token, JSONObject body, String action) {
|
|
|
|
|
+ HttpUrl url = HttpUrl.parse(apiUrl).newBuilder()
|
|
|
|
|
+ .addQueryParameter("access_token", token)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ Request request = new Request.Builder()
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .post(RequestBody.create(JSON, body.toJSONString()))
|
|
|
|
|
+ .build();
|
|
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
|
|
+ String responseBody = response.body() == null ? "" : response.body().string();
|
|
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
|
|
+ logger.warn("{}, httpStatus={}, body={}", action, response.code(), responseBody);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject result = JSONObject.parseObject(responseBody);
|
|
|
|
|
+ if (result == null || result.getIntValue("errcode") != 0) {
|
|
|
|
|
+ logger.warn("{} returned failure: {}", action, responseBody);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ logger.warn("{} exception: {}", action, ex.getMessage(), ex);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<String> split(String value) {
|
|
|
|
|
+ if (StringUtils.isBlank(value)) {
|
|
|
|
|
+ return java.util.Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ return Stream.of(value.split(","))
|
|
|
|
|
+ .map(String::trim)
|
|
|
|
|
+ .filter(StringUtils::isNotBlank)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+}
|