Ver código fonte

download file get correct suffix

master
Patrick Peng Sun 8 anos atrás
pai
commit
1c003dd990
10 arquivos alterados com 280 adições e 160 exclusões
  1. +13
    -84
      chatState.go
  2. +7
    -17
      chatState_test.go
  3. +1
    -1
      common_test.go
  4. +12
    -1
      download.go
  5. +7
    -6
      kfsend_test.go
  6. +1
    -1
      outMsg.go
  7. +3
    -3
      procGetBasicUserInfo.go
  8. +104
    -0
      procedure.go
  9. +60
    -0
      serveCommand.go
  10. +72
    -47
      server.go

+ 13
- 84
chatState.go Ver arquivo

"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
//back pointer style information, managed by general process //back pointer style information, managed by general process
OpenID string `json:"OpenID"` //whom is this belongs to OpenID string `json:"OpenID"` //whom is this belongs to
Procedure string `json:"Procedure"` //which procedure 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 //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 Save map[string]string `json:"Save"` //the state save some data for later usage

//runtime memory only
response string //被动回复消息的内容
} }


//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)
//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)
} }
//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 //check whether state is for the correct openID and procedure
if result.OpenID != openID { if result.OpenID != openID {
err = errors.New("Error: State for " + openID + " is actually for " + result.OpenID) err = errors.New("Error: State for " + openID + " is actually for " + result.OpenID)
//Validator function type for validating all wechat inputs //Validator function type for validating all wechat inputs
type Validator func(s chatState) ValidationResult 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
} }

+ 7
- 17
chatState_test.go Ver arquivo

s := chatState{} s := chatState{}
s.Name = "waiting for username" s.Name = "waiting for username"
s.Expire = int32(time.Now().Unix() + 200) 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?", "txt": "What is your date of birth?",
"icon": "/mnt/data/abc.jpg", "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 //save
n, err := setCurrentState(openID, procedure, s) n, err := setCurrentState(openID, procedure, s)
//compare //compare
AssertEqual(t, m.Name, n.Name, "Name should be equal") AssertEqual(t, m.Name, n.Name, "Name should be equal")
AssertEqual(t, m.Expire, n.Expire, "Expire 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.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.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) err = deleteChatState(openID, procedure)
AssertEqual(t, err, nil, "delete chatState should be good") AssertEqual(t, err, nil, "delete chatState should be good")

+ 1
- 1
common_test.go Ver arquivo

"cmtWK2teRnLOXyO5dw7lJkETv9jCeNAqYyguEu5D8gG", "cmtWK2teRnLOXyO5dw7lJkETv9jCeNAqYyguEu5D8gG",
"wx876e233fde456b7b", "wx876e233fde456b7b",
"4a91aa328569b10a9fb97adeb8b0af58", "4a91aa328569b10a9fb97adeb8b0af58",
"/tmp/wechat_hitxy_access_token",
"/tmp/wechat_hitxy_token",
"gh_f09231355c68"} "gh_f09231355c68"}


CRMConfig = EspoCRMAPIConfig{ CRMConfig = EspoCRMAPIConfig{

+ 12
- 1
download.go Ver arquivo

"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"mime"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
log.Println(err) log.Println(err)
return "", contentType, err return "", contentType, err
} }
tmpFile = file.Name()
noSuffix := file.Name()
file.Close() file.Close()
tmpFile = noSuffix + contentType2Suffix(contentType)
os.Rename(noSuffix, tmpFile)


//see if its a video url //see if its a video url
if len < 4096 && contentType == "text/plain" { if len < 4096 && contentType == "text/plain" {
} }
return saveHTTPRequest(req) return saveHTTPRequest(req)
} }

func contentType2Suffix(typ string) string {
exts, err := mime.ExtensionsByType(typ)
if err != nil {
return ""
}
return exts[0]
}

+ 7
- 6
kfsend_test.go Ver arquivo



func TestSendVideo(t *testing.T) { func TestSendVideo(t *testing.T) {
SetupConfig() 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) { func TestSendMusic(t *testing.T) {

+ 1
- 1
outMsg.go Ver arquivo

} }


//BuildTextMsg Given a text message send it to wechat client //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 == "" { if txt == "" || ToUserName == "" {
err = errors.New("Empty text body or Empty destination") err = errors.New("Empty text body or Empty destination")
xml = "" xml = ""

+ 3
- 3
procGetBasicUserInfo.go Ver arquivo

s.Name = "AskName" s.Name = "AskName"
s.Expire = 300 //5 minutes s.Expire = 300 //5 minutes
s.Save = map[string]string{} //clear s.Save = map[string]string{} //clear
s.Send.Message["q"] = "请输入您的真实中文名,没有请填写 ”无“ "
s.Receive.Validator = "validateChineseName"
} }


func validateChineseName(s chatState) (r ValidationResult) { func validateChineseName(s chatState) (r ValidationResult) {
r.accept = true r.accept = true
r.Error = "" r.Error = ""


input := s.Receive.Message["name"]
//TODO
input := "abc"
// input := s.Receive.Message["name"]
r.Hint = "通常中文名只有三个字或者四个字,比如 王更新,诸葛亮,司马相如,慕容白雪" r.Hint = "通常中文名只有三个字或者四个字,比如 王更新,诸葛亮,司马相如,慕容白雪"
if len(input) >= 10 { if len(input) >= 10 {
r.accept = false r.accept = false

+ 104
- 0
procedure.go Ver arquivo

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
}

+ 60
- 0
serveCommand.go Ver arquivo

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
}

+ 72
- 47
server.go Ver arquivo

case "GET": case "GET":
answerInitialAuth(w, r) answerInitialAuth(w, r)
default: 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")
} }
} }


} }
} }


//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) { 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 <a href=http://www.hitxy.org.au/> some link </a>", 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 <a href=http://www.hitxy.org.au/> some link </a>", 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 { } 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") w.Header().Set("Content-Type", "text/xml; charset=utf-8")
fmt.Fprint(w, reply) 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 return
} }


s := ReadEncryptedMsg(string(body)) s := ReadEncryptedMsg(string(body))
//fmt.Printf("to decrypt %s", s.Encrypt) //fmt.Printf("to decrypt %s", s.Encrypt)
d := Decode(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) e := strings.Replace(d, "ToUserName", "FDDD20170506xyzunique", 2)
f := strings.Replace(e, "FromUserName", "ToUserName", 2) f := strings.Replace(e, "FromUserName", "ToUserName", 2)

Carregando…
Cancelar
Salvar