All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wl4g.infra.common.notification.dingtalk.internal.DingtalkAPI Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017 ~ 2025 the original author or authors. James Wong James Wong 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.wl4g.infra.common.notification.dingtalk.internal;

import static com.wl4g.infra.common.lang.Assert2.hasTextOf;
import static com.wl4g.infra.common.lang.Assert2.notNullOf;
import static com.wl4g.infra.common.lang.EnvironmentUtil.getBooleanProperty;
import static com.wl4g.infra.common.lang.EnvironmentUtil.getIntProperty;
import static com.wl4g.infra.common.serialize.JacksonUtils.parseToNode;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
import static java.util.Objects.isNull;

import java.net.URI;
import java.util.Map;
import java.util.function.Consumer;

import javax.annotation.Nullable;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.wl4g.infra.common.remoting.HttpRequestEntity;
import com.wl4g.infra.common.remoting.HttpResponseEntity;
import com.wl4g.infra.common.remoting.Netty4ClientHttpRequestFactory;
import com.wl4g.infra.common.remoting.RestClient;
import com.wl4g.infra.common.remoting.standard.HttpHeaders;

import io.netty.handler.codec.http.HttpMethod;
import lombok.Builder.Default;
import lombok.CustomLog;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

/**
 * {@link DingtalkAPI}
 * 
 * @author James Wong
 * @version 2023-01-06
 * @since v3.0.0
 */
@CustomLog
public class DingtalkAPI {
    private final RestClient restClient;

    public DingtalkAPI() {
        final String configPrefix = DingtalkAPI.class.getSimpleName().toLowerCase();
        final boolean debug = getBooleanProperty(configPrefix + ".debug", false);
        final int connectTimeoutMs = getIntProperty(configPrefix + ".connectTimeout", 0);
        final int readTimeoutMs = getIntProperty(configPrefix + ".readTimeout", 0);
        final int maxResponseSize = getIntProperty(configPrefix + ".maxResponseSize", 0);
        this.restClient = new RestClient(
                new Netty4ClientHttpRequestFactory(debug, connectTimeoutMs, readTimeoutMs, maxResponseSize));
    }

    public DingtalkAPI(final @NotNull RestClient restClient) {
        this.restClient = notNullOf(restClient, "restClient");
    }

    public AccessTokenResult getAccessToken(final @NotNull AccessToken request) {
        return doRequest(ACCESS_TOKEN_URI, HttpMethod.POST, null, request, AccessTokenResult.class, true, false);
    }

    public GetUserIdByMobileResult getUserIdByMobile(final @NotBlank String accessToken, final @NotNull String mobile) {
        return doRequest(format(GET_USERID_BY_MOBILE_URI, accessToken, mobile), HttpMethod.GET, null, null,
                GetUserIdByMobileResult.class, false, false);
    }

    public CreateSceneGroupV2Result createSceneGroupV2(
            final @NotBlank String accessToken,
            final @NotNull CreateSceneGroupV2 request) {
        return doRequest(format(CREATE_GROUP_V2_URI, accessToken), HttpMethod.POST, null, request, CreateSceneGroupV2Result.class,
                true, false);
    }

    public RobotGroupMessagesSendResult sendRobotGroupMessages(
            final @NotBlank String accessToken,
            final @NotNull RobotGroupMessagesSend request) {
        return doRequest(SEND_GROUP_MESSAGE_URI, HttpMethod.POST, accessToken, request, RobotGroupMessagesSendResult.class, true,
                true);
    }

