├── internal ├── utils │ └── logger.go ├── lark │ ├── const.go │ ├── group_bot.go │ ├── client.go │ └── schema.go ├── yuque │ ├── const.go │ ├── notify.go │ ├── schema.go │ └── lark.go └── config │ └── config.go ├── go.mod ├── .gitignore ├── cmd ├── main.go └── scf │ ├── main.go │ └── httpadapter │ └── adapter.go ├── LICENSE ├── api └── views.go ├── README.md └── go.sum /internal/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | -------------------------------------------------------------------------------- /internal/lark/const.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | const CustomBotHost = "https://open.feishu.cn/open-apis" 4 | 5 | var ( 6 | maxConnsPerhost = 100 7 | defaultTimeout = 2 8 | LarkClient = NewHttpClient(maxConnsPerhost, defaultTimeout) 9 | ) 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gusibi/yuque_webhook 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.21.0 7 | github.com/awslabs/aws-lambda-go-api-proxy v0.5.0 8 | github.com/gin-gonic/gin v1.6.3 9 | github.com/sirupsen/logrus v1.7.0 10 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 11 | github.com/valyala/fasthttp v1.18.0 12 | ) 13 | -------------------------------------------------------------------------------- /internal/yuque/const.go: -------------------------------------------------------------------------------- 1 | package yuque 2 | 3 | import "fmt" 4 | 5 | const Host = "https://www.yuque.com" 6 | 7 | func GetArticleUrl(user, bookSlug, articleSlug string) string { 8 | return fmt.Sprintf("%s/%s/%s/%s", Host, user, bookSlug, articleSlug) 9 | } 10 | 11 | func GetBookeUrl(user, bookSlug string) string { 12 | return fmt.Sprintf("%s/%s/%s", Host, user, bookSlug) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # IDE 18 | .idea/ 19 | 20 | http_test/ 21 | 22 | # build 23 | build/api.zip 24 | build/lark_yuque_webhook.zip 25 | build/main 26 | 27 | .DS_Store -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "os" 4 | 5 | type Config struct { 6 | LarkHookId string 7 | } 8 | 9 | func NewConfig() *Config { 10 | return &Config{} 11 | } 12 | 13 | func (c *Config) Load() { 14 | c.LarkHookId = c.GetLarkHookId() 15 | } 16 | 17 | func (c *Config) GetLarkHookId() string { 18 | return os.Getenv("HOOK_ID") 19 | } 20 | 21 | var Settings *Config 22 | 23 | func init() { 24 | config := NewConfig() 25 | config.Load() 26 | Settings = config 27 | } 28 | -------------------------------------------------------------------------------- /internal/yuque/notify.go: -------------------------------------------------------------------------------- 1 | package yuque 2 | 3 | import "context" 4 | 5 | // yuque webhook request 转发 6 | type Notifer interface { 7 | Push(ctx context.Context, req *WebHookRequest) error 8 | } 9 | 10 | type WebHook struct { 11 | Hooks []Notifer 12 | } 13 | 14 | func NewWebHook() *WebHook { 15 | return &WebHook{} 16 | } 17 | 18 | func (h *WebHook) Register(ctx context.Context, hook Notifer) error { 19 | h.Hooks = append(h.Hooks, hook) 20 | return nil 21 | } 22 | 23 | func (h *WebHook) Notify(ctx context.Context, req *WebHookRequest) error { 24 | for _, hook := range h.Hooks { 25 | hook.Push(ctx, req) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/gusibi/yuque_webhook/api" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | r := gin.Default() 12 | r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { 13 | // your custom format 14 | return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", 15 | param.ClientIP, 16 | param.TimeStamp.Format(time.RFC1123), 17 | param.Method, 18 | param.Path, 19 | param.Request.Proto, 20 | param.StatusCode, 21 | param.Latency, 22 | param.Request.UserAgent(), 23 | param.ErrorMessage, 24 | ) 25 | })) 26 | r.Use(gin.Recovery()) 27 | r.GET("/ping", api.Ping) 28 | r.POST("/api/lark_webhook/:hook_id", api.LarkWebHook) 29 | r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") 30 | } 31 | -------------------------------------------------------------------------------- /internal/lark/group_bot.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | func (c *Client) BotHook(ctx context.Context, hookId string, text *TextMessage, post *PostMessage, card *CardMessage) (*BotResponse, error) { 11 | url := fmt.Sprintf("%s/%s/%s", CustomBotHost, "bot/v2/hook", hookId) 12 | var body []byte 13 | var err error 14 | if text != nil { 15 | if body, err = json.Marshal(text); err != nil { 16 | return nil, err 17 | } 18 | } else if post != nil { 19 | if body, err = json.Marshal(post); err != nil { 20 | return nil, err 21 | } 22 | } else if card != nil { 23 | if body, err = json.Marshal(card); err != nil { 24 | return nil, err 25 | } 26 | } 27 | resp, err := c.Post(ctx, url, body, time.Duration(2)*time.Second) 28 | if err != nil { 29 | return nil, err 30 | } 31 | fmt.Printf("resp: %s", resp.Body()) 32 | return nil, nil 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 gusibi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/views.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/gusibi/yuque_webhook/internal/yuque" 8 | "net/http" 9 | ) 10 | 11 | func Ping(c *gin.Context) { 12 | c.JSON(200, gin.H{ 13 | "message": "pong", 14 | }) 15 | } 16 | 17 | func LarkWebHook(c *gin.Context) { 18 | 19 | var req yuque.WebHookRequest 20 | if err := c.ShouldBindJSON(&req); err != nil { 21 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 22 | return 23 | } 24 | //fmt.Printf("req:%+v \n", req) 25 | if err := yuque.RequestValidate(&req); err != nil { 26 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 27 | } 28 | if v, err := json.Marshal(req); err == nil { 29 | fmt.Printf("%s\n", v) 30 | } 31 | hookId := c.Param("hook_id") 32 | if hookId == ""{ 33 | c.JSON(http.StatusBadRequest, gin.H{"error": "hook_id is invalid"}) 34 | } 35 | larkNotify := &yuque.LarkWebHook{ 36 | MessageType: yuque.PostMessage, 37 | HookId: hookId, 38 | DefaultTimeout: 2, 39 | } 40 | webhook := yuque.NewWebHook() 41 | webhook.Register(c, larkNotify) 42 | webhook.Notify(c, &req) 43 | c.JSON(http.StatusOK, req) 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yuque_webhook 2 | 3 | yuque webhook 4 | 5 | ### API path 6 | 7 | /api/lark_webhook/:hook_id 8 | 9 | https://service-ki47z7xm-1254035985.gz.apigw.tencentcs.com:443/api/lark_webhook/55669168-4c28-4434-b3bd-5899a113c96a 10 | https://open.feishu.cn/open-apis/bot/v2/hook/55669168-4c28-4434-b3bd-5899a113c96a 11 | 12 | ### 支持webhook 通知类型 13 | 14 | * [x] 发布文档 15 | * [x] 更新文档 16 | * [x] 删除文档 17 | * [x] 新增评论 18 | * [x] 更新评论 19 | * [x] 删除评论 20 | * [x] 新增评论回复 21 | * [x] 更新评论回复 22 | * [x] 删除评论回复 23 | 24 | ### 使用腾讯云 serverless 部署语雀飞书机器人 25 | 26 | > 1. 语雀很好用,但是没有App,无法通知 27 | > 2. 飞书群自定义机器人很方便,但是不知道用来干啥 28 | > 3. 腾讯云函数计算有免费额度,是不是可以利用一下? 29 | > 4. 那是不是可以把语雀的webhook 使用飞书通知,部署在腾讯云上呢? 30 | > 5. 好主意 31 | 32 | 33 | ### 使用方式 34 | 35 | 1. 在飞书群中添加自定义机器人,复制webhook 地址,拿到其中的 hook_id 36 | 2. 执行代码build 中 scf_build.sh 脚本,得到 api.zip 包 37 | 3. 在腾讯云云函数中创建云函数, 38 | * 选择golang 39 | * 选择本地zip 包 40 | * 选择上一步生成的 api.zip 41 | * 超时时间为3S 42 | * 创建云函数 43 | 4. 新建API网关服务 44 | * 新建API 45 | * 路径为 /api/webhook 46 | * 请求方法为POST 47 | * 免鉴权 48 | * 后端配置,后端类型为云函数SCF 49 | * 选择上一步创建的云函数 50 | * 超时时间为3S 51 | * 启用响应集成 52 | * 响应结果返回类型为JSON 53 | * 完成选择发布 54 | * 从网关基础配置中拿到公网访问地址,host 55 | 5. 打开想要添加webhook的语雀知识库 56 | * 知识库设置,开发者,添加webhook,命名为飞书机器人 57 | * URL为 host/api/webhook(这里host 为上一步得到的host地址,包含80 或者 443) 58 | 59 | DONE 60 | -------------------------------------------------------------------------------- /cmd/scf/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // https://github.com/go-swagger/go-swagger/issues/962 4 | 5 | import ( 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "log" 9 | "time" 10 | 11 | "github.com/gusibi/yuque_webhook/api" 12 | 13 | "github.com/aws/aws-lambda-go/events" 14 | scf "github.com/tencentyun/scf-go-lib/cloudevents/scf" 15 | "github.com/tencentyun/scf-go-lib/cloudfunction" 16 | 17 | "github.com/gusibi/yuque_webhook/cmd/scf/httpadapter" 18 | ) 19 | 20 | var httpAdapter *httpadapter.GinLambda 21 | 22 | func init() { 23 | log.Println("start server...") 24 | r := gin.Default() 25 | r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { 26 | // your custom format 27 | return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", 28 | param.ClientIP, 29 | param.TimeStamp.Format(time.RFC1123), 30 | param.Method, 31 | param.Path, 32 | param.Request.Proto, 33 | param.StatusCode, 34 | param.Latency, 35 | param.Request.UserAgent(), 36 | param.ErrorMessage, 37 | ) 38 | })) 39 | r.Use(gin.Recovery()) 40 | r.GET("/ping", api.Ping) 41 | r.POST("/api/lark_webhook/:hook_id", api.LarkWebHook) 42 | httpAdapter = httpadapter.New(r) 43 | log.Println("adapter: ", httpAdapter) 44 | } 45 | 46 | // Handler go swagger aws lambda handler 47 | func Handler(req events.APIGatewayProxyRequest) (scf.APIGatewayProxyResponse, error) { 48 | 49 | return httpAdapter.Proxy(req) 50 | } 51 | 52 | func main() { 53 | cloudfunction.Start(Handler) 54 | } 55 | -------------------------------------------------------------------------------- /internal/lark/client.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | import ( 4 | "context" 5 | logger "github.com/sirupsen/logrus" 6 | "github.com/valyala/fasthttp" 7 | urllib "net/url" 8 | "time" 9 | ) 10 | 11 | type Client struct { 12 | HttpClient *fasthttp.Client 13 | } 14 | 15 | func NewHttpClient(maxConns, defaultTimeout int) *Client { 16 | return &Client{HttpClient: &fasthttp.Client{ 17 | Name: "larkHttpClient", 18 | MaxConnsPerHost: maxConns, 19 | ReadTimeout: time.Second * time.Duration(defaultTimeout), 20 | RetryIf: nil, 21 | }} 22 | } 23 | 24 | func (c *Client) request(ctx context.Context, method, url string, query string, body []byte, timeout time.Duration) (*fasthttp.Response, error) { 25 | req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse() 26 | defer func() { 27 | fasthttp.ReleaseResponse(resp) 28 | fasthttp.ReleaseRequest(req) 29 | }() 30 | 31 | req.SetRequestURI(url) 32 | 33 | // 默认是application/x-www-form-urlencoded 34 | req.Header.SetContentType("application/json") 35 | req.Header.SetMethod(method) 36 | 37 | req.SetBody(body) 38 | req.URI().SetQueryString(query) 39 | 40 | if err := c.HttpClient.DoTimeout(req, resp, timeout); err != nil { 41 | logger.Error("request: %s url:%s fail | err:%+v", method, url, err) 42 | return nil, err 43 | } 44 | return resp, nil 45 | } 46 | 47 | func (c *Client) Get(ctx context.Context, url string, queryParams urllib.Values, timeout time.Duration) (*fasthttp.Response, error) { 48 | query := queryParams.Encode() 49 | return c.request(ctx, fasthttp.MethodGet, url, query, nil, timeout) 50 | } 51 | 52 | func (c *Client) Post(ctx context.Context, url string, body []byte, timeout time.Duration) (*fasthttp.Response, error) { 53 | return c.request(ctx, fasthttp.MethodPost, url, "", body, timeout) 54 | } 55 | -------------------------------------------------------------------------------- /internal/lark/schema.go: -------------------------------------------------------------------------------- 1 | package lark 2 | 3 | type TextContent struct { 4 | Text string `json:"text"` 5 | } 6 | 7 | // TextMessage 文本消息 8 | type TextMessage struct { 9 | MessageType string `json:"msg_type"` 10 | Content *TextContent `json:"content"` 11 | } 12 | 13 | type PostContentItem struct { 14 | Tag string `json:"tag"` 15 | Text string `json:"text"` 16 | Href string `json:"href,omitempty"` 17 | } 18 | 19 | type PostContentData struct { 20 | Title string `json:"title"` 21 | Content [][]*PostContentItem `json:"content"` 22 | } 23 | 24 | type ZhCnPostContentData struct { 25 | ZhCn PostContentData `json:"zh_cn"` 26 | } 27 | 28 | type PostContent struct { 29 | Post *ZhCnPostContentData `json:"post"` 30 | } 31 | 32 | // PostMessage 富文本消息 33 | type PostMessage struct { 34 | MessageType string `json:"msg_type"` 35 | Content *PostContent `json:"content"` 36 | } 37 | 38 | type CardConfig struct { 39 | WideScreenMode bool `json:"wide_screen_mode"` 40 | EnableForward bool `json:"enable_forward"` 41 | } 42 | 43 | type CardElementText struct { 44 | Content string `json:"content"` 45 | Tag string `json:"tag"` 46 | } 47 | 48 | type CardElementAction struct { 49 | Tag string `json:"tag"` 50 | Text CardElementText `json:"text"` 51 | Url string `json:"url"` 52 | Type string `json:"type"` 53 | } 54 | 55 | type CardElement struct { 56 | Tag string `json:"tag"` 57 | Text CardElementText `json:"text"` 58 | Action CardElementAction `json:"actions"` 59 | } 60 | 61 | type CardHeader struct { 62 | Title CardElementText `json:"title"` 63 | } 64 | 65 | type Card struct { 66 | Config CardConfig `json:"config"` 67 | Elements []*CardElement `json:"elements"` 68 | Header CardHeader `json:"header"` 69 | } 70 | 71 | // CardMessage 卡片消息 72 | type CardMessage struct { 73 | MessageType string `json:"msg_type"` 74 | Card Card `json:"card"` 75 | } 76 | 77 | /* 78 | 发表/更新 文章 79 | { 80 | "config": { 81 | "wide_screen_mode": true 82 | }, 83 | "header": { 84 | "title": { 85 | "content": "螺旋上升", 86 | "tag": "plain_text" 87 | } 88 | }, 89 | "elements": [ 90 | { 91 | "tag": "div", 92 | "text": { 93 | "tag": "lark_md", 94 | "content": "[魔魔飞书发表了-----一篇文章] (https://www.feishu.cn)" 95 | } 96 | }, 97 | { 98 | "tag": "div", 99 | "text": { 100 | "tag": "lark_md", 101 | "content": "[飞书](https://www.feishu.cn)整合即时沟通、日历、音视频会议、云文档、云盘、工作台等功能于一体,成就组织和个人,更高效、更愉悦。" 102 | } 103 | }, 104 | { 105 | "tag": "hr" 106 | }, 107 | { 108 | "tag": "note", 109 | "elements": [ 110 | { 111 | "tag": "img", 112 | "img_key": "img_e344c476-1e58-4492-b40d-7dcffe9d6dfg", 113 | "alt": { 114 | "tag": "plain_text", 115 | "content": "hover" 116 | } 117 | }, 118 | { 119 | "tag": "plain_text", 120 | "content": "公号hiiiapril推送" 121 | } 122 | ] 123 | } 124 | ] 125 | } 126 | */ 127 | 128 | type BotResponse struct { 129 | Code int `json:"code"` 130 | Message string `json:"msg"` 131 | } 132 | -------------------------------------------------------------------------------- /cmd/scf/httpadapter/adapter.go: -------------------------------------------------------------------------------- 1 | // Package ginadapter adds Gin support for the aws-severless-go-api library. 2 | // Uses the core package behind the scenes and exposes the New method to 3 | // get a new instance and Proxy method to send request to the Gin engine. 4 | package httpadapter 5 | 6 | import ( 7 | "context" 8 | "github.com/tencentyun/scf-go-lib/cloudevents/scf" 9 | "log" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/aws/aws-lambda-go/events" 14 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | // GinLambda makes it easy to send API Gateway proxy events to a Gin 19 | // Engine. The library transforms the proxy event into an HTTP request and then 20 | // creates a proxy response object from the http.ResponseWriter 21 | type GinLambda struct { 22 | core.RequestAccessor 23 | 24 | ginEngine *gin.Engine 25 | } 26 | 27 | // New creates a new instance of the GinLambda object. 28 | // Receives an initialized *gin.Engine object - normally created with gin.Default(). 29 | // It returns the initialized instance of the GinLambda object. 30 | func New(gin *gin.Engine) *GinLambda { 31 | return &GinLambda{ginEngine: gin} 32 | } 33 | 34 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 35 | // object, and sends it to the gin.Engine for routing. 36 | // It returns a proxy response object generated from the http.ResponseWriter. 37 | func (g *GinLambda) Proxy(req events.APIGatewayProxyRequest) (scf.APIGatewayProxyResponse, error) { 38 | ginRequest, err := g.ProxyEventToHTTPRequest(req) 39 | return g.proxyInternal(ginRequest, err) 40 | } 41 | 42 | // ProxyWithContext receives context and an API Gateway proxy event, 43 | // transforms them into an http.Request object, and sends it to the gin.Engine for routing. 44 | // It returns a proxy response object generated from the http.ResponseWriter. 45 | func (g *GinLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (scf.APIGatewayProxyResponse, error) { 46 | ginRequest, err := g.EventToRequestWithContext(ctx, req) 47 | return g.proxyInternal(ginRequest, err) 48 | } 49 | 50 | func (g *GinLambda) proxyInternal(req *http.Request, err error) (scf.APIGatewayProxyResponse, error) { 51 | 52 | if err != nil { 53 | return lambdaResponse2scf(core.GatewayTimeout()), core.NewLoggedError("Could not convert proxy event to request: %v", err) 54 | } 55 | 56 | respWriter := core.NewProxyResponseWriter() 57 | g.ginEngine.ServeHTTP(http.ResponseWriter(respWriter), req) 58 | 59 | proxyResponse, err := respWriter.GetProxyResponse() 60 | if err != nil { 61 | return lambdaResponse2scf(core.GatewayTimeout()), core.NewLoggedError("Error while generating proxy response: %v", err) 62 | } 63 | 64 | return lambdaResponse2scf(proxyResponse), nil 65 | } 66 | 67 | func lambdaResponse2scf(resp events.APIGatewayProxyResponse) scf.APIGatewayProxyResponse { 68 | var headers = make(map[string]string) 69 | mHeaders := resp.MultiValueHeaders 70 | if mHeaders == nil { 71 | headers = map[string]string{ 72 | "Content-Type": "text/html; charset=utf-8", 73 | } 74 | } else { 75 | for k, v := range mHeaders { 76 | headers[k] = strings.Join(v, ",") 77 | } 78 | } 79 | log.Println("finally headers: ", headers) 80 | return scf.APIGatewayProxyResponse{ 81 | StatusCode: resp.StatusCode, 82 | // Headers: resp.MultiValueHeaders, 83 | Headers: headers, 84 | Body: resp.Body, 85 | IsBase64Encoded: false, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/yuque/schema.go: -------------------------------------------------------------------------------- 1 | package yuque 2 | 3 | type WebhookSubjectType string 4 | 5 | const ( 6 | CommentReplyCreate WebhookSubjectType = "comment_reply_create" 7 | CommentReplyUpdate WebhookSubjectType = "comment_reply_update" 8 | CommentReplyDelete WebhookSubjectType = "comment_reply_delete" 9 | CommentCreate WebhookSubjectType = "comment_create" 10 | CommentUpdate WebhookSubjectType = "comment_update" 11 | CommentDelete WebhookSubjectType = "comment_delete" 12 | Publish WebhookSubjectType = "publish" 13 | Update WebhookSubjectType = "update" 14 | Delete WebhookSubjectType = "delete" 15 | ) 16 | 17 | type ValidRequest interface { 18 | Validate() error 19 | } 20 | 21 | type YuqueUser struct { 22 | Id int `json:"id"` 23 | Type string `json:"type"` 24 | Login string `json:""` // 登录用户名 25 | Name string `json:"name"` // 用户名 26 | Description string `json:"description"` // 登录用户描述 27 | AvatarUrl string `json:"avatar_url"` // 头像 28 | FollowersCount int `json:"followers_count"` 29 | FollowingCount int `json:"following_count"` 30 | CreatedAt string `json:"created_at"` //created_at - 注册时间 31 | UpdatedAt string `json:"updated_at"` //updated_at - 更新时间 32 | Serializer string `json:"_serializer"` // 33 | } 34 | 35 | type YuqueBook struct { 36 | Id int `json:"id"` 37 | Type string `json:"type"` 38 | Slug string `json:"slug"` //slug - 文档路径 39 | Name string `json:"name"` //title - 标题 40 | UserId int `json:"user_id"` 41 | User YuqueUser `json:"user"` 42 | CreatorId int `json:"creator_id"` 43 | Public int `json:"public"` //public - 公开级别 [0 - 私密, 1 - 公开] 44 | LikesCount int `json:"likes_count"` //likes_count - 赞数量 45 | WatchesCount int `json:"watches_count"` //watches_count - 赞数量 46 | ItemsCount int `json:"items_count"` //items_count - 赞数量 47 | Description string `json:"description"` // 登录用户描述 48 | CreatedAt string `json:"created_at"` //created_at - 注册时间 49 | UpdatedAt string `json:"updated_at"` //updated_at - 更新时间 50 | ContentUpdated string `json:"content_updated"` //updated_at - 更新时间 51 | 52 | } 53 | 54 | type CommentAbleData struct { 55 | Id int `json:"id"` 56 | Slug string `json:"slug"` //slug - 文档路径 57 | Title string `json:"title"` //title - 标题 58 | BookId int `json:"book_id"` //book_id - 仓库编号,就是 repo_id 59 | Book YuqueBook `json:"book"` //book - 仓库信息 ,就是 repo 信息 60 | Serializer string `json:"_serializer"` // 61 | } 62 | 63 | type WebhookData struct { 64 | Id int `json:"id"` // id - 文档编号 65 | Slug string `json:"slug"` //slug - 文档路径 66 | Title string `json:"title"` //title - 标题 67 | BookId int `json:"book_id"` //book_id - 仓库编号,就是 repo_id 68 | Book YuqueBook `json:"book"` //book - 仓库信息 ,就是 repo 信息 69 | UserId int `json:"user_id"` //user_id - 用户/团队编号 70 | User YuqueUser `json:"user"` //user - 用户/团队信息 71 | Format string `json:"format"` //format - 描述了正文的格式 [lake , markdown] 72 | Body string `json:"body"` //body - 正文 Markdown 源代码 73 | BodyDraft string `json:"body_draft"` //body_draft - 草稿 Markdown 源代码 74 | BodyHtml string `json:"body_html"` //body_html - 转换过后的正文 HTML 75 | BodyLake string `json:"body_lake"` //body_lake - 语雀 lake 格式的文档内容 76 | CreatorId int `json:"creator_id"` //creator_id - 文档创建人 User Id 77 | Public int `json:"public"` //public - 公开级别 [0 - 私密, 1 - 公开] 78 | Status int `json:"status"` //status - 状态 [0 - 草稿, 1 - 发布] 79 | ViewStatus int `json:"view_status"` //view_status - 状态 [] 80 | ReadStatus int `json:"read_status"` //read_status - 状态 [] 81 | LikesCount int `json:"likes_count"` //likes_count - 赞数量 82 | CommentsCount int `json:"comments_count"` //comments_count - 评论数量 83 | ContentUpdatedAt string `json:"content_updated_at"` //content_updated_at - 文档内容更新时间 84 | DeletedAt string `json:"deleted_at"` //deleted_at - 删除时间,未删除为 null 85 | CreatedAt string `json:"created_at"` //created_at - 创建时间 86 | UpdatedAt string `json:"updated_at"` //updated_at - 更新时间 87 | Serializer string `json:"_serializer"` // 88 | WebhookSubjectType WebhookSubjectType `json:"webhook_subject_type"` // 89 | CommentAble CommentAbleData `json:"commentable"` 90 | } 91 | 92 | type WebHookRequest struct { 93 | Data WebhookData `form:"data" json:"data" binding:"required"` 94 | } 95 | 96 | func (req *WebHookRequest) Validate() error { 97 | //if req.ActionType != nil && *req.ActionType != Publish && *req.ActionType != Update && *req.ActionType != Delete { 98 | // return fmt.Errorf("invalid action type:%s", *req.ActionType) 99 | //} 100 | return nil 101 | } 102 | 103 | func RequestValidate(req interface{}) error { 104 | if vr, ok := req.(ValidRequest); ok { 105 | if err := vr.Validate(); err != nil { 106 | return err 107 | } 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/yuque/lark.go: -------------------------------------------------------------------------------- 1 | package yuque 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gusibi/yuque_webhook/internal/lark" 7 | "time" 8 | ) 9 | 10 | type MessageType string 11 | 12 | const ( 13 | TextMessage MessageType = "text" 14 | PostMessage MessageType = "post" 15 | CardMessage MessageType = "card" 16 | ) 17 | 18 | type LarkWebHook struct { 19 | MessageType MessageType 20 | 21 | HookId string 22 | // timeout := time.Duration(2) * time.Second 23 | DefaultTimeout time.Duration 24 | } 25 | 26 | func NewLarkWebHook(hookId string, messageType MessageType) *LarkWebHook { 27 | return &LarkWebHook{ 28 | MessageType: messageType, 29 | HookId: hookId, 30 | DefaultTimeout: 2, 31 | } 32 | } 33 | 34 | func (l *LarkWebHook) requestToTextMessage(ctx context.Context, req *WebHookRequest) (*lark.TextMessage, error) { 35 | message := &lark.TextMessage{ 36 | MessageType: "text", 37 | } 38 | var content string 39 | webhookType := req.Data.WebhookSubjectType 40 | switch webhookType { 41 | case CommentCreate, CommentReplyCreate: 42 | content = fmt.Sprintf("文章 《%s》有一条来自%s的新评论", req.Data.CommentAble.Title, req.Data.User.Name) 43 | case CommentUpdate, CommentReplyUpdate: 44 | content = fmt.Sprintf("%s 修改了文章 《%s》的评论", req.Data.User.Name, req.Data.CommentAble.Title) 45 | case CommentDelete, CommentReplyDelete: 46 | content = fmt.Sprintf("%s 删除了文章 《%s》的评论", req.Data.User.Name, req.Data.CommentAble.Title) 47 | case Publish: 48 | content = fmt.Sprintf("「%s」有一篇新文章", req.Data.Book.Name) 49 | case Update: 50 | content = fmt.Sprintf("文章「%s」有更新", req.Data.Title) 51 | case Delete: 52 | content = fmt.Sprintf("文章「%s」被删除了", req.Data.Title) 53 | } 54 | message.Content = &lark.TextContent{Text: content} 55 | return message, nil 56 | } 57 | 58 | func (l *LarkWebHook) requestToPostMessage(ctx context.Context, req *WebHookRequest) (*lark.PostMessage, error) { 59 | message := &lark.PostMessage{ 60 | MessageType: "post", 61 | } 62 | var title string 63 | var content []*lark.PostContentItem 64 | webhookType := req.Data.WebhookSubjectType 65 | switch webhookType { 66 | case CommentCreate, CommentReplyCreate: 67 | title = fmt.Sprintf("文章「%s」有一条新评论", req.Data.CommentAble.Title) 68 | content = []*lark.PostContentItem{ 69 | { 70 | Tag: "text", 71 | Text: "文章:", 72 | }, 73 | { 74 | Tag: "a", 75 | Text: fmt.Sprintf("《%s》", req.Data.CommentAble.Title), 76 | Href: GetArticleUrl(req.Data.User.Login, req.Data.CommentAble.Book.Slug, req.Data.CommentAble.Slug), 77 | }, 78 | { 79 | Tag: "text", 80 | Text: fmt.Sprintf("有一条来自「%s」的新评论", req.Data.User.Name), 81 | }, 82 | } 83 | case CommentUpdate, CommentReplyUpdate: 84 | title = fmt.Sprintf("文章「%s」评论更新", req.Data.CommentAble.Title) 85 | content = []*lark.PostContentItem{ 86 | { 87 | Tag: "text", 88 | Text: fmt.Sprintf("「%s」修改了文章:", req.Data.User.Name), 89 | }, 90 | { 91 | Tag: "a", 92 | Text: fmt.Sprintf("《%s》", req.Data.CommentAble.Title), 93 | Href: GetArticleUrl(req.Data.User.Login, req.Data.CommentAble.Book.Slug, req.Data.CommentAble.Slug), 94 | }, 95 | { 96 | Tag: "text", 97 | Text: "的评论", 98 | }, 99 | } 100 | case CommentDelete, CommentReplyDelete: 101 | title = fmt.Sprintf("文章「%s」删除了一条评论", req.Data.CommentAble.Title) 102 | content = []*lark.PostContentItem{ 103 | { 104 | Tag: "text", 105 | Text: fmt.Sprintf("「%s」删除了文章:", req.Data.User.Name), 106 | }, 107 | { 108 | Tag: "a", 109 | Text: fmt.Sprintf("《%s》", req.Data.CommentAble.Title), 110 | Href: GetArticleUrl(req.Data.User.Login, req.Data.CommentAble.Book.Slug, req.Data.CommentAble.Slug), 111 | }, 112 | { 113 | Tag: "text", 114 | Text: "的评论", 115 | }, 116 | } 117 | case Publish: 118 | title = fmt.Sprintf("「%s」有一篇新文章", req.Data.Book.Name) 119 | content = []*lark.PostContentItem{ 120 | { 121 | Tag: "text", 122 | Text: fmt.Sprintf("「%s」在知识库", req.Data.User.Name), 123 | }, 124 | { 125 | Tag: "a", 126 | Text: fmt.Sprintf("《%s》:", req.Data.Book.Name), 127 | Href: GetBookeUrl(req.Data.User.Login, req.Data.Book.Slug), 128 | }, 129 | { 130 | Tag: "text", 131 | Text: "发布了一篇新文章:", 132 | }, 133 | { 134 | Tag: "a", 135 | Text: fmt.Sprintf("《%s》", req.Data.Title), 136 | Href: GetArticleUrl(req.Data.User.Login, req.Data.Book.Slug, req.Data.Slug), 137 | }, 138 | } 139 | case Update: 140 | title = fmt.Sprintf("文章「%s」有更新", req.Data.Title) 141 | content = []*lark.PostContentItem{ 142 | { 143 | Tag: "text", 144 | Text: fmt.Sprintf("「%s」更新了知识库:", req.Data.User.Name), 145 | }, 146 | { 147 | Tag: "a", 148 | Text: fmt.Sprintf("《%s》:", req.Data.Book.Name), 149 | Href: GetBookeUrl(req.Data.User.Login, req.Data.Book.Slug), 150 | }, 151 | { 152 | Tag: "text", 153 | Text: "中的文章:", 154 | }, 155 | { 156 | Tag: "a", 157 | Text: fmt.Sprintf("《%s》", req.Data.Title), 158 | Href: GetArticleUrl(req.Data.User.Login, req.Data.Book.Slug, req.Data.Slug), 159 | }, 160 | } 161 | case Delete: 162 | title = fmt.Sprintf("文章「%s」被删除了", req.Data.Title) 163 | content = []*lark.PostContentItem{ 164 | { 165 | Tag: "text", 166 | Text: fmt.Sprintf("「%s」删除了知识库:", req.Data.User.Name), 167 | }, 168 | { 169 | Tag: "a", 170 | Text: fmt.Sprintf("《%s》:", req.Data.Book.Name), 171 | Href: GetBookeUrl(req.Data.User.Login, req.Data.Book.Slug), 172 | }, 173 | { 174 | Tag: "text", 175 | Text: "中的文章:", 176 | }, 177 | { 178 | Tag: "text", 179 | Text: fmt.Sprintf("《%s》", req.Data.Title), 180 | }, 181 | } 182 | } 183 | message.Content = &lark.PostContent{ 184 | Post: &lark.ZhCnPostContentData{ 185 | ZhCn: lark.PostContentData{ 186 | Title: title, 187 | Content: [][]*lark.PostContentItem{content}, 188 | }, 189 | }, 190 | } 191 | return message, nil 192 | } 193 | 194 | func (l *LarkWebHook) requestToCardMessage(ctx context.Context, req *WebHookRequest) (*lark.CardMessage, error) { 195 | return nil, nil 196 | } 197 | 198 | func (l *LarkWebHook) pushTextMessage(ctx context.Context, req *WebHookRequest) (*lark.TextMessage, error) { 199 | message, err := l.requestToTextMessage(ctx, req) 200 | if err != nil { 201 | return nil, err 202 | } 203 | lark.LarkClient.BotHook(ctx, l.HookId, message, nil, nil) 204 | return nil, nil 205 | } 206 | 207 | func (l *LarkWebHook) pushPostMessage(ctx context.Context, req *WebHookRequest) (*lark.PostMessage, error) { 208 | post, err := l.requestToPostMessage(ctx, req) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | lark.LarkClient.BotHook(ctx, l.HookId, nil, post, nil) 214 | return nil, nil 215 | } 216 | 217 | func (l *LarkWebHook) pushCardMessage(ctx context.Context, req *WebHookRequest) (*lark.CardMessage, error) { 218 | return nil, nil 219 | } 220 | 221 | func (l *LarkWebHook) Push(ctx context.Context, req *WebHookRequest) error { 222 | if req == nil { 223 | return fmt.Errorf("req is invalid") 224 | } 225 | if l.MessageType == TextMessage { 226 | l.pushTextMessage(ctx, req) 227 | 228 | } else if l.MessageType == PostMessage { 229 | l.pushPostMessage(ctx, req) 230 | 231 | } else if l.MessageType == CardMessage { 232 | l.pushCardMessage(ctx, req) 233 | } 234 | return nil 235 | } 236 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Bowery/prompt v0.0.0-20190419144237-972d0ceb96f5/go.mod h1:4/6eNcqZ09BZ9wLK3tZOjBA1nDj+B0728nlX5YRlSmQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/Joker/hpp v0.0.0-20180418125244-6893e659854a/go.mod h1:MzD2WMdSxvbHw5fM/OXOFily/lipJWRc9C1px0Mt0ZE= 4 | github.com/Joker/jade v1.0.0/go.mod h1:efZIdO0py/LtcJRSa/j2WEklMSAw84WV0zZVMxNToB8= 5 | github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= 6 | github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= 7 | github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= 8 | github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 9 | github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 10 | github.com/aws/aws-lambda-go v0.0.0-20190129190457-dcf76fe64fb6/go.mod h1:zUsUQhAUjYzR8AuduJPCfhBuKWUaDbQiPOG+ouzmE1A= 11 | github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= 12 | github.com/aws/aws-lambda-go v1.21.0 h1:6fF3tSipETaUQbTmo9zPcMlVYM/Khm9rYb94jJseHRs= 13 | github.com/aws/aws-lambda-go v1.21.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= 14 | github.com/awslabs/aws-lambda-go-api-proxy v0.5.0 h1:mmzE5dJ2yt23lmWr6QNtCCAA3H0k4DGWsttilSRnSdI= 15 | github.com/awslabs/aws-lambda-go-api-proxy v0.5.0/go.mod h1:9ZpbR64sd0A73+ylC1tP63Kyz2VhijeDw1O8naJqehA= 16 | github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 18 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185/go.mod h1:cFRxtTwTOJkz2x3rQUNCYKWC93yP1VKjR8NUhqFxZNU= 22 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 23 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= 24 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 25 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 26 | github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= 27 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 28 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 29 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 30 | github.com/gin-gonic/gin v0.0.0-20180126034611-783c7ee9c14e/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 31 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 32 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 33 | github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= 34 | github.com/go-chi/chi v0.0.0-20180202194135-e223a795a06a/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 35 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 36 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 37 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 38 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 39 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 40 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 41 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 42 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 43 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 44 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 45 | github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 48 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 49 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 50 | github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= 51 | github.com/google/uuid v0.0.0-20171129191014-dec09d789f3d/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 52 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 53 | github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= 54 | github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= 55 | github.com/gorilla/mux v0.0.0-20180120075819-c0091a029979/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 56 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 57 | github.com/gusibi/nCov v0.0.0-20200323141147-ed48dc582bdb h1:ecHfrN/AImf2d8gDxpQmm8M7CC+D7tRFhXjkBOAOQFE= 58 | github.com/gusibi/nCov v0.0.0-20200323141147-ed48dc582bdb/go.mod h1:6ZT9BgqPrWe1Gi5d2F2b254PyxzxDLqopp+lN3lY1xg= 59 | github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= 60 | github.com/iris-contrib/formBinder v5.0.0+incompatible/go.mod h1:i8kTYUOEstd/S8TG0ChTXQdf4ermA/e8vJX0+QruD9w= 61 | github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= 62 | github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= 63 | github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= 64 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 65 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 66 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 67 | github.com/json-iterator/go v0.0.0-20180128142709-bca911dae073/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 68 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 69 | github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= 70 | github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= 71 | github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= 72 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 73 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 74 | github.com/kardianos/govendor v1.0.9/go.mod h1:yvmR6q9ZZ7nSF5Wvh40v0wfP+3TwwL8zYQp+itoZSVM= 75 | github.com/kataras/golog v0.0.0-20190624001437-99c81de45f40/go.mod h1:PcaEvfvhGsqwXZ6S3CgCbmjcp+4UDUh2MIfF2ZEul8M= 76 | github.com/kataras/iris v11.1.1+incompatible/go.mod h1:ki9XPua5SyAJbIxDdsssxevgGrbpBmmvoQmo/A0IodY= 77 | github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= 78 | github.com/klauspost/compress v1.7.4/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 79 | github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= 80 | github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 81 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 82 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 83 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 84 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 85 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 86 | github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= 87 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 88 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 89 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 90 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 91 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 92 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 93 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 94 | github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 95 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 96 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 97 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 98 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 99 | github.com/onsi/ginkgo v0.0.0-20180119174237-747514b53ddd/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 100 | github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 101 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 102 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 103 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 104 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 105 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 106 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 107 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 108 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 109 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 110 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 111 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 112 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 113 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 114 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 115 | github.com/tencentyun/scf-go-lib v0.0.0-20200116145541-9a6ea1bf75b8/go.mod h1:K3DbqPpP2WE/9MWokWWzgFZcbgtMb9Wd5CYk9AAbEN8= 116 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9 h1:JdeXp/XPi7lBmpQNSUxElMAvwppMlFSiamTtXYRFuUc= 117 | github.com/tencentyun/scf-go-lib v0.0.0-20200624065115-ba679e2ec9c9/go.mod h1:K3DbqPpP2WE/9MWokWWzgFZcbgtMb9Wd5CYk9AAbEN8= 118 | github.com/ugorji/go v0.0.0-20180129160544-d2b24cf3d3b4/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= 119 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 120 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 121 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 122 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 123 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 124 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 125 | github.com/urfave/negroni v0.0.0-20180130044549-22c5532ea862/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 126 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 127 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 128 | github.com/valyala/fasthttp v1.18.0 h1:IV0DdMlatq9QO1Cr6wGJPVW1sV1Q8HvZXAIcjorylyM= 129 | github.com/valyala/fasthttp v1.18.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= 130 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 131 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 132 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 133 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 134 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 135 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 136 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 137 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 138 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 139 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 140 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 141 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 142 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 143 | golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 144 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 145 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 147 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 150 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 152 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 153 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 154 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 155 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 156 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 157 | golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 159 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 160 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 161 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 162 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 163 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 164 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 165 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 166 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 167 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 168 | --------------------------------------------------------------------------------