├── .all-contributorsrc.json ├── .dockerignore ├── .env ├── .gitignore ├── .prettierrc.json ├── .vscode └── launch.json ├── Dockerfile ├── README.md ├── cloud-gitlab ├── chat.js ├── config.json ├── index.js ├── serverless.yaml └── test.json ├── cloud ├── chat.js ├── config.json ├── index.js └── test.js ├── docs ├── add_new.png ├── cloud1.png ├── github-demo.png ├── issue_demo.png ├── mr_demo.png ├── push_demo.png ├── robot_demo.jpg ├── save_new.png └── wework-demo.jpg ├── ecosystem.config.js ├── littleplan.md ├── package-lock.json ├── package.json ├── src ├── config.ts ├── controller │ ├── chat.ts │ ├── general.ts │ ├── github.ts │ ├── gitlab.ts │ ├── index.ts │ └── user.ts ├── entity │ └── user.ts ├── example │ ├── GithubPullRequestExample.json │ └── gihubPushEventExample.json ├── log.ts ├── middleware │ ├── chatRobot.ts │ └── logging.ts ├── routes.ts └── server.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.all-contributorsrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["readme.md"], 3 | "imageSize": 100, 4 | "contributorsPerLine": 7, 5 | "badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square)](#contributors)", 6 | "contributorTemplate": "<%= avatarBlock %>
<%= contributions %>", 7 | "types": { 8 | "custom": { 9 | "symbol": "🔭", 10 | "description": "A custom contribution type.", 11 | "link": "[<%= symbol %>](<%= url %> \"<%= description %>\")," 12 | } 13 | }, 14 | "skipCi": "true", 15 | "contributors": [] 16 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | error.log -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | NODE_ENV=development 3 | JWT_SECRET=your-secret-whatever 4 | DATABASE_URL=postgres://user:pass@localhost:5432/apidb 5 | CHAT_ID=82c08203-82a6-4824-8319-04a361bc0b2a -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.log 5 | .vscode 6 | error.log -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "arrowParens": "always" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/cloud/test.js", 12 | "outFiles": [ 13 | "${workspaceFolder}/**/*.js" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | 3 | # 创建 app 目录 4 | WORKDIR /app 5 | 6 | # 直接复制整个源项目 7 | COPY ./ /app/ 8 | 9 | RUN npm install 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 8080 14 | 15 | CMD [ "node", "./dist/server.js" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![git-webhook-wework-robot](https://socialify.git.ci/LeoEatle/git-webhook-wework-robot/image?description=1&font=Raleway&forks=1&language=1&logo=https%3A%2F%2Fwwcdn.weixin.qq.com%2Fnode%2Fwework%2Fimages%2FRtxThumb_2x.c70ae513d7.png&owner=1&pattern=Plus&pulls=1&stargazers=1&theme=Light) 2 | 3 | 4 | 6 | 7 | # 快速使用 8 | 9 | 直接在git项目中配置webhook `https://service-d6if097q-1251767583.gz.apigw.tencentcs.com/release/wechat-work-gitlab-robot?id={robotid}` 10 | 11 | 其中robotid是你的机器人id,可以在企业微信的机器人列表中查看,见图: 12 | 13 | 14 | 15 | # Changelog 16 | 2020-10 17 | 支持了 gitlab 的 review/wiki 事件 18 | 19 | 2020-9 20 | 支持了 gitlab 的腾讯云函数 git 机器人 21 | 22 | API网关地址: https://service-d6if097q-1251767583.gz.apigw.tencentcs.com/release/wechat-work-gitlab-robot?id={robotid} 23 | 24 | 自建云函数、设置 webhook 请参考下面 github 的介绍,是一样。 25 | 26 | 2020-1 27 | 支持了腾讯云云函数的创建 28 | 29 | 使用方式: 30 | 在github中的`Webhook`配置 API 的网关地址:https://service-5mv1fv1k-1251767583.gz.apigw.tencentcs.com/release/wechatwork_git_robot?id={robotid} 31 | 32 | **注意:其中robotid是你需要推送的机器人id** 33 | 34 | 35 | 自建云函数方式: 36 | 1. `git clone https://github.com/LeoEatle/git-webhook-wework-robot.git` 37 | 2. 注册并登陆腾讯云管理后台,新建一个云函数,可以先选个Node的Helloworld模板 38 | 3. 将代码中的`cloud`目录上传,见图 39 | ![](./docs/cloud1.png) 40 | 41 | 4. 点击保存(保存后🉑️测试试试) 42 | 43 | 5. 选择触发方式,添加新的触发方式,类型选择API网关,保存后得到url 44 | ![](./docs/add_new.png) 45 | 46 | 6. ok!可以填到Github的webhook里了,类型选择`Send me everything`,也可以自定义,url填上上面的url,**别忘了要在后面加上`?id={你的机器人id}`作为参数**。 47 | 48 | 可见下面[如何使用](https://github.com/LeoEatle/git-webhook-wework-robot#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8)。 49 | 50 | 2019-8 51 | 1. docker镜像上传到新地址:https://cloud.docker.com/repository/docker/leoeatle/wxwork-git-robot 52 | 53 | 2019-7 54 | 1. 由于一直在维护公司内的机器人,有些改动不适用于外部使用。单独分开两个项目,不再作为两个分支管理。 55 | 56 | 2019-6 57 | 1. 重新审视之前的`dockerfile`感觉过于臃肿,不如直接把dist打包进docker,所以进行了修改 58 | 2. 之前的腾讯云服务器没钱了,wework-robot.xyz 宣告停止服务,如有需要请自行搭建 59 | 60 | # 目前支持的事件 61 | ## Push event 示例 62 | 63 | 64 | 65 | ## Issue event 示例 66 | 67 | 68 | 69 | ## Merge Request 示例 70 | 71 | 72 | 73 | Merge Request 会有发起、合并、关闭、重新发起等几种情况,文案会有所不同。 74 | 75 | # 如何使用 76 | 77 | ## Github 78 | 79 | 如果是使用github,在github项目中的`Setting`中选择`Webhooks`,选择`Add Webhooks`,填写url,如`http://{{你的域名或者IP}}/github?id=7048958e-8b4b-4381-9758-af84347c240c`。 80 | 81 | ![](./docs/github-demo.png) 82 | 83 | `/github`用来区分github和gitlab,这两者的处理方式不同。 84 | 85 | `id`参数代表自定义的机器人id,可以在企业微信的机器人列表中查看(注意,这个必须要自己新建的机器人才能看到),见图: 86 | 87 | ![](./docs/robot-demo.jpg) 88 | 89 | ## Gitlab 90 | 91 | 如果是gitlab,将webhook地址改为`http://{{你的域名或者IP}}/git?id={{机器人id}}` 92 | 93 | 注意这里的路由是**git** 94 | 95 | 2019-10-17 更新 96 | 现在**gitlab**路由也会指向同样的功能了,所以两种路由都可以 97 | 98 | 99 | # 如何部署 100 | 101 | **建议将此服务部署在自己的机器上** 102 | 103 | ## 最简单的方式 104 | 105 | ```bash 106 | # 在服务器上 107 | git pull https://github.com/LeoEatle/git-webhook-wework-robot.git 108 | npm install 109 | npm run build 110 | pm2 start ./dist/server.js 111 | ``` 112 | 113 | ## 使用docker 114 | 115 | 目前已经编译出了一份镜像文件,地址:https://cloud.docker.com/repository/docker/leoeatle/wxwork-git-robot 116 | ```shell 117 | // 先登录 118 | sudo docker pull https://cloud.docker.com/repository/docker/leoeatle/wxwork-git-robot:latest 119 | docker run -d leoeatle/wxwork-git-robot 120 | ``` 121 | 当然,也可以使用pm2-docker来同时利用到pm2和docker。 122 | 123 | ## 机器人id配置 124 | 125 | 如果需要修改服务器端的默认机器人id设置,请修改项目根目录下的`.env` 126 | 127 | ```conf 128 | PORT=8080 129 | NODE_ENV=development 130 | JWT_SECRET=your-secret-whatever 131 | DATABASE_URL=postgres://user:pass@localhost:5432/apidb 132 | CHAT_ID=82c08203-82a6-4824-8319-04a361bc0b2a # 改这里! 133 | ``` 134 | # 项目介绍 && 开发(热烈欢迎提PR) 135 | 136 | 此项目用于连接git webhook和企业微信机器人webhook,采用koa2 + typescript开发,大部分git webhook 和 企业微信机器人的数据结构已经定义好typing,如: 137 | 138 | ```typescript 139 | interface Repository { 140 | name: string; 141 | description: string; 142 | homepage: string; 143 | git_http_url: string; 144 | git_ssh_url: string; 145 | url: string; 146 | visibility_level: number; 147 | } 148 | ``` 149 | 150 | 并且项目有配置严格的tslint和lint-staged等检查。 151 | 152 | 异步解决方案为`async/await` 153 | 154 | github事件handler: `github.ts` 155 | gitlab事件handler: `gilab.ts` 156 | 157 | chatRobot推送信息相关: `chat.ts` 158 | 159 | ## 提交 160 | 161 | ```bash 162 | git add . 163 | npm run commit # 让commitlint自动生成commit信息 164 | ``` 165 | 166 | # TODO 167 | 168 | * 目前gitlab只做了`push`和`merge request`事件的handler,以及只做了文字和mardown信息的推送,其余事件和其他类型的推送还需开发。 169 | 170 | * github推送目前只考虑`push` `pr` `issue`,其他有待添加 171 | 172 | * ~~为了方便其他团队甚至外面开源的使用,考虑使用docker方便自己部署。~~ 173 | 174 | * ~~考虑是不是可以在配置webhook的地方直接配置机器人id,分别推送~~ 175 | 176 | * ~~进一步考虑是不是可以用GUI统一管理项目和机器人id的关系~~ 177 | 178 | * 考虑可以补全gitlab的typing,实在太多了,有人帮忙就好了,github已经使用了有人开源整理的typing依赖库 179 | 180 | ## Contributors ✨ 181 | 182 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 |
soul11201
soul11201

🐛 💻
Haitao
Haitao

