浏览代码

change test user to 刘建明

master
Patrick Peng Sun 8 年前
父节点
当前提交
7ef84e0aba
共有 11 个文件被更改,包括 158 次插入180 次删除
  1. +36
    -53
      chatState.go
  2. +11
    -12
      chatState_test.go
  3. +3
    -4
      echoServer.go
  4. +1
    -1
      kfsend_test.go
  5. +19
    -91
      procedure.go
  6. +51
    -4
      serveCommand.go
  7. +4
    -7
      server.go
  8. +8
    -7
      sessionManager.go
  9. +1
    -1
      wechatApiError.go
  10. +24
    -0
      wechatMsg.go
  11. 二进制
      wechat_hitxy

+ 36
- 53
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

+ 11
- 12
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")

}

+ 3
- 4
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

+ 1
- 1
kfsend_test.go 查看文件

@@ -6,7 +6,7 @@ import (
"time"
)

var toUser = "oUN420bxqFqlx0ZQHciUOesZO3PE"
var toUser = "oUN420Wj78vnkNeAJY7RMPXA28oc" // "oUN420bxqFqlx0ZQHciUOesZO3PE"

func TestSendTxt(t *testing.T) {
SetupConfig()

+ 19
- 91
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
}

+ 51
- 4
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
}

+ 4
- 7
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

+ 8
- 7
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)

+ 1
- 1
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

+ 24
- 0
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
}

二进制
wechat_hitxy 查看文件


正在加载...
取消
保存