diff --git a/models/repo.go b/models/repo.go index db2694617..4770e5415 100755 --- a/models/repo.go +++ b/models/repo.go @@ -2749,15 +2749,10 @@ func ReadLatestFileInRepo(userName, repoName, refName, treePath string) (*RepoFi log.Error("ReadLatestFileInRepo: Close: %v", err) } }() - - buf := make([]byte, 1024) - n, _ := reader.Read(buf) - if n >= 0 { - buf = buf[:n] - } + d, _ := ioutil.ReadAll(reader) commitId := "" if blob != nil { commitId = fmt.Sprint(blob.ID) } - return &RepoFile{CommitId: commitId, Content: buf}, nil + return &RepoFile{CommitId: commitId, Content: d}, nil } diff --git a/modules/auth/wechat/auto_reply.go b/modules/auth/wechat/auto_reply.go new file mode 100644 index 000000000..440f6de6a --- /dev/null +++ b/modules/auth/wechat/auto_reply.go @@ -0,0 +1,139 @@ +package wechat + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "encoding/json" + "github.com/patrickmn/go-cache" + "strings" + "time" +) + +var WechatReplyCache = cache.New(2*time.Minute, 1*time.Minute) + +const ( + WECHAT_REPLY_CACHE_KEY = "wechat_response" +) + +const ( + ReplyTypeText = "text" + ReplyTypeImage = "image" + ReplyTypeVoice = "voice" + ReplyTypeVideo = "video" + ReplyTypeMusic = "music" + ReplyTypeNews = "news" +) + +type ReplyConfigType string + +const ( + SubscribeReply ReplyConfigType = "subscribe" + AutoMsgReply ReplyConfigType = "autoMsg" +) + +func (r ReplyConfigType) Name() string { + switch r { + case SubscribeReply: + return "subscribe" + case AutoMsgReply: + return "autoMsg" + } + return "" +} + +func (r ReplyConfigType) TreePath() string { + switch r { + case SubscribeReply: + return setting.TreePathOfSubscribe + case AutoMsgReply: + return setting.TreePathOfAutoMsgReply + } + return "" +} + +type WechatReplyContent struct { + Reply *ReplyContent + ReplyType string + KeyWords []string + IsFullMatch int +} + +type ReplyContent struct { + Content string + MediaId string + Title string + Description string + MusicUrl string + HQMusicUrl string + ThumbMediaId string + Articles []ArticlesContent +} + +func GetAutomaticReply(msg string) *WechatReplyContent { + r, err := LoadReplyFromCacheAndDisk(AutoMsgReply) + if err != nil { + return nil + } + if r == nil || len(r) == 0 { + return nil + } + for i := 0; i < len(r); i++ { + if r[i].IsFullMatch == 0 { + for _, v := range r[i].KeyWords { + if strings.Contains(msg, v) { + return r[i] + } + } + } else if r[i].IsFullMatch > 0 { + for _, v := range r[i].KeyWords { + if msg == v { + return r[i] + } + } + } + } + return nil + +} + +func loadReplyFromDisk(replyConfig ReplyConfigType) ([]*WechatReplyContent, error) { + log.Info("LoadReply from disk") + repo, err := models.GetRepositoryByOwnerAndAlias(setting.UserNameOfWechatReply, setting.RepoNameOfWechatReply) + if err != nil { + log.Error("get AutomaticReply repo failed, error=%v", err) + return nil, err + } + repoFile, err := models.ReadLatestFileInRepo(setting.UserNameOfWechatReply, repo.Name, setting.RefNameOfWechatReply, replyConfig.TreePath()) + if err != nil { + log.Error("get AutomaticReply failed, error=%v", err) + return nil, err + } + res := make([]*WechatReplyContent, 0) + json.Unmarshal(repoFile.Content, &res) + if res == nil || len(res) == 0 { + return nil, err + } + return res, nil +} + +func LoadReplyFromCacheAndDisk(replyConfig ReplyConfigType) ([]*WechatReplyContent, error) { + v, success := WechatReplyCache.Get(replyConfig.Name()) + if success { + log.Info("LoadReply from cache,value = %v", v) + if v == nil { + return nil, nil + } + n := v.([]*WechatReplyContent) + return n, nil + } + + content, err := loadReplyFromDisk(replyConfig) + if err != nil { + log.Error("LoadReply failed, error=%v", err) + WechatReplyCache.Set(replyConfig.Name(), nil, 30*time.Second) + return nil, err + } + WechatReplyCache.Set(replyConfig.Name(), content, 60*time.Second) + return content, nil +} diff --git a/modules/auth/wechat/client.go b/modules/auth/wechat/client.go index 6734977a1..9ed4b543f 100644 --- a/modules/auth/wechat/client.go +++ b/modules/auth/wechat/client.go @@ -17,7 +17,8 @@ var ( const ( GRANT_TYPE = "client_credential" ACCESS_TOKEN_PATH = "/cgi-bin/token" - QR_CODE_Path = "/cgi-bin/qrcode/create" + QR_CODE_PATH = "/cgi-bin/qrcode/create" + GET_MATERIAL_PATH = "/cgi-bin/material/batchget_material" ACTION_QR_STR_SCENE = "QR_STR_SCENE" ERR_CODE_ACCESSTOKEN_EXPIRE = 42001 @@ -40,6 +41,11 @@ type QRCodeRequest struct { Action_info ActionInfo `json:"action_info"` Expire_seconds int `json:"expire_seconds"` } +type MaterialRequest struct { + Type string `json:"type"` + Offset int `json:"offset"` + Count int `json:"count"` +} type ActionInfo struct { Scene Scene `json:"scene"` @@ -97,7 +103,7 @@ func callQRCodeCreate(sceneStr string) (*QRCodeResponse, bool) { SetQueryParam("access_token", GetWechatAccessToken()). SetBody(bodyJson). SetResult(&result). - Post(setting.WechatApiHost + QR_CODE_Path) + Post(setting.WechatApiHost + QR_CODE_PATH) if err != nil { log.Error("create QR code failed,e=%v", err) return nil, false @@ -113,6 +119,37 @@ func callQRCodeCreate(sceneStr string) (*QRCodeResponse, bool) { return &result, false } +//getMaterial +// api doc: https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html +func getMaterial(mType string, offset, count int) (interface{}, bool) { + client := getWechatRestyClient() + + body := &MaterialRequest{ + Type: mType, + Offset: offset, + Count: count, + } + bodyJson, _ := json.Marshal(body) + r, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetQueryParam("access_token", GetWechatAccessToken()). + SetBody(bodyJson). + Post(setting.WechatApiHost + GET_MATERIAL_PATH) + if err != nil { + log.Error("create QR code failed,e=%v", err) + return nil, false + } + a := r.Body() + resultMap := make(map[string]interface{}, 0) + json.Unmarshal(a, &resultMap) + errcode := resultMap["errcode"] + if errcode == fmt.Sprint(ERR_CODE_ACCESSTOKEN_EXPIRE) || errcode == fmt.Sprint(ERR_CODE_ACCESSTOKEN_INVALID) { + return nil, true + } + log.Info("%v", r) + return &resultMap, false +} + func getErrorCodeFromResponse(r *resty.Response) int { a := r.Body() resultMap := make(map[string]interface{}, 0) diff --git a/modules/auth/wechat/event_handle.go b/modules/auth/wechat/event_handle.go index b40ab3101..27edf7343 100644 --- a/modules/auth/wechat/event_handle.go +++ b/modules/auth/wechat/event_handle.go @@ -18,7 +18,7 @@ import ( // // // -type WechatEvent struct { +type WechatMsg struct { ToUserName string FromUserName string CreateTime int64 @@ -26,9 +26,13 @@ type WechatEvent struct { Event string EventKey string Ticket string + Content string + MsgId string + MsgDataId string + Idx string } -type EventReply struct { +type MsgReply struct { XMLName xml.Name `xml:"xml"` ToUserName string FromUserName string @@ -37,16 +41,97 @@ type EventReply struct { Content string } +type TextMsgReply struct { + XMLName xml.Name `xml:"xml"` + ToUserName string + FromUserName string + CreateTime int64 + MsgType string + Content string +} +type ImageMsgReply struct { + XMLName xml.Name `xml:"xml"` + ToUserName string + FromUserName string + CreateTime int64 + MsgType string + Image ImageContent +} +type VoiceMsgReply struct { + XMLName xml.Name `xml:"xml"` + ToUserName string + FromUserName string + CreateTime int64 + MsgType string + Voice VoiceContent +} +type VideoMsgReply struct { + XMLName xml.Name `xml:"xml"` + ToUserName string + FromUserName string + CreateTime int64 + MsgType string + Video VideoContent +} +type MusicMsgReply struct { + XMLName xml.Name `xml:"xml"` + ToUserName string + FromUserName string + CreateTime int64 + MsgType string + Music MusicContent +} +type NewsMsgReply struct { + XMLName xml.Name `xml:"xml"` + ToUserName string + FromUserName string + CreateTime int64 + MsgType string + ArticleCount int + Articles ArticleItem +} + +type ArticleItem struct { + Item []ArticlesContent +} + +type ImageContent struct { + MediaId string +} +type VoiceContent struct { + MediaId string +} +type VideoContent struct { + MediaId string + Title string + Description string +} +type MusicContent struct { + Title string + Description string + MusicUrl string + HQMusicUrl string + ThumbMediaId string +} +type ArticlesContent struct { + XMLName xml.Name `xml:"item"` + Title string + Description string + PicUrl string + Url string +} + const ( WECHAT_EVENT_SUBSCRIBE = "subscribe" WECHAT_EVENT_SCAN = "SCAN" ) const ( - WECHAT_MSG_TYPE_TEXT = "text" + WECHAT_MSG_TYPE_TEXT = "text" + WECHAT_MSG_TYPE_EVENT = "event" ) -func HandleSubscribeEvent(we WechatEvent) string { +func HandleScanEvent(we WechatMsg) string { eventKey := we.EventKey if eventKey == "" { return "" @@ -74,3 +159,11 @@ func HandleSubscribeEvent(we WechatEvent) string { return BIND_REPLY_SUCCESS } + +func HandleSubscribeEvent(we WechatMsg) *WechatReplyContent { + r, err := LoadReplyFromCacheAndDisk(SubscribeReply) + if err != nil || len(r) == 0 { + return nil + } + return r[0] +} diff --git a/modules/auth/wechat/material.go b/modules/auth/wechat/material.go new file mode 100644 index 000000000..526156af5 --- /dev/null +++ b/modules/auth/wechat/material.go @@ -0,0 +1,13 @@ +package wechat + +import "code.gitea.io/gitea/modules/log" + +func GetWechatMaterial(mType string, offset, count int) interface{} { + result, retryFlag := getMaterial(mType, offset, count) + if retryFlag { + log.Info("retryGetWechatMaterial calling") + refreshAccessToken() + result, _ = getMaterial(mType, offset, count) + } + return result +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5c87b68c5..412bc09b1 100755 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -545,6 +545,13 @@ var ( WechatQRCodeExpireSeconds int WechatAuthSwitch bool + //wechat auto reply config + UserNameOfWechatReply string + RepoNameOfWechatReply string + RefNameOfWechatReply string + TreePathOfAutoMsgReply string + TreePathOfSubscribe string + //nginx proxy PROXYURL string RadarMap = struct { @@ -1372,6 +1379,11 @@ func NewContext() { WechatAppSecret = sec.Key("APP_SECRET").MustString("e48e13f315adc32749ddc7057585f198") WechatQRCodeExpireSeconds = sec.Key("QR_CODE_EXPIRE_SECONDS").MustInt(120) WechatAuthSwitch = sec.Key("AUTH_SWITCH").MustBool(true) + UserNameOfWechatReply = sec.Key("AUTO_REPLY_USER_NAME").MustString("OpenIOSSG") + RepoNameOfWechatReply = sec.Key("AUTO_REPLY_REPO_NAME").MustString("promote") + RefNameOfWechatReply = sec.Key("AUTO_REPLY_REF_NAME").MustString("master") + TreePathOfAutoMsgReply = sec.Key("AUTO_REPLY_TREE_PATH").MustString("wechat/auto_reply.json") + TreePathOfSubscribe = sec.Key("SUBSCRIBE_TREE_PATH").MustString("wechat/subscribe_reply.json") SetRadarMapConfig() diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0c280b0cb..963bf2cee 100755 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1046,6 +1046,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/prd/event", authentication.ValidEventSource) m.Post("/prd/event", authentication.AcceptWechatEvent) }) + m.Get("/wechat/material", authentication.GetMaterial) }, securityHeaders(), context.APIContexter(), sudo()) } diff --git a/routers/authentication/wechat.go b/routers/authentication/wechat.go index 72871afb3..f4a31ea0c 100644 --- a/routers/authentication/wechat.go +++ b/routers/authentication/wechat.go @@ -8,9 +8,11 @@ import ( "code.gitea.io/gitea/modules/redis/redis_client" "code.gitea.io/gitea/modules/redis/redis_key" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/response" "encoding/json" "errors" gouuid "github.com/satori/go.uuid" + "strconv" "time" ) @@ -124,3 +126,23 @@ func createQRCode4Bind(userId int64) (*QRCodeResponse, error) { } return result, nil } + +// GetMaterial +func GetMaterial(ctx *context.Context) { + mType := ctx.Query("type") + offsetStr := ctx.Query("offset") + countStr := ctx.Query("count") + var offset, count int + if offsetStr == "" { + offset = 0 + } else { + offset, _ = strconv.Atoi(offsetStr) + } + if countStr == "" { + count = 20 + } else { + count, _ = strconv.Atoi(countStr) + } + r := wechat.GetWechatMaterial(mType, offset, count) + ctx.JSON(200, response.SuccessWithData(r)) +} diff --git a/routers/authentication/wechat_event.go b/routers/authentication/wechat_event.go index 9b1cebec6..887bfba0d 100644 --- a/routers/authentication/wechat_event.go +++ b/routers/authentication/wechat_event.go @@ -14,24 +14,48 @@ import ( // https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html func AcceptWechatEvent(ctx *context.Context) { b, _ := ioutil.ReadAll(ctx.Req.Request.Body) - we := wechat.WechatEvent{} + we := wechat.WechatMsg{} xml.Unmarshal(b, &we) - + switch we.MsgType { + case wechat.WECHAT_MSG_TYPE_EVENT: + HandleEventMsg(ctx, we) + case wechat.WECHAT_MSG_TYPE_TEXT: + HandleTextMsg(ctx, we) + } log.Info("accept wechat event= %+v", we) - var replyStr string - switch we.Event { - case wechat.WECHAT_EVENT_SUBSCRIBE, wechat.WECHAT_EVENT_SCAN: - replyStr = wechat.HandleSubscribeEvent(we) - break + +} + +// ValidEventSource +func ValidEventSource(ctx *context.Context) { + echostr := ctx.Query("echostr") + ctx.Write([]byte(echostr)) + return +} + +func HandleEventMsg(ctx *context.Context, msg wechat.WechatMsg) { + switch msg.Event { + case wechat.WECHAT_EVENT_SCAN: + HandleEventScan(ctx, msg) + case wechat.WECHAT_EVENT_SUBSCRIBE: + if msg.EventKey != "" { + HandleEventScan(ctx, msg) + } else { + HandleEventSubscribe(ctx, msg) + } + } +} +func HandleEventScan(ctx *context.Context, msg wechat.WechatMsg) { + replyStr := wechat.HandleScanEvent(msg) if replyStr == "" { log.Info("reply str is empty") return } - reply := &wechat.EventReply{ - ToUserName: we.FromUserName, - FromUserName: we.ToUserName, + reply := &wechat.MsgReply{ + ToUserName: msg.FromUserName, + FromUserName: msg.ToUserName, CreateTime: time.Now().Unix(), MsgType: wechat.WECHAT_MSG_TYPE_TEXT, Content: replyStr, @@ -39,9 +63,99 @@ func AcceptWechatEvent(ctx *context.Context) { ctx.XML(200, reply) } -// ValidEventSource -func ValidEventSource(ctx *context.Context) { - echostr := ctx.Query("echostr") - ctx.Write([]byte(echostr)) - return +func HandleEventSubscribe(ctx *context.Context, msg wechat.WechatMsg) { + r := wechat.HandleSubscribeEvent(msg) + if r == nil { + return + } + reply := buildReplyContent(msg, r) + ctx.XML(200, reply) +} + +func HandleTextMsg(ctx *context.Context, msg wechat.WechatMsg) { + r := wechat.GetAutomaticReply(msg.Content) + if r == nil { + log.Info("TextMsg reply is empty") + return + } + reply := buildReplyContent(msg, r) + ctx.XML(200, reply) +} + +func buildReplyContent(msg wechat.WechatMsg, r *wechat.WechatReplyContent) interface{} { + reply := &wechat.MsgReply{ + ToUserName: msg.FromUserName, + FromUserName: msg.ToUserName, + CreateTime: time.Now().Unix(), + MsgType: r.ReplyType, + } + switch r.ReplyType { + case wechat.ReplyTypeText: + return &wechat.TextMsgReply{ + ToUserName: msg.FromUserName, + FromUserName: msg.ToUserName, + CreateTime: time.Now().Unix(), + MsgType: r.ReplyType, + Content: r.Reply.Content, + } + + case wechat.ReplyTypeImage: + return &wechat.ImageMsgReply{ + ToUserName: msg.FromUserName, + FromUserName: msg.ToUserName, + CreateTime: time.Now().Unix(), + MsgType: r.ReplyType, + Image: wechat.ImageContent{ + MediaId: r.Reply.MediaId, + }, + } + case wechat.ReplyTypeVoice: + return &wechat.VoiceMsgReply{ + ToUserName: msg.FromUserName, + FromUserName: msg.ToUserName, + CreateTime: time.Now().Unix(), + MsgType: r.ReplyType, + Voice: wechat.VoiceContent{ + MediaId: r.Reply.MediaId, + }, + } + case wechat.ReplyTypeVideo: + return &wechat.VideoMsgReply{ + ToUserName: msg.FromUserName, + FromUserName: msg.ToUserName, + CreateTime: time.Now().Unix(), + MsgType: r.ReplyType, + Video: wechat.VideoContent{ + MediaId: r.Reply.MediaId, + Title: r.Reply.Title, + Description: r.Reply.Description, + }, + } + case wechat.ReplyTypeMusic: + return &wechat.MusicMsgReply{ + ToUserName: msg.FromUserName, + FromUserName: msg.ToUserName, + CreateTime: time.Now().Unix(), + MsgType: r.ReplyType, + Music: wechat.MusicContent{ + Title: r.Reply.Title, + Description: r.Reply.Description, + MusicUrl: r.Reply.MusicUrl, + HQMusicUrl: r.Reply.HQMusicUrl, + ThumbMediaId: r.Reply.ThumbMediaId, + }, + } + case wechat.ReplyTypeNews: + return &wechat.NewsMsgReply{ + ToUserName: msg.FromUserName, + FromUserName: msg.ToUserName, + CreateTime: time.Now().Unix(), + MsgType: r.ReplyType, + ArticleCount: len(r.Reply.Articles), + Articles: wechat.ArticleItem{ + Item: r.Reply.Articles}, + } + + } + return reply }