| @@ -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,110 @@ | |||
| package wechat | |||
| import ( | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "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 AutomaticResponseContent 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 | |||
| } | |||
| func GetAutomaticReply(msg string) *AutomaticResponseContent { | |||
| r, err := LoadAutomaticReplyFromCacheAndDisk() | |||
| 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 loadAutomaticReplyFromDisk() ([]*AutomaticResponseContent, error) { | |||
| log.Debug("LoadAutomaticResponseMap from disk") | |||
| repo, err := models.GetRepositoryByOwnerAndAlias("OpenIOSSG", "promote") | |||
| if err != nil { | |||
| log.Error("get notice repo failed, error=%v", err) | |||
| return nil, err | |||
| } | |||
| repoFile, err := models.ReadLatestFileInRepo("OpenIOSSG", repo.Name, "master", "wechat/auto_reply.json") | |||
| if err != nil { | |||
| log.Error("GetNewestNotice failed, error=%v", err) | |||
| return nil, err | |||
| } | |||
| res := make([]*AutomaticResponseContent, 0) | |||
| json.Unmarshal(repoFile.Content, &res) | |||
| if res == nil || len(res) == 0 { | |||
| return nil, err | |||
| } | |||
| return res, nil | |||
| } | |||
| func LoadAutomaticReplyFromCacheAndDisk() ([]*AutomaticResponseContent, error) { | |||
| v, success := WechatReplyCache.Get(WECHAT_REPLY_CACHE_KEY) | |||
| if success { | |||
| log.Debug("LoadAutomaticResponse from cache,value = %v", v) | |||
| if v == nil { | |||
| return nil, nil | |||
| } | |||
| n := v.([]*AutomaticResponseContent) | |||
| return n, nil | |||
| } | |||
| content, err := loadAutomaticReplyFromDisk() | |||
| if err != nil { | |||
| log.Error("GetNewestNotice failed, error=%v", err) | |||
| WechatReplyCache.Set(WECHAT_REPLY_CACHE_KEY, nil, 30*time.Second) | |||
| return nil, err | |||
| } | |||
| WechatReplyCache.Set(WECHAT_REPLY_CACHE_KEY, content, 60*time.Second) | |||
| return content, nil | |||
| } | |||
| @@ -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,22 @@ type WechatEvent struct { | |||
| Event string | |||
| EventKey string | |||
| Ticket string | |||
| Content string | |||
| MsgId string | |||
| MsgDataId string | |||
| Idx string | |||
| } | |||
| type MsgReply struct { | |||
| XMLName xml.Name `xml:"xml"` | |||
| ToUserName string | |||
| FromUserName string | |||
| CreateTime int64 | |||
| MsgType string | |||
| Content string | |||
| } | |||
| type EventReply struct { | |||
| type TextMsgReply struct { | |||
| XMLName xml.Name `xml:"xml"` | |||
| ToUserName string | |||
| FromUserName string | |||
| @@ -36,6 +49,71 @@ type EventReply struct { | |||
| 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 ArticlesMsgReply struct { | |||
| XMLName xml.Name `xml:"xml"` | |||
| ToUserName string | |||
| FromUserName string | |||
| CreateTime int64 | |||
| MsgType string | |||
| Articles []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 { | |||
| Title string | |||
| Description string | |||
| PicUrl string | |||
| Url string | |||
| } | |||
| const ( | |||
| WECHAT_EVENT_SUBSCRIBE = "subscribe" | |||
| @@ -43,10 +121,11 @@ const ( | |||
| ) | |||
| const ( | |||
| WECHAT_MSG_TYPE_TEXT = "text" | |||
| WECHAT_MSG_TYPE_TEXT = "text" | |||
| WECHAT_MSG_TYPE_EVENT = "event" | |||
| ) | |||
| func HandleSubscribeEvent(we WechatEvent) string { | |||
| func HandleSubscribeEvent(we WechatMsg) string { | |||
| eventKey := we.EventKey | |||
| if eventKey == "" { | |||
| return "" | |||
| @@ -14,14 +14,30 @@ 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) | |||
| } | |||
| // ValidEventSource | |||
| func ValidEventSource(ctx *context.Context) { | |||
| echostr := ctx.Query("echostr") | |||
| ctx.Write([]byte(echostr)) | |||
| return | |||
| } | |||
| func HandleEventMsg(ctx *context.Context, msg wechat.WechatMsg) { | |||
| var replyStr string | |||
| switch we.Event { | |||
| switch msg.Event { | |||
| case wechat.WECHAT_EVENT_SUBSCRIBE, wechat.WECHAT_EVENT_SCAN: | |||
| replyStr = wechat.HandleSubscribeEvent(we) | |||
| replyStr = wechat.HandleSubscribeEvent(msg) | |||
| break | |||
| } | |||
| @@ -29,9 +45,9 @@ func AcceptWechatEvent(ctx *context.Context) { | |||
| 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 +55,79 @@ 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 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.AutomaticResponseContent) 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, | |||
| }, | |||
| } | |||
| } | |||
| return reply | |||
| } | |||