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)