打通Gitlab與釘釘之間的通訊

[TOC]

公司使用了Gitlab,Jira等工具來管理,溝通方面主要是釘釘,但郁悶的是各系統相互獨立,而我已經習慣了前公司那種方式:

有bug的時候會自動發送消息到聊天框中,而不是目前這樣,需要開發人員手動定時去刷新jira頁面才能知道,效率低下;

gitlab也是一樣,有merge請求的時候,我希望不需要別人提醒我去審核代碼,而是gitlab直接發送merge消息到我釘釘即可;

可能其他同事習慣郵件通知吧,公司并無打通各系統與釘釘聯系的計劃,所以我只能自己擼一套了,我不是專職后端,輕噴,功能夠我用就好;

已遷移到 掘金, 歡迎關注;

更新記錄:

1. 2017.04.17 發現釘釘直接支持幾個平臺的webhook推送

  1. 打開釘釘群聊天框右上角的聊天機器人


    聊天機器人
  2. 選擇其中需要的平臺


    hook列表
  3. 添加完后再對應平臺的設置中添加webhook地址即可;
    但感覺這個比較粗糙,以jira為例,消息過于精簡,而且通知到群里的話,會讓用戶操心了本不需要操心的內容,個人覺得,這個比較適合gitlab的merge代碼被通過時的通知,通知成員用戶更新本地代碼:


    jira通知示例

2. 2017.8.30 重構項目

使用gradle/kotlin/rxjava/retrofit等改造了之前的項目,支持快速新增gitlab項目部門,手動刷新accessToken及部門信息等功能,部分示例如下,具體請看 項目 :
P.S.重構后,war包大小由原來的30+M減小到5M左右 ==!

gitlab有新merge代碼審核請求時會通知審核人

gitlab merge 請求被通過時,會通知相關項目部門所有成員更新代碼

Github項目地址

相關文檔

Gitlab webhook document
Jira webhook document
釘釘開放文檔-服務器端

步驟

  1. gitlab 上啟用 Webhooks 通知(可指定要 Webhooks 的操作,這里僅hook merge 操作,注意:需要項目管理權限才能設定, jira 也是類似)
    gitlab添加webhook
  2. 在服務端,根據post請求的head信息來區分不同系統發來的hook消息:
  3. gitlab的merge請求包含: X-Gitlab-Event:Merge Request Hook
  4. jira的hook請求包含: user-agent:Atlassian HttpClient0.17.3 / JIRA-6.3.15 (6346) / Default
  5. 在服務器端打開獲取釘釘的人員信息,并調用其 企業會話消息接口 發送指定信息;
    由于該會話接口需要 員工id和企業應用id以及access_token,而 獲取access_token 需要 CorpIdCorpSecret (二者是企業的唯一標識), 因此可知:
  6. 雖然公司的釘釘后臺上有 CorpId 等信息,但不一定會開放,而等公司組織人員開發又可能遙遙無期,因此還是自己注冊一下企業,創建部門并添加你想通知的人員作為部門員工即可,這樣也能獲取員工 通訊錄詳情 , 得到用userId,從而發送釘釘消息;
  7. 需要創建一個微應用,以該應用為會話發起人來發送消息;


    釘釘管理后臺

建立釘釘微應用

  1. 釘釘開放平臺 中搜索 微應用 就可以找到 Step 1 -- 注冊釘釘企業鏈接;
  2. 根據上面的 step 引導操作注冊企業并添加部門和員工,然后進入 釘釘管理后臺;
  3. 企業應用 標簽頁左側導航條中選擇 微應用設置 即可在右側看到 CorpIDCorpSecret;
  4. 企業應用 標簽下 新建應用 即可;
  5. 完成后點擊新建的微應用圖標,選擇 設置 接口查看到微應用的 AgentID;

通訊錄規則

在通訊錄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
    釘釘通訊錄

釘釘發送消息流程

1. retrofit請求

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信息

// 給請求添加統一的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

    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及名稱,id用于唯一確定部門,以便后續查找指定部門成員信息;
部門名稱需跟gitlab項目名稱對應,其中部門id為1的是公司的根部門,主要要將所有人員都添加進去,因為通知指定人員時,是從根部門中查找用戶姓名,若匹配就發出消息,而子部門的存在只為適配gitlab項目路徑;

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. 發送釘釘消息

/**
* 向指定用戶[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。未被限流的接收者仍會被成功發送。限流規則包括:1、給同一用戶發相同內容消息一天僅允許一次;2、如果是ISV接入方式,給同一用戶發消息一天不得超過50次;如果是企業接入方式,此上限為500。

  1. jira的hook信息若是存在 changelog 則表明有用戶修改了issue的狀態或者內容,另外, issuse.comment 一定存在, 數組 comments 存儲了用戶提交的所有備注信息,按時間先后順序排列;
  2. accessToken的有效期為7200秒,因此項目中需要定時刷新token;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,830評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,765評論 25 708
  • 第一杯酒,陽光明媚,窗外的青藤爬進了我的眼。 第二杯酒,春風輕漾,葉梢輕拂著我的眉。 第三杯酒,鳥兒鳴叫,輕啄著我...
    瑤幺兒閱讀 1,510評論 0 0
  • 最近一個周,每天早上步行四十分鐘到上班地點。最初的目的是減肥。走著走著發現生活狀態都開始改變,我會惦記著好好吃早餐...
    龜苓膏味的菜菜閱讀 160評論 0 0
  • 有這么一位偶像,每一次和她的相遇都教會了我許許多多。 即使在后臺累到睡著,但是為了大家,為了她的粉絲們 不管再怎么...
    曾艷芬微光站閱讀 380評論 0 1