diff --git a/README.md b/README.md index c8d2e29..f86a4c4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ todo: - [X] 使用mustache模板 - [X] 增加note通知 +- [X] 增加system hook通知,参考 [gitlab system hook](https://docs.gitlab.com/ee/administration/system_hooks.html) - [ ] 增加消息模板配置文件 - [ ] 支持飞书机器人 - [ ] 支持按天统计数据 diff --git a/app/controller/home.js b/app/controller/home.js index bcf559c..d042269 100644 --- a/app/controller/home.js +++ b/app/controller/home.js @@ -1,31 +1,71 @@ 'use strict'; const Controller = require('egg').Controller; +const { X_GITLAB_EVENT } = require('../imports/const'); class HomeController extends Controller { async index() { - const { ctx } = this; + const { ctx, config } = this; const { path = '' } = ctx.params; - const webhookUrl = - process.env['WEBHOOK_URL' + (path ? '_' + path.toUpperCase() : '')]; - - this.logger.debug('webhookUrl', webhookUrl); - this.logger.info('request body: ', ctx.request.body); - const message = await ctx.service.webhook.translateMsg(ctx.request.body); + this.logger.info('====> request headers: ', ctx.request.headers); + this.logger.info('====> request body: ', ctx.request.body); - if (!message) { - this.logger.info('====> message is empty, suppressed.'); - ctx.body = { msg: 'message is empty or not supported, suppressed.' }; + // platform check + const platform = process.env['PLATFORM'] || config.platform; + this.logger.debug('platform: ', platform); + if (config.supportPlatforms.indexOf(platform) === -1) { + const errMsg = `====> platform "${platform}" is not supported, only support: ${config.supportPlatforms.join( + ', ' + )}`; + this.logger.error(errMsg); + ctx.body = { + error: errMsg, + }; return; } + // webhookUrl check + const webhookUrl = + process.env['WEBHOOK_URL' + (path ? '_' + path.toUpperCase() : '')]; + + this.logger.debug('webhookUrl: ', webhookUrl); + + // if webhookUrl not match, exit if (!webhookUrl) { - this.logger.error('webhook url error, webhookUrl: ' + webhookUrl); + this.logger.error('====> webhook url error, webhookUrl: ' + webhookUrl); ctx.body = { error: 'webhook url error, webhookUrl: ' + webhookUrl, }; return; } + let gitlabEvent = X_GITLAB_EVENT.push; + + // check x-gitlab-event + if (ctx.request.headers['x-gitlab-event']) { + gitlabEvent = ctx.request.headers['x-gitlab-event']; + if (Object.values(X_GITLAB_EVENT).indexOf(gitlabEvent) === -1) { + const errMsg = `====> x-gitlab-event "${gitlabEvent}" is not supported}`; + this.logger.error(errMsg); + ctx.body = { + error: errMsg, + }; + return; + } + } + + const message = await ctx.service.webhook.translateMsg( + ctx.request.body, + platform, + gitlabEvent + ); + + if (!message) { + this.logger.info('====> message is empty, suppressed.'); + ctx.body = { + msg: '====> message is empty or not supported, suppressed.', + }; + return; + } const result = await ctx.curl(webhookUrl, { method: 'POST', diff --git a/app/imports/const.js b/app/imports/const.js new file mode 100644 index 0000000..758bd3a --- /dev/null +++ b/app/imports/const.js @@ -0,0 +1,41 @@ +const OBJECT_KIND = { + push: 'push', + tag_push: 'tag_push', + issue: 'issue', + note: 'note', + merge_request: 'merge_request', + wiki_page: 'wiki_page', + pipeline: 'pipeline', + build: 'build', // todo +}; + +const EVENT_TYPE = { + group_create: 'group_create', + group_destroy: 'group_destroy', + group_rename: 'group_rename', + key_create: 'key_create', + key_destroy: 'key_destroy', + project_create: 'project_create', + project_destroy: 'project_destroy', + project_rename: 'project_rename', + project_transfer: 'project_transfer', + project_update: 'project_update', + repository_update: 'repository_update', + user_add_to_group: 'user_add_to_group', + user_add_to_team: 'user_add_to_team', + user_create: 'user_create', + user_destroy: 'user_destroy', + user_failed_login: 'user_failed_login', + user_remove_from_group: 'user_remove_from_group', + user_remove_from_team: 'user_remove_from_team', + user_rename: 'user_rename', + user_update_for_group: 'user_update_for_group', + user_update_for_team: 'user_update_for_team', +}; + +const X_GITLAB_EVENT = { + push: 'Push Hook', + system: 'System Hook', +}; + +module.exports = { OBJECT_KIND, EVENT_TYPE, X_GITLAB_EVENT }; diff --git a/app/service/webhook.js b/app/service/webhook.js index d11b9f8..abd5226 100644 --- a/app/service/webhook.js +++ b/app/service/webhook.js @@ -4,6 +4,8 @@ const Service = require('egg').Service; const moment = require('moment'); const Mustache = require('mustache'); +const { OBJECT_KIND, X_GITLAB_EVENT, EVENT_TYPE } = require('../imports/const'); + // set default lang moment.locale('zh-cn'); @@ -11,59 +13,53 @@ moment.locale('zh-cn'); Mustache.escape = text => text.toString().replace('\n', ' ').replace(/\s+/g, ' '); -const OBJECT_KIND = { - push: 'push', - tag_push: 'tag_push', - issue: 'issue', - note: 'note', - merge_request: 'merge_request', - wiki_page: 'wiki_page', - pipeline: 'pipeline', - build: 'build', // todo -}; - -const REDIS_KEY = { - pipeline: id => `gitlab.pipeline.${id}`, -}; - -const REDIS_VAL = { - pipeline: ({ pipelineId, stages, status, duration, builds }) => { +// all customized variables start with GB_ +class WebhookService extends Service { + async translateMsg(data = {}, platform, gitlabEvent) { + this.platform = platform; + + const { object_kind } = data; + + const content = []; + switch (gitlabEvent) { + case X_GITLAB_EVENT.push: + this.pushHookHandler(content, data); + break; + case X_GITLAB_EVENT.system: + // system hook to push hook if object_kind exists + OBJECT_KIND[object_kind] + ? this.pushHookHandler(content, data) + : this.systemHookHandler(content, data); + break; + default: + // controller make sure not to here + break; + } + return { - type: 'pipeline', - id: pipelineId, - duration: duration, - durationMin: Math.round(duration / 60 - 0.5), - durationSec: duration % 60, - status: status, - stages: stages, - builds: builds, + msgtype: 'markdown', + markdown: { content: content.join(' \n ') }, }; - }, -}; + } + + async pushHookHandler(content, data) { + const { object_kind } = data; -// all customized variables start with GB_ -class WebhookService extends Service { - async translateMsg(data) { - const { object_kind } = data || {}; if (!OBJECT_KIND[object_kind]) { return {}; } let res = true; - const content = []; switch (object_kind) { case OBJECT_KIND.push: res = await this.assemblePushMsg(content, data); break; - case OBJECT_KIND.pipeline: res = await this.assemblePipelineMsg(content, data); break; - case OBJECT_KIND.merge_request: res = await this.assembleMergeMsg(content, data); break; - case OBJECT_KIND.tag_push: res = await this.assembleTagPushMsq(content, data); break; @@ -80,12 +76,36 @@ class WebhookService extends Service { res = false; break; } - if (!res) return false; + return res; + } - return { - msgtype: 'markdown', - markdown: { content: content.join(' \n ') }, - }; + async systemHookHandler(content, data) { + const template = this.getTemplateByPlatform(this.platform); + + const { event_name } = data; + if (!EVENT_TYPE[event_name]) { + const errMsg = `====> event_name "${event_name}" is not supported, suppressed}`; + this.logger.error(errMsg); + return false; + } + + this.logger.debug('template: ', template.push); + this.logger.debug('content: ', content); + if (template[event_name]) { + // match template by event_name first + content.push(Mustache.render(template[event_name], data)); + } else { + const event_arr = event_name.split('_'); + const event_action = event_arr[0] + '_action'; + if (!template[event_action]) { + const errMsg = `====> event_action "${event_action}" is not supported, suppressed}`; + this.logger.error(errMsg); + return false; + } + content.push(Mustache.render(template[event_action], data)); + } + + return content; } async assemblePushMsg(content, data) { @@ -103,7 +123,7 @@ class WebhookService extends Service { GB_op = '将代码推至'; } - const template = this.getTemplateByPlatform('qywx'); + const template = this.getTemplateByPlatform(this.platform); this.logger.debug('template: ', template.push); this.logger.debug('content: ', content); @@ -180,7 +200,7 @@ class WebhookService extends Service { // gitlab 11.3 未支持source参数 GB_sourceString = `${name}`; } - const template = this.getTemplateByPlatform('qywx'); + const template = this.getTemplateByPlatform(this.platform); const pipeline = Mustache.render(template.pipeline, { ...data, GB_pipelineId, @@ -222,7 +242,7 @@ class WebhookService extends Service { default: } - const template = this.getTemplateByPlatform('qywx'); + const template = this.getTemplateByPlatform(this.platform); const merge_request = Mustache.render(template.merge_request, { ...data, GB_stateAction, @@ -247,7 +267,7 @@ class WebhookService extends Service { GB_op = '删除'; } - const template = this.getTemplateByPlatform('qywx'); + const template = this.getTemplateByPlatform(this.platform); const tag_push = Mustache.render(template.tag_push, { ...data, GB_tag, @@ -262,7 +282,7 @@ class WebhookService extends Service { const { object_attributes = {} } = data; const { state } = object_attributes; - const template = this.getTemplateByPlatform('qywx'); + const template = this.getTemplateByPlatform(this.platform); const issue = Mustache.render(template.issue, { ...data, GB_state: this.formatStatus(state), @@ -274,7 +294,7 @@ class WebhookService extends Service { const { object_attributes = {} } = data; const { action } = object_attributes; - const template = this.getTemplateByPlatform('qywx'); + const template = this.getTemplateByPlatform(this.platform); const issue = Mustache.render(template.wiki, { ...data, GB_action: this.formatAction(action), @@ -286,7 +306,7 @@ class WebhookService extends Service { const { object_attributes = {} } = data; const { action } = object_attributes; - const template = this.getTemplateByPlatform('qywx'); + const template = this.getTemplateByPlatform(this.platform); const note = Mustache.render(template.note, { ...data, GB_action: this.formatAction(action), diff --git a/config/config.default.js b/config/config.default.js index 60f2e3c..e4d6997 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -26,7 +26,8 @@ module.exports = appInfo => { // add your user config here const userConfig = { - platform: ['qywx'], + supportPlatforms: ['qywx'], + platform: 'qywx', response: { qywx: { content: 'markdown.content', @@ -47,8 +48,7 @@ module.exports = appInfo => { }, template: { qywx: { - push: -`\`{{user_name}}\` {{GB_op}} [[{{project.name}} | {{GB_branch}}分支]({{project.web_url}}/tree/{{GB_branch}})] + push: `\`{{user_name}}\` {{GB_op}} [[{{project.name}} | {{GB_branch}}分支]({{project.web_url}}/tree/{{GB_branch}})] > 包含\`{{total_commits_count}}\`个提交, \`{{GB_changes.added}}\`新增 | \`{{GB_changes.modified}}\`修改 | \`{{GB_changes.removed}}\`删除 {{#commits}} > 》 \`{{author.name}}\`: [{{title}}]({{url}}) @@ -57,22 +57,20 @@ module.exports = appInfo => { {{#project}}项目信息: [[{{name}} / {{namespace}}]({{web_url}})]{{/project}} `, - pipeline: -`[[#{{GB_pipelineId}}流水线 | {{object_attributes.ref}}分支]({{GB_pipelineUrl}})] {{GB_status.str}},由\`{{user.name}}\`通过\`{{GB_sourceString}}\`触发。 + pipeline: `[[#{{GB_pipelineId}}流水线 | {{object_attributes.ref}}分支]({{GB_pipelineUrl}})] {{GB_status.str}},由\`{{user.name}}\`通过\`{{GB_sourceString}}\`触发。 > **流水线详情:** 耗时\`{{GB_duration}}\`, {{object_attributes.stages.length}}个阶段 {{#object_attributes.stages}}{{.}} | {{/object_attributes.stages}} > {{#merge_request}}**合并详情:** [{{title}}]({{url}}), \`{{source_branch}}\`合并至\`{{target_branch}}\`{{/merge_request}} > {{#commit}}**提交详情:** \`{{author.name}}\`: [{{message}}]({{url}}){{/commit}} > **编译详情**: {{#GB_builds}}> 》 \`{{stage}}\`: -{{#builds}}> - [\`{{name}}\`{{#GB_duration}} ({{GB_duration}}){{/GB_duration}} -> {{GB_status.str}}{{#failure_reason}}, {{failure_reason}}{{/failure_reason}} {{#GB_user}}({{GB_user}}){{/GB_user}}]({{GB_url}}) +{{#builds}}> - [\`{{name}}\`{{#GB_duration}}({{GB_duration}}){{/GB_duration}} -> {{GB_status.str}}{{#failure_reason}}, {{failure_reason}}{{/failure_reason}} {{#GB_user}}({{GB_user}}){{/GB_user}}]({{GB_url}}) {{/builds}} {{/GB_builds}} {{#project}}项目信息: [[{{name}} / {{namespace}}]({{web_url}})]{{/project}} `, - merge_request: -`{{#GB_stateAction}}{{GB_stateAction}} :{{/GB_stateAction}}\`{{user.name}}\`**{{GB_stateString}}{{#object_attributes}}**[[#{{iid}}合并请求 {{title}}]({{iid}})],从\`{{source_branch}}\`合并至\`{{target_branch}}\`{{/object_attributes}} + merge_request: `{{#GB_stateAction}}{{GB_stateAction}} :{{/GB_stateAction}}\`{{user.name}}\`**{{GB_stateString}}{{#object_attributes}}**[[#{{iid}}合并请求 {{title}}]({{iid}})],从\`{{source_branch}}\`合并至\`{{target_branch}}\`{{/object_attributes}} > **MR详情:** > 提交时间: {{GB_updated_at}} > 提交详情: @@ -80,15 +78,13 @@ module.exports = appInfo => { {{#project}}项目信息: [[{{name}} / {{namespace}}]({{web_url}})]{{/project}} `, - tag_push: -`\`{{user_name}}\`{{GB_op}}标签 [[{{project.name}} | {{GB_tag}}]({{web_url}}/-/tags/{{GB_tag}})]。 + tag_push: `\`{{user_name}}\`{{GB_op}}标签 [[{{project.name}} | {{GB_tag}}]({{web_url}}/-/tags/{{GB_tag}})]。 > 包含\`{{total_commits_count}}\`个提交, \`{{GB_changes.added}}\`新增 | \`{{GB_changes.modified}}\`修改 | \`{{GB_changes.removed}}\`删除 {{#commits}} > 》 \`{{author.name}}\`: [{{title}}]({{url}}) {{/commits}} `, - issue: -`\`{{user.name}}\`{{GB_state.str}} {{#object_attributes}}[[#{{id}}议题]({{url}})]{{/object_attributes}} + issue: `\`{{user.name}}\`{{GB_state.str}} {{#object_attributes}}[[#{{id}}议题]({{url}})]{{/object_attributes}} > **议题详情:** {{#object_attributes}}> 标题: [{{title}}]({{url}}) > 描述: {{description}} @@ -98,14 +94,12 @@ module.exports = appInfo => { {{#project}}项目信息: [[{{name}} / {{namespace}}]({{web_url}})]{{/project}} `, - wiki: -`\`{{user.name}}\` {{#object_attributes}}{{GB_action.actionString}} WIKI页 [{{title}}](url) + wiki: `\`{{user.name}}\` {{#object_attributes}}{{GB_action.actionString}} WIKI页 [{{title}}](url) > 内容: {{content}}{{/object_attributes}} {{#project}}项目信息: [[{{name}} / {{namespace}}]({{web_url}})]{{/project}} `, - note: -`\`{{user.name}}\` 评论了 {{#object_attributes}} [{{note}}](url) + note: `\`{{user.name}}\` 评论了 {{#object_attributes}} [{{note}}](url) > **评论详情:** > 类型: \`{{noteable_type}}\`{{/object_attributes}} {{#merge_request}}> 标题: [{{title}}]({{url}}) @@ -118,7 +112,12 @@ module.exports = appInfo => { 文件: {{file_name}}{{/snippet}} {{#project}}项目信息: [[{{name}} / {{namespace}}]({{web_url}})]{{/project}} -` +`, + project_action: `{{owner_name}} 做了 \`{{event_name}}\` 操作 +> 项目路径:{{path_with_namespace}}`, + repository_action: `{{user_name}} 做了 \`{{event_name}}\` 操作 +{{#project}}> 项目信息: [[{{name}} / {{namespace}}]({{web_url}})]{{/project}}`, + user_action: '\`{{event_name}}\`: {{name}}({{username}} {{email}})', }, }, };