打通Gitlab与钉钉之间的通信

[TOC]html

公司使用了Gitlab,Jira等工具来管理,沟通方面主要是钉钉,但郁闷的是各系统相互独立,而我已经习惯了前公司那种方式:
有bug的时候会自动发送消息到聊天框中,而不是目前这样,须要开发人员手动定时去刷新jira页面才能知道,效率低下;java

gitlab也是同样,有merge请求的时候,我但愿不须要别人提醒我去审核代码,而是gitlab直接发送merge消息到我钉钉便可;android

可能其余同事习惯邮件通知吧,公司并没有打通各系统与钉钉联系的计划,因此我只能本身撸一套了,我不是专职后端,轻喷,功可以我用就好;git

Github项目地址

原理: 利用各系统自带的 Webhook 功能, 在触发指定操做时,发送一条 hook 信息到咱们的服务器上, 服务器作出处理后转发消息到对应人员的钉钉上;github

效果展现

gitlab有新merge代码审核请求时会通知审核人
gitlab merge 请求被经过时,会通知相关项目部门全部成员更新代码

相关文档

步骤

  1. gitlab 上启用 Webhooks 通知(可指定要 Webhooks 的操做,这里hook了 merge 操做);
    注意:须要项目管理权限才能设定, jira 也是相似;
    gitlab添加webhook
  2. 在server端,根据 post 请求的 head 信息来区分不一样系统发来的 hook 消息:
    • gitlabmerge 请求包含: X-Gitlab-Event:Merge Request Hook
    • jirahook 请求包含: user-agent:Atlassian HttpClient0.17.3 / JIRA-6.3.15 (6346) / Default
  3. server 端获取钉钉的人员信息,并调用其 企业会话消息接口 发送指定消息;
    因为该会话接口须要 员工id企业应用id 以及 access_token ,而 获取access_token 须要 CorpIdCorpSecret (两者是企业的惟一标识);

    公司固然不可能对我的开放其 CorpId 等信息 ,所以仍是本身注册一个企业,建立部门并添加你想通知的人员做为部门员工便可,这样也能获取员工的 通信录详情 , 获得其 userId ,从而发送消息到其钉钉上;web

  4. 建立一个微应用,以该应用为会话发起人来发送消息;
    钉钉管理后台

创建钉钉微应用

  1. 钉钉开放平台 中搜索 微应用 就能够找到 Step 1 -- 注册钉钉企业连接;
  2. 根据上面的 step 引导操做注册企业并添加部门和员工,而后进入 钉钉管理后台;
  3. 切换到 工做台 标签页(即上图中的 企业应用,现已更名,偷懒就不从新截图了╮( ̄▽ ̄)╭) , 点击下方的 自建应用 ,按需填写信息;
  4. 完成后点击新建的微应用图标,选择 设置 便可查看到微应用的 AgentID;

获取企业的 CorpIDCorpSecret

  1. 登陆 钉钉管理后台;
  2. 点击 工做台 - 应用开发 便可查看到企业的 CorpIDCorpSecret信息;

    文档连接如有变化, 请自行到 钉钉开放平台 搜索 CorpSecret ;json

通信录规则

在通信录root部门中添加全部人,以便发送消息到特定用户时能够从root部门中经过查询用户姓名获得用户id;
gitlab会特殊一点,有些操做须要通知项目全部成员,因此还须要根据项目来建立部门:后端

  • 假设gitlab项目地址为: https://gitlab.lynxz.org/demo-android/detail-android ,则表示项目名称(name) 为: detail-android ,项目所在空间(namespace)为: demo-android
  • 在钉钉后台通信录中须要先建立部门: demo_android ,而后建立其子部门 detail_android;
    注意:
    • 因为钉钉部门名称不容许使用 -,所以建立时改成 _ 替代;
    • 目前只支持两级部门结构,如有多个部门符合上述规则 gitlab merge 经过时会通知全部匹配的部门成员;

