基于scala向钉钉群组发送消息实践

一个祥和的月黑风高夜,系统在有条不紊运行着. lol s9赛季群里有一句没一句的聊着,但钉钉声此起彼伏,各种系统信息接踵而至,大家把注意力从视频直播,转向钉钉消息.有针对性的各自分析问题,很快钉钉声再次停歇,大家欢庆lpl胜利,嗨皮继续.
--- 钉钉使用实录

钉钉有了阿里加成,成为最近几年最热门即时通信和办公软件.大有微信主导日常,钉钉支持工作的趋势.在众多的功能中,支持向群组发送消息给运维提供了方便, 引言就是运维的一个例子.

本文实现基于scala向钉钉群组发送消息功能.

内容导航
  • 实现原理 - 介绍实现的原理

  • 开发优缺点对比

  • 代码外部依赖

  • 功能介绍 - 分别支持功能,实现方式

  • 代码

实现原理

钉钉聊天群内支持的群机器人, 类似QQ 群机器人, 可以发天气, 讲笑话那样; 钉钉群机器人支持自定义机器人, 允许开发者管理机器人做预警消息通知.本文是基于该功能实现向群组发送消息功能的.

咱们是使用自定义机器人来完成消息发送.钉钉给我们开了一扇窗,我们要好好使用.

钉钉群机器人官方自我介绍,详见传送门机器人自我介绍, 也可以使用链接地址: https://ding-doc.dingtalk.com/doc#/serverapi3/iydd5h

在赘述下,钉钉支持的消息样式:

  • text类型

  • link类型

  • markdown类型

  • 整体跳转ActionCard类型

  • 独立跳转ActionCard类型

  • FeedCard类型

从简约到典雅格式,反正总有一款格式适合你.我们实现的基本的文本格式.

开发方式
  • SDK jar的方式开发
    官方提供Jar包,包含需要的API,可以直接使用.

    • 优点

    它是官方提供封装依赖包,提供完整的功能支持和依赖包含.只需要调用api接口完成.https://ding-doc.dingtalk.com/doc#/serverapi3/iydd5h测试机器人章节还提供javaPHP的样例.

    • 缺点

    jar包并不是官方提供的标准api,发生变化如何识别,是个问题.另外,如何同步更新依赖也是个问题.

  • 通过Http的方式开发

    • 优点

      不需要依赖官方依赖包,满足官方消息格式就能完成.

    • 缺点

      重复造轮子.嘎嘎,用等哥的话说,其实我不怕.

接下来,我们展示代码实现逻辑.

代码依赖

代码开发依赖库如下所示,版本使用根据自己需要选择.

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>${configuration.version}</version>
</dependency>

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>

</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>${httpcore.version}</version>
</dependency>