🐛 💻
192 | 193 | 194 | 195 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 196 | -------------------------------------------------------------------------------- /cloud-gitlab/chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 企业微信机器人 3 | * @author LeoEatle 4 | */ 5 | const request = require("request"); 6 | // 默认的企业微信机器人webhook地址 7 | const defaultUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/"; 8 | 9 | 10 | class ChatRobot { 11 | constructor(robotId, options) { 12 | this.robotId = robotId; 13 | this.url = defaultUrl; 14 | console.log('defaultUrl: ', defaultUrl); 15 | } 16 | 17 | /** 18 | * 向机器人webhook发出请求 19 | * @param json json信息 20 | */ 21 | async sendHttpRequest(json) { 22 | let self = this; 23 | return new Promise(function(resolve, reject) { 24 | request.post( 25 | `${self.url}send?key=${self.robotId}`, 26 | { 27 | json: json 28 | }, 29 | function (error, response, body) { 30 | if (!error && response.statusCode == 200) { 31 | if (body.errcode === 0 && body.errmsg === "ok") { 32 | console.log("机器人成功发送通知", body); 33 | resolve (response); 34 | } else { 35 | console.error("机器人发送通知失败", body); 36 | reject (body); 37 | } 38 | } else { 39 | console.error("调用机器人webhook失败", error); 40 | reject (error); 41 | } 42 | } 43 | ); 44 | 45 | }); 46 | } 47 | 48 | /** 49 | * 发送文本消息 50 | * @param msg 文本信息 51 | * @param chatid 单独通知的群聊id,默认undefined 52 | * @param options 对应参数,请参考官方文档 53 | */ 54 | async sendTextMsg(msg, chatid = undefined, options) { 55 | const textMsgInfo = { 56 | msgtype: "text", 57 | chatid, 58 | text: { 59 | "content": msg, 60 | ...options 61 | } 62 | }; 63 | return await this.sendHttpRequest(textMsgInfo); 64 | } 65 | 66 | /** 67 | * 发送markdown信息 68 | * @param content Markdown内容 69 | * @param chatid 单独通知的群聊id,默认undefined 70 | * @param options 其他参数,请参考官方文档 71 | */ 72 | async sendMdMsg(content, chatid = undefined, options) { 73 | const markdownMsgInfo = { 74 | "msgtype": "markdown", 75 | "chatid": chatid, 76 | "markdown": { 77 | "content": content 78 | } 79 | }; 80 | return await this.sendHttpRequest(markdownMsgInfo); 81 | } 82 | 83 | // TODO: 发送图文消息 84 | 85 | // TODO: 接受@消息 86 | 87 | // TODO: 获取群消息 88 | } 89 | 90 | module.exports = ChatRobot; -------------------------------------------------------------------------------- /cloud-gitlab/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverless-cloud-function-application": { 3 | "application": { 4 | "Chinese": { 5 | "name": "gitlab-wechat-robot", 6 | "description": "gitlab企业微信机器人云函数", 7 | "attention": "无", 8 | "readme": { 9 | "file": "", 10 | "content": "" 11 | }, 12 | "license": { 13 | "file": "", 14 | "content": "公开" 15 | }, 16 | "author": { 17 | "name": "LeoEatle" 18 | } 19 | }, 20 | "English": { 21 | "name": "gitlab-wechat-robot", 22 | "description": "gitlab-wechat-robot cloud function", 23 | "attention": "No", 24 | "readme": { 25 | "file": "", 26 | "content": "" 27 | }, 28 | "license": { 29 | "file": "", 30 | "content": "Open" 31 | }, 32 | "author": { 33 | "name": "LeoEatle" 34 | } 35 | }, 36 | "input_parameters": {}, 37 | "output_parameters": {}, 38 | "download_address": "https://github.com/LeoEatle/git-webhook-wework-robot/clou", 39 | "tags": [ 40 | "Nodejs8.9", 41 | "wechatwork", 42 | "robot" 43 | ], 44 | "version": "1.0.0" 45 | }, 46 | "functions": { 47 | "name": "gitlab-wechat-robot", 48 | "description": "gitlab企业微信机器人云函数", 49 | "handler": "index.main_handler", 50 | "memorySize": 128, 51 | "timeout": 3, 52 | "runtime": "Nodejs8.9", 53 | "Environment": {}, 54 | "Events": {}, 55 | "VpcConfig": {}, 56 | "codeObject": { 57 | "codeFile": ["index.js"], 58 | "CodeUri": [ 59 | "https://github.com/LeoEatle/git-webhook-wework-robot" 60 | ] 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cloud-gitlab/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * gitlab webhook handler 3 | * @author LeoEatle 4 | */ 5 | const querystring = require('querystring'); 6 | const ChatRobot = require('./chat'); 7 | 8 | const HEADER_KEY = "x-gitlab-event"; 9 | 10 | const HEADER_KEY_V2 = "X-Gitlab-Event"; 11 | 12 | const EVENTS = { 13 | "Push Hook": "push", 14 | "Tag Push Hook": "tag_push", 15 | "Issue Hook": "issue", 16 | "Note Hook": "note", 17 | "Merge Request Hook": "merge_request", 18 | "Review Hook": "review", 19 | "Wiki Page Hook": "wiki" 20 | }; 21 | 22 | const actionWords = { 23 | "open": "发起", 24 | "close": "关闭", 25 | "reopen": "重新发起", 26 | "update": "更新", 27 | "merge": "合并" 28 | }; 29 | 30 | /** 31 | * 处理test事件 32 | * @param {*} ctx koa context 33 | * @param {*} robotid 机器人id 34 | */ 35 | async function handleTest(body, robotid) { 36 | const msg = "收到一次webhook test"; 37 | const robot = new ChatRobot( 38 | robotid || config.chatid 39 | ); 40 | await robot.sendTextMsg(msg); 41 | ctx.status = 200; 42 | return; 43 | } 44 | 45 | /** 46 | * 处理push事件 47 | * @param ctx koa context 48 | * @param robotid 机器人id 49 | */ 50 | async function handlePush(body, robotid) { 51 | const robot = new ChatRobot( 52 | robotid || config.chatid 53 | ); 54 | let msg; 55 | const { user_name, repository, commits, ref} = body; 56 | if (repository.name === "project_test" && user_name === "user_test") { 57 | msg = "收到一次webhook test"; 58 | return await robot.sendTextMsg(msg); 59 | } else { 60 | const lastCommit = commits[0]; 61 | const branchName = ref.replace("refs/heads/", ""); 62 | msg = `项目 ${repository.name} 收到了一次push,提交者:${user_name},最新提交信息:${lastCommit.message}`; 63 | const mdMsg = `项目 [${repository.name}](${repository.homepage}) 收到一次push提交 64 | 提交者: \${user_name}\ 65 | 分支: \${branchName}\ 66 | 最新提交信息: ${lastCommit.message}`; 67 | await robot.sendMdMsg(mdMsg); 68 | return; 69 | } 70 | } 71 | 72 | /** 73 | * 处理merge request事件 74 | * @param ctx koa context 75 | */ 76 | async function handleMR(body, robotid) { 77 | const robot = new ChatRobot( 78 | robotid || config.chatid 79 | ); 80 | const {user, object_attributes} = body; 81 | const attr = object_attributes; 82 | const mdMsg = `${user.name}在 [${attr.source.name}](${attr.source.web_url}) ${actionWords[attr.action]}了一个MR 83 | 标题:${attr.title} 84 | 源分支:${attr.source_branch} 85 | 目标分支:${attr.target_branch} 86 | [查看MR详情](${attr.url})`; 87 | await robot.sendMdMsg(mdMsg); 88 | return; 89 | } 90 | 91 | async function handleIssue(body, robotid) { 92 | const robot = new ChatRobot( 93 | robotid || config.chatid 94 | ); 95 | console.log("[Issue handler]Req Body", body); 96 | const {user, object_attributes, repository} = body; 97 | const attr = object_attributes; 98 | const mdMsg = `有人在 [${repository.name}](${repository.url}) ${actionWords[attr.action]}了一个issue 99 | 标题:${attr.title} 100 | 发起人:${user.name} 101 | [查看详情](${attr.url})`; 102 | await robot.sendMdMsg(mdMsg); 103 | return; 104 | } 105 | 106 | async function handleNote(body, robotid) { 107 | const robot = new ChatRobot( 108 | robotid || config.chatid 109 | ); 110 | const { user, project, object_attributes, repository } = body; 111 | const { noteable_type, url } = object_attributes; 112 | if (noteable_type === 'Issue') { 113 | const mdMsg = `${user.name} 在[${repository.name}](${repository.url})评论了一个issue 114 | 标题:${object_attributes} 115 | [查看详情](${url})` 116 | await robot.sendMdMsg(mdMsg); 117 | } 118 | return; 119 | } 120 | 121 | async function handleWiki(body, robotid) { 122 | const robot = new ChatRobot( 123 | robotid || config.chatid 124 | ); 125 | const { user, project, object_attributes, wiki } = body; 126 | const { title, url } = object_attributes; 127 | const mdMsg = `${user.name} 在[${project.name}](${project.git_http_url})更新了wiki 128 | 标题:${title} 129 | [查看详情](${url})` 130 | await robot.sendMdMsg(mdMsg); 131 | 132 | return; 133 | } 134 | 135 | async function handleDefault(event) { 136 | const msg = `Sorry,暂时还没有处理${event}事件`; 137 | console.log(msg) 138 | return; 139 | } 140 | 141 | exports.main_handler = async (event, context, callback) => { 142 | console.log('event', event); 143 | const gitEvent = event.headers[HEADER_KEY] || event.headers[HEADER_KEY_V2]; 144 | if (!event) { 145 | return `Sorry,这可能不是一个gitlab的webhook请求`; 146 | } 147 | const robotid = event.queryString.id; 148 | const payload = JSON.parse(event.body); // 我的天啊腾讯云竟然在这里返回一个 string 149 | console.log('payload', payload); 150 | // 检查是否是test事件 151 | if (event.headers["x-event-test"] == "true") { 152 | // test事件中仅处理push,否则推送太多 153 | if (EVENTS[gitEvent] == "push") { 154 | return await handleTest(payload, robotid); 155 | } else { 156 | console.log("其他test请求我可不会管"); 157 | return; 158 | } 159 | } 160 | switch (EVENTS[gitEvent]) { 161 | case "push": 162 | return await handlePush(payload, robotid); 163 | case "merge_request": 164 | return await handleMR(payload, robotid); 165 | case "issue": 166 | return await handleIssue(payload, robotid); 167 | case "note": 168 | return await handleNote(payload, robotid); 169 | case "wiki": 170 | return await handleWiki(payload, robotid); 171 | default: 172 | return await handleDefault(gitEvent); 173 | } 174 | } -------------------------------------------------------------------------------- /cloud-gitlab/serverless.yaml: -------------------------------------------------------------------------------- 1 | component: scf 2 | name: ap-guangzhou_gitlab_wechatwork_robot 3 | org: leoeatle 4 | app: gitlab-wechatwork-robot 5 | stage: dev 6 | inputs: 7 | name: gitlab-wechatwork-robot 8 | src: ./ 9 | description: 企业微信git机器人 for gitlab 10 | handler: index.main_handler 11 | runtime: Nodejs8.9 12 | namespace: default 13 | region: ap-guangzhou 14 | memorySize: 128 15 | timeout: 3 16 | -------------------------------------------------------------------------------- /cloud-gitlab/test.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/cloud-gitlab/test.json -------------------------------------------------------------------------------- /cloud/chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 企业微信机器人 3 | * @author LeoEatle 4 | */ 5 | const request = require("request"); 6 | // 默认的企业微信机器人webhook地址 7 | const defaultUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/"; 8 | 9 | 10 | class ChatRobot { 11 | constructor(robotId, options) { 12 | this.robotId = robotId; 13 | this.url = defaultUrl; 14 | console.log('defaultUrl: ', defaultUrl); 15 | } 16 | 17 | /** 18 | * 向机器人webhook发出请求 19 | * @param json json信息 20 | */ 21 | async sendHttpRequest(json) { 22 | let self = this; 23 | return new Promise(function(resolve, reject) { 24 | request.post( 25 | `${self.url}send?key=${self.robotId}`, 26 | { 27 | json: json 28 | }, 29 | function (error, response, body) { 30 | if (!error && response.statusCode == 200) { 31 | if (body.errcode === 0 && body.errmsg === "ok") { 32 | console.log("机器人成功发送通知", body); 33 | resolve (response); 34 | } else { 35 | console.error("机器人发送通知失败", body); 36 | reject (body); 37 | } 38 | } else { 39 | console.error("调用机器人webhook失败", error); 40 | reject (error); 41 | } 42 | } 43 | ); 44 | 45 | }); 46 | } 47 | 48 | /** 49 | * 发送文本消息 50 | * @param msg 文本信息 51 | * @param chatid 单独通知的群聊id,默认undefined 52 | * @param options 对应参数,请参考官方文档 53 | */ 54 | async sendTextMsg(msg, chatid = undefined, options) { 55 | const textMsgInfo = { 56 | msgtype: "text", 57 | chatid, 58 | text: { 59 | "content": msg, 60 | ...options 61 | } 62 | }; 63 | return await this.sendHttpRequest(textMsgInfo); 64 | } 65 | 66 | /** 67 | * 发送markdown信息 68 | * @param content Markdown内容 69 | * @param chatid 单独通知的群聊id,默认undefined 70 | * @param options 其他参数,请参考官方文档 71 | */ 72 | async sendMdMsg(content, chatid = undefined, options) { 73 | const markdownMsgInfo = { 74 | "msgtype": "markdown", 75 | "chatid": chatid, 76 | "markdown": { 77 | "content": content 78 | } 79 | }; 80 | return await this.sendHttpRequest(markdownMsgInfo); 81 | } 82 | 83 | // TODO: 发送图文消息 84 | 85 | // TODO: 接受@消息 86 | 87 | // TODO: 获取群消息 88 | } 89 | 90 | module.exports = ChatRobot; -------------------------------------------------------------------------------- /cloud/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverless-cloud-function-application": { 3 | "application": { 4 | "Chinese": { 5 | "name": "github-wechat-robot", 6 | "description": "github企业微信机器人云函数", 7 | "attention": "无", 8 | "readme": { 9 | "file": "", 10 | "content": "" 11 | }, 12 | "license": { 13 | "file": "", 14 | "content": "公开" 15 | }, 16 | "author": { 17 | "name": "LeoEatle" 18 | } 19 | }, 20 | "English": { 21 | "name": "github-wechat-robot", 22 | "description": "github-wechat-robot cloud function", 23 | "attention": "No", 24 | "readme": { 25 | "file": "", 26 | "content": "" 27 | }, 28 | "license": { 29 | "file": "", 30 | "content": "Open" 31 | }, 32 | "author": { 33 | "name": "LeoEatle" 34 | } 35 | }, 36 | "input_parameters": {}, 37 | "output_parameters": {}, 38 | "download_address": "https://github.com/LeoEatle/git-webhook-wework-robot", 39 | "tags": [ 40 | "Nodejs8.9", 41 | "wechatwork", 42 | "robot" 43 | ], 44 | "version": "1.0.0" 45 | }, 46 | "functions": { 47 | "name": "github-wechat-robot", 48 | "description": "github企业微信机器人云函数", 49 | "handler": "index.main_handler", 50 | "memorySize": 128, 51 | "timeout": 3, 52 | "runtime": "Nodejs8.9", 53 | "Environment": {}, 54 | "Events": {}, 55 | "VpcConfig": {}, 56 | "codeObject": { 57 | "codeFile": ["index.js"], 58 | "CodeUri": [ 59 | "https://github.com/LeoEatle/git-webhook-wework-robot" 60 | ] 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cloud/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HEADER_KEY = "x-github-event"; 4 | 5 | const actionWords = { 6 | "opened": "发起", 7 | "closed": "关闭", 8 | "reopened": "重新发起", 9 | "edited": "更新", 10 | "merge": "合并", 11 | "created": "创建", 12 | "requested": "请求", 13 | "completed": "完成", 14 | "synchronize": "同步更新" 15 | }; 16 | 17 | const querystring = require('querystring'); 18 | const ChatRobot = require('./chat'); 19 | /** 20 | * 处理ping事件 21 | * @param ctx koa context 22 | * @param robotid 机器人id 23 | */ 24 | async function handlePing(body, robotid) { 25 | const robot = new ChatRobot( 26 | robotid 27 | ); 28 | 29 | const { repository } = body; 30 | const msg = "成功收到了来自Github的Ping请求,项目名称:" + repository.name; 31 | await robot.sendTextMsg(msg); 32 | return msg; 33 | } 34 | 35 | /** 36 | * 处理push事件 37 | * @param ctx koa context 38 | * @param robotid 机器人id 39 | */ 40 | async function handlePush(body, robotid) { 41 | const robot = new ChatRobot( 42 | robotid 43 | ); 44 | let msg; 45 | const { pusher, repository, commits, ref} = body; 46 | const user_name = pusher.name; 47 | const lastCommit = commits[0]; 48 | msg = `项目 ${repository.name} 收到了一次push,提交者:${user_name},最新提交信息:${lastCommit.message}`; 49 | const mdMsg = `项目 [${repository.name}](${repository.url}) 收到一次push提交 50 | 提交者: \${user_name}\ 51 | 分支: \${ref}\ 52 | 最新提交信息: ${lastCommit.message}`; 53 | await robot.sendMdMsg(mdMsg); 54 | return mdMsg; 55 | } 56 | 57 | /** 58 | * 处理merge request事件 59 | * @param ctx koa context 60 | * @param robotid 机器人id 61 | */ 62 | async function handlePR(body, robotid) { 63 | const robot = new ChatRobot( 64 | robotid 65 | ); 66 | const {action, sender, pull_request, repository} = body; 67 | const mdMsg = `${sender.login}在 [${repository.full_name}](${repository.html_url}) ${actionWords[action]}了PR 68 | 标题:${pull_request.title} 69 | 源分支:${pull_request.head.ref} 70 | 目标分支:${pull_request.base.ref} 71 | [查看PR详情](${pull_request.html_url})`; 72 | await robot.sendMdMsg(mdMsg); 73 | return mdMsg; 74 | } 75 | 76 | /** 77 | * 处理issue 事件 78 | * @param ctx koa context 79 | * @param robotid 机器人id 80 | */ 81 | async function handleIssue(body, robotid) { 82 | const robot = new ChatRobot( 83 | robotid 84 | ); 85 | const { action, issue, repository } = body; 86 | if (action !== "opened") { 87 | return `除非有人开启新的issue,否则无需通知机器人`; 88 | } 89 | const mdMsg = `有人在 [${repository.name}](${repository.html_url}) ${actionWords[action]}了一个issue 90 | 标题:${issue.title} 91 | 发起人:[${issue.user.login}](${issue.user.html_url}) 92 | [查看详情](${issue.html_url})`; 93 | await robot.sendMdMsg(mdMsg); 94 | return; 95 | } 96 | 97 | /** 98 | * 对于未处理的事件,统一走这里 99 | * @param ctx koa context 100 | * @param event 事件名 101 | */ 102 | function handleDefault(body, event) { 103 | return `Sorry,暂时还没有处理${event}事件`; 104 | } 105 | 106 | exports.main_handler = async (event, context, callback) => { 107 | console.log('event: ', event); 108 | if (!(event.headers && event.headers[HEADER_KEY])) { 109 | return 'Not a github webhook deliver' 110 | } 111 | const gitEvent = event.headers[HEADER_KEY] 112 | const robotid = event.queryString.id 113 | const query = querystring.parse(event.body); 114 | // console.log('query: ', query); 115 | const payload = JSON.parse(query.payload); 116 | console.log('payload: ', payload); 117 | console.log('robotid: ', robotid); 118 | switch (gitEvent) { 119 | case "push": 120 | return await handlePush(payload, robotid); 121 | case "pull_request": 122 | return await handlePR(payload, robotid); 123 | case "ping": 124 | return await handlePing(payload, robotid); 125 | case "issues": 126 | return await handleIssue(payload, robotid); 127 | default: 128 | return handleDefault(payload, gitEvent); 129 | } 130 | }; -------------------------------------------------------------------------------- /cloud/test.js: -------------------------------------------------------------------------------- 1 | const handler = require("./index").main_handler; 2 | const event = { 3 | body: 4 | "id=82c08203-82a6-4824-8319-04a361bc0b2a&payload=%7B%22zen%22%3A%22Anything+added+dilutes+everything+else.%22%2C%22hook_id%22%3A175190809%2C%22hook%22%3A%7B%22type%22%3A%22Repository%22%2C%22id%22%3A175190809%2C%22name%22%3A%22web%22%2C%22active%22%3Atrue%2C%22events%22%3A%5B%22%2A%22%5D%2C%22config%22%3A%7B%22content_type%22%3A%22form%22%2C%22insecure_ssl%22%3A%220%22%2C%22url%22%3A%22https%3A%2F%2Fservice-5mv1fv1k-1251767583.gz.apigw.tencentcs.com%2Frelease%2Fwechatwork_git_robot%3Fid%3D82c08203-82a6-4824-8319-04a361bc0b2a%22%7D%2C%22updated_at%22%3A%222020-01-15T13%3A33%3A36Z%22%2C%22created_at%22%3A%222020-01-15T13%3A33%3A36Z%22%2C%22url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fhooks%2F175190809%22%2C%22test_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fhooks%2F175190809%2Ftest%22%2C%22ping_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fhooks%2F175190809%2Fpings%22%2C%22last_response%22%3A%7B%22code%22%3Anull%2C%22status%22%3A%22unused%22%2C%22message%22%3Anull%7D%7D%2C%22repository%22%3A%7B%22id%22%3A166331677%2C%22node_id%22%3A%22MDEwOlJlcG9zaXRvcnkxNjYzMzE2Nzc%3D%22%2C%22name%22%3A%22git-webhook-wework-robot%22%2C%22full_name%22%3A%22LeoEatle%2Fgit-webhook-wework-robot%22%2C%22private%22%3Afalse%2C%22owner%22%3A%7B%22login%22%3A%22LeoEatle%22%2C%22id%22%3A14247110%2C%22node_id%22%3A%22MDQ6VXNlcjE0MjQ3MTEw%22%2C%22avatar_url%22%3A%22https%3A%2F%2Favatars0.githubusercontent.com%2Fu%2F14247110%3Fv%3D4%22%2C%22gravatar_id%22%3A%22%22%2C%22url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%22%2C%22html_url%22%3A%22https%3A%2F%2Fgithub.com%2FLeoEatle%22%2C%22followers_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Ffollowers%22%2C%22following_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Ffollowing%7B%2Fother_user%7D%22%2C%22gists_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Fgists%7B%2Fgist_id%7D%22%2C%22starred_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Fstarred%7B%2Fowner%7D%7B%2Frepo%7D%22%2C%22subscriptions_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Fsubscriptions%22%2C%22organizations_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Forgs%22%2C%22repos_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Frepos%22%2C%22events_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Fevents%7B%2Fprivacy%7D%22%2C%22received_events_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Freceived_events%22%2C%22type%22%3A%22User%22%2C%22site_admin%22%3Afalse%7D%2C%22html_url%22%3A%22https%3A%2F%2Fgithub.com%2FLeoEatle%2Fgit-webhook-wework-robot%22%2C%22description%22%3A%22%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1github%2Fgitlab%E6%9C%BA%E5%99%A8%E4%BA%BA%22%2C%22fork%22%3Afalse%2C%22url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%22%2C%22forks_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fforks%22%2C%22keys_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fkeys%7B%2Fkey_id%7D%22%2C%22collaborators_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fcollaborators%7B%2Fcollaborator%7D%22%2C%22teams_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fteams%22%2C%22hooks_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fhooks%22%2C%22issue_events_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fissues%2Fevents%7B%2Fnumber%7D%22%2C%22events_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fevents%22%2C%22assignees_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fassignees%7B%2Fuser%7D%22%2C%22branches_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fbranches%7B%2Fbranch%7D%22%2C%22tags_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Ftags%22%2C%22blobs_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fgit%2Fblobs%7B%2Fsha%7D%22%2C%22git_tags_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fgit%2Ftags%7B%2Fsha%7D%22%2C%22git_refs_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fgit%2Frefs%7B%2Fsha%7D%22%2C%22trees_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fgit%2Ftrees%7B%2Fsha%7D%22%2C%22statuses_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fstatuses%2F%7Bsha%7D%22%2C%22languages_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Flanguages%22%2C%22stargazers_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fstargazers%22%2C%22contributors_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fcontributors%22%2C%22subscribers_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fsubscribers%22%2C%22subscription_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fsubscription%22%2C%22commits_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fcommits%7B%2Fsha%7D%22%2C%22git_commits_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fgit%2Fcommits%7B%2Fsha%7D%22%2C%22comments_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fcomments%7B%2Fnumber%7D%22%2C%22issue_comment_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fissues%2Fcomments%7B%2Fnumber%7D%22%2C%22contents_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fcontents%2F%7B%2Bpath%7D%22%2C%22compare_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fcompare%2F%7Bbase%7D...%7Bhead%7D%22%2C%22merges_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fmerges%22%2C%22archive_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2F%7Barchive_format%7D%7B%2Fref%7D%22%2C%22downloads_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fdownloads%22%2C%22issues_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fissues%7B%2Fnumber%7D%22%2C%22pulls_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fpulls%7B%2Fnumber%7D%22%2C%22milestones_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fmilestones%7B%2Fnumber%7D%22%2C%22notifications_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fnotifications%7B%3Fsince%2Call%2Cparticipating%7D%22%2C%22labels_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Flabels%7B%2Fname%7D%22%2C%22releases_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Freleases%7B%2Fid%7D%22%2C%22deployments_url%22%3A%22https%3A%2F%2Fapi.github.com%2Frepos%2FLeoEatle%2Fgit-webhook-wework-robot%2Fdeployments%22%2C%22created_at%22%3A%222019-01-18T02%3A39%3A47Z%22%2C%22updated_at%22%3A%222020-01-15T03%3A24%3A24Z%22%2C%22pushed_at%22%3A%222019-10-17T07%3A14%3A22Z%22%2C%22git_url%22%3A%22git%3A%2F%2Fgithub.com%2FLeoEatle%2Fgit-webhook-wework-robot.git%22%2C%22ssh_url%22%3A%22git%40github.com%3ALeoEatle%2Fgit-webhook-wework-robot.git%22%2C%22clone_url%22%3A%22https%3A%2F%2Fgithub.com%2FLeoEatle%2Fgit-webhook-wework-robot.git%22%2C%22svn_url%22%3A%22https%3A%2F%2Fgithub.com%2FLeoEatle%2Fgit-webhook-wework-robot%22%2C%22homepage%22%3Anull%2C%22size%22%3A151%2C%22stargazers_count%22%3A56%2C%22watchers_count%22%3A56%2C%22language%22%3A%22TypeScript%22%2C%22has_issues%22%3Atrue%2C%22has_projects%22%3Atrue%2C%22has_downloads%22%3Atrue%2C%22has_wiki%22%3Atrue%2C%22has_pages%22%3Afalse%2C%22forks_count%22%3A21%2C%22mirror_url%22%3Anull%2C%22archived%22%3Afalse%2C%22disabled%22%3Afalse%2C%22open_issues_count%22%3A3%2C%22license%22%3Anull%2C%22forks%22%3A21%2C%22open_issues%22%3A3%2C%22watchers%22%3A56%2C%22default_branch%22%3A%22master%22%7D%2C%22sender%22%3A%7B%22login%22%3A%22LeoEatle%22%2C%22id%22%3A14247110%2C%22node_id%22%3A%22MDQ6VXNlcjE0MjQ3MTEw%22%2C%22avatar_url%22%3A%22https%3A%2F%2Favatars0.githubusercontent.com%2Fu%2F14247110%3Fv%3D4%22%2C%22gravatar_id%22%3A%22%22%2C%22url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%22%2C%22html_url%22%3A%22https%3A%2F%2Fgithub.com%2FLeoEatle%22%2C%22followers_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Ffollowers%22%2C%22following_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Ffollowing%7B%2Fother_user%7D%22%2C%22gists_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Fgists%7B%2Fgist_id%7D%22%2C%22starred_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Fstarred%7B%2Fowner%7D%7B%2Frepo%7D%22%2C%22subscriptions_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Fsubscriptions%22%2C%22organizations_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Forgs%22%2C%22repos_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Frepos%22%2C%22events_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Fevents%7B%2Fprivacy%7D%22%2C%22received_events_url%22%3A%22https%3A%2F%2Fapi.github.com%2Fusers%2FLeoEatle%2Freceived_events%22%2C%22type%22%3A%22User%22%2C%22site_admin%22%3Afalse%7D%7D", 5 | 6 | headerParameters: {}, 7 | 8 | headers: { 9 | accept: "*/*", 10 | 11 | "content-length": "9853", 12 | 13 | "content-type": "application/x-www-form-urlencoded", 14 | 15 | host: "service-5mv1fv1k-1251767583.gz.apigw.tencentcs.com", 16 | 17 | "user-agent": "GitHub-Hookshot/7ea4e29", 18 | 19 | "x-anonymous-consumer": "true", 20 | 21 | "x-api-requestid": "f4195f0a498ba9d9e997aca082338fb8", 22 | 23 | "x-b3-traceid": "f4195f0a498ba9d9e997aca082338fb8", 24 | 25 | "x-github-delivery": "a1aab800-379b-11ea-87cc-2eb3ac5508aa", 26 | 27 | "x-github-event": "ping", 28 | 29 | "x-qualifier": "$LATEST" 30 | }, 31 | 32 | httpMethod: "POST", 33 | 34 | path: "/wechatwork_git_robot", 35 | 36 | pathParameters: {}, 37 | 38 | queryString: { id: "82c08203-82a6-4824-8319-04a361bc0b2a" }, 39 | 40 | queryStringParameters: {}, 41 | 42 | requestContext: { 43 | httpMethod: "ANY", 44 | 45 | identity: {}, 46 | 47 | path: "/wechatwork_git_robot", 48 | 49 | serviceId: "service-5mv1fv1k", 50 | 51 | sourceIp: "192.30.252.99", 52 | 53 | stage: "release" 54 | } 55 | }; 56 | const context = { 57 | hello: "hello" 58 | }; 59 | 60 | const callback = function(param) { 61 | console.log("param", param); 62 | }; 63 | let result = handler(event, context, callback); 64 | console.log("result: ", result); 65 | -------------------------------------------------------------------------------- /docs/add_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/add_new.png -------------------------------------------------------------------------------- /docs/cloud1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/cloud1.png -------------------------------------------------------------------------------- /docs/github-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/github-demo.png -------------------------------------------------------------------------------- /docs/issue_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/issue_demo.png -------------------------------------------------------------------------------- /docs/mr_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/mr_demo.png -------------------------------------------------------------------------------- /docs/push_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/push_demo.png -------------------------------------------------------------------------------- /docs/robot_demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/robot_demo.jpg -------------------------------------------------------------------------------- /docs/save_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/save_new.png -------------------------------------------------------------------------------- /docs/wework-demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoEatle/git-webhook-wework-robot/69d422ac823d033661ed66dd80c505a70af967ca/docs/wework-demo.jpg -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [{ 3 | name: 'wework-robot', 4 | script: './dist/server.js', 5 | 6 | // Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/ 7 | instances: 1, 8 | autorestart: true, 9 | watch: true, 10 | max_memory_restart: '1G', 11 | env: { 12 | NODE_ENV: 'production' 13 | }, 14 | env_production: { 15 | NODE_ENV: 'production' 16 | } 17 | }], 18 | 19 | deploy : { 20 | production : { 21 | user : 'root', 22 | host : '134.175.32.212', 23 | ref : 'origin/master', 24 | repo : 'git@github.com:LeoEatle/git-webhook-wework-robot.git', 25 | path : '/root/pm2/wework-robot', 26 | 'post-deploy' : 'npm install && npm run build && pm2 reload ecosystem.config.js --env production' 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /littleplan.md: -------------------------------------------------------------------------------- 1 | # 生产环境 2 | 3 | 目前生产环境是使用`webpack`配合`ts-loader`来实现对生产环境代码的编译的,但是webpack对于source-map的生成实在是太慢了。 4 | 5 | 目前有几种办法: 6 | 7 | ## 使用ts-node作为生产环境 8 | 9 | 在网上查了一些资料,是有人尝试直接使用ts-node作为生产环境的,似乎有参数可以让它只编译,不检查type,这样就会快很多,而且如果也能支持`--inspect`远程调试的话,岂不美哉。 10 | 11 | ## 使用tsc编译 12 | 13 | 用`tsc`其实是比较通用的做法,它会保留整个项目结构,看起来似乎值得一试。 14 | 15 | # 实现node热更新 16 | 17 | 热更新在前端开发很有用,但在node端似乎不太常用,因为`nodemon`重启的速度一般也不慢。 18 | 19 | 不过确实在思考如果node项目很大了怎么办,事实上目前业务上就遇到了这个问题。看到一篇[文章](https://segmentfault.com/a/1190000009023924)有介绍怎么通过webpack实现热更新,其实是行得通的。 20 | 21 | 剩下的就是性价比高不高的问题了,因为不比成熟的`react-hot-loader`,如果要自己处理热更新模块,还是比较麻烦的,目录结构上也做了诸多限制。 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitcode-wework-robot", 3 | "version": "1.0.0", 4 | "description": "连接git.code.oa.com和企业微信的机器人,监听git.code的webhook来推送push、PR等信息到群内。", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon --watch 'src/**/*' -e ts,tsx --exec ts-node src/server.ts", 8 | "build": "webpack --config webpack.config.js --progress", 9 | "lint": "tslint --fix ./src/", 10 | "commit": "git-cz", 11 | "precommit": "lint-staged", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "config": { 15 | "commitizen": { 16 | "path": "./node_modules/cz-conventional-changelog" 17 | } 18 | }, 19 | "lint-staged": { 20 | "*.{js,css,less,ts,tsx,jsx}": [ 21 | "npm run lint", 22 | "git add" 23 | ] 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "http://git.code.oa.com/leoytliu/gitcode-wework-robot.git" 28 | }, 29 | "keywords": [ 30 | "wework", 31 | "robot", 32 | "git.code" 33 | ], 34 | "author": "leoytliu", 35 | "license": "ISC", 36 | "devDependencies": { 37 | "@types/dotenv": "^4.0.3", 38 | "@types/koa": "2.0.44", 39 | "@types/koa-bodyparser": "^4.2.0", 40 | "@types/koa-helmet": "^3.1.2", 41 | "@types/koa-router": "^7.0.28", 42 | "@types/koa__cors": "^2.2.2", 43 | "@types/node": "^10.7.0", 44 | "@types/shelljs": "^0.8.0", 45 | "@types/request": "^2.48.1", 46 | "commitizen": "^3.0.5", 47 | "commitlint": "^7.3.2", 48 | "lint-staged": "^8.1.1", 49 | "nodemon": "^1.17.4", 50 | "shelljs": "^0.8.2", 51 | "ts-loader": "^5.3.3", 52 | "ts-node": "^7.0.1", 53 | "tslint": "^5.10.0", 54 | "typescript": "^3.0.1", 55 | "webpack": "^4.28.4", 56 | "webpack-cli": "^3.2.1" 57 | }, 58 | "dependencies": { 59 | "@koa/cors": "^2.2.1", 60 | "class-validator": "^0.9.1", 61 | "dotenv": "^6.0.0", 62 | "github-webhook-event-types": "^1.2.1", 63 | "koa": "^2.5.1", 64 | "koa-bodyparser": "^4.2.1", 65 | "koa-helmet": "^4.0.0", 66 | "koa-jwt": "^3.3.2", 67 | "koa-router": "^7.4.0", 68 | "pg": "^7.4.3", 69 | "pg-connection-string": "^2.0.0", 70 | "reflect-metadata": "^0.1.12", 71 | "request": "^2.88.0", 72 | "typeorm": "^0.2.6", 73 | "winston": "^3.0.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | 3 | dotenv.config({ path: ".env" }); 4 | 5 | export interface IConfig { 6 | port: number; 7 | debugLogging: boolean; 8 | dbsslconn: boolean; 9 | jwtSecret: string; 10 | databaseUrl: string; 11 | chatid: string; // 暂时机器人id由配置文件管理,之后可以考虑由GUI提供动态配置 12 | } 13 | 14 | const config: IConfig = { 15 | port: +process.env.PORT || 3000, 16 | debugLogging: process.env.NODE_ENV == "development", 17 | dbsslconn: process.env.NODE_ENV != "development", 18 | jwtSecret: process.env.JWT_SECRET || "your-secret-whatever", 19 | databaseUrl: 20 | process.env.DATABASE_URL || "postgres://user:pass@localhost:5432/apidb", 21 | chatid: process.env.CHAT_ID || "82c08203-82a6-4824-8319-04a361bc0b2a" // 这个是jenkins-robot 22 | }; 23 | 24 | export { config }; -------------------------------------------------------------------------------- /src/controller/chat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 企业微信机器人 3 | * @author LeoEatle 4 | */ 5 | // const request = require("request"); 6 | import * as request from "request"; 7 | import customLog from "../log"; 8 | const log = customLog("gitlab handler"); 9 | // 默认的企业微信机器人webhook地址 10 | const defaultUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/"; 11 | // 企业机器人发送textMsg的格式 12 | interface TextMsgInfo { 13 | msgtype: String; 14 | chatid?: String; // 当只想通知某个群的时候,才需要用到chatid 15 | text: { 16 | content: String; 17 | mentioned_list?: Array; // 提醒群中的指定成员 18 | mentioned_mobile_list?: Array; // 提醒某个手机号的成员 19 | }; 20 | } 21 | 22 | // 企业机器人发送markdown格式 23 | interface MarkdownMsgInfo { 24 | msgtype: String; 25 | chatid?: String; 26 | markdown: { 27 | content: String; 28 | }; 29 | } 30 | 31 | // 企业机器人返回的body体 32 | interface ResponseBody { 33 | errcode: Number; 34 | errmsg: String; 35 | } 36 | 37 | export default class ChatRobot { 38 | readonly robotId: String; 39 | readonly url: String = defaultUrl; 40 | constructor(robotId: String, options?) { 41 | this.robotId = robotId; 42 | if (options) { 43 | this.url = options.url || defaultUrl; 44 | } 45 | } 46 | 47 | /** 48 | * 向机器人webhook发出请求 49 | * @param json json信息 50 | */ 51 | private async sendHttpRequest(json) { 52 | console.log("http request"); 53 | request.post( 54 | `${this.url}send?key=${this.robotId}`, 55 | { 56 | json: json 57 | }, 58 | function (error, response, body: ResponseBody) { 59 | if (!error && response.statusCode == 200) { 60 | if (body.errcode === 0 && body.errmsg === "ok") { 61 | log.info("机器人成功发送通知" + body); 62 | return (response); 63 | } else { 64 | console.error("机器人发送通知失败", body); 65 | throw (body); 66 | } 67 | } else { 68 | console.error("调用机器人webhook失败", error); 69 | throw (error); 70 | } 71 | } 72 | ); 73 | return; 74 | } 75 | 76 | /** 77 | * 发送文本消息 78 | * @param msg 文本信息 79 | * @param chatid 单独通知的群聊id,默认undefined 80 | * @param options 对应参数,请参考官方文档 81 | */ 82 | public async sendTextMsg(msg, chatid = undefined, options?) { 83 | const textMsgInfo: TextMsgInfo = { 84 | msgtype: "text", 85 | chatid, 86 | text: { 87 | "content": msg, 88 | ...options 89 | } 90 | }; 91 | return await this.sendHttpRequest(textMsgInfo); 92 | } 93 | 94 | /** 95 | * 发送markdown信息 96 | * @param content Markdown内容 97 | * @param chatid 单独通知的群聊id,默认undefined 98 | * @param options 其他参数,请参考官方文档 99 | */ 100 | public async sendMdMsg(content, chatid = undefined, options?) { 101 | const markdownMsgInfo: MarkdownMsgInfo = { 102 | "msgtype": "markdown", 103 | "chatid": chatid, 104 | "markdown": { 105 | "content": content 106 | } 107 | }; 108 | return await this.sendHttpRequest(markdownMsgInfo); 109 | } 110 | 111 | // TODO: 发送图文消息 112 | 113 | // TODO: 接受@消息 114 | 115 | // TODO: 获取群消息 116 | } -------------------------------------------------------------------------------- /src/controller/general.ts: -------------------------------------------------------------------------------- 1 | import { BaseContext } from "koa"; 2 | import ChatRobot from "./chat"; 3 | import { config } from "../config"; 4 | 5 | export default class GeneralController { 6 | public static async helloWorld(ctx: BaseContext) { 7 | ctx.body = "Hello World!"; 8 | } 9 | 10 | // silly endpoint to show where the payload data from the token gets stored 11 | public static async getJwtPayload(ctx: BaseContext) { 12 | // example just to set a different status 13 | ctx.status = 201; 14 | // the body of the response will contain the information contained as payload in the JWT 15 | ctx.body = ctx.state.user; 16 | } 17 | 18 | public static async sendText(ctx: BaseContext) { 19 | const url = ctx.request.url; 20 | const ROBOTID_REGEX = /key=([a-zA-Z0-9-]+)/g; 21 | const robotidRe = ROBOTID_REGEX.exec(url); 22 | const robotid = robotidRe && robotidRe[1]; 23 | const robot: ChatRobot = new ChatRobot( 24 | robotid || config.chatid 25 | ); 26 | const body = ctx.request.body; 27 | console.log("ctx.request.body", body); 28 | const msg = body.text; 29 | await robot.sendTextMsg(msg); 30 | ctx.status = 200; 31 | ctx.body = { 32 | res: 0 33 | }; 34 | return; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/controller/github.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * github webhook handler 3 | * github 的webhook格式跟gitlab的不一样,天啊 4 | * 所以这里是需要重新开发的!注意! 5 | * @author LeoEatle 6 | */ 7 | import { BaseContext } from "koa"; 8 | import ChatRobot from "./chat"; 9 | import { config } from "../config"; 10 | import customLog from "../log"; 11 | const log = customLog("github handler"); 12 | import { Issues, Push, PullRequest } from "github-webhook-event-types"; 13 | 14 | const HEADER_KEY: string = "x-github-event"; 15 | // 陷入了沉思,为什么gitlab这里没有过去式,而github这里的action全加上了过去式 16 | const actionWords = { 17 | "opened": "发起", 18 | "closed": "关闭", 19 | "reopened": "重新发起", 20 | "edited": "更新", 21 | "merge": "合并", 22 | "created": "创建", 23 | "requested": "请求", 24 | "completed": "完成", 25 | "synchronize": "同步更新" 26 | }; 27 | 28 | 29 | export default class GithubWebhookController { 30 | public static async getWebhook(ctx: BaseContext) { 31 | console.log("git webhook req", ctx.request); 32 | const event: string = ctx.request.header[HEADER_KEY]; 33 | if (!event) { 34 | ctx.status = 403; 35 | ctx.body = `Sorry,这可能不是一个来自 Github 的webhook请求`; 36 | log.info(ctx.body); 37 | return; 38 | } 39 | const url = ctx.request.url; 40 | // 这一行必须要重置regex,不能放在全局!!参考:https://stackoverflow.com/questions/4724701/regexp-exec-returns-null-sporadically 41 | const ROBOTID_REGEX = /id=([a-zA-Z0-9-]+)/g; 42 | const robotidRe = ROBOTID_REGEX.exec(url); 43 | const robotid = robotidRe && robotidRe[1]; 44 | robotid && console.log("robotid", robotid); 45 | switch (event) { 46 | case "push": 47 | return await GithubWebhookController.handlePush(ctx, robotid); 48 | case "pull_request": 49 | return await GithubWebhookController.handlePR(ctx, robotid); 50 | case "ping": 51 | return await GithubWebhookController.handlePing(ctx, robotid); 52 | case "issues": 53 | return await GithubWebhookController.handleIssue(ctx, robotid); 54 | default: 55 | return await GithubWebhookController.handleDefault(ctx, event); 56 | } 57 | } 58 | 59 | /** 60 | * 处理ping事件 61 | * @param ctx koa context 62 | * @param robotid 机器人id 63 | */ 64 | public static async handlePing(ctx: BaseContext, robotid?: string) { 65 | const robot: ChatRobot = new ChatRobot( 66 | config.chatid 67 | ); 68 | const body: any = ctx.request.body; 69 | console.log(body); 70 | const { payload } = body; 71 | const { repository } = JSON.parse(payload); 72 | const msg = "成功收到了来自Github的Ping请求,项目名称:" + repository.name; 73 | await robot.sendTextMsg(msg); 74 | ctx.status = 200; 75 | return msg; 76 | } 77 | 78 | /** 79 | * 处理push事件 80 | * @param ctx koa context 81 | * @param robotid 机器人id 82 | */ 83 | public static async handlePush(ctx: BaseContext, robotid?: string) { 84 | const body: Push = JSON.parse(ctx.request.body.payload); 85 | const robot: ChatRobot = new ChatRobot( 86 | config.chatid 87 | ); 88 | let msg: String; 89 | log.info("push http body", body); 90 | const { pusher, repository, commits, ref} = body; 91 | const user_name = pusher.name; 92 | if (repository.name === "project_test" && user_name === "user_test") { 93 | msg = "收到一次webhook test"; 94 | ctx.body = msg; 95 | return await robot.sendTextMsg(msg); 96 | } else { 97 | const lastCommit = commits[0]; 98 | msg = `项目 ${repository.name} 收到了一次push,提交者:${user_name},最新提交信息:${lastCommit.message}`; 99 | ctx.body = msg; 100 | const mdMsg = `项目 [${repository.name}](${repository.url}) 收到一次push提交 101 | 提交者: \${user_name}\ 102 | 分支: \${ref}\ 103 | 最新提交信息: ${lastCommit.message}`; 104 | await robot.sendMdMsg(mdMsg); 105 | ctx.status = 200; 106 | return; 107 | } 108 | } 109 | 110 | /** 111 | * 处理merge request事件 112 | * @param ctx koa context 113 | * @param robotid 机器人id 114 | */ 115 | public static async handlePR(ctx: BaseContext, robotid?: string) { 116 | const body: PullRequest = JSON.parse(ctx.request.body.payload); 117 | const robot: ChatRobot = new ChatRobot( 118 | config.chatid 119 | ); 120 | log.info("pr http body", body); 121 | const {action, sender, pull_request, repository} = body; 122 | const mdMsg = `${sender.login}在 [${repository.full_name}](${repository.html_url}) ${actionWords[action]}了PR 123 | 标题:${pull_request.title} 124 | 源分支:${pull_request.head.ref} 125 | 目标分支:${pull_request.base.ref} 126 | [查看PR详情](${pull_request.html_url})`; 127 | await robot.sendMdMsg(mdMsg); 128 | ctx.status = 200; 129 | return; 130 | } 131 | 132 | /** 133 | * 处理issue 事件 134 | * @param ctx koa context 135 | * @param robotid 机器人id 136 | */ 137 | public static async handleIssue(ctx: BaseContext, robotid?: string) { 138 | const body: Issues = JSON.parse(ctx.request.body.payload); 139 | const robot: ChatRobot = new ChatRobot( 140 | config.chatid 141 | ); 142 | log.info("issues", body); 143 | console.log(body); 144 | const { action, issue, repository } = body; 145 | if (action !== "opened") { 146 | ctx.body = `除非有人开启新的issue,否则无需通知机器人`; 147 | return; 148 | } 149 | const mdMsg = `有人在 [${repository.name}](${repository.html_url}) ${actionWords[action]}了一个issue 150 | 标题:${issue.title} 151 | 发起人:[${issue.user.login}](${issue.user.html_url}) 152 | [查看详情](${issue.html_url})`; 153 | await robot.sendMdMsg(mdMsg); 154 | ctx.status = 200; 155 | return; 156 | } 157 | 158 | /** 159 | * 对于未处理的事件,统一走这里 160 | * @param ctx koa context 161 | * @param event 事件名 162 | */ 163 | public static handleDefault(ctx: BaseContext, event: String) { 164 | console.log(ctx.request.body); 165 | ctx.body = `Sorry,暂时还没有处理${event}事件`; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/controller/gitlab.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * gitlab webhook handler 3 | * @author LeoEatle 4 | */ 5 | import { BaseContext } from "koa"; 6 | import ChatRobot from "./chat"; 7 | import { config } from "../config"; 8 | import customLog from "../log"; 9 | const log = customLog("gitlab handler"); 10 | interface Repository { 11 | name: string; 12 | description: string; 13 | homepage: string; 14 | git_http_url: string; 15 | git_ssh_url: string; 16 | url: string; 17 | visibility_level: number; 18 | } 19 | 20 | interface Commit { 21 | id: string; 22 | message: string; 23 | timestamp: string; 24 | url: string; 25 | author: object; 26 | added: Array; 27 | modified: Array; 28 | removed: Array; 29 | } 30 | 31 | interface User { 32 | name: string; 33 | username: string; 34 | avatar_url: string; 35 | } 36 | 37 | interface Source { 38 | name: string; 39 | ssh_url: string; 40 | http_url: string; 41 | web_url: string; 42 | namespace: string; 43 | visibility_level: number; 44 | } 45 | 46 | /** 47 | * 收到push通知时的http body 48 | */ 49 | interface PushBody { 50 | object_kind: string; 51 | before: string; 52 | after: string; 53 | ref: string; 54 | checkout_sha: string; 55 | user_name: string; 56 | user_id: number; 57 | user_email: string; 58 | project_id: number; 59 | repository: Repository; 60 | commits: Array; 61 | total_commits_count: number; 62 | } 63 | 64 | interface MRBody { 65 | object_kind: string; 66 | user: User; 67 | object_attributes: { 68 | // 这里并不包括所有的object_attribute,因为实在太多了暂时只列出我们需要的几个属性 69 | id: number, 70 | target_branch: string, 71 | source_branch: string, 72 | title: string, 73 | created_at: string, 74 | updated_at: string, 75 | merge_status: string, 76 | description: string, 77 | url: string, 78 | source: Source, 79 | action: string // action 可能是open/update/close/reopen 80 | }; 81 | } 82 | 83 | interface IssueBody { 84 | user: User; 85 | repository: Repository; 86 | object_attributes: { 87 | id: number, 88 | title: string, 89 | created_at: string, 90 | updated_at: string, 91 | merge_status: string, 92 | description: string, 93 | url: string, 94 | state: string, 95 | action: string // action 可能是open/update/close/reopen 96 | }; 97 | } 98 | 99 | const HEADER_KEY: string = "x-gitlab-event"; 100 | 101 | const HEADER_KEY_V2: string = "X-Gitlab-Event"; 102 | 103 | const EVENTS = { 104 | "Push Hook": "push", 105 | "Tag Push Hook": "tag_push", 106 | "Issue Hook": "issue", 107 | "Note Hook": "note", 108 | "Merge Request Hook": "merge_request", 109 | "Review Hook": "review" 110 | }; 111 | 112 | const actionWords = { 113 | "open": "发起", 114 | "close": "关闭", 115 | "reopen": "重新发起", 116 | "update": "更新", 117 | "merge": "合并" 118 | }; 119 | export default class GitWebhookController { 120 | public static async getWebhook(ctx: BaseContext) { 121 | console.log("git webhook req", ctx.request); 122 | const event: string = ctx.request.header[HEADER_KEY] || ctx.request.header[HEADER_KEY_V2]; 123 | if (!event) { 124 | ctx.body = `Sorry,这可能不是一个gitlab的webhook请求`; 125 | return; 126 | } 127 | const url = ctx.request.url; 128 | const ROBOTID_REGEX = /id=([a-zA-Z0-9-]+)/g; 129 | const robotidRe = ROBOTID_REGEX.exec(url); 130 | const robotid = robotidRe && robotidRe[1]; 131 | robotid && log.info(robotid); 132 | // 检查是否是test事件 133 | if (ctx.request.header["x-event-test"] == "true") { 134 | // test事件中仅处理push,否则推送太多 135 | if (EVENTS[event] == "push") { 136 | return await GitWebhookController.handleTest(ctx, ctx.robotid); 137 | } else { 138 | ctx.status = 200; 139 | ctx.body = "其他test请求我可不会管"; 140 | return; 141 | } 142 | } 143 | switch (EVENTS[event]) { 144 | case "push": 145 | return await GitWebhookController.handlePush(ctx, robotid); 146 | case "merge_request": 147 | return await GitWebhookController.handleMR(ctx, robotid); 148 | case "issue": 149 | return await GitWebhookController.handleIssue(ctx, robotid); 150 | default: 151 | return await GitWebhookController.handleDefault(ctx, event); 152 | } 153 | } 154 | 155 | /** 156 | * 处理push事件 157 | * @param ctx koa context 158 | * @param robotid 机器人id 159 | */ 160 | public static async handlePush(ctx: BaseContext, robotid?: string) { 161 | const body: PushBody = ctx.request.body; 162 | const robot: ChatRobot = new ChatRobot( 163 | robotid || config.chatid 164 | ); 165 | let msg: String; 166 | log.info(body); 167 | console.log("ctx", ctx); 168 | const { user_name, repository, commits, ref} = body; 169 | if (repository.name === "project_test" && user_name === "user_test") { 170 | msg = "收到一次webhook test"; 171 | ctx.body = msg; 172 | return await robot.sendTextMsg(msg); 173 | } else { 174 | const lastCommit: Commit = commits[0]; 175 | const branchName = ref.replace("refs/heads/", ""); 176 | msg = `项目 ${repository.name} 收到了一次push,提交者:${user_name},最新提交信息:${lastCommit.message}`; 177 | ctx.body = msg; 178 | const mdMsg = `项目 [${repository.name}](${repository.homepage}) 收到一次push提交 179 | 提交者: \${user_name}\ 180 | 分支: \${branchName}\ 181 | 最新提交信息: ${lastCommit.message}`; 182 | await robot.sendMdMsg(mdMsg); 183 | ctx.status = 200; 184 | return; 185 | } 186 | } 187 | 188 | /** 189 | * 处理merge request事件 190 | * @param ctx koa context 191 | */ 192 | public static async handleMR(ctx: BaseContext, robotid?: string) { 193 | const body: MRBody = ctx.request.body; 194 | const robot: ChatRobot = new ChatRobot( 195 | robotid || config.chatid 196 | ); 197 | log.info(body); 198 | const {user, object_attributes} = body; 199 | const attr = object_attributes; 200 | const mdMsg = `${user.name}在 [${attr.source.name}](${attr.source.web_url}) ${actionWords[attr.action]}了一个MR 201 | 标题:${attr.title} 202 | 源分支:${attr.source_branch} 203 | 目标分支:${attr.target_branch} 204 | [查看MR详情](${attr.url})`; 205 | await robot.sendMdMsg(mdMsg); 206 | ctx.status = 200; 207 | return; 208 | } 209 | 210 | public static async handleIssue(ctx: BaseContext, robotid?: string) { 211 | const body: IssueBody = ctx.request.body; 212 | const robot: ChatRobot = new ChatRobot( 213 | robotid || config.chatid 214 | ); 215 | console.log("[Issue handler]Req Body", body); 216 | const {user, object_attributes, repository} = body; 217 | const attr = object_attributes; 218 | // 由于工蜂的issue webhook在项目url这少了个s,给它暂时hack一下补上 219 | // update 这个问题又修复了 见工蜂的issue 220 | // const url = attr.url.replace("issue", "issues"); 221 | const mdMsg = `有人在 [${repository.name}](${repository.url}) ${actionWords[attr.action]}了一个issue 222 | 标题:${attr.title} 223 | 发起人:${user.name} 224 | [查看详情](${attr.url})`; 225 | await robot.sendMdMsg(mdMsg); 226 | ctx.status = 200; 227 | return; 228 | } 229 | 230 | public static async handleTest(ctx: BaseContext, robotid?: string) { 231 | const msg = "收到一次webhook test"; 232 | const robot: ChatRobot = new ChatRobot( 233 | robotid || config.chatid 234 | ); 235 | await robot.sendTextMsg(msg); 236 | ctx.status = 200; 237 | return; 238 | } 239 | 240 | public static handleDefault(ctx: BaseContext, event: String) { 241 | ctx.body = `Sorry,暂时还没有处理${event}事件`; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/controller/index.ts: -------------------------------------------------------------------------------- 1 | // export { default as user } from './user'; 2 | export { default as general } from "./general"; 3 | export { default as gitlab } from "./gitlab"; 4 | export { default as github } from "./github"; -------------------------------------------------------------------------------- /src/controller/user.ts: -------------------------------------------------------------------------------- 1 | import { BaseContext } from "koa"; 2 | import { getManager, Repository, Not, Equal } from "typeorm"; 3 | import { validate, ValidationError } from "class-validator"; 4 | import { User } from "../entity/user"; 5 | 6 | export default class UserController { 7 | public static async getUsers(ctx: BaseContext) { 8 | // get a user repository to perform operations with user 9 | const userRepository: Repository = getManager().getRepository( 10 | User 11 | ); 12 | 13 | // load all users 14 | const users: User[] = await userRepository.find(); 15 | 16 | // return OK status code and loaded users array 17 | ctx.status = 200; 18 | ctx.body = users; 19 | } 20 | 21 | public static async getUser(ctx: BaseContext) { 22 | // get a user repository to perform operations with user 23 | const userRepository: Repository = getManager().getRepository( 24 | User 25 | ); 26 | 27 | // load user by id 28 | const user: User = await userRepository.findOne(+ctx.params.id || 0); 29 | 30 | if (user) { 31 | // return OK status code and loaded user object 32 | ctx.status = 200; 33 | ctx.body = user; 34 | } else { 35 | // return a BAD REQUEST status code and error message 36 | ctx.status = 400; 37 | ctx.body = 38 | "The user you are trying to retrieve doesn't exist in the db"; 39 | } 40 | } 41 | 42 | public static async createUser(ctx: BaseContext) { 43 | // get a user repository to perform operations with user 44 | const userRepository: Repository = getManager().getRepository( 45 | User 46 | ); 47 | 48 | // build up entity user to be saved 49 | const userToBeSaved: User = new User(); 50 | userToBeSaved.name = ctx.request.body.name; 51 | userToBeSaved.email = ctx.request.body.email; 52 | 53 | // validate user entity 54 | const errors: ValidationError[] = await validate(userToBeSaved); // errors is an array of validation errors 55 | 56 | if (errors.length > 0) { 57 | // return BAD REQUEST status code and errors array 58 | ctx.status = 400; 59 | ctx.body = errors; 60 | } else if ( 61 | await userRepository.findOne({ email: userToBeSaved.email }) 62 | ) { 63 | // return BAD REQUEST status code and email already exists error 64 | ctx.status = 400; 65 | ctx.body = "The specified e-mail address already exists"; 66 | } else { 67 | // save the user contained in the POST body 68 | const user = await userRepository.save(userToBeSaved); 69 | // return CREATED status code and updated user 70 | ctx.status = 201; 71 | ctx.body = user; 72 | } 73 | } 74 | 75 | public static async updateUser(ctx: BaseContext) { 76 | // get a user repository to perform operations with user 77 | const userRepository: Repository = getManager().getRepository( 78 | User 79 | ); 80 | 81 | // update the user by specified id 82 | // build up entity user to be updated 83 | const userToBeUpdated: User = new User(); 84 | userToBeUpdated.id = +ctx.params.id || 0; // will always have a number, this will avoid errors 85 | userToBeUpdated.name = ctx.request.body.name; 86 | userToBeUpdated.email = ctx.request.body.email; 87 | 88 | // validate user entity 89 | const errors: ValidationError[] = await validate(userToBeUpdated); // errors is an array of validation errors 90 | 91 | if (errors.length > 0) { 92 | // return BAD REQUEST status code and errors array 93 | ctx.status = 400; 94 | ctx.body = errors; 95 | } else if (!(await userRepository.findOne(userToBeUpdated.id))) { 96 | // check if a user with the specified id exists 97 | // return a BAD REQUEST status code and error message 98 | ctx.status = 400; 99 | ctx.body = 100 | "The user you are trying to update doesn't exist in the db"; 101 | } else if ( 102 | await userRepository.findOne({ 103 | id: Not(Equal(userToBeUpdated.id)), 104 | email: userToBeUpdated.email 105 | }) 106 | ) { 107 | // return BAD REQUEST status code and email already exists error 108 | ctx.status = 400; 109 | ctx.body = "The specified e-mail address already exists"; 110 | } else { 111 | // save the user contained in the PUT body 112 | const user = await userRepository.save(userToBeUpdated); 113 | // return CREATED status code and updated user 114 | ctx.status = 201; 115 | ctx.body = user; 116 | } 117 | } 118 | 119 | public static async deleteUser(ctx: BaseContext) { 120 | // get a user repository to perform operations with user 121 | const userRepository = getManager().getRepository(User); 122 | 123 | // find the user by specified id 124 | const userToRemove: User = await userRepository.findOne( 125 | +ctx.params.id || 0 126 | ); 127 | if (!userToRemove) { 128 | // return a BAD REQUEST status code and error message 129 | ctx.status = 400; 130 | ctx.body = 131 | "The user you are trying to delete doesn't exist in the db"; 132 | } else if (+ctx.state.user.id !== userToRemove.id) { 133 | // check user's token id and user id are the same 134 | // if not, return a FORBIDDEN status code and error message 135 | ctx.status = 403; 136 | ctx.body = "A user can only be deleted by himself"; 137 | } else { 138 | // the user is there so can be removed 139 | await userRepository.remove(userToRemove); 140 | // return a NO CONTENT status code 141 | ctx.status = 204; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"; 2 | import { Length, IsEmail } from "class-validator"; 3 | 4 | @Entity() 5 | export class User { 6 | @PrimaryGeneratedColumn() id: number; 7 | 8 | @Column({ 9 | length: 80 10 | }) 11 | @Length(10, 80) 12 | name: string; 13 | 14 | @Column({ 15 | length: 100 16 | }) 17 | @Length(10, 100) 18 | @IsEmail() 19 | email: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/example/GithubPullRequestExample.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "closed", 3 | "number": 1, 4 | "pull_request": { 5 | "url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/1", 6 | "id": 191568743, 7 | "node_id": "MDExOlB1bGxSZXF1ZXN0MTkxNTY4NzQz", 8 | "html_url": "https://github.com/Codertocat/Hello-World/pull/1", 9 | "diff_url": "https://github.com/Codertocat/Hello-World/pull/1.diff", 10 | "patch_url": "https://github.com/Codertocat/Hello-World/pull/1.patch", 11 | "issue_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/1", 12 | "number": 1, 13 | "state": "closed", 14 | "locked": false, 15 | "title": "Update the README with new information", 16 | "user": { 17 | "login": "Codertocat", 18 | "id": 21031067, 19 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 20 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 21 | "gravatar_id": "", 22 | "url": "https://api.github.com/users/Codertocat", 23 | "html_url": "https://github.com/Codertocat", 24 | "followers_url": "https://api.github.com/users/Codertocat/followers", 25 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 26 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 27 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 28 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 29 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 30 | "repos_url": "https://api.github.com/users/Codertocat/repos", 31 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 32 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 33 | "type": "User", 34 | "site_admin": false 35 | }, 36 | "body": "This is a pretty simple change that we need to pull into master.", 37 | "created_at": "2018-05-30T20:18:30Z", 38 | "updated_at": "2018-05-30T20:18:50Z", 39 | "closed_at": "2018-05-30T20:18:50Z", 40 | "merged_at": null, 41 | "merge_commit_sha": "414cb0069601a32b00bd122a2380cd283626a8e5", 42 | "assignee": null, 43 | "assignees": [ 44 | 45 | ], 46 | "requested_reviewers": [ 47 | 48 | ], 49 | "requested_teams": [ 50 | 51 | ], 52 | "labels": [ 53 | 54 | ], 55 | "milestone": null, 56 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/1/commits", 57 | "review_comments_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/1/comments", 58 | "review_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}", 59 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/1/comments", 60 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/34c5c7793cb3b279e22454cb6750c80560547b3a", 61 | "head": { 62 | "label": "Codertocat:changes", 63 | "ref": "changes", 64 | "sha": "34c5c7793cb3b279e22454cb6750c80560547b3a", 65 | "user": { 66 | "login": "Codertocat", 67 | "id": 21031067, 68 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 69 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 70 | "gravatar_id": "", 71 | "url": "https://api.github.com/users/Codertocat", 72 | "html_url": "https://github.com/Codertocat", 73 | "followers_url": "https://api.github.com/users/Codertocat/followers", 74 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 75 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 76 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 77 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 78 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 79 | "repos_url": "https://api.github.com/users/Codertocat/repos", 80 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 81 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 82 | "type": "User", 83 | "site_admin": false 84 | }, 85 | "repo": { 86 | "id": 135493233, 87 | "node_id": "MDEwOlJlcG9zaXRvcnkxMzU0OTMyMzM=", 88 | "name": "Hello-World", 89 | "full_name": "Codertocat/Hello-World", 90 | "owner": { 91 | "login": "Codertocat", 92 | "id": 21031067, 93 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 94 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 95 | "gravatar_id": "", 96 | "url": "https://api.github.com/users/Codertocat", 97 | "html_url": "https://github.com/Codertocat", 98 | "followers_url": "https://api.github.com/users/Codertocat/followers", 99 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 100 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 101 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 102 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 103 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 104 | "repos_url": "https://api.github.com/users/Codertocat/repos", 105 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 106 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 107 | "type": "User", 108 | "site_admin": false 109 | }, 110 | "private": false, 111 | "html_url": "https://github.com/Codertocat/Hello-World", 112 | "description": null, 113 | "fork": false, 114 | "url": "https://api.github.com/repos/Codertocat/Hello-World", 115 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 116 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 117 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 118 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 119 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 120 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 121 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 122 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 123 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 124 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 125 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 126 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 127 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 128 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 129 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 130 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 131 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 132 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 133 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 134 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 135 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 136 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 137 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 138 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 139 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 140 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 141 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 142 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 143 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 144 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 145 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 146 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 147 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 148 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 149 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 150 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 151 | "created_at": "2018-05-30T20:18:04Z", 152 | "updated_at": "2018-05-30T20:18:50Z", 153 | "pushed_at": "2018-05-30T20:18:48Z", 154 | "git_url": "git://github.com/Codertocat/Hello-World.git", 155 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 156 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 157 | "svn_url": "https://github.com/Codertocat/Hello-World", 158 | "homepage": null, 159 | "size": 0, 160 | "stargazers_count": 0, 161 | "watchers_count": 0, 162 | "language": null, 163 | "has_issues": true, 164 | "has_projects": true, 165 | "has_downloads": true, 166 | "has_wiki": true, 167 | "has_pages": true, 168 | "forks_count": 0, 169 | "mirror_url": null, 170 | "archived": false, 171 | "open_issues_count": 1, 172 | "license": null, 173 | "forks": 0, 174 | "open_issues": 1, 175 | "watchers": 0, 176 | "default_branch": "master" 177 | } 178 | }, 179 | "base": { 180 | "label": "Codertocat:master", 181 | "ref": "master", 182 | "sha": "a10867b14bb761a232cd80139fbd4c0d33264240", 183 | "user": { 184 | "login": "Codertocat", 185 | "id": 21031067, 186 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 187 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 188 | "gravatar_id": "", 189 | "url": "https://api.github.com/users/Codertocat", 190 | "html_url": "https://github.com/Codertocat", 191 | "followers_url": "https://api.github.com/users/Codertocat/followers", 192 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 193 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 194 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 195 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 196 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 197 | "repos_url": "https://api.github.com/users/Codertocat/repos", 198 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 199 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 200 | "type": "User", 201 | "site_admin": false 202 | }, 203 | "repo": { 204 | "id": 135493233, 205 | "node_id": "MDEwOlJlcG9zaXRvcnkxMzU0OTMyMzM=", 206 | "name": "Hello-World", 207 | "full_name": "Codertocat/Hello-World", 208 | "owner": { 209 | "login": "Codertocat", 210 | "id": 21031067, 211 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 212 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 213 | "gravatar_id": "", 214 | "url": "https://api.github.com/users/Codertocat", 215 | "html_url": "https://github.com/Codertocat", 216 | "followers_url": "https://api.github.com/users/Codertocat/followers", 217 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 218 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 219 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 220 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 221 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 222 | "repos_url": "https://api.github.com/users/Codertocat/repos", 223 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 224 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 225 | "type": "User", 226 | "site_admin": false 227 | }, 228 | "private": false, 229 | "html_url": "https://github.com/Codertocat/Hello-World", 230 | "description": null, 231 | "fork": false, 232 | "url": "https://api.github.com/repos/Codertocat/Hello-World", 233 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 234 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 235 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 236 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 237 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 238 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 239 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 240 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 241 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 242 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 243 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 244 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 245 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 246 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 247 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 248 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 249 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 250 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 251 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 252 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 253 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 254 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 255 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 256 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 257 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 258 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 259 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 260 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 261 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 262 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 263 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 264 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 265 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 266 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 267 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 268 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 269 | "created_at": "2018-05-30T20:18:04Z", 270 | "updated_at": "2018-05-30T20:18:50Z", 271 | "pushed_at": "2018-05-30T20:18:48Z", 272 | "git_url": "git://github.com/Codertocat/Hello-World.git", 273 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 274 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 275 | "svn_url": "https://github.com/Codertocat/Hello-World", 276 | "homepage": null, 277 | "size": 0, 278 | "stargazers_count": 0, 279 | "watchers_count": 0, 280 | "language": null, 281 | "has_issues": true, 282 | "has_projects": true, 283 | "has_downloads": true, 284 | "has_wiki": true, 285 | "has_pages": true, 286 | "forks_count": 0, 287 | "mirror_url": null, 288 | "archived": false, 289 | "open_issues_count": 1, 290 | "license": null, 291 | "forks": 0, 292 | "open_issues": 1, 293 | "watchers": 0, 294 | "default_branch": "master" 295 | } 296 | }, 297 | "_links": { 298 | "self": { 299 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/1" 300 | }, 301 | "html": { 302 | "href": "https://github.com/Codertocat/Hello-World/pull/1" 303 | }, 304 | "issue": { 305 | "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/1" 306 | }, 307 | "comments": { 308 | "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/1/comments" 309 | }, 310 | "review_comments": { 311 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/1/comments" 312 | }, 313 | "review_comment": { 314 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}" 315 | }, 316 | "commits": { 317 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/1/commits" 318 | }, 319 | "statuses": { 320 | "href": "https://api.github.com/repos/Codertocat/Hello-World/statuses/34c5c7793cb3b279e22454cb6750c80560547b3a" 321 | } 322 | }, 323 | "author_association": "OWNER", 324 | "merged": false, 325 | "mergeable": true, 326 | "rebaseable": true, 327 | "mergeable_state": "clean", 328 | "merged_by": null, 329 | "comments": 0, 330 | "review_comments": 1, 331 | "maintainer_can_modify": false, 332 | "commits": 1, 333 | "additions": 1, 334 | "deletions": 1, 335 | "changed_files": 1 336 | }, 337 | "repository": { 338 | "id": 135493233, 339 | "node_id": "MDEwOlJlcG9zaXRvcnkxMzU0OTMyMzM=", 340 | "name": "Hello-World", 341 | "full_name": "Codertocat/Hello-World", 342 | "owner": { 343 | "login": "Codertocat", 344 | "id": 21031067, 345 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 346 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 347 | "gravatar_id": "", 348 | "url": "https://api.github.com/users/Codertocat", 349 | "html_url": "https://github.com/Codertocat", 350 | "followers_url": "https://api.github.com/users/Codertocat/followers", 351 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 352 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 353 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 354 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 355 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 356 | "repos_url": "https://api.github.com/users/Codertocat/repos", 357 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 358 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 359 | "type": "User", 360 | "site_admin": false 361 | }, 362 | "private": false, 363 | "html_url": "https://github.com/Codertocat/Hello-World", 364 | "description": null, 365 | "fork": false, 366 | "url": "https://api.github.com/repos/Codertocat/Hello-World", 367 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 368 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 369 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 370 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 371 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 372 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 373 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 374 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 375 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 376 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 377 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 378 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 379 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 380 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 381 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 382 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 383 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 384 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 385 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 386 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 387 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 388 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 389 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 390 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 391 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 392 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 393 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 394 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 395 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 396 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 397 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 398 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 399 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 400 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 401 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 402 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 403 | "created_at": "2018-05-30T20:18:04Z", 404 | "updated_at": "2018-05-30T20:18:50Z", 405 | "pushed_at": "2018-05-30T20:18:48Z", 406 | "git_url": "git://github.com/Codertocat/Hello-World.git", 407 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 408 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 409 | "svn_url": "https://github.com/Codertocat/Hello-World", 410 | "homepage": null, 411 | "size": 0, 412 | "stargazers_count": 0, 413 | "watchers_count": 0, 414 | "language": null, 415 | "has_issues": true, 416 | "has_projects": true, 417 | "has_downloads": true, 418 | "has_wiki": true, 419 | "has_pages": true, 420 | "forks_count": 0, 421 | "mirror_url": null, 422 | "archived": false, 423 | "open_issues_count": 1, 424 | "license": null, 425 | "forks": 0, 426 | "open_issues": 1, 427 | "watchers": 0, 428 | "default_branch": "master" 429 | }, 430 | "sender": { 431 | "login": "Codertocat", 432 | "id": 21031067, 433 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 434 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 435 | "gravatar_id": "", 436 | "url": "https://api.github.com/users/Codertocat", 437 | "html_url": "https://github.com/Codertocat", 438 | "followers_url": "https://api.github.com/users/Codertocat/followers", 439 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 440 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 441 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 442 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 443 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 444 | "repos_url": "https://api.github.com/users/Codertocat/repos", 445 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 446 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 447 | "type": "User", 448 | "site_admin": false 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/example/gihubPushEventExample.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/tags/simple-tag", 3 | "before": "a10867b14bb761a232cd80139fbd4c0d33264240", 4 | "after": "0000000000000000000000000000000000000000", 5 | "created": false, 6 | "deleted": true, 7 | "forced": false, 8 | "base_ref": null, 9 | "compare": "https://github.com/Codertocat/Hello-World/compare/a10867b14bb7...000000000000", 10 | "commits": [ 11 | 12 | ], 13 | "head_commit": null, 14 | "repository": { 15 | "id": 135493233, 16 | "node_id": "MDEwOlJlcG9zaXRvcnkxMzU0OTMyMzM=", 17 | "name": "Hello-World", 18 | "full_name": "Codertocat/Hello-World", 19 | "owner": { 20 | "name": "Codertocat", 21 | "email": "21031067+Codertocat@users.noreply.github.com", 22 | "login": "Codertocat", 23 | "id": 21031067, 24 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 25 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 26 | "gravatar_id": "", 27 | "url": "https://api.github.com/users/Codertocat", 28 | "html_url": "https://github.com/Codertocat", 29 | "followers_url": "https://api.github.com/users/Codertocat/followers", 30 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 31 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 32 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 33 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 34 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 35 | "repos_url": "https://api.github.com/users/Codertocat/repos", 36 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 37 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 38 | "type": "User", 39 | "site_admin": false 40 | }, 41 | "private": false, 42 | "html_url": "https://github.com/Codertocat/Hello-World", 43 | "description": null, 44 | "fork": false, 45 | "url": "https://github.com/Codertocat/Hello-World", 46 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 47 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 48 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 49 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 50 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 51 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 52 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 53 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 54 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 55 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 56 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 57 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 58 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 59 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 60 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 61 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 62 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 63 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 64 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 65 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 66 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 67 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 68 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 69 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 70 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 71 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 72 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 73 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 74 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 75 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 76 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 77 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 78 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 79 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 80 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 81 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 82 | "created_at": 1527711484, 83 | "updated_at": "2018-05-30T20:18:35Z", 84 | "pushed_at": 1527711528, 85 | "git_url": "git://github.com/Codertocat/Hello-World.git", 86 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 87 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 88 | "svn_url": "https://github.com/Codertocat/Hello-World", 89 | "homepage": null, 90 | "size": 0, 91 | "stargazers_count": 0, 92 | "watchers_count": 0, 93 | "language": null, 94 | "has_issues": true, 95 | "has_projects": true, 96 | "has_downloads": true, 97 | "has_wiki": true, 98 | "has_pages": true, 99 | "forks_count": 0, 100 | "mirror_url": null, 101 | "archived": false, 102 | "open_issues_count": 2, 103 | "license": null, 104 | "forks": 0, 105 | "open_issues": 2, 106 | "watchers": 0, 107 | "default_branch": "master", 108 | "stargazers": 0, 109 | "master_branch": "master" 110 | }, 111 | "pusher": { 112 | "name": "Codertocat", 113 | "email": "21031067+Codertocat@users.noreply.github.com" 114 | }, 115 | "sender": { 116 | "login": "Codertocat", 117 | "id": 21031067, 118 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 119 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 120 | "gravatar_id": "", 121 | "url": "https://api.github.com/users/Codertocat", 122 | "html_url": "https://github.com/Codertocat", 123 | "followers_url": "https://api.github.com/users/Codertocat/followers", 124 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 125 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 126 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 127 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 128 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 129 | "repos_url": "https://api.github.com/users/Codertocat/repos", 130 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 131 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 132 | "type": "User", 133 | "site_admin": false 134 | } 135 | } -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * customed log via winston 3 | * @author LeoEatle 4 | */ 5 | 6 | // const logger = (label: string) => winston.createLogger({ 7 | // transports: [new winston.transports.Console()], 8 | // format: winston.format.combine( 9 | // winston.format.timestamp({ 10 | // format: "YYYY-MM-DD HH:mm:ss" 11 | // }), 12 | // winston.format.colorize({ all: true }), 13 | // winston.format.json(), 14 | // winston.format.label({label}), 15 | // winston.format.printf(info => `${info.level} ${info.timestamp} ${info.label} : ${info.message}`) 16 | // ) 17 | // }); 18 | 19 | const logger2 = (label: string) => { 20 | return { 21 | info: (...args) => console.log(label, args) 22 | }; 23 | }; 24 | 25 | export default logger2; -------------------------------------------------------------------------------- /src/middleware/chatRobot.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from "koa"; 2 | -------------------------------------------------------------------------------- /src/middleware/logging.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from "koa"; 2 | import { config } from "../config"; 3 | import * as winston from "winston"; 4 | 5 | export function logger(winstonInstance) { 6 | return async (ctx: Koa.Context, next: () => Promise) => { 7 | const start = new Date().getMilliseconds(); 8 | 9 | await next(); 10 | 11 | const ms = new Date().getMilliseconds() - start; 12 | 13 | let logLevel: string; 14 | if (ctx.status >= 500) { 15 | logLevel = "error"; 16 | } 17 | if (ctx.status >= 400) { 18 | logLevel = "warn"; 19 | } 20 | if (ctx.status >= 100) { 21 | logLevel = "info"; 22 | } 23 | 24 | const msg: string = `${ctx.method} ${ctx.originalUrl} ${ 25 | ctx.status 26 | } ${ms}ms`; 27 | 28 | winstonInstance.configure({ 29 | level: config.debugLogging ? "debug" : "info", 30 | transports: [ 31 | // 32 | // - Write all logs error (and below) to `error.log`. 33 | new winston.transports.File({ 34 | filename: "error.log", 35 | level: "error" 36 | }), 37 | // 38 | // - Write to all logs with specified level to console. 39 | new winston.transports.Console({ 40 | format: winston.format.combine( 41 | winston.format.colorize(), 42 | winston.format.simple() 43 | ) 44 | }) 45 | ] 46 | }); 47 | 48 | winstonInstance.log(logLevel, msg); 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import * as Router from "koa-router"; 2 | import controller = require("./controller"); 3 | 4 | const router = new Router(); 5 | 6 | // GENERAL ROUTES 7 | router.get("/", controller.general.helloWorld); 8 | router.get("/jwt", controller.general.getJwtPayload); 9 | // 用于避开企业微信机器人不支持CORS的问题 10 | router.post("/sendText", controller.general.sendText); 11 | 12 | router.post("/git", controller.gitlab.getWebhook); 13 | router.post("/gitlab", controller.gitlab.getWebhook); 14 | 15 | router.post("/github", controller.github.getWebhook); 16 | 17 | // USER ROUTES 18 | // router.get('/users', controller.user.getUsers); 19 | // router.get('/users/:id', controller.user.getUser); 20 | // router.post('/users', controller.user.createUser); 21 | // router.put('/users/:id', controller.user.updateUser); 22 | // router.delete('/users/:id', controller.user.deleteUser); 23 | 24 | export { router }; 25 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from "koa"; 2 | import * as jwt from "koa-jwt"; 3 | import * as bodyParser from "koa-bodyparser"; 4 | import * as helmet from "koa-helmet"; 5 | import * as cors from "@koa/cors"; 6 | import * as winston from "winston"; 7 | import * as dotenv from "dotenv"; 8 | // import { createConnection } from 'typeorm'; 9 | // import 'reflect-metadata'; 10 | // import * as PostgressConnectionStringParser from 'pg-connection-string'; 11 | 12 | import { logger } from "./middleware/logging"; 13 | import { config } from "./config"; 14 | import { router } from "./routes"; 15 | import customLog from "./log"; 16 | const log = customLog("server"); 17 | 18 | log.info("开始加载配置"); 19 | // Load environment variables from .env file, where API keys and passwords are configured 20 | dotenv.config({ path: ".env" }); 21 | 22 | const app = new Koa(); 23 | 24 | // Provides important security headers to make your app more secure 25 | app.use(helmet()); 26 | 27 | // Enable cors with default options 28 | app.use(cors()); 29 | 30 | // Logger middleware -> use winston as logger (logging.ts with config) 31 | app.use(logger(winston)); 32 | 33 | // Enable bodyParser with default options 34 | app.use(bodyParser()); 35 | 36 | // JWT middleware -> below this line routes are only reached if JWT token is valid, secret as env variable 37 | // app.use(jwt({ secret: config.jwtSecret })); 38 | 39 | // this routes are protected by the JWT middleware, also include middleware to respond with "Method Not Allowed - 405". 40 | app.use(router.routes()); 41 | 42 | // 企业微信机器人中间件 待实践 43 | // 这里的设想是通过存储一些变量在ctx上,最后让中间件去负责通知 44 | // 缺点:可读性可能不太好?还是更喜欢命令式的调用 45 | // 优点:机器人通知逻辑单独分离,也许可以把中间件单独作为开源的一部分 46 | // app.use() 47 | 48 | app.listen(config.port); 49 | 50 | console.log(`Server running on port ${config.port}`); 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["es6"], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "dist", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | // "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 26 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | 47 | /* Source Map Options */ 48 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 49 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 50 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 51 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 52 | 53 | /* Experimental Options */ 54 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 55 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 56 | }, 57 | "include": [ 58 | "src/**/*" 59 | ] 60 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "indent": [true, "spaces"], 6 | "one-line": [true, "check-open-brace", "check-whitespace"], 7 | "no-var-keyword": true, 8 | "quotemark": [ 9 | true, 10 | // "single", 11 | "avoid-escape" 12 | ], 13 | "semicolon": [true, "always", "ignore-bound-class-methods"], 14 | "whitespace": [ 15 | true, 16 | "check-branch", 17 | "check-decl", 18 | "check-operator", 19 | "check-module", 20 | "check-separator", 21 | "check-type" 22 | ], 23 | "typedef-whitespace": [ 24 | true, 25 | { 26 | "call-signature": "nospace", 27 | "index-signature": "nospace", 28 | "parameter": "nospace", 29 | "property-declaration": "nospace", 30 | "variable-declaration": "nospace" 31 | }, 32 | { 33 | "call-signature": "onespace", 34 | "index-signature": "onespace", 35 | "parameter": "onespace", 36 | "property-declaration": "onespace", 37 | "variable-declaration": "onespace" 38 | } 39 | ], 40 | "no-internal-module": true, 41 | "no-trailing-whitespace": true, 42 | "no-null-keyword": true, 43 | "prefer-const": true, 44 | "jsdoc-format": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./src/server.ts", 5 | mode: "production", 6 | target: "node", 7 | output: { 8 | filename: "server.js", 9 | path: path.resolve(__dirname, "dist") 10 | }, 11 | devtool: "cheap-source-map", 12 | plugins: [ 13 | 14 | ], 15 | resolve: { 16 | // Add `.ts` and `.tsx` as a resolvable extension. 17 | extensions: [".ts", ".tsx", ".js"] 18 | }, 19 | module: { 20 | rules: [ 21 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 22 | { test: /\.tsx?$/, loader: "ts-loader", exclude: /node_modules/ } 23 | ] 24 | } 25 | }; 26 | --------------------------------------------------------------------------------