备注: 更新钉钉通信录后,记得及时通知 server 刷新本地数据,本版支持经过url出发刷新命令,直接访问以下网址便可(其中 yourServerHost 是war包运行后的访问地址): {yourServerHost}/action/updateDepartmentInfo api

钉钉通信录

钉钉发送消息流程

1. retrofit请求

// kotlin
interface ApiService {
    /** * [获取钉钉AccessToken](https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.dfrJ5p&treeId=172&articleId=104980&docType=1) * @param id corpid 企业id * @param secret corpsecret 企业应用的凭证密钥 * */
    @GET("gettoken")
    fun getAccessToken(@Query("corpid") id: String, @Query("corpsecret") secret: String): Observable<AccessTokenBean>

    /** * [获取部门列表信息](https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xIVqtB&treeId=172&articleId=104979&docType=1#s0) */
    @GET("department/list")
    fun getDepartmentList(): Observable<DepartmentListBean>

    /** * [获取指定部门的成员信息,默认获取所有成员](https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xIVqtB&treeId=172&articleId=104979&docType=1#s12) * */
    @GET("user/simplelist")
    fun getDepartmentMemberList(@Query("department_id") id: Int = 1): Observable<DepartmentMemberListBean>

    /** * [向指定用户发送普通文本消息](https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.oavHEu&treeId=172&articleId=104973&docType=1#s2) */
    @POST("message/send")
    fun sendTextMessage(@Body bean: MessageTextBean): Observable<MessageResponseBean>
}
复制代码

2. 添加必要的request信息

// kotlin
// 给请求添加统一的query参数:access_token
// 这里的ConstantsPara.accessToken是全局变量,存储获取到的accessToken 
val queryInterceptor = Interceptor { chain ->
    val original = chain.request()
    val url = original.url().newBuilder()
            .addQueryParameter("access_token", ConstantsPara.accessToken)
            .build()

    val requestBuilder = original.newBuilder().url(url)
    chain.proceed(requestBuilder.build())
}

// 给请求添加统一的header参数:Content-Type
val headerInterceptor = Interceptor { chain ->
    val request = chain.request().newBuilder()
            .addHeader("Content-Type", "application/json")
            .build()
    chain.proceed(request)
}

val okHttpClient: OkHttpClient = OkHttpClient()
        .newBuilder()
        .addInterceptor(headerInterceptor)
        .addInterceptor(queryInterceptor)
        .build()

val ddRetrofit: Retrofit = Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl("https://oapi.dingtalk.com/") // 钉钉后台服务地址
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .build()
    
val apiService: ApiService = ddRetrofit.create(ApiService::class.java)
复制代码

3. 刷新钉钉的AccessToken

// kotlin
apiService.getAccessToken(ConstantsPara.dd_corp_id, ConstantsPara.dd_corp_secret)
        .retry(1)
        .subscribe(object : Observer<AccessTokenBean> {
            override fun onError(e: Throwable) {
                e.printStackTrace()
            }

            override fun onSubscribe(d: Disposable) {
                addDisposable(d)
            }

            override fun onComplete() {
            }

            override fun onNext(t: AccessTokenBean) {
                println("refreshAccessToken $t")
                ConstantsPara.accessToken = t.access_token ?: ""
            }
        })
复制代码

4. 获取部门列表及各部门下的成员信息

  • 部门信息存放在 ConstantsPara.departmentNameMap 中,是一个 HashMap ,记录部门id及名称;
  • 部门成员通信录存放在 ConstantsPara.departmentMemberMap中, 也是一个 HashMap, 记录部门id及部门中的全部成员信息;

备注:数组

  • 部门名称需跟 gitlab 项目名称对应,须要群发时经过项目名称查找对应的部门id; (目的: 建立 gitlab 项目 与 钉钉部门之间的映射关系);
  • 部门id用于惟一肯定部门,用于查找指定部门成员信息;
  • 其中部门id为 1 的是公司的根部门(root部门),要将全部人员都添加进去,以便在须要通知指定人员时,能从root部门成员中经过查找用户姓名获取其用户id,而后发出钉钉消息;
