diff --git a/chatState.go b/chatState.go index 1ff70a4..0ef1dde 100644 --- a/chatState.go +++ b/chatState.go @@ -2,10 +2,10 @@ package main import ( "encoding/json" - "errors" "io/ioutil" "log" "os" + "time" ) //chat state that we might be use for collecting infomation from user @@ -16,20 +16,37 @@ type chatState struct { //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 + Name string `json:"Name"` //state name + CreateAt int32 `json:"CreateAt"` //unix timestamp , when its created + 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 + Data map[string]string `json:"Data"` //the state save some data for later usage //runtime memory only response string //被动回复消息的内容 } +func createEmptyState(openID, procedure string, expireInSeconds int32) chatState { + r := chatState{} + r.Name = "" + r.OpenID = openID + r.Procedure = procedure + r.CreateAt = int32(time.Now().Unix()) + r.Expire = r.CreateAt + expireInSeconds + r.Data = map[string]string{} + return r +} + +func (m *chatState) Load(openID, procedure string) (result chatState, err error) { + result, err = getCurrentState(openID, procedure) + *m = result + return +} + //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) body, err := ioutil.ReadFile(path) if err != nil { //read file error if isFileExist(path) { @@ -42,33 +59,21 @@ func getCurrentState(openID string, procedure string) (result chatState, err err log.Printf("Session Content [path=%s] not correct: ", path) log.Println(err) } - err = sanityCheckState(openID, procedure, result) + result.OpenID = openID + result.Procedure = procedure 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) - return +func (m *chatState) Save() (err error) { + if m.isEndingState() { + return m.Delete() } - - if result.Procedure != procedure { - err = errors.New("Error: Proecdure for " + procedure + " is actually for " + result.Procedure) - return - } - return -} - -func setCurrentState(openID, procedure string, state chatState) (err error) { - state.OpenID = openID - state.Procedure = procedure - - j, err := json.Marshal(state) + //save to disk + j, err := json.Marshal(*m) if err != nil { return } - path := getProcedurePath(openID, procedure) + path := getProcedurePath(m.OpenID, m.Procedure) err = ioutil.WriteFile(path, j, 0600) if err != nil { log.Println("write state error" + path) @@ -78,38 +83,16 @@ func setCurrentState(openID, procedure string, state chatState) (err error) { return } -func deleteChatState(openID, procedure string) (err error) { - path := getProcedurePath(openID, procedure) - err = os.Remove(path) - return -} - -//ValidationResult After input validation, what is the result -type ValidationResult struct { - accept bool - Hint string - Error string - Warning string -} - -//Validator function type for validating all wechat inputs -type Validator func(s chatState) ValidationResult - -func saveChatState(openID, procedure string, state chatState) (err error) { - if isExpired(state.Expire) { - //skip saving sate - return +func (m *chatState) Delete() (err error) { + path := getProcedurePath(m.OpenID, m.Procedure) + if isFileExist(path) { + err = os.Remove(path) } - err = setCurrentState(openID, procedure, state) return } -func isEndingState(state chatState) bool { - if isExpired(state.Expire) || state.Name == "" { - return true - } - - if state.Name == "delete" { +func (m *chatState) isEndingState() bool { + if isExpired(m.Expire) || m.Name == "" { return true } return false diff --git a/chatState_test.go b/chatState_test.go index 1708826..d2dc469 100644 --- a/chatState_test.go +++ b/chatState_test.go @@ -2,40 +2,39 @@ package main import ( "testing" - "time" ) func TestChatState(t *testing.T) { openID := "id" procedure := "getUserBasicProfile" - s := chatState{} + s := createEmptyState(openID, procedure, 100) s.Name = "waiting for username" - s.Expire = int32(time.Now().Unix() + 200) - s.Save = map[string]string{ + s.Data = map[string]string{ "txt": "What is your date of birth?", "icon": "/mnt/data/abc.jpg", } - s.response = "somexml less than 2018bytes" + s.response = "somexml less than 2048 bytes" //save - err := setCurrentState(openID, procedure, s) + err := s.Save() AssertEqual(t, err, nil, "save state should be successful") //read out - m, _ := getCurrentState(openID, procedure) + m := chatState{} + m.Load(openID, procedure) //compare AssertEqual(t, m.Name, s.Name, "Name should be equal") AssertEqual(t, m.Expire, s.Expire, "Expire should be equal") - 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.Data["txt"], s.Data["txt"], "Message[txt] should be equal") + AssertEqual(t, m.Data["icon"], s.Data["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.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.Data["txt"], s.Data["txt"], "Message[txt] should be equal") + AssertEqual(t, m.Data["icon"], s.Data["icon"], "Message[icon] should be equal") - err = deleteChatState(openID, procedure) + err = s.Delete() AssertEqual(t, err, nil, "delete chatState should be good") } diff --git a/echoServer.go b/echoServer.go index 0337a24..411342b 100644 --- a/echoServer.go +++ b/echoServer.go @@ -9,15 +9,14 @@ import ( func echoCommand(openID string, in InWechatMsg) (state chatState, processed bool) { processed = true str, err := BuildTextMsg(openID, "default") - log.Println(in.header.MsgType) + log.Println("echoCommand :" + in.header.MsgType) switch in.body.(type) { case TextMsg: m := in.body.(TextMsg) - str, err = BuildTextMsg(openID, m.Content+"\n\n关键词 [转接] 将后续信息都转接到 客服") + str, err = BuildTextMsg(openID, m.Content+"\n\n关键词 [转接] 将后续信息都转接到 客服 测试版") if m.Content == "转接" { processed = false } - case PicMsg: m := in.body.(PicMsg) str = buildPicMsg(openID, m.MediaId) @@ -53,7 +52,7 @@ func echoCommand(openID string, in InWechatMsg) (state chatState, processed bool state.OpenID = openID state.Procedure = "" state.response = str - log.Println(str) + //log.Println(str) if err != nil { log.Println("build response failed") processed = false diff --git a/kfsend_test.go b/kfsend_test.go index 8923a2f..a6bc208 100644 --- a/kfsend_test.go +++ b/kfsend_test.go @@ -6,7 +6,7 @@ import ( "time" ) -var toUser = "oUN420bxqFqlx0ZQHciUOesZO3PE" +var toUser = "oUN420Wj78vnkNeAJY7RMPXA28oc" // "oUN420bxqFqlx0ZQHciUOesZO3PE" func TestSendTxt(t *testing.T) { SetupConfig() diff --git a/procedure.go b/procedure.go index 1d93a92..722b4bf 100644 --- a/procedure.go +++ b/procedure.go @@ -1,106 +1,34 @@ 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) +// a description of +type chatProcedure struct { + init func() //house keeping + clean func() //house keeping - //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 + "]") - - //TODO: save session to "" 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 + start func(*openIDSessionData, InWechatMsg) //for first message + serve func(*openIDSessionData, InWechatMsg) //for all subsequent message + summary func() //after all message has been done + intro func() //initial text/video/voice introduction } -//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 - } +//AllProc all procedure that we implemented +var AllProc = map[string]chatProcedure{} - if isExpired(r.Expire) { - return false, state - } +func initAllProc() { + AllProc["Dummy"] = procDummy + //Simple Echo Proc + AllProc["Echo"] = procEcho + //Get Basic UserInfo + AllProc["GetUserBasicInfo"] = procGetBasicUserInfo - 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(state chatState, input InWechatMsg) (next chatState) { +func getProcedurePath(openID, ProcedureName string) (path string) { + path = procedureDir + string(os.PathSeparator) + ProcedureName + string(os.PathSeparator) + openID + ".json" + ensurePathExist(path) return } diff --git a/serveCommand.go b/serveCommand.go index d5f4c5c..788c34e 100644 --- a/serveCommand.go +++ b/serveCommand.go @@ -1,10 +1,57 @@ package main import ( - "log" + "fmt" + "time" ) -func serveCommand(openID string, in InWechatMsg) (state chatState, processed bool) { - log.Println("process command") - return echoCommand(openID, in) +type commandFunction func(*openIDSessionData, InWechatMsg) bool + +var commandMap = map[string]commandFunction{ + "版本信息": cmdVersion, + "version": cmdVersion, + //"所有命令": allCommand, //include it will cause initialization loop + "echo": cmdEcho, + "回音": cmdEcho, +} + +func (ss *openIDSessionData) serveCommand(in InWechatMsg) (processed bool) { + if in.header.MsgType == "text" { + cmd := in.body.(TextMsg).Content + if f, hasFunction := commandMap[cmd]; hasFunction { + return f(ss, in) + } + if cmd == "所有命令" || cmd == "all command" { + return allCommand(ss, in) + } + } + processed = false + return +} + +func allCommand(ss *openIDSessionData, in InWechatMsg) (processed bool) { + processed = true + msg := "命令如下:\n" + count := 0 + for k := range commandMap { + count++ + msg = msg + fmt.Sprintf("%0d : %s \n", count, k) + } + + str, _ := BuildTextMsg(in.header.FromUserName, msg) + in.immediateResponse(str) + return +} + +func cmdVersion(ss *openIDSessionData, in InWechatMsg) (processed bool) { + processed = true + str, _ := BuildTextMsg(in.header.FromUserName, "这是测试版本"+time.Now().Format("2006/01/02 03:04:05")) + in.immediateResponse(str) + return +} + +func cmdEcho(ss *openIDSessionData, in InWechatMsg) (processed bool) { + procEcho.init() + procEcho.start(ss, in) + return } diff --git a/server.go b/server.go index 0c74275..1ffdc4c 100644 --- a/server.go +++ b/server.go @@ -54,17 +54,11 @@ func answerInitialAuth(w http.ResponseWriter, r *http.Request) { } } -// -//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) { in, valid := readWechatInput(r) + in.instantResponse = make(chan string) reply := "" //nothing w.Header().Set("Content-Type", "text/xml; charset=utf-8") @@ -75,6 +69,9 @@ func answerWechatPost(w http.ResponseWriter, r *http.Request) { AllInMessage <- in } + //read instant response + reply = <-in.instantResponse + close(in.instantResponse) //uncomment the followin two lines to enable echo test // state, _ := echoCommand(in.header.FromUserName, in) // reply = state.response diff --git a/sessionManager.go b/sessionManager.go index 933b820..60274d5 100644 --- a/sessionManager.go +++ b/sessionManager.go @@ -10,8 +10,7 @@ type openIDSession struct { openID string //who is this? count int // number of message in the Queue, channel jobs chan InWechatMsg - data openIDSessionData //session data, that needs to be saved to disk - states map[string]chatState //map from procedure to its current states + data openIDSessionData //session data, that needs to be saved to disk } type workDone struct { @@ -81,8 +80,9 @@ func (m *SessionManager) createSession(openID string) openIDSession { s.openID = openID s.count = 0 s.jobs = make(chan InWechatMsg, 50) - s.data, _ = getCurrentSesssion(openID) //either load or create new - m.sessions[openID] = s //register it to memory + s.data = openIDSessionData{} + s.data.Load(openID) //either load or create new + m.sessions[openID] = s //register it to memory return s } @@ -92,9 +92,6 @@ func (m *SessionManager) clearJobDone(d workDone) { if found { s.count -= d.consumed if s.count == 0 { //no job to do - //save session data to disk - data := m.sessions[d.openID].data - data.Save() //remove from memory m.destroySession(d.openID) log.Printf("destroy session %s", d.openID) @@ -111,6 +108,10 @@ func (m *SessionManager) clearJobDone(d workDone) { } func (m *SessionManager) destroySession(openID string) { + //save session data to disk + data := m.sessions[openID].data + data.Save() + //close job channels s := m.sessions[openID] close(s.jobs) diff --git a/wechatApiError.go b/wechatApiError.go index 1173cd6..d939854 100644 --- a/wechatApiError.go +++ b/wechatApiError.go @@ -36,4 +36,4 @@ func isJSON(r *http.Response) bool { return false } -//To Do: test error message @todo +//ToDo: test error message @todo diff --git a/wechatMsg.go b/wechatMsg.go new file mode 100644 index 0000000..789160e --- /dev/null +++ b/wechatMsg.go @@ -0,0 +1,24 @@ +package main + +import "net/http" + +// +//InWechatMsg what we received currently from wechat +type InWechatMsg struct { + header CommonHeader + body interface{} //dynamic type + req *http.Request + instantResponse chan string //instance reply channel +} + +func (m *InWechatMsg) init() { + m.instantResponse = make(chan string) +} + +func (m *InWechatMsg) destroy() { + close(m.instantResponse) +} + +func (m *InWechatMsg) immediateResponse(s string) { + m.instantResponse <- s +} diff --git a/wechat_hitxy b/wechat_hitxy new file mode 100755 index 0000000..9f49bae Binary files /dev/null and b/wechat_hitxy differ