From 910e50ae17327b982f170044424af6ef1bf12e9f Mon Sep 17 00:00:00 2001 From: "LAPTOP-SB56SG4Q\\86185" Date: Tue, 2 Nov 2021 11:08:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=BC=80=E6=BA=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=9D=90=E6=96=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- JSD-8604需求确认书.docx | Bin 0 -> 40447 bytes README.md | 5 +- plugin.xml | 26 ++ .../dingding/webhook/OutputDBAccess.java | 43 +++ .../webhook/OutputPluginLifecycleMonitor.java | 45 +++ .../dingding/webhook/PluginConstants.java | 11 + .../dingding/webhook/dao/WebHookDao.java | 21 ++ .../webhook/entity/OutputWebHook.java | 62 ++++ .../webhook/entity/WebHookEntity.java | 48 +++ .../webhook/format/PNGOutputFormat.java | 40 ++ .../dingding/webhook/fun/TicketFun.java | 66 ++++ .../webhook/handle/WebHookHandle.java | 116 ++++++ .../plugin/dingding/webhook/js/FileDef.java | 54 +++ .../dingding/webhook/js/JSCSSBridge.java | 25 ++ .../dingding/webhook/util/DesECBUtil.java | 66 ++++ .../dingding/webhook/util/HttpUtil.java | 133 +++++++ .../dingding/webhook/util/HttpsUtil.java | 346 ++++++++++++++++++ .../com/fr/plugin/dingding/webhook/theme.js | 71 ++++ 18 files changed, 1177 insertions(+), 1 deletion(-) create mode 100644 JSD-8604需求确认书.docx create mode 100644 plugin.xml create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/OutputDBAccess.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/OutputPluginLifecycleMonitor.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/PluginConstants.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/dao/WebHookDao.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/entity/OutputWebHook.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/entity/WebHookEntity.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/format/PNGOutputFormat.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/fun/TicketFun.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/handle/WebHookHandle.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/js/FileDef.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/js/JSCSSBridge.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/util/DesECBUtil.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/util/HttpUtil.java create mode 100644 src/main/java/com/fr/plugin/dingding/webhook/util/HttpsUtil.java create mode 100644 src/main/resources/com/fr/plugin/dingding/webhook/theme.js diff --git a/JSD-8604需求确认书.docx b/JSD-8604需求确认书.docx new file mode 100644 index 0000000000000000000000000000000000000000..214019c94ec7dd28fecc2300ea60cb451769a075 GIT binary patch literal 40447 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
+ com.fr.plugin.dingding.webhook + + yes + 1.4 + 10.0 + 2018-07-31 + fr.open + + + [2021-09-14]【1.1】重新打包
+ [2021-09-15]【1.2】增加定时调度链接传参
+ [2021-09-15]【1.3】增加获取默认图片
+ [2021-09-15]【1.4】增加授权和增加获取ticket的函数
+ ]]>
+ + + + + + + + + +
\ No newline at end of file diff --git a/src/main/java/com/fr/plugin/dingding/webhook/OutputDBAccess.java b/src/main/java/com/fr/plugin/dingding/webhook/OutputDBAccess.java new file mode 100644 index 0000000..d0328bf --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/OutputDBAccess.java @@ -0,0 +1,43 @@ +package com.fr.plugin.dingding.webhook; + +import com.fr.decision.plugin.db.AbstractDecisionDBAccessProvider; +import com.fr.plugin.dingding.webhook.dao.WebHookDao; +import com.fr.plugin.dingding.webhook.entity.WebHookEntity; +import com.fr.stable.db.accessor.DBAccessor; +import com.fr.stable.db.dao.BaseDAO; +import com.fr.stable.db.dao.DAOProvider; + +/** + * @Author fr,open + * @Date 2020/9/15 + * @Description + **/ +public class OutputDBAccess extends AbstractDecisionDBAccessProvider { + + private static DBAccessor dbAccessor; + public DBAccessor getDbAccessor() { + return dbAccessor; + } + + @Override + public DAOProvider[] registerDAO() { + return new DAOProvider[]{ + new DAOProvider() { + @Override + public Class getEntityClass() { + return WebHookEntity.class; + } + + @Override + public Class getDAOClass() { + return WebHookDao.class; + } + } + }; + } + + @Override + public void onDBAvailable(DBAccessor dbAccessor) { + OutputDBAccess.dbAccessor = dbAccessor; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/OutputPluginLifecycleMonitor.java b/src/main/java/com/fr/plugin/dingding/webhook/OutputPluginLifecycleMonitor.java new file mode 100644 index 0000000..ad24f59 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/OutputPluginLifecycleMonitor.java @@ -0,0 +1,45 @@ +package com.fr.plugin.dingding.webhook; + +import com.fr.intelli.record.Focus; +import com.fr.intelli.record.Original; +import com.fr.plugin.context.PluginContext; +import com.fr.plugin.context.PluginContexts; +import com.fr.plugin.dingding.webhook.entity.OutputWebHook; +import com.fr.plugin.dingding.webhook.entity.WebHookEntity; +import com.fr.plugin.dingding.webhook.format.PNGOutputFormat; +import com.fr.plugin.dingding.webhook.handle.WebHookHandle; +import com.fr.plugin.observer.inner.AbstractPluginLifecycleMonitor; +import com.fr.schedule.extension.report.job.output.BaseOutputFormat; +import com.fr.schedule.feature.ScheduleOutputActionEntityRegister; +import com.fr.schedule.feature.output.OutputActionHandler; +import com.fr.stable.fun.Authorize; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +@Authorize(callSignKey = PluginConstants.PLUGIN_ID) +public class OutputPluginLifecycleMonitor extends AbstractPluginLifecycleMonitor { + @Override + @Focus(id = PluginConstants.PLUGIN_ID, text = "钉钉webhook", source = Original.PLUGIN) + public void afterRun(PluginContext pluginContext) { + if (!PluginContexts.currentContext().isAvailable()) { + return; + } + BaseOutputFormat.registerOutputFormat(new PNGOutputFormat()); + OutputActionHandler.registerHandler(new WebHookHandle(), OutputWebHook.class.getName()); + ScheduleOutputActionEntityRegister.getInstance().addClass(WebHookEntity.class); + } + + @Override + @Focus(id = PluginConstants.PLUGIN_ID, text = "钉钉webhook", source = Original.PLUGIN) + public void beforeStop(PluginContext pluginContext) { + if (!PluginContexts.currentContext().isAvailable()) { + return; + } + BaseOutputFormat.removeOutputFormat(PNGOutputFormat.CONVERT_TO_PNG); + OutputActionHandler.removeOutputHandler(OutputWebHook.class.getName()); + ScheduleOutputActionEntityRegister.getInstance().removeClass(WebHookEntity.class); + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/PluginConstants.java b/src/main/java/com/fr/plugin/dingding/webhook/PluginConstants.java new file mode 100644 index 0000000..de46511 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/PluginConstants.java @@ -0,0 +1,11 @@ +package com.fr.plugin.dingding.webhook; + +/** + * @Author fr.open + * @Date 2021/3/1 + * @Description + **/ +public class PluginConstants { + + public static final String PLUGIN_ID = "com.fr.plugin.dingding.webhook"; +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/dao/WebHookDao.java b/src/main/java/com/fr/plugin/dingding/webhook/dao/WebHookDao.java new file mode 100644 index 0000000..0940f15 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/dao/WebHookDao.java @@ -0,0 +1,21 @@ +package com.fr.plugin.dingding.webhook.dao; + +import com.fr.plugin.dingding.webhook.entity.WebHookEntity; +import com.fr.stable.db.dao.BaseDAO; +import com.fr.stable.db.session.DAOSession; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +public class WebHookDao extends BaseDAO { + public WebHookDao(DAOSession daoSession) { + super(daoSession); + } + + @Override + protected Class getEntityClass() { + return WebHookEntity.class; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/entity/OutputWebHook.java b/src/main/java/com/fr/plugin/dingding/webhook/entity/OutputWebHook.java new file mode 100644 index 0000000..ee8f533 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/entity/OutputWebHook.java @@ -0,0 +1,62 @@ +package com.fr.plugin.dingding.webhook.entity; + +import com.fr.schedule.base.bean.output.BaseOutputAction; +import com.fr.schedule.base.entity.AbstractScheduleEntity; +import com.fr.schedule.base.type.RunType; +import com.fr.third.fasterxml.jackson.annotation.JsonSubTypes; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +@JsonSubTypes.Type(value = OutputWebHook.class, name = "OutputWebHook") +public class OutputWebHook extends BaseOutputAction { + + private static final long serialVersionUID = 8921116228585639504L; + + private String hookUrl = null; + + public OutputWebHook() { + super(); + } + + @Override + public boolean willExecuteByUser() { + return false; + } + + @Override + public RunType runType() { + return RunType.SEND_FILE; + } + + @Override + public Class outputActionEntityClass() { + return WebHookEntity.class; + } + + @Override + public AbstractScheduleEntity createOutputActionEntity() { + return (new WebHookEntity()).id(this.getId()).hookUrl(this.hookUrl); + } + + @Override + public OutputWebHook id(String id) { + setId(id); + return this; + } + + public String getHookUrl() { + return hookUrl; + } + + public void setHookUrl(String hookUrl) { + this.hookUrl = hookUrl; + } + + public OutputWebHook hookUrl(String hookUrl) { + setHookUrl(hookUrl); + return this; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/entity/WebHookEntity.java b/src/main/java/com/fr/plugin/dingding/webhook/entity/WebHookEntity.java new file mode 100644 index 0000000..6848e5d --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/entity/WebHookEntity.java @@ -0,0 +1,48 @@ +package com.fr.plugin.dingding.webhook.entity; + +import com.fr.schedule.base.bean.BaseBean; +import com.fr.schedule.base.entity.AbstractScheduleEntity; +import com.fr.stable.db.entity.TableAssociation; +import com.fr.third.javax.persistence.Column; +import com.fr.third.javax.persistence.Entity; +import com.fr.third.javax.persistence.Table; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +@Entity +@Table(name = "fine_output_dingding_webHook") //表名 +@TableAssociation(associated = true) +public class WebHookEntity extends AbstractScheduleEntity { + + @Column(name = "hookUrl") + private String hookUrl; + + public WebHookEntity() { + } + + @Override + public BaseBean createBean() { + return new OutputWebHook().id(this.getId()).hookUrl(this.hookUrl); + } + + public String getHookUrl() { + return hookUrl; + } + + public void setHookUrl(String hookUrl) { + this.hookUrl = hookUrl; + } + + public WebHookEntity hookUrl(String hookUrl) { + setHookUrl(hookUrl); + return this; + } + + public WebHookEntity id(String id) { + setId(id); + return this; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/format/PNGOutputFormat.java b/src/main/java/com/fr/plugin/dingding/webhook/format/PNGOutputFormat.java new file mode 100644 index 0000000..2329713 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/format/PNGOutputFormat.java @@ -0,0 +1,40 @@ +package com.fr.plugin.dingding.webhook.format; + +import com.fr.io.exporter.ImageExporter; +import com.fr.main.workbook.ResultWorkBook; +import com.fr.schedule.extension.report.job.output.BaseOutputFormat; + +import java.io.OutputStream; +import java.util.List; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +public class PNGOutputFormat extends BaseOutputFormat { + public static final int CONVERT_TO_PNG = 64; + + public PNGOutputFormat() { + } + + @Override + public int getFormat() { + return CONVERT_TO_PNG; + } + + @Override + public String getFileSuffix() { + return ".png"; + } + + @Override + public boolean withParentPath() { + return true; + } + + @Override + public void flushWithParentPath(OutputStream var1, ResultWorkBook var2, String var3, final List var4) throws Exception { + new ImageExporter("png", 96).export(var1, var2); + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/fun/TicketFun.java b/src/main/java/com/fr/plugin/dingding/webhook/fun/TicketFun.java new file mode 100644 index 0000000..006f465 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/fun/TicketFun.java @@ -0,0 +1,66 @@ +package com.fr.plugin.dingding.webhook.fun; + +import com.fr.general.http.HttpRequest; +import com.fr.general.http.HttpToolbox; +import com.fr.intelli.record.Focus; +import com.fr.intelli.record.Original; +import com.fr.log.FineLoggerFactory; +import com.fr.plugin.context.PluginContexts; +import com.fr.plugin.dingding.webhook.PluginConstants; +import com.fr.script.AbstractFunction; +import com.fr.stable.ArrayUtils; +import com.fr.stable.Primitive; +import com.fr.stable.StringUtils; +import com.fr.stable.exception.FormulaException; +import com.fr.stable.fun.Authorize; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @Author fr.open + * @Date 2021/9/15 + * @Description + **/ +@Authorize(callSignKey = PluginConstants.PLUGIN_ID) +public class TicketFun extends AbstractFunction { + @Override + @Focus(id = PluginConstants.PLUGIN_ID, text = "钉钉webhook", source = Original.PLUGIN) + public Object run(Object[] args) throws FormulaException { + if (!PluginContexts.currentContext().isAvailable()) { + return null; + } + int len = ArrayUtils.getLength(args); + if (len == 0) { + return Primitive.ERROR_VALUE; + } + String user = (String) args[0]; + String target = (String) args[1]; + + if(StringUtils.isBlank(user) || StringUtils.isBlank(target)){ + return Primitive.ERROR_VALUE; + } + String url =StringUtils.EMPTY; + if(args.length < 3){ + url = "http://xxxx/trusted"; + }else { + url = (String) args[2]; + if(StringUtils.isBlank(url)){ + url = "http://xxxx/trusted"; + } + } + + Map param = new HashMap<>(); + param.put("username",user); + param.put("target_site",target); + try { + String execute = HttpToolbox.executeAndParse(HttpRequest.custom().post(param).url(url).build()); + return execute; + } catch (IOException e) { + FineLoggerFactory.getLogger().error(e.getMessage(),e); + } + + return null; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/handle/WebHookHandle.java b/src/main/java/com/fr/plugin/dingding/webhook/handle/WebHookHandle.java new file mode 100644 index 0000000..52adae0 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/handle/WebHookHandle.java @@ -0,0 +1,116 @@ +package com.fr.plugin.dingding.webhook.handle; + +import com.fr.base.Formula; +import com.fr.base.PropertiesUtils; +import com.fr.io.utils.ResourceIOUtils; +import com.fr.json.JSONObject; +import com.fr.log.FineLoggerFactory; +import com.fr.plugin.dingding.webhook.entity.OutputWebHook; +import com.fr.plugin.dingding.webhook.util.DesECBUtil; +import com.fr.plugin.dingding.webhook.util.HttpUtil; +import com.fr.schedule.base.constant.ScheduleConstants; +import com.fr.schedule.feature.output.OutputActionHandler; +import com.fr.stable.CodeUtils; +import com.fr.stable.StringUtils; + +import java.net.URLEncoder; +import java.util.List; +import java.util.Map; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +public class WebHookHandle extends OutputActionHandler { + private static final String key = "xxxx"; + + private static final String file = "/resources/Robot.png"; + + @Override + public void doAction(OutputWebHook outputWebHook, Map map) throws Exception { + String[] files = (String[]) map.get(ScheduleConstants.OUTPUT_FILES); + getUrl(outputWebHook, map); + String mediaId = StringUtils.EMPTY; + String imageFile = StringUtils.EMPTY; + if(ResourceIOUtils.exist(file)){ + FineLoggerFactory.getLogger().info("read default image {}",file); + imageFile = file; + }else { + for (String file : files) { + if (file.endsWith("png")) { + imageFile = file; + continue; + } + } + } + + JSONObject object = HttpUtil.uploadMedia(imageFile); + FineLoggerFactory.getLogger().info("upload media res is {}", object); + if (HttpUtil.isOk(object)) { + mediaId = object.getJSONObject("url").getString("media_id"); + } + if (StringUtils.isBlank(mediaId)) { + throw new Exception("not contain media or upload failed"); + } + String taskName = CodeUtils.encodeURIComponent((String) map.get(ScheduleConstants.TASK_NAME)); + String path = CodeUtils.encodeURIComponent((String) map.get(ScheduleConstants.SAVE_DIRECTORY)); + path = path.replaceAll("\\+", "%20"); + String showType = (String) map.get(ScheduleConstants.SHOW_TYPE); + int taskType = (Integer) map.get(ScheduleConstants.TASK_TYPE); + String scheduleUsername = (String) map.get(ScheduleConstants.USERNAME); + String resultUrl = getParam(map, "resultUrl"); + String mobileUrl = outputWebHook.getResultURL()+"/url/mobile/schedule/result?taskName=" + taskName + "&username=" + scheduleUsername + "&path=" + path + "&showType=" + showType + "&taskType=" + taskType; + HttpUtil.sendWebhook(mediaId, outputWebHook.getHookUrl(), getParam(map, "title"), getParam(map, "text"), delaSign(StringUtils.isNotBlank(resultUrl)?resultUrl:mobileUrl)); + } + + private String delaSign(String url) { + String path = url; + try { + String user = PropertiesUtils.getProperties("dingtalk").getProperty("resultUser"); + String encode = URLEncoder.encode(DesECBUtil.encryptDES(user, key), "UTF-8"); + if (path.indexOf("?") != -1) { + path += "&result_sign=" + encode; + } else { + path += "?result_sign=" + encode; + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + FineLoggerFactory.getLogger().info("push dingding url is {}",path); + return path+"&op=h5"; + } + + private String getParam(Map map, String p) { + List list = (List) map.get(ScheduleConstants.RECORD_LIST); + Object title = StringUtils.EMPTY; + Object param = list.get(0).get(p); + if (param instanceof Formula) { + Formula formula = (Formula) param; + title = formula.getResult(); + } else { + title = param.toString(); + } + return title.toString(); + } + + private String getUrl(OutputWebHook outputWebHook, Map map) { + String taskName = CodeUtils.encodeURIComponent((String) map.get(ScheduleConstants.TASK_NAME)); + String path = CodeUtils.encodeURIComponent((String) map.get(ScheduleConstants.SAVE_DIRECTORY)); + path = path.replaceAll("\\+", "%20"); + String showType = (String) map.get(ScheduleConstants.SHOW_TYPE); + int taskType = (Integer) map.get(ScheduleConstants.TASK_TYPE); + String scheduleUsername = (String) map.get(ScheduleConstants.USERNAME); + return outputWebHook.getResultURL() + "/url/mobile/schedule/result?taskName=" + taskName + "&username=" + scheduleUsername + "&path=" + path + "&showType=" + showType + "&taskType=" + taskType; + } + + private JSONObject getJSONObject(String md5, String base) { + JSONObject obj = JSONObject.create(); + obj.put("msgtype", "image"); + JSONObject image = JSONObject.create(); + image.put("base64", base); + image.put("md5", md5); + obj.put("image", image); + return obj; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/js/FileDef.java b/src/main/java/com/fr/plugin/dingding/webhook/js/FileDef.java new file mode 100644 index 0000000..db245c5 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/js/FileDef.java @@ -0,0 +1,54 @@ +package com.fr.plugin.dingding.webhook.js; + +import com.fr.plugin.transform.ExecuteFunctionRecord; +import com.fr.web.struct.Component; +import com.fr.web.struct.Filter; +import com.fr.web.struct.browser.RequestClient; +import com.fr.web.struct.category.ScriptPath; +import com.fr.web.struct.category.StylePath; + +/** + * @author fr.open + * @date 2021/9/15 + */ +public class FileDef extends Component { + public static final FileDef KEY = new FileDef(); + private FileDef(){} + /** + * 返回需要引入的JS脚本路径 + * @param client 请求客户端描述 + * @return JS脚本路径 + */ + @Override + public ScriptPath script(RequestClient client ) { + //如果不需要就直接返回 ScriptPath.EMPTY + return ScriptPath.build("com/fr/plugin/dingding/webhook/theme.js"); + } + + /** + * 返回需要引入的CSS样式路径 + * @param client 请求客户端描述 + * @return CSS样式路径 + */ + @Override + public StylePath style(RequestClient client ) { + //如果不需要就直接返回 StylePath.EMPTY; + return StylePath.EMPTY; + } + + /** + * 通过给定的资源过滤器控制是否加载这个资源 + * @return 资源过滤器 + */ + @ExecuteFunctionRecord + @Override + public Filter filter() { + return new Filter(){ + @Override + public boolean accept() { + //任何情况下我们都在平台组件加载时加载我们的组件 + return true; + } + }; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/js/JSCSSBridge.java b/src/main/java/com/fr/plugin/dingding/webhook/js/JSCSSBridge.java new file mode 100644 index 0000000..0723c8f --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/js/JSCSSBridge.java @@ -0,0 +1,25 @@ +package com.fr.plugin.dingding.webhook.js; + +import com.fr.decision.fun.impl.AbstractWebResourceProvider; +import com.fr.decision.web.MainComponent; +import com.fr.plugin.transform.FunctionRecorder; +import com.fr.web.struct.Atom; + +/** + * @author fr.open + * @date 2021/9/15 + */ +@FunctionRecorder +public class JSCSSBridge extends AbstractWebResourceProvider { + @Override + public Atom attach() { + //在平台主组件加载时添加我们自己的组件 + return MainComponent.KEY; + } + + @Override + public Atom client() { + //我们自己要引入的组件 + return FileDef.KEY; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/util/DesECBUtil.java b/src/main/java/com/fr/plugin/dingding/webhook/util/DesECBUtil.java new file mode 100644 index 0000000..16db2f9 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/util/DesECBUtil.java @@ -0,0 +1,66 @@ +package com.fr.plugin.dingding.webhook.util; + +import com.fr.third.org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +public class DesECBUtil { + /** + * 加密数据 + * + * @param encryptString + * @param encryptKey + * @return + * @throws Exception + */ + public static String encryptDES(String encryptString, String encryptKey) throws Exception { + Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(getKey(encryptKey), "DES")); + byte[] encryptedData = cipher.doFinal(encryptString.getBytes("UTF-8")); + return Base64.encodeBase64String(encryptedData); + } + + /** + * key 不足8位补位 + * + * @param + */ + public static byte[] getKey(String keyRule) { + Key key = null; + byte[] keyByte = keyRule.getBytes(); + // 创建一个空的八位数组,默认情况下为0 + byte[] byteTemp = new byte[8]; + // 将用户指定的规则转换成八位数组 + for (int i = 0; i < byteTemp.length && i < keyByte.length; i++) { + byteTemp[i] = keyByte[i]; + } + key = new SecretKeySpec(byteTemp, "DES"); + return key.getEncoded(); + } + + /*** + * 解密数据 + * @param decryptString + * @param decryptKey + * @return + * @throws Exception + */ + + public static String decryptDES(String decryptString, String decryptKey) throws Exception { + byte[] sourceBytes = Base64.decodeBase64(decryptString); + Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(getKey(decryptKey), "DES")); + byte[] decoded = cipher.doFinal(sourceBytes); + return new String(decoded, "UTF-8"); + + } + +} + diff --git a/src/main/java/com/fr/plugin/dingding/webhook/util/HttpUtil.java b/src/main/java/com/fr/plugin/dingding/webhook/util/HttpUtil.java new file mode 100644 index 0000000..e97b9bc --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/util/HttpUtil.java @@ -0,0 +1,133 @@ +package com.fr.plugin.dingding.webhook.util; + +import com.fr.general.PropertiesUtils; +import com.fr.io.utils.ResourceIOUtils; +import com.fr.json.JSONObject; +import com.fr.log.FineLoggerFactory; +import com.fr.stable.StringUtils; +import com.fr.third.org.apache.http.NameValuePair; +import com.fr.third.org.apache.http.client.entity.UrlEncodedFormEntity; +import com.fr.third.org.apache.http.entity.ContentType; +import com.fr.third.org.apache.http.entity.mime.HttpMultipartMode; +import com.fr.third.org.apache.http.entity.mime.MultipartEntityBuilder; +import com.fr.third.org.apache.http.message.BasicNameValuePair; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +public class HttpUtil { + + + public static JSONObject uploadMedia(String path) { + JSONObject result = null; + InputStream fileInputStream = ResourceIOUtils.read(path); + String fileName = ResourceIOUtils.getName(path); + // 定时调度图片没有后缀名,用钉钉上传文件接口以type=image的方式传会返回错误,这里伪造一个后缀 + fileName = addSuffixToScheduleImage(fileName, ".jpg"); + if (fileInputStream == null || StringUtils.isEmpty(fileName)) { + FineLoggerFactory.getLogger().warn(String.format("%s路径下未能找到文件!", path)); + } else { + try { + MultipartEntityBuilder builder = MultipartEntityBuilder.create() + .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .setContentType(ContentType.MULTIPART_FORM_DATA) + .addBinaryBody("media", ResourceIOUtils.read(path), ContentType.MULTIPART_FORM_DATA, new String(fileName.getBytes("UTF-8"), "ISO_8859_1")); + Map header = new HashMap(); + header.put("Content-type", "multipart/form-data"); + String res = HttpsUtil.doEntityPost(uploadMediaUrl("image"),null,builder.build()); + result = new JSONObject(res); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(),e); + } + } + return result; + } + + public static String uploadMediaUrl(String type) { + return String.format(getBaseUrl() + "/addons/sjwdingtalk/msg/upload?xxxx=%s&type=%s", getCode(), type); + } + + private static String getCode() { + String property = PropertiesUtils.getProperties("dingtalk").getProperty("xxxx"); + return property; + } + + private static String getBaseUrl() { + String property = PropertiesUtils.getProperties("dingtalk").getProperty("host"); + return property; + } + + public static String addSuffixToScheduleImage(String fileName, String suffix) { + if (!StringUtils.contains(fileName, ".")) { + return fileName + suffix; + } + return fileName; + } + + public static boolean isOk(JSONObject result) { + return result != null && result.optInt("code", -999) + ==1; + } + + public static String sendMessageUrl(String token) { + return getBaseUrl() + "/addons/sjwdingtalk/msg/send?xxxx=" + token; + } + + public static String sendWebhook( String meadia, String hookUrl,String title, String text, String url) throws IOException { + String path = String.format(getBaseUrl() + "/api/cloudapi/sendrobot?xxxx=%s", getCode()); + + String result = null; + try { + List params = new ArrayList(); + params.add(new BasicNameValuePair("webhook", hookUrl)); + params.add(new BasicNameValuePair("x_xxxx", getCode())); + params.add(new BasicNameValuePair("msgtype", "card")); + params.add(new BasicNameValuePair("media_id", meadia)); + params.add(new BasicNameValuePair("title", title)); + params.add(new BasicNameValuePair("text", text)); + params.add(new BasicNameValuePair("singleURL", url)); + UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(params, "UTF-8"); + Map header = new HashMap(); + header.put("Content-type", "application/x-www-form-urlencoded"); + result = HttpsUtil.doEntityPost(path,header,urlEncodedFormEntity); + FineLoggerFactory.getLogger().info("send webhook result is {}", result); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + throw e; + } + return result; + } + + public static void getMessage(String msgid) throws Exception { + List params = new ArrayList(); + params.add(new BasicNameValuePair("msg_id", msgid)); + UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(params, "UTF-8"); + Map header = new HashMap(); + header.put("Content-type", "application/x-www-form-urlencoded"); + String result = null; + try { + result = HttpsUtil.doEntityPost(messageUrl(getCode()),header,urlEncodedFormEntity); + FineLoggerFactory.getLogger().info("query message result is {}", result); + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + throw e; + } + JSONObject res = new JSONObject(result); + if (res.getInt("code") == 0) { + throw new Exception("get message error is" + res.getString("msg")); + } + } + + private static String messageUrl(String token) { + return getBaseUrl() + "/addons/sjwdingtalk/msg/query?xxxx=" + token; + } +} diff --git a/src/main/java/com/fr/plugin/dingding/webhook/util/HttpsUtil.java b/src/main/java/com/fr/plugin/dingding/webhook/util/HttpsUtil.java new file mode 100644 index 0000000..8dd3919 --- /dev/null +++ b/src/main/java/com/fr/plugin/dingding/webhook/util/HttpsUtil.java @@ -0,0 +1,346 @@ +package com.fr.plugin.dingding.webhook.util; + +import com.fr.json.JSONObject; +import com.fr.log.FineLoggerFactory; +import com.fr.third.org.apache.http.HttpEntity; +import com.fr.third.org.apache.http.HttpResponse; +import com.fr.third.org.apache.http.HttpStatus; +import com.fr.third.org.apache.http.client.HttpClient; +import com.fr.third.org.apache.http.client.methods.HttpPost; +import com.fr.third.org.apache.http.config.Registry; +import com.fr.third.org.apache.http.config.RegistryBuilder; +import com.fr.third.org.apache.http.conn.socket.ConnectionSocketFactory; +import com.fr.third.org.apache.http.conn.socket.PlainConnectionSocketFactory; +import com.fr.third.org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import com.fr.third.org.apache.http.entity.StringEntity; +import com.fr.third.org.apache.http.impl.client.HttpClients; +import com.fr.third.org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import com.fr.third.org.apache.http.util.EntityUtils; + +import javax.net.ssl.*; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.security.cert.CertificateException; +import java.util.Iterator; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @Author fr.open + * @Date 2020/9/15 + * @Description + **/ +public class HttpsUtil { + + private static HostnameVerifier hv = new HostnameVerifier() { + @Override + public boolean verify(String urlHostName, SSLSession session) { + System.out.println("Warning: URL Host: " + urlHostName + " vs. " + + session.getPeerHost()); + return true; + } + }; + + /** + * 发送get请求 + * + * @param url + * @param param + * @param header + * @return + * @throws IOException + */ + public static String sendGet(String url, Map param, Map header) { + String result = ""; + BufferedReader in = null; + String urlNameString = url; + try { + if (param != null) { + urlNameString += "?"; + urlNameString += param.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } + + URL realUrl = new URL(urlNameString); + // 打开和URL之间的连接 + HttpURLConnection connection; + if (url.startsWith("https")) { + trustAllHttpsCertificates(); + HttpsURLConnection.setDefaultHostnameVerifier(hv); + connection = (HttpURLConnection) realUrl.openConnection(); + } else { + connection = (HttpURLConnection) realUrl.openConnection(); + } + //设置超时时间 + connection.setDoInput(true); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(15000); + // 设置通用的请求属性 + if (header != null) { + Iterator> it = header.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + System.out.println(entry.getKey() + ":::" + entry.getValue()); + connection.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + // 建立实际的连接 + connection.connect(); + // 定义 BufferedReader输入流来读取URL的响应,设置utf8防止中文乱码 + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8")); + String line; + while ((line = in.readLine()) != null) { + result += line; + } + if (in != null) { + in.close(); + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e, "get url error ,url is:{},error is {}", urlNameString, e.getMessage()); + } + return result; + } + + public static String sendPost(String url, Map header, JSONObject body) { + PrintWriter out = null; + BufferedReader in = null; + String result = null; + String res = null; + try { + String urlNameString = url; + + URL realUrl = new URL(urlNameString); + // 打开和URL之间的连接 + HttpURLConnection conn; + if (url.startsWith("https")) { + trustAllHttpsCertificates(); + HttpsURLConnection.setDefaultHostnameVerifier(hv); + conn = (HttpURLConnection) realUrl.openConnection(); + } else { + conn = (HttpURLConnection) realUrl.openConnection(); + } + // 设置通用的请求属性 + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); +// conn.setRequestProperty("user-agent", +// "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); + if (header != null) { + header.forEach((k, v) -> { + conn.setRequestProperty(k, String.valueOf(v)); + }); + } + // 发送POST请求必须设置如下两行 + conn.setDoOutput(true); + conn.setDoInput(true); + //获取请求头 + + // 获取URLConnection对象对应的输出流 + out = new PrintWriter(conn.getOutputStream()); + // 发送请求参数 + if (body != null) { + FineLoggerFactory.getLogger().error("content data: {}", body.toString()); + FineLoggerFactory.getLogger().error("content cover data: {}", new String(body.toString().getBytes("UTF-8"), "UTF-8")); + out.print(new String(body.toString().getBytes("UTF-8"), "UTF-8")); + } + // flush输出流的缓冲 + out.flush(); + // 定义BufferedReader输入流来读取URL的响应 + in = new BufferedReader( + new InputStreamReader(conn.getInputStream())); + String line; + while ((line = in.readLine()) != null) { + result += line; + } + res = result; + if (res.startsWith("null")) { + res = res.replace("null", ""); + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + //使用finally块来关闭输出流、输入流 + finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (IOException e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + } + return res; + } + + + public static String doPost(String url, Map header, JSONObject json) { + HttpClient client = HttpClients.createDefault(); + if (url.startsWith("https")) { + SSLContext sslcontext = createIgnoreVerifySSL(); + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + client = HttpClients.custom().setConnectionManager(connManager).build(); + } + HttpPost post = new HttpPost(url); + post.setHeader("accept", "*/*"); + post.setHeader("connection", "Keep-Alive"); + post.setHeader("Content-Type", "application/json"); + if (header != null) { + header.forEach((k, v) -> { + post.setHeader(k, String.valueOf(v)); + }); + } + try { + StringEntity s = new StringEntity(json.toString(),"UTF-8"); + s.setContentEncoding("UTF-8"); + s.setContentType("application/json; charset=UTF-8");//发送json数据需要设置contentType + post.setEntity(s); + HttpResponse res = client.execute(post); + if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + String result = EntityUtils.toString(res.getEntity());// 返回json格式: + return result; + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(),e); + } + return null; + } + + + public static String doEntityPost(String url, Map header, HttpEntity entity) { + HttpClient client = HttpClients.createDefault(); + if (url.startsWith("https")) { + SSLContext sslcontext = createIgnoreVerifySSL(); + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + client = HttpClients.custom().setConnectionManager(connManager).build(); + } + HttpPost post = new HttpPost(url); + post.setHeader("accept", "*/*"); + post.setHeader("connection", "Keep-Alive"); + if (header != null) { + header.forEach((k, v) -> { + post.setHeader(k, String.valueOf(v)); + }); + } + try { + post.setEntity(entity); + HttpResponse res = client.execute(post); + if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + String result = EntityUtils.toString(res.getEntity());// 返回json格式: + return result; + } + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(),e); + } + return null; + } + + private static void trustAllHttpsCertificates() throws Exception { + TrustManager[] trustAllCerts = new TrustManager[1]; + TrustManager tm = new miTM(); + trustAllCerts[0] = tm; + SSLContext sc = SSLContext.getInstance("SSL", "SunJSSE"); + sc.init(null, trustAllCerts, null); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } + + + /** + * encode url by UTF-8 + * + * @param url url before encoding + * @return url after encoding + */ + public static String encodeUrl(String url) { + String eurl = url; + try { + eurl = URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + } + return eurl; + } + + private static class miTM implements TrustManager, + X509TrustManager { + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + + public boolean isServerTrusted( + java.security.cert.X509Certificate[] certs) { + return true; + } + + public boolean isClientTrusted( + java.security.cert.X509Certificate[] certs) { + return true; + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] certs, String authType) + throws CertificateException { + return; + } + + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] certs, String authType) + throws CertificateException { + return; + } + } + + public static SSLContext createIgnoreVerifySSL() { + try { + SSLContext sc = SSLContext.getInstance("SSLv3"); + + // 实现一个X509TrustManager接口,用于绕过验证,不用修改里面的方法 + X509TrustManager trustManager = new X509TrustManager() { + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) throws CertificateException { + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) throws CertificateException { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sc.init(null, new TrustManager[]{trustManager}, null); + return sc; + } catch (Exception e) { + FineLoggerFactory.getLogger().error(e.getMessage(), e); + } + return null; + } +} diff --git a/src/main/resources/com/fr/plugin/dingding/webhook/theme.js b/src/main/resources/com/fr/plugin/dingding/webhook/theme.js new file mode 100644 index 0000000..f44350f --- /dev/null +++ b/src/main/resources/com/fr/plugin/dingding/webhook/theme.js @@ -0,0 +1,71 @@ +!(function () { + BI.config("dec.provider.schedule", function (provider) { + provider.registerTaskAttached({ + value: 64, + text: "png" + }, [DecCst.Schedule.TaskType.REPORT, DecCst.Schedule.TaskType.BI]); + }); + + var Plugin = BI.inherit(BI.Widget, { + props: { + baseCls: "", + value: { + hookUrl: null, + } + }, + render: function () { + var t = this, + e = this.options.value[0]==undefined?{hookUrl: null}: this.options.value[0]; + return { + type: "bi.flex_vertical", + tgap: 15, + items: [{ + type: "dec.label.editor.item", + errorTop: 16, + textCls: "dec-font-weight-bold", + text: "webHookUrl", + textWidth: 115, + editorWidth: 300, + value: e.hookUrl, + ref: function(e) { + t.hookUrl = e + } + }] + }; + }, + /** + * + * + * @returns {boolean} + */ + validation: function () { + var e = !0, + t = this.isVisible(); + return BI.isEmpty(this.hookUrl.getValue()) && (t && this.hookUrl.showError(BI.i18nText("Dec-Error_Null")), e = !1), + e + }, + /** + * + * outputActionList + * @returns {{}} + */ + getValue: function() { + var _self= this; + return {OutputWebHook: BI.extend(_self.value,{ + "@class": "com.fr.plugin.dingding.webhook.entity.OutputWebHook", + actionName: "com.fr.plugin.dingding.webhook.entity.OutputWebHook", + hookUrl: _self.hookUrl.getValue(), + })} + }, + }); + BI.shortcut("dec.schedule.task.file.handling.plugin", Plugin); + + BI.config("dec.provider.schedule", function (provider) { + provider.registerHandingWay({ + text: "webHook", + value: "com.fr.plugin.dingding.webhook.entity.OutputWebHook", // actionName + cardType: "dec.schedule.task.file.handling.plugin", + actions: [] // action + }, [DecCst.Schedule.TaskType.DEFAULT, DecCst.Schedule.TaskType.REPORT, DecCst.Schedule.TaskType.BI]); + }); +}());