From 1c003dd990704c4c0a285d79142538a2069931cb Mon Sep 17 00:00:00 2001 From: Patrick Peng Sun Date: Sun, 28 May 2017 01:11:30 +1000 Subject: [PATCH] download file get correct suffix --- chatState.go | 97 +++++--------------------------- chatState_test.go | 24 +++----- common_test.go | 2 +- download.go | 13 ++++- kfsend_test.go | 13 +++-- outMsg.go | 2 +- procGetBasicUserInfo.go | 6 +- procedure.go | 104 +++++++++++++++++++++++++++++++++++ serveCommand.go | 60 ++++++++++++++++++++ server.go | 119 ++++++++++++++++++++++++---------------- 10 files changed, 280 insertions(+), 160 deletions(-) create mode 100644 procedure.go create mode 100644 serveCommand.go diff --git a/chatState.go b/chatState.go index b687651..4f1261c 100644 --- a/chatState.go +++ b/chatState.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "log" "os" - "time" ) //chat state that we might be use for collecting infomation from user @@ -14,31 +13,23 @@ type chatState struct { //back pointer style information, managed by general process OpenID string `json:"OpenID"` //whom is this belongs to Procedure string `json:"Procedure"` //which procedure this belongs to - Sent bool `json:"Sent"` //whether the message has been sent or not - Received bool `json:"Received"` //whether the expected message has been received or not - //below is managed by individual procedure + //below is managed by procedure only //real state information - Name string `json:"Name"` //state name - Expire int32 `json:"Expire"` //unix timestamp when this state expire - Send struct { //anything we need to send? - Type string `json:"Type"` //what type of message - Message map[string]string `json:"Message"` //the message to be sent,key value pair to describe the message - } `json:"Send"` - - Receive struct { //anything we expect to receive - Validator string `json:"Validator"` - Hint string `json:"Hint"` - Message map[string]string `json:"Message"` //the description for receiving message - } `json:"Receive"` + Name string `json:"Name"` //state name + Expire int32 `json:"Expire"` //unix timestamp when this state expire + //persistant sate Save map[string]string `json:"Save"` //the state save some data for later usage + + //runtime memory only + response string //被动回复消息的内容 } //for individual state func getCurrentState(openID string, procedure string) (result chatState, err error) { path := getProcedurePath(openID, procedure) - log.Printf("read state from %s\r\n", path) + //log.Printf("read state from %s\r\n", path) body, err := ioutil.ReadFile(path) if err != nil { //read file error if isFileExist(path) { @@ -51,9 +42,11 @@ func getCurrentState(openID string, procedure string) (result chatState, err err log.Printf("Session Content [path=%s] not correct: ", path) log.Println(err) } - //we don't check Expire, we give the caller full control on - //how to deal wiht expired session + err = sanityCheckState(openID, procedure, result) + return +} +func sanityCheckState(openID, procedure string, result chatState) (err error) { //check whether state is for the correct openID and procedure if result.OpenID != openID { err = errors.New("Error: State for " + openID + " is actually for " + result.OpenID) @@ -103,70 +96,6 @@ type ValidationResult struct { //Validator function type for validating all wechat inputs type Validator func(s chatState) ValidationResult -//start a procedure -func startProcedure(openID, procedure string) (err error) { - //init procedure state - init := getProcedureInit(openID, procedure) - if init == nil { - msg := "FATAL: cannot initialize procedure [" + procedure + "] " - err = errors.New(msg) - return - } - //init and get initial state - state := init(openID) - - //do the real concret work for processing the state - err = processProcedureState(state) - - return -} - -//resume a previous Procedure's state -func resumeProcedure(openID, procedure string) (err error) { - state, err := getCurrentState(openID, procedure) - if err != nil { - return - } - return processProcedureState(state) -} - -//finish a procedure, regardless its been finished or not -//normally not finished normally -func stopProcedure(openID, procedure string) { - path := getProcedurePath(openID, procedure) - os.Remove(path) - log.Println("Clearing [" + openID + "] @ [" + procedure + "]") -} - -func processProcedureState(state chatState) (err error) { - //send what we need to send - if isExpired(state.Expire) { - return errors.New("State has expired " + state.Name) - } - //mark we have sent. - - //do we need input? waiting for input +func saveChatState(state chatState) { - //if not, what is next state - - log.Println(state) - return -} - -type initProcedureFunction func(openid string) (initState chatState) - -func getProcedureInit(openID, procedure string) initProcedureFunction { - initFunc := map[string]initProcedureFunction{ - "TestDummy": nil, - "TestEcho": initTestEcho, - "GetBasicUserInfo": initGetBasicUserInfo, - "GetEmailAddr": initGetBasicUserInfo, - } - return initFunc[procedure] -} - -func initTestEcho(openid string) (r chatState) { - r.Name = openid - r.Expire = int32(time.Now().Unix() + 100) - return } diff --git a/chatState_test.go b/chatState_test.go index 2473d5e..78dc237 100644 --- a/chatState_test.go +++ b/chatState_test.go @@ -11,19 +11,11 @@ func TestChatState(t *testing.T) { s := chatState{} s.Name = "waiting for username" s.Expire = int32(time.Now().Unix() + 200) - s.Send.Message = map[string]string{ + s.Save = map[string]string{ "txt": "What is your date of birth?", "icon": "/mnt/data/abc.jpg", } - s.Send.Type = "text" - s.Sent = false - s.Received = false - s.Receive.Hint = "hint" - s.Receive.Validator = "validator email" - s.Receive.Message = map[string]string{ - "rtxt": "should be 3 chars at least", - "ricon": "icon path should be available", - } + s.response = "somexml less than 2018bytes" //save n, err := setCurrentState(openID, procedure, s) @@ -35,15 +27,13 @@ func TestChatState(t *testing.T) { //compare AssertEqual(t, m.Name, n.Name, "Name should be equal") AssertEqual(t, m.Expire, n.Expire, "Expire should be equal") - AssertEqual(t, m.Send.Type, n.Send.Type, "Send.Type should be equal") - AssertEqual(t, m.Received, false, "Receive.Received should be false") - AssertEqual(t, m.Sent, false, "Send.Sent should be false") + AssertEqual(t, m.Save["txt"], s.Save["txt"], "Message[txt] should be equal") + AssertEqual(t, m.Save["icon"], s.Save["icon"], "Message[icon] should be equal") AssertEqual(t, m.OpenID, openID, "openID should be "+openID) + AssertEqual(t, m.response, "", "response should be empty") AssertEqual(t, m.Procedure, procedure, "procedure should be "+procedure) - AssertEqual(t, m.Send.Message["txt"], n.Send.Message["txt"], "Message[txt] should be equal") - AssertEqual(t, m.Send.Message["icon"], n.Send.Message["icon"], "Message[icon] should be equal") - AssertEqual(t, m.Receive.Message["rtxt"], n.Receive.Message["rtxt"], "Message[rtxt] should be equal") - AssertEqual(t, m.Receive.Message["ricon"], n.Receive.Message["ricon"], "Message[ricon] should be equal") + AssertEqual(t, m.Save["txt"], n.Save["txt"], "Message[txt] should be equal") + AssertEqual(t, m.Save["icon"], n.Save["icon"], "Message[icon] should be equal") err = deleteChatState(openID, procedure) AssertEqual(t, err, nil, "delete chatState should be good") diff --git a/common_test.go b/common_test.go index 7834420..48de365 100644 --- a/common_test.go +++ b/common_test.go @@ -12,7 +12,7 @@ func SetupConfig() { "cmtWK2teRnLOXyO5dw7lJkETv9jCeNAqYyguEu5D8gG", "wx876e233fde456b7b", "4a91aa328569b10a9fb97adeb8b0af58", - "/tmp/wechat_hitxy_access_token", + "/tmp/wechat_hitxy_token", "gh_f09231355c68"} CRMConfig = EspoCRMAPIConfig{ diff --git a/download.go b/download.go index b3a2c7e..b8fa7b7 100644 --- a/download.go +++ b/download.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "log" + "mime" "net/http" "net/url" "os" @@ -80,8 +81,10 @@ func saveHTTPRequest(req *http.Request) (tmpFile string, contentType string, err log.Println(err) return "", contentType, err } - tmpFile = file.Name() + noSuffix := file.Name() file.Close() + tmpFile = noSuffix + contentType2Suffix(contentType) + os.Rename(noSuffix, tmpFile) //see if its a video url if len < 4096 && contentType == "text/plain" { @@ -104,3 +107,11 @@ func saveURLwithHTTPHeader(url string, headers map[string]string) (tmpFile strin } return saveHTTPRequest(req) } + +func contentType2Suffix(typ string) string { + exts, err := mime.ExtensionsByType(typ) + if err != nil { + return "" + } + return exts[0] +} diff --git a/kfsend_test.go b/kfsend_test.go index 469d719..bdbbd9e 100644 --- a/kfsend_test.go +++ b/kfsend_test.go @@ -31,12 +31,13 @@ func TestSendVoice(t *testing.T) { func TestSendVideo(t *testing.T) { SetupConfig() - //kfSendVideo(toUser, "media_for_test/video.mp4", "测试时品", "普通描述", "media_for_test/music-thumb.jpg") - kfSendVideoByMediaID(toUser, - "xwcgPCY8TRHP_PIy_4qunL8ad9mq7vD3hc9-OpNVRKG1qTwjKkQHN4GKb9mAcJ3J", - "视频测试标题", - "视频测试描述信息", - "6QKTfDxkQS2ACDzVhY0ddKjlIsBTyB6cf9fFWG88uwbJ0Mlh_gSIMxnaGvdqU4y0") + kfSendVideo(toUser, "media_for_test/video.mp4", "测试时品", "普通描述", "media_for_test/music-thumb.jpg") + + // kfSendVideoByMediaID(toUser, + // "xwcgPCY8TRHP_PIy_4qunL8ad9mq7vD3hc9-OpNVRKG1qTwjKkQHN4GKb9mAcJ3J", + // "视频测试标题", + // "视频测试描述信息", + // "6QKTfDxkQS2ACDzVhY0ddKjlIsBTyB6cf9fFWG88uwbJ0Mlh_gSIMxnaGvdqU4y0") } func TestSendMusic(t *testing.T) { diff --git a/outMsg.go b/outMsg.go index e3c520f..f12d052 100644 --- a/outMsg.go +++ b/outMsg.go @@ -18,7 +18,7 @@ type Article struct { } //BuildTextMsg Given a text message send it to wechat client -func BuildTextMsg(txt string, ToUserName string) (xml string, err error) { +func BuildTextMsg(ToUserName string, txt string) (xml string, err error) { if txt == "" || ToUserName == "" { err = errors.New("Empty text body or Empty destination") xml = "" diff --git a/procGetBasicUserInfo.go b/procGetBasicUserInfo.go index 483d97c..055c60a 100644 --- a/procGetBasicUserInfo.go +++ b/procGetBasicUserInfo.go @@ -21,15 +21,15 @@ func proc000AskName(openid string) { s.Name = "AskName" s.Expire = 300 //5 minutes s.Save = map[string]string{} //clear - s.Send.Message["q"] = "请输入您的真实中文名,没有请填写 ”无“ " - s.Receive.Validator = "validateChineseName" } func validateChineseName(s chatState) (r ValidationResult) { r.accept = true r.Error = "" - input := s.Receive.Message["name"] + //TODO + input := "abc" + // input := s.Receive.Message["name"] r.Hint = "通常中文名只有三个字或者四个字,比如 王更新,诸葛亮,司马相如,慕容白雪" if len(input) >= 10 { r.accept = false diff --git a/procedure.go b/procedure.go new file mode 100644 index 0000000..a57a940 --- /dev/null +++ b/procedure.go @@ -0,0 +1,104 @@ +package main + +import ( + "errors" + "log" + "os" + "time" +) + +//start a procedure +func startProcedure(openID, procedure string) (err error) { + //init procedure state + init := getProcedureInit(openID, procedure) + if init == nil { + msg := "FATAL: cannot initialize procedure [" + procedure + "] " + err = errors.New(msg) + return + } + //init and get initial state + state := init(openID) + + //save it + setCurrentState(openID, procedure, state) + + //next is to waiting for user's input + //which may or may not happen very soon + return +} + +//resume a previous Procedure's state +func resumeProcedure(openID, procedure string) (err error) { + state, err := getCurrentState(openID, procedure) + if err != nil { + return + } + //re-introduce what we are doing + // showProcIntro(openID, peocedure) + //tell user what has been achieved + // showProcSumary(openID, procedure) + + return processProcedureState(state) +} + +//finish a procedure, regardless its been finished or not +//normally not finished normally +func cleanProcedure(openID, procedure string) { + path := getProcedurePath(openID, procedure) + os.Remove(path) + log.Println("Clearing [" + openID + "] @ [" + procedure + "]") +} + +func processProcedureState(state chatState) (err error) { + //send what we need to send + if isExpired(state.Expire) { + return errors.New("State has expired " + state.Name) + } + //mark we have sent. + + //do we need input? waiting for input + + //if not, what is next state + + log.Println(state) + return +} + +func getProcedureInit(openID, procedure string) initProcFunc { + initFunc := map[string]initProcFunc{ + "TestDummy": nil, + "TestEcho": initTestEcho, + "GetBasicUserInfo": initGetBasicUserInfo, + "GetEmailAddr": initGetBasicUserInfo, + } + return initFunc[procedure] +} + +func initTestEcho(openid string) (r chatState) { + r.Name = openid + r.Expire = int32(time.Now().Unix() + 100) + return +} + +//are we inside a procedure, and not finished? +func isInProc(openID string) (result bool, state chatState) { + r, err := getCurrentSesssion(openID) + if err != nil { + return false, state + } + + if isExpired(r.Expire) { + return false, state + } + + state, err = getCurrentState(openID, r.Procedure) + if err != nil || isExpired(state.Expire) { + return false, state + } + return true, state +} + +//follow procedure, if there is any +func serveProc(openID string, input InWechatMsg) (next chatState) { + return +} diff --git a/serveCommand.go b/serveCommand.go new file mode 100644 index 0000000..5961b56 --- /dev/null +++ b/serveCommand.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "log" +) + +func serveCommand(openID string, in InWechatMsg) (state chatState, processed bool) { + log.Println("process command") + return echoCommand(openID, in) +} + +func echoCommand(openID string, in InWechatMsg) (state chatState, processed bool) { + processed = true + str, err := BuildTextMsg(openID, "default") + log.Println(in.header.MsgType) + switch in.body.(type) { + case TextMsg: + m := in.body.(TextMsg) + str, err = BuildTextMsg(openID, m.Content) + case PicMsg: + m := in.body.(PicMsg) + str = buildPicMsg(openID, m.MediaId) + + case VoiceMsg: + m := in.body.(VoiceMsg) + str = buildVoiceMsg(openID, m.MediaId) + kfSendTxt(openID, "翻译结果:"+m.Recognition) + case VideoMsg: + m := in.body.(VideoMsg) + str = buildVideoMsg(openID, "e2iNEiSxCX5TV1WbFd0TQMn5lilY3bylh1--lDBwi7I", "航拍春日哈工大", m.MediaId) + case ShortVideoMsg: + m := in.body.(ShortVideoMsg) + str = buildVideoMsg(openID, "e2iNEiSxCX5TV1WbFd0TQMn5lilY3bylh1--lDBwi7I", "航拍春日哈工大", m.MediaId) + case LocationMsg: + m := in.body.(LocationMsg) + str, _ = BuildTextMsg(openID, fmt.Sprintf("long=%f, lat=%f, scale=%d", m.Location_X, m.Location_Y, m.Scale)) + case EventMsg: + m := in.body.(EventMsg) + log.Println(m) + url := fmt.Sprintf("https://maps.googleapis.com/maps/api/staticmap?center=%f,%f&markers=color:red|label:S|%f,%f&zoom=12&size=414x736", m.Latitude, m.Longitude, m.Latitude, m.Longitude) + log.Println(url) + file, _, _ := saveURL(url) + str = buildUploadPicMsg(openID, file) + //str, _ = BuildTextMsg(openID, fmt.Sprintf("long=%f, lat=%f, scal =%f", m.Longitude, m.Latitude, m.Precision)) + default: + str, err = BuildTextMsg(openID, "text message") + } + + state.OpenID = openID + state.Procedure = "" + state.response = str + log.Println(str) + if err != nil { + log.Println("build response failed") + processed = false + } + //state is any state that + return +} diff --git a/server.go b/server.go index 0f89a2b..38bd15c 100644 --- a/server.go +++ b/server.go @@ -33,8 +33,9 @@ func apiV1Main(w http.ResponseWriter, r *http.Request) { case "GET": answerInitialAuth(w, r) default: - log.Fatalln(fmt.Sprintf("Unhandled HTTP %s", r.Method)) - fmt.Fprintf(w, "Protocol Error: Expect GET or POST only") + log.Println(fmt.Sprintf("FATAL: Unhandled HTTP %s", r.Method)) + response404Handler(w) + //fmt.Fprintf(w, "Protocol Error: Expect GET or POST only") } } @@ -53,57 +54,81 @@ func answerInitialAuth(w http.ResponseWriter, r *http.Request) { } } -//answerWechatPost distribute PostRequest according to xml body info // +//InWechatMsg what we received currently from wechat +type InWechatMsg struct { + header CommonHeader + body interface{} //dynamic type +} + +// +//answerWechatPost distribute PostRequest according to xml body info func answerWechatPost(w http.ResponseWriter, r *http.Request) { - body, _ := ioutil.ReadAll(r.Body) - d := decryptToXML(string(body)) - fmt.Printf("decrypt as: %s\n", d) - h := ReadCommonHeader(d) - reply, _ := BuildTextMsg(h.MsgType, h.FromUserName) - if h.MsgType == "text" { - - // url := "http://www.google.com.au/" - // first := "很高兴有你参加志愿者" - // remark := "明天给你发1万块钱" - // name := "利于修" - // staffID := "1235465" - // joinDate := time.Now().Format("2006-01-02 15:04:06 Mon MST -07") - // totalCount := "50次" - // totalTime := "2小时" - // log.Println("send kf msg") - // templateSendJoinVolunteer(h.FromUserName, url, first, remark, name, staffID, joinDate, totalCount, totalTime) - - reply, _ = BuildKFTransferAnyOneMsg(h.FromUserName) - //reply, _ = BuildKFTransferMsg(h.FromUserName, "kf2001@gh_f09231355c68") - //reply, _ = BuildTextMsg("test some link ", h.FromUserName) - } - if h.MsgType == "voice" { - a := ReadVoiceMsg(d) - reply, _ = BuildTextMsg(a.Recognition, h.FromUserName) - } - if h.MsgType == "event" { - a := ReadEventMsg(d) - if a.Event == "LOCATION" { - reply, _ = BuildTextMsg("test some link ", h.FromUserName) - fmt.Printf("output %s", reply) + in, valid := readWechatInput(r) + reply := "" //nothing + if !valid { + log.Println("Error: Invalid Input ") + } + + //are we in an existing procedure + openID := in.header.FromUserName + yes, state := isInProc(openID) + if yes { + state := serveProc(openID, in) + reply = state.response + } else { + state, processed := serveCommand(openID, in) //search or other command + if !processed { // transfer to Customer Service (kf) + reply = buildKfForwardMsg(openID, "") } else { - reply, _ = BuildTextMsg(a.Event+"/"+a.EventKey, h.FromUserName) + reply = state.response } - //mediaID := "cU8BYvAEp3H25V-yGO3WBtMVk2bZcEBgf_kje7V-EPkRA_U4x-OAWb_ONg6Y-Qxt" //video - //mediaID := "e2iNEiSxCX5TV1WbFd0TQPTdMx8WbvpkOs_iNhSVQHY" // 236 second mp3 - //mediaID := "e2iNEiSxCX5TV1WbFd0TQDVyk970GxTcBWMnqc2RzF0" //5 sec mp3 - //mediaID := "e2iNEiSxCX5TV1WbFd0TQMqvVrqFDbDOacdjgQ-OAuE" //news - //reply = buildVideoMsg(h.FromUserName, mediaID, "标题", a.Event+"/"+a.EventKey) - - //reply = buildUploadPicMsg(h.FromUserName, "media_for_test/640x480.jpg") - //reply = buildUploadVoiceMsg(h.FromUserName, "media_for_test/music.mp3") - //reply = buildVoiceMsg(h.FromUserName, mediaID) - reply = buildSampleMusicMsg(h.FromUserName) - //reply = buildSampleArticleMsg(h.FromUserName) } + log.Println(reply) w.Header().Set("Content-Type", "text/xml; charset=utf-8") fmt.Fprint(w, reply) + + saveChatState(state) + return +} + +func readWechatInput(r *http.Request) (result InWechatMsg, valid bool) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println(err) + valid = false + return + } + d := decryptToXML(string(body)) + if d == "" { + log.Println("Cannot decode Message : \r\n" + string(body)) + valid = false + return + } + valid = true + fmt.Printf("decrypt as: %s\n", d) + result.header = ReadCommonHeader(d) + switch result.header.MsgType { + case "text": + result.body = ReadTextMsg(d) + case "image": + result.body = ReadPicMsg(d) + case "voice": + result.body = ReadVoiceMsg(d) + case "video": + result.body = ReadVideoMsg(d) + case "shortvideo": + result.body = ReadShortVideoMsg(d) + case "location": + result.body = ReadLocationMsg(d) + case "link": + result.body = ReadLinkMsg(d) + case "event": + result.body = ReadEventMsg(d) + default: + log.Println("Fatal: unknown incoming message type" + result.header.MsgType) + valid = false + } return } @@ -113,7 +138,7 @@ func answerWechatPostEcho(w http.ResponseWriter, r *http.Request) { s := ReadEncryptedMsg(string(body)) //fmt.Printf("to decrypt %s", s.Encrypt) d := Decode(s.Encrypt) - fmt.Printf("echo as: \n%s", d) + fmt.Printf("echo as: \r\n%s", d) e := strings.Replace(d, "ToUserName", "FDDD20170506xyzunique", 2) f := strings.Replace(e, "FromUserName", "ToUserName", 2)