    /**
     * Validation and processing the subscribed group event callback request.
     * 
     * @param aesToken
     * @param aesKey
     * @param corpId
     *            1. 开发者后台配置的订阅事件为应用级事件推送,此时OWNER_KEY为应用的APP_KEY. 
* 2. 调用订阅事件接口订阅的事件为企业级事件推送, * 此时OWNER_KEY为:企业的appkey(企业内部应用)或SUITE_KEY(三方应用) * @param signature * @param timestamp * @param nonce * @param bodyJson * @return * @see https://open.dingtalk.com/document/orgapp-server/configure-event-subcription * @see https://github.com/open-dingtalk/dingtalk-callback-Crypto?spm=ding_open_doc.document.0.0.6ecd7008UVMrZc */ public Map processCallback( final @NotBlank String aesToken, final @NotBlank String aesKey, final @NotBlank String corpId, final @NotBlank String signature, final @NotBlank String timestamp, final @NotBlank String nonce, final @NotBlank String bodyJson, final @NotNull Consumer process) { hasTextOf(aesToken, "aesToken"); hasTextOf(aesKey, "aesKey"); hasTextOf(corpId, "corpId"); hasTextOf(timestamp, "timestamp"); hasTextOf(nonce, "nonce"); hasTextOf(bodyJson, "bodyJson"); notNullOf(process, "process"); try { final JsonNode json = parseToNode(bodyJson); final String encryptMsg = json.requiredAt("/encrypt").asText(); final DingCallbackCrypto crypto = new DingCallbackCrypto(aesToken, aesKey, corpId); final String decryptMsg = crypto.getDecryptMsg(signature, timestamp, nonce, encryptMsg); // 提取回调请求的明文事件JSON数据 final JsonNode eventJson = parseToNode(decryptMsg); log.debug("Received event: {}", eventJson.requiredAt("/EventType").asText()); // 处理事件消息 process.accept(eventJson); // 返回success的加密数据 return crypto.getEncryptedMap("success"); } catch (Exception e) { throw new IllegalArgumentException("Failed to validation subscribed callback request.", e); } } R doRequest( final @NotBlank String url, final @NotNull HttpMethod method, final @Nullable String accessToken, final @Nullable T request, final @NotNull Class resultClass, final boolean addContentJson, final boolean addAccessToken) { notNullOf(method, "method"); notNullOf(resultClass, "resultClass"); final HttpHeaders headers = new HttpHeaders(); if (addContentJson) { headers.add("Content-Type", "application/json"); } if (addAccessToken) { headers.add("x-acs-dingtalk-access-token", accessToken); } final HttpRequestEntity entity = new HttpRequestEntity<>(request, headers, method, URI.create(url)); final HttpResponseEntity result = restClient.exchange(entity, resultClass); return result.getBody(); } public static DingtalkAPI getInstance() { if (isNull(DEFAULT)) { synchronized (DingtalkAPI.class) { if (isNull(DEFAULT)) { DEFAULT = new DingtalkAPI(); } } } return DEFAULT; } @Getter @Setter @NoArgsConstructor @ToString(callSuper = true) public static class BaseResult { String requestId; Integer errcode; String errmsg; Boolean success; } /** * 1. API 获取 accessToken: * https://open.dingtalk.com/document/orgapp-server/obtain-the-access_token-of-an-internal-app?spm=ding_open_doc.document.0.0.545715a7gp83NX
* 2. 要求权限(默认开通): 调用企业API基础权限/(qyapi_base) */ @Getter @Setter @NoArgsConstructor @SuperBuilder @ToString(callSuper = true) public static class AccessToken { @NotBlank String appKey; @NotBlank String appSecret; public AccessToken validate() { hasTextOf(getAppKey(), "appKey"); hasTextOf(getAppSecret(), "appSecret"); return this; } } @Getter @Setter @NoArgsConstructor @ToString(callSuper = true) public static class AccessTokenResult extends BaseResult { @NotNull Integer expireIn; @NotBlank String accessToken; } /** * 1. API 根据手机号获取 userId: * https://open.dingtalk.com/document/orgapp-server/retrieve-userid-from-mobile-phone-number?spm=ding_open_doc.document.0.0.60dc722fpc6eP6#topic-1936808
* 2. 要求权限: 根据手机号姓名获取成员信息的接口访问权限(qyapi_get_member_by_mobile) */ @Getter @Setter @NoArgsConstructor @ToString(callSuper = true) public static class GetUserIdByMobileResult extends BaseResult { String userid; } /** * 1. API 创建场景群: * https://open.dingtalk.com/document/group/create-a-scene-group-v2
* 2. 要求权限: chat相关接口的管理权限(qyapi_chat_manage) */ @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class CreateSceneGroupV2 { @NotBlank @Default String title = "测试群-" + currentTimeMillis(); @NotBlank @JsonProperty("template_id") String templateId; @NotBlank @JsonProperty("owner_user_id") String ownerUserId; @NotBlank @JsonProperty("user_ids") String userIds; @JsonProperty("subadmin_ids") @NotBlank String subadminIds; String uuid; String icon; @JsonProperty("mention_all_authority") @Default String mentionAllAuthority = "0"; @JsonProperty("show_history_type") @Default String showHistoryType = "0"; @JsonProperty("validation_type") @Default String validationType = "0"; @Default String searchable = "0"; @JsonProperty("chat_banned_type") @Default String chatVannedType = "0"; @JsonProperty("management_type") @Default String managementType = "0"; @JsonProperty("only_admin_can_ding") @Default String onlyAdminCanDing = "0"; @JsonProperty("all_members_can_create_mcs_conf") @Default String allMembersCanCreateMcsConf = "1"; @JsonProperty("all_members_can_create_calendar") @Default String allMembersCanCreateCalendar = "0"; @JsonProperty("group_email_disabled") @Default String groupEmailDisabled = "0"; @JsonProperty("only_admin_can_set_msg_top") @Default String onlyAdminCanSetMsgTop = "0"; @JsonProperty("add_friend_forbidden") @Default String addFriendForbidden = "0"; @JsonProperty("group_live_switch") @Default String groupLiveSwitch = "1"; @JsonProperty("members_to_admin_chat") @Default String membersToAdminChat = "0"; public CreateSceneGroupV2 validate() { hasTextOf(getTitle(), "title"); hasTextOf(getTemplateId(), "templateId"); hasTextOf(getOwnerUserId(), "ownerUserId"); hasTextOf(getUserIds(), "userIds"); hasTextOf(getSubadminIds(), "subadminIds"); return this; } } @Getter @Setter @NoArgsConstructor @ToString(callSuper = true) public static class CreateSceneGroupV2Result extends BaseResult { CreateSceneGroupV2ResultContent result; @JsonProperty("request_id") public String getRequestId() { return this.requestId; } } @Getter @Setter @NoArgsConstructor @ToString(callSuper = true) public static class CreateSceneGroupV2ResultContent { @JsonProperty("chat_id") String chatId; @JsonProperty("open_conversation_id") String openConversationId; } /** * 1. API 群发消息(企业内部开发-机器人): * https://open.dingtalk.com/document/group/the-robot-sends-a-group-message
* 2. 要求权限: 企业内机器人发送消息权限(qyapi_robot_sendmsg) */ @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class RobotGroupMessagesSend { @NotBlank MsgKeyType msgKey; @NotBlank String msgParam; @NotBlank String openConversationId; @NotBlank String robotCode; public RobotGroupMessagesSend validate() { notNullOf(getMsgKey(), "msgKey"); hasTextOf(getMsgParam(), "msgParam"); hasTextOf(getOpenConversationId(), "openConversationId"); hasTextOf(getRobotCode(), "robotCode"); return this; } } @Getter @Setter @NoArgsConstructor @ToString(callSuper = true) public static class RobotGroupMessagesSendResult extends BaseResult { String processQueryKey; } /** * 参见(发送内容格式定义): * https://open.dingtalk.com/document/group/message-types-and-data-format */ public static enum MsgKeyType { sampleText, sampleMarkdown, sampleImageMsg, sampleLink, sampleActionCard, sampleActionCard2, sampleActionCard3, sampleActionCard4, sampleActionCard5, sampleActionCard6 } // @formatter:off //@Schema(oneOf = { SimpleTextMsgParam.class, SimpleMarkdownMsgParam.class, SimpleImageMsgParam.class, SimpleLinkMsgParam.class, // SampleActionCardParam.class, SampleActionCard2Param.class, SampleActionCard3Param.class, SampleActionCard4Param.class, // SampleActionCard5Param.class, SampleActionCard6Param.class }, discriminatorProperty = "@type") //@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type", visible = true) //@JsonSubTypes({ @Type(value = SimpleTextMsgParam.class, name = "sampleText"), // @Type(value = SimpleMarkdownMsgParam.class, name = "sampleMarkdown"), // @Type(value = SimpleImageMsgParam.class, name = "sampleImageMsg"), // @Type(value = SimpleLinkMsgParam.class, name = "sampleLink"), // @Type(value = SampleActionCardParam.class, name = "sampleActionCard"), // @Type(value = SampleActionCard2Param.class, name = "sampleActionCard2"), // @Type(value = SampleActionCard3Param.class, name = "sampleActionCard3"), // @Type(value = SampleActionCard4Param.class, name = "sampleActionCard4"), // @Type(value = SampleActionCard5Param.class, name = "sampleActionCard5"), // @Type(value = SampleActionCard6Param.class, name = "sampleActionCard6") }) // @formatter:on @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static abstract class MsgParamBase { // @formatter:off //@Schema(name = "@type", implementation = MsgKeyType.class) //@JsonProperty(value = "@type") //@NotBlank @EnumValue(enumCls = MsgKeyType.class) MsgKeyType type; // @formatter:on } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SimpleTextMsgParam extends MsgParamBase { @NotBlank String content; public SimpleTextMsgParam validate() { hasTextOf(getContent(), "content"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SimpleMarkdownMsgParam extends MsgParamBase { @NotBlank String title; @NotBlank String text; public SimpleMarkdownMsgParam validate() { hasTextOf(getTitle(), "title"); hasTextOf(getText(), "text"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SimpleImageMsgParam extends MsgParamBase { @NotBlank String photoURL; public SimpleImageMsgParam validate() { hasTextOf(getPhotoURL(), "photoURL"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SimpleLinkMsgParam extends MsgParamBase { @NotBlank String title; @NotBlank String text; @NotBlank String picUrl; @NotBlank String messageUrl; public SimpleLinkMsgParam validate() { hasTextOf(getTitle(), "title"); hasTextOf(getText(), "text"); hasTextOf(getPicUrl(), "picUrl"); hasTextOf(getMessageUrl(), "messageUrl"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SampleActionCardParam extends MsgParamBase { @NotBlank String title; @NotBlank String text; @NotBlank String singleTitle; @NotBlank String singleURL; public SampleActionCardParam validate() { hasTextOf(getTitle(), "title"); hasTextOf(getText(), "text"); hasTextOf(getSingleTitle(), "singleTitle"); hasTextOf(getSingleURL(), "singleURL"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SampleActionCard2Param extends MsgParamBase { @NotBlank String title; @NotBlank String text; @NotBlank String actionTitle1; @NotBlank String actionURL1; @NotBlank String actionTitle2; @NotBlank String actionURL2; public SampleActionCard2Param validate() { hasTextOf(getTitle(), "title"); hasTextOf(getText(), "text"); hasTextOf(getActionTitle1(), "actionTitle1"); hasTextOf(getActionURL1(), "actionURL1"); hasTextOf(getActionTitle2(), "actionTitle2"); hasTextOf(getActionURL2(), "actionURL2"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SampleActionCard3Param extends MsgParamBase { @NotBlank String title; @NotBlank String text; @NotBlank String actionTitle1; @NotBlank String actionURL1; @NotBlank String actionTitle2; @NotBlank String actionURL2; @NotBlank String actionTitle3; @NotBlank String actionURL3; public SampleActionCard3Param validate() { hasTextOf(getTitle(), "title"); hasTextOf(getText(), "text"); hasTextOf(getActionTitle1(), "actionTitle1"); hasTextOf(getActionURL1(), "actionURL1"); hasTextOf(getActionTitle2(), "actionTitle2"); hasTextOf(getActionURL2(), "actionURL2"); hasTextOf(getActionTitle3(), "actionTitle3"); hasTextOf(getActionURL3(), "actionURL3"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SampleActionCard4Param extends MsgParamBase { @NotBlank String title; @NotBlank String text; @NotBlank String actionTitle1; @NotBlank String actionURL1; @NotBlank String actionTitle2; @NotBlank String actionURL2; @NotBlank String actionTitle3; @NotBlank String actionURL3; @NotBlank String actionTitle4; @NotBlank String actionURL4; public SampleActionCard4Param validate() { hasTextOf(getTitle(), "title"); hasTextOf(getText(), "text"); hasTextOf(getActionTitle1(), "actionTitle1"); hasTextOf(getActionURL1(), "actionURL1"); hasTextOf(getActionTitle2(), "actionTitle2"); hasTextOf(getActionURL2(), "actionURL2"); hasTextOf(getActionTitle3(), "actionTitle3"); hasTextOf(getActionURL3(), "actionURL3"); hasTextOf(getActionTitle4(), "actionTitle4"); hasTextOf(getActionURL4(), "actionURL4"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SampleActionCard5Param extends MsgParamBase { @NotBlank String title; @NotBlank String text; @NotBlank String actionTitle1; @NotBlank String actionURL1; @NotBlank String actionTitle2; @NotBlank String actionURL2; @NotBlank String actionTitle3; @NotBlank String actionURL3; @NotBlank String actionTitle4; @NotBlank String actionURL4; @NotBlank String actionTitle5; @NotBlank String actionURL5; public SampleActionCard5Param validate() { hasTextOf(getTitle(), "title"); hasTextOf(getText(), "text"); hasTextOf(getActionTitle1(), "actionTitle1"); hasTextOf(getActionURL1(), "actionURL1"); hasTextOf(getActionTitle2(), "actionTitle2"); hasTextOf(getActionURL2(), "actionURL2"); hasTextOf(getActionTitle3(), "actionTitle3"); hasTextOf(getActionURL3(), "actionURL3"); hasTextOf(getActionTitle4(), "actionTitle4"); hasTextOf(getActionURL4(), "actionURL4"); hasTextOf(getActionTitle5(), "actionTitle5"); hasTextOf(getActionURL5(), "actionURL5"); return this; } } @Getter @Setter @SuperBuilder @NoArgsConstructor @ToString(callSuper = true) public static class SampleActionCard6Param extends MsgParamBase { @NotBlank String title; @NotBlank String text; @NotBlank String buttonTitle1; @NotBlank String buttonUrl1; @NotBlank String buttonTitle2; @NotBlank String buttonUrl2; public SampleActionCard6Param validate() { hasTextOf(getTitle(), "title"); hasTextOf(getText(), "text"); hasTextOf(getButtonTitle1(), "buttonTitle1"); hasTextOf(getButtonUrl1(), "buttonUrl1"); hasTextOf(getButtonTitle2(), "buttonTitle2"); hasTextOf(getButtonUrl2(), "buttonUrl2"); return this; } } private static DingtalkAPI DEFAULT; static final String ACCESS_TOKEN_URI = "https://api.dingtalk.com/v1.0/oauth2/accessToken"; static final String GET_USERID_BY_MOBILE_URI = "https://oapi.dingtalk.com/user/get_by_mobile?access_token=%s&mobile=%s"; static final String CREATE_GROUP_V2_URI = "https://oapi.dingtalk.com/topapi/im/chat/scenegroup/create?access_token=%s"; static final String SEND_GROUP_MESSAGE_URI = "https://api.dingtalk.com/v1.0/robot/groupMessages/send"; }