| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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) | |||
| @@ -18,7 +18,7 @@ import ( | |||
| // <EventKey><![CDATA[SCENE_VALUE]]></EventKey> | |||
| // <Ticket><![CDATA[TICKET]]></Ticket> | |||
| //</xml> | |||
| 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] | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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() | |||
| @@ -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()) | |||
| } | |||
| @@ -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)) | |||
| } | |||
| @@ -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 | |||
| } | |||