功能介绍
  • 支持代理服务器发送消息

    应用系统部署环境如果是需要公司网关,消息直接发送就会失败.代理设置是解决的思路.我们支持设置代理的功能,用户根据需要设置代理服务器.实现代码如下:

    def setProxy(proxy: String, port: Int, username: String, psswd: String): (RequestConfig, BasicCredentialsProvider) = {
    import org.apache.http.auth.{AuthScope, UsernamePasswordCredentials}
    val host = new HttpHost(proxy, port)
    val provider = new BasicCredentialsProvider()
    provider.setCredentials(new AuthScope(host), new UsernamePasswordCredentials(username, psswd))

    (RequestConfig.custom.setProxy(host).build, provider)
    }

    proxy, port, username, psswd代理设置需要参数,返回类型是RequestConfig, BasicCredentialsProvider,在后面调用是需要设置到httpClient和HttpPost中,后文会说明.

  • 支持文本格式发送

    钉钉支持固定几类消息发送,错误的格式导致响应失败.正确的消息样式如下:

    { "msg":{ "msgtype":"text", "text":{ "content":"test #keyword#" } } }

    代码实现如下:

    public class TextMessage {
    private String text;
    private TextMessage() {};
    public TextMessage(String text) {
    this.text = text;
    }

    public String toJsonString() {
    Map<String, Object> items = new HashMap<String, Object>();
    items.put("msgtype", "text");

    Map<String, String> textContent = new HashMap<String, String>();
    byte[] byt = StringUtils.getBytesUtf8(text);
    String txt = StringUtils.newStringUtf8(byt);
    textContent.put("content", txt);
    items.put("text", textContent);

    return JSON.toJSONString(items);
    }
    }

  • 支持安全方式:群组关键词

    为了安全,钉钉支持多种消息校验机制,除了access token之外,用户可选择关键词模式,消息加密模式,和过滤ip地址方式.
    这种方式在消息体内包含群组机器指定的关键字,群组可以正常接受消息.
    我们选择关键词模式.

  • 支持Http请求发送

    http请求包括构建HttpPost和send2个接口.如下所示:

    • 构建HttpPost

      def constructHttpPost(msg: String): HttpPost = {
      val post = new HttpPost(webHook + accessToken)
      val entity = new StringEntity(msg, StandardCharsets.UTF_8)
      post.setEntity(entity)
      post.addHeader("Content-Type", "application/json; charset=utf-8")

      post
      }

    • 消息发送send 接口

      val post = constructHttpPost(msg)
      val client: CloseableHttpClient = if (isProxyEnable) {
      //根据是否使用代理来决定http post参数设置
      val (config, provider) = setProxy(proxy, port, username, password)
      post.setConfig(config)
      HttpClients.custom().setDefaultCredentialsProvider(provider).build()
      } else {
      HttpClients.custom().build()
      }

      val rsp = client.execute(post) //执行send操作

  • 完整实现代码:

    //webHook,accessToken组合起来构成发送的https链接,在设置群组机器人时,记得保存哦.
    //群组机器设置方式: https://ding-doc.dingtalk.com/doc#/serverapi3/iydd5h
    //根据自己的需要传递过来
    class Robot(webHook: String, accessToken: String) {
    lazy val jsonObj = new JSONObject()

    //校验代理是否被激活
    def isProxyEnable: Boolean = proxy.nonEmpty && port != 0

    def setProxy(proxy: String, port: Int, username: String, psswd: String): (RequestConfig, BasicCredentialsProvider) = {
    import org.apache.http.auth.{AuthScope, UsernamePasswordCredentials}
    val host = new HttpHost(proxy, port)
    val provider = new BasicCredentialsProvider()
    provider.setCredentials(new AuthScope(host), new UsernamePasswordCredentials(username, psswd))

    (RequestConfig.custom.setProxy(host).build, provider)
    }

    def constructHttpPost(msg: String): HttpPost = {
    val post = new HttpPost(webHook + accessToken)
    val entity = new StringEntity(msg, StandardCharsets.UTF_8)
    post.setEntity(entity)
    post.addHeader("Content-Type", "application/json; charset=utf-8")

    post
    }

    def sendMsg(msg: String): DingTalkResponse = {
    val res = DingTalkResponse(HttpStatus.SC_NOT_FOUND)
    val post = constructHttpPost(msg)
    val client: CloseableHttpClient = if (isProxyEnable) {
    val (config, provider) = setProxy(proxy, port, username, password)
    post.setConfig(config)
    HttpClients.custom().setDefaultCredentialsProvider(provider).build()
    } else {
    HttpClients.custom().build()
    }

    try {
    val rsp = client.execute(post) //消息发送
    try {//资源释放处理
    if (rsp.getStatusLine().getStatusCode == HttpStatus.SC_OK) {
    val result: String = EntityUtils.toString(rsp.getEntity)
    val toJson = jsonObj.getJSONObject(result)
    println(result)
    val errCode = toJson.getInteger("errcode")
    val msg = toJson.getString("errmsg")
    res.copy(status = HttpStatus.SC_OK, errorCode = errCode, errMsg = msg)
    }
    }
    catch {
    case io: IOException => println(s"http response IOException {io}") case cpe: ClientProtocolException => println(s"http ClientProtocolException{cpe}")
    case ex: Exception => throw ex
    } finally if (rsp != null) rsp.close()
    } catch {
    case ex: Exception => println(s"http response exception ${ex}")
    } finally {
    if (client != null) client.close()
    }

    res
    }
    }
    //结果响应解析类
    case class DingTalkResponse(status: Int, errorCode: Int = 0, errMsg: String = "")
    把依赖库,和上面代码组合就可以直接运行了哦

后记

本文代码实现文本消息发送,如果看官姥爷有需要,其它方式代码也会贴出来.
希望你能有收获,高兴的话上个赞撒.

推荐阅读更多精彩内容