├── .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": "[](#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 | 
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 | 
40 |
41 | 4. 点击保存(保存后🉑️测试试试)
42 |
43 | 5. 选择触发方式,添加新的触发方式,类型选择API网关,保存后得到url
44 | 
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 | 
82 |
83 | `/github`用来区分github和gitlab,这两者的处理方式不同。
84 |
85 | `id`参数代表自定义的机器人id,可以在企业微信的机器人列表中查看(注意,这个必须要自己新建的机器人才能看到),见图:
86 |
87 | 
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 |
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 |
--------------------------------------------------------------------------------