打通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 消息, 不過只能轉發到 中, 對不須要關注的成員來講, 就是垃圾消息, 並且消息格式固定死板,靈活性不強,具體操做可到 釘釘開放平臺 搜索 機器人 查看;
相關文章
相關標籤/搜索