// kotlin
apiService.getDepartmentList()
        .flatMap { list ->
            ConstantsPara.departmentList = list
            list.department.forEach { ConstantsPara.departmentNameMap.put(it.id, it.name) }
            Observable.fromIterable(list.department)
        }
        .map { departmentBean -> departmentBean.id }
        .flatMap { departmentId ->
            Observable.zip(Observable.create({ it.onNext(departmentId) }),
                    apiService.getDepartmentMemberList(departmentId),
                    BiFunction<Int, DepartmentMemberListBean, DepartmentMemberListBean> { t1, t2 ->
                        t2.departmentId = t1
                        t2
                    })
        }
        .retry(1)
        .subscribe(object : Observer<DepartmentMemberListBean> {
            override fun onNext(t: DepartmentMemberListBean) {
                ConstantsPara.departmentMemberMap.put(t.departmentId, t.userlist)
            }

            override fun onSubscribe(d: Disposable) {
                addDisposable(d)
            }

            override fun onError(e: Throwable) {
                e.printStackTrace()
            }

            override fun onComplete() {
                println("getDepartmentInfo onComplete:\n${ConstantsPara.departmentMemberMap.keys.forEach { println("departId: $it") }}")
// sendTextMessage(ConstantsPara.defaultNoticeUserName, "test from server")
            }
        })
复制代码

5. 发送钉钉消息

/** * kotlin * 向指定用户[targetUserName]发送文本内容[message] * 若目标用户名[targetUserName]为空,则发送给指定部门[departmentId]全部人,好比gitlab merge请求经过时,通知全部人 * */
fun sendTextMessage(targetUserName: String? = null, message: String = "", departmentId: Int = 1) {
    ConstantsPara.departmentMemberMap[departmentId]?.apply {
        stream().filter { targetUserName.isNullOrBlank() or it.name.equals(targetUserName, true) }
                .forEach {
                    val textBean = MessageTextBean().apply {
                        touser = it.userid
                        agentid = ConstantsPara.dd_agent_id
                        msgtype = MessageType.TEXT
                        text = MessageTextBean.TextBean().apply {
                            content = message
                        }
                    }
                    apiService.sendTextMessage(textBean)
                            .subscribeOn(Schedulers.io())
                            .subscribe(object : Observer<MessageResponseBean> {
                                override fun onComplete() {
                                }

                                override fun onSubscribe(d: Disposable) {
                                    addDisposable(d)
                                }

                                override fun onNext(t: MessageResponseBean) {
                                    println("${msec2date()} sendTextMessage $t")
                                }

                                override fun onError(e: Throwable) {
                                    e.printStackTrace()
                                }
                            })
                }
    }
}
复制代码

其余说明

  1. 钉钉消息有个 限制, 所以我在全部消息文本中添加服务器当前时间,尽可能确保每条消息都不一样:

forbiddenUserId: 因发送消息过于频繁或超量而被流控过滤后实际未发送的userid。未被限流的接收者仍会被成功发送。
限流规则包括:
一、给同一用户发相同内容消息一天仅容许一次;
二、若是是ISV接入方式,给同一用户发消息一天不得超过50次;若是是企业接入方式,此上限为500。

  1. jira的hook信息如果存在 changelog 则代表有用户修改了issue的状态或者内容,另外, issuse.comment 必定存在, 数组 comments 存储了用户提交的全部备注信息,按时间前后顺序排列;
  2. accessToken的有效期为7200秒,所以项目中须要定时刷新token;
  3. 钉钉自带有 聊天机器人 , 直接支持几个平台的 webhook 消息, 不过只能转发到 中, 对不须要关注的成员来讲, 就是垃圾消息, 并且消息格式固定死板,灵活性不强,具体操做可到 钉钉开放平台 搜索 机器人 查看;
相关文章
相关标签/搜索