Explorar el Código

change test user to 刘建明

master
Patrick Peng Sun hace 8 años
padre
commit
7ef84e0aba
Se han modificado 11 ficheros con 158 adiciones y 180 borrados
  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. BIN
      wechat_hitxy

+ 36
- 53
chatState.go Ver fichero



import ( import (
"encoding/json" "encoding/json"
"errors"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"time"
) )


//chat state that we might be use for collecting infomation from user //chat state that we might be use for collecting infomation from user


//below is managed by procedure only //below is managed by procedure only
//real state information //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 //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 //runtime memory only
response string //被动回复消息的内容 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 //for individual state
func getCurrentState(openID string, procedure string) (result chatState, err error) { func getCurrentState(openID string, procedure string) (result chatState, err error) {
path := getProcedurePath(openID, procedure) path := getProcedurePath(openID, procedure)
//log.Printf("read state from %s\r\n", path)
body, err := ioutil.ReadFile(path) body, err := ioutil.ReadFile(path)
if err != nil { //read file error if err != nil { //read file error
if isFileExist(path) { if isFileExist(path) {
log.Printf("Session Content [path=%s] not correct: ", path) log.Printf("Session Content [path=%s] not correct: ", path)
log.Println(err) log.Println(err)
} }
err = sanityCheckState(openID, procedure, result)
result.OpenID = openID
result.Procedure = procedure
return 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 { if err != nil {
return return
} }
path := getProcedurePath(openID, procedure)
path := getProcedurePath(m.OpenID, m.Procedure)
err = ioutil.WriteFile(path, j, 0600) err = ioutil.WriteFile(path, j, 0600)
if err != nil { if err != nil {
log.Println("write state error" + path) log.Println("write state error" + path)
return 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 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 true
} }
return false return false

+ 11
- 12
chatState_test.go Ver fichero



import ( import (
"testing" "testing"
"time"
) )


func TestChatState(t *testing.T) { func TestChatState(t *testing.T) {
openID := "id" openID := "id"
procedure := "getUserBasicProfile" procedure := "getUserBasicProfile"
s := chatState{}
s := createEmptyState(openID, procedure, 100)
s.Name = "waiting for username" 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?", "txt": "What is your date of birth?",
"icon": "/mnt/data/abc.jpg", "icon": "/mnt/data/abc.jpg",
} }
s.response = "somexml less than 2018bytes"
s.response = "somexml less than 2048 bytes"


//save //save
err := setCurrentState(openID, procedure, s)
err := s.Save()
AssertEqual(t, err, nil, "save state should be successful") AssertEqual(t, err, nil, "save state should be successful")


//read out //read out
m, _ := getCurrentState(openID, procedure)
m := chatState{}
m.Load(openID, procedure)


//compare //compare
AssertEqual(t, m.Name, s.Name, "Name should be equal") AssertEqual(t, m.Name, s.Name, "Name should be equal")
AssertEqual(t, m.Expire, s.Expire, "Expire 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.OpenID, openID, "openID should be "+openID)
AssertEqual(t, m.response, "", "response should be empty") AssertEqual(t, m.response, "", "response should be empty")
AssertEqual(t, m.Procedure, procedure, "procedure should be "+procedure) 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") AssertEqual(t, err, nil, "delete chatState should be good")


} }

+ 3
- 4
echoServer.go Ver fichero

func echoCommand(openID string, in InWechatMsg) (state chatState, processed bool) { func echoCommand(openID string, in InWechatMsg) (state chatState, processed bool) {
processed = true processed = true
str, err := BuildTextMsg(openID, "default") str, err := BuildTextMsg(openID, "default")
log.Println(in.header.MsgType)
log.Println("echoCommand :" + in.header.MsgType)
switch in.body.(type) { switch in.body.(type) {
case TextMsg: case TextMsg:
m := in.body.(TextMsg) m := in.body.(TextMsg)
str, err = BuildTextMsg(openID, m.Content+"\n\n关键词 [转接] 将后续信息都转接到 客服")
str, err = BuildTextMsg(openID, m.Content+"\n\n关键词 [转接] 将后续信息都转接到 客服 测试版")
if m.Content == "转接" { if m.Content == "转接" {
processed = false processed = false
} }

case PicMsg: case PicMsg:
m := in.body.(PicMsg) m := in.body.(PicMsg)
str = buildPicMsg(openID, m.MediaId) str = buildPicMsg(openID, m.MediaId)
state.OpenID = openID state.OpenID = openID
state.Procedure = "" state.Procedure = ""
state.response = str state.response = str
log.Println(str)
//log.Println(str)
if err != nil { if err != nil {
log.Println("build response failed") log.Println("build response failed")
processed = false processed = false

+ 1
- 1
kfsend_test.go Ver fichero

"time" "time"
) )


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


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

+ 19
- 91
procedure.go Ver fichero

package main package main


import ( import (
"errors"
"log"
"os" "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 return
} }

+ 51
- 4
serveCommand.go Ver fichero

package main package main


import ( 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 Ver fichero

} }
} }


//
//InWechatMsg what we received currently from wechat
type InWechatMsg struct {
header CommonHeader
body interface{} //dynamic type
}

// //
//answerWechatPost distribute PostRequest according to xml body info //answerWechatPost distribute PostRequest according to xml body info
func answerWechatPost(w http.ResponseWriter, r *http.Request) { func answerWechatPost(w http.ResponseWriter, r *http.Request) {
in, valid := readWechatInput(r) in, valid := readWechatInput(r)
in.instantResponse = make(chan string)
reply := "" //nothing reply := "" //nothing
w.Header().Set("Content-Type", "text/xml; charset=utf-8") w.Header().Set("Content-Type", "text/xml; charset=utf-8")


AllInMessage <- in AllInMessage <- in
} }


//read instant response
reply = <-in.instantResponse
close(in.instantResponse)
//uncomment the followin two lines to enable echo test //uncomment the followin two lines to enable echo test
// state, _ := echoCommand(in.header.FromUserName, in) // state, _ := echoCommand(in.header.FromUserName, in)
// reply = state.response // reply = state.response

+ 8
- 7
sessionManager.go Ver fichero

openID string //who is this? openID string //who is this?
count int // number of message in the Queue, channel count int // number of message in the Queue, channel
jobs chan InWechatMsg 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 { type workDone struct {
s.openID = openID s.openID = openID
s.count = 0 s.count = 0
s.jobs = make(chan InWechatMsg, 50) 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 return s
} }


if found { if found {
s.count -= d.consumed s.count -= d.consumed
if s.count == 0 { //no job to do if s.count == 0 { //no job to do
//save session data to disk
data := m.sessions[d.openID].data
data.Save()
//remove from memory //remove from memory
m.destroySession(d.openID) m.destroySession(d.openID)
log.Printf("destroy session %s", d.openID) log.Printf("destroy session %s", d.openID)
} }


func (m *SessionManager) destroySession(openID string) { func (m *SessionManager) destroySession(openID string) {
//save session data to disk
data := m.sessions[openID].data
data.Save()

//close job channels //close job channels
s := m.sessions[openID] s := m.sessions[openID]
close(s.jobs) close(s.jobs)

+ 1
- 1
wechatApiError.go Ver fichero

return false return false
} }


//To Do: test error message @todo
//ToDo: test error message @todo

+ 24
- 0
wechatMsg.go Ver fichero

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
}

BIN
wechat_hitxy Ver fichero


Cargando…
Cancelar
Guardar