├── .gitignore ├── .travis.yml ├── AI.go ├── AI_test.go ├── LICENSE ├── README.md ├── bot.go ├── config.go ├── config.json ├── config_test.go ├── conn ├── conn_test.go ├── pool.go └── redis.go ├── main.go ├── script └── build.sh ├── source.go ├── util.go ├── util_test.go └── web ├── annyang.js ├── favicon.ico ├── github.png ├── index.html ├── jquery-1.11.0.min.js ├── samaritan.js ├── screenfull.min.js ├── style.css └── talk.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store/ 2 | .idea/ 3 | bot 4 | server 5 | robot 6 | web/web 7 | test.go -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.6 4 | - 1.7 5 | - 1.8 6 | 7 | script: 8 | - go test -v ./ 9 | -------------------------------------------------------------------------------- /AI.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | tlKey = "a5052a22b8232be1e387ff153e823975" 15 | ) 16 | 17 | //get reply from tlAI 18 | func tlAI(info string) string { 19 | 20 | tuLingURL := fmt.Sprintf("http://www.tuling123.com/openapi/api?key=%s&info=%s", tlKey, url.QueryEscape(info)) 21 | resp, err := http.Get(tuLingURL) 22 | if err != nil { 23 | log.Println(err) 24 | return "" 25 | } 26 | defer resp.Body.Close() 27 | reply := new(tlReply) 28 | decoder := json.NewDecoder(resp.Body) 29 | decoder.Decode(reply) 30 | log.Printf("reply from tuling machine: %s", reply.Text+"\n"+reply.URL) 31 | wl := []string{"", "", "
", "\n"} 32 | srp := strings.NewReplacer(wl...) 33 | ret := srp.Replace(reply.Text + "\n" + reply.URL) 34 | return ret 35 | } 36 | 37 | type tlReply struct { 38 | code int 39 | URL string `json:"url,omitempty"` 40 | Text string `json:"text"` 41 | } 42 | 43 | //get reply from qinAI 44 | func qinAI(info string) string { 45 | //info = strings.Replace(info, " ", "+", -1) 46 | qinURL := fmt.Sprintf("http://api.qingyunke.com/api.php?key=free&appid=0&msg=%s", url.QueryEscape(info)) 47 | timeout := time.Duration(2 * time.Second) 48 | client := http.Client{ 49 | Timeout: timeout, 50 | } 51 | resp, err := client.Get(qinURL) 52 | //resp, err := http.Get(qinURL) 53 | if err != nil { 54 | return "" 55 | } 56 | defer resp.Body.Close() 57 | reply := new(qinReply) 58 | decoder := json.NewDecoder(resp.Body) 59 | decoder.Decode(reply) 60 | log.Printf("reply from qingyunke machine: %s", reply.Content) 61 | wl := []string{"{br}", "\n", "菲菲", "Jarvis"} 62 | srp := strings.NewReplacer(wl...) 63 | ret := srp.Replace(reply.Content) 64 | return ret 65 | } 66 | 67 | type qinReply struct { 68 | result int 69 | Content string `json:"content"` 70 | } 71 | 72 | //get reply from mitAI 73 | func mitAI(info string) string { 74 | //todo fix mitAI 75 | return tlAI(info) 76 | mitURL := "https://demo.pandorabots.com/atalk/mitsuku/mitsukudemo" 77 | resp, err := http.PostForm(mitURL, url.Values{"input": {info}, "user_key": {"pb3568993377180953528873199695415106305"}}) 78 | if err != nil { 79 | log.Printf(err.Error()) 80 | return "" 81 | } 82 | defer resp.Body.Close() 83 | reply := new(mitReply) 84 | decoder := json.NewDecoder(resp.Body) 85 | err = decoder.Decode(reply) 86 | if err != nil { 87 | return "" 88 | } 89 | log.Printf("reply from mit machine: %s", reply.Responses[0]) 90 | return reply.Responses[0] 91 | } 92 | 93 | type mitReply struct { 94 | Responses []string `json:"responses"` 95 | } 96 | 97 | //get reply from iceAI 98 | func iceAI(info string) string { 99 | iceURL := fmt.Sprintf("http://127.0.0.1:8008/openxiaoice/ask?q=%s", url.QueryEscape(info)) 100 | timeout := time.Duration(4 * time.Second) 101 | client := http.Client{ 102 | Timeout: timeout, 103 | } 104 | resp, err := client.Get(iceURL) 105 | if err != nil { 106 | return "" 107 | } 108 | defer resp.Body.Close() 109 | reply := new(iceReply) 110 | decoder := json.NewDecoder(resp.Body) 111 | decoder.Decode(reply) 112 | log.Printf("reply from xiaoice: %s", reply.Answer) 113 | return reply.Answer 114 | } 115 | 116 | type iceReply struct { 117 | Code int `json:"code"` 118 | Answer string `json:"answer"` 119 | } 120 | -------------------------------------------------------------------------------- /AI_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestAIReply(t *testing.T) { 6 | askEn := "test" 7 | askZh := "测试" 8 | if tlAI(askEn) == "" || tlAI(askZh) == "" { 9 | t.Error("tlAI error") 10 | } 11 | //if qinAI(askEn) == "" || qinAI(askZh) == "" { 12 | // t.Error("qinAI error") 13 | //} 14 | if mitAI(askEn) == "" || mitAI(askZh) == "" { 15 | t.Error("mitAI error") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 robot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **robot** 2 | An amazing telegram robot. 3 | 4 | [JianShu Blog](http://www.jianshu.com/p/46c7d36a95d2) 5 | 6 | [![Build Status](https://api.travis-ci.org/evolsnow/robot.svg?branch=master)](https://travis-ci.org/evolsnow/robot) 7 | 8 | ### **Feature** 9 | Basically, you can talk with the robot with both ```Chinese``` and ```English``` because it has 4 Artificial Intelligence inside: 10 | 11 | * [Turing Robot](http://www.tuling123.com/) 12 | * [Mitsuku Chatbot](http://www.pandorabots.com/) 13 | * [Microsoft's Chatbot Xiaoice](http://www.msxiaoice.com/) 14 | * [Qingyunke chatbot](http://api.qingyunke.com/) 15 | 16 | 17 | Additionally, it supports the following commands: 18 | 19 | ``` 20 | /alarm - set an alarm 21 | /alarms - show all of your alarms 22 | /rmalarm - remove an alarm 23 | /memo - save a memo 24 | /memos - show all of your memos 25 | /rmmemo - remove a memo 26 | /movie - find movie download links 27 | /show - find American show download links 28 | /trans - translate words between english and chinese 29 | /exit - exit any interactive mode 30 | /help - show this help message 31 | ``` 32 | 33 | Is that all? 34 | More skills are developing and any suggestions will be greatly appreciated. 35 | 36 | For more advanced usage, contact [@EvolsnowBot](https://telegram.me/EvolsnowBot) right away. 37 | 38 | ### **Demo** 39 | Remember that the website can just show a part of the robot's skills. 40 | To be precise, it's just able to talk with you... 41 | 42 | https://samaritan.tech (deprecated) 43 | 44 | ### **Installation** 45 | You can simply try the robot in telegram as mentioned above. 46 | But if you want to build your own robot: 47 | 48 | * [redis](http://redis.io/download) is required. 49 | * create a telegram robot follow the [reference](https://core.telegram.org/bots) 50 | * create your own configure file from the ```config.json``` file 51 | 52 | After that, you have two options to run the robot: 53 | 54 | * Execute the binary file from [release](https://github.com/evolsnow/robot/releases): 55 | ``` 56 | /path/to/file/robot -c /path/to/config.json 57 | ``` 58 | * Or clone the object and build it yourself (```golang``` required): 59 | ``` 60 | go get -u github.com/evolsnow/robot 61 | go build github.com/evolsnow/robot 62 | /path/to/file/robot -c /path/to/config.json 63 | ``` 64 | 65 | You may want to add ```-d``` flag to debug. 66 | 67 | ### **Development** 68 | This project is written in [go](https://golang.org/doc/install). 69 | 70 | Contact me for further questions: [@evolsnow](https://telegram.me/evolsnow) 71 | 72 | ### **License** 73 | [The MIT License (MIT)](https://raw.githubusercontent.com/evolsnow/robot/master/LICENSE) 74 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/evolsnow/robot/conn" 17 | "github.com/go-telegram-bot-api/telegram-bot-api" 18 | ) 19 | 20 | const ( 21 | FirstFormat = "1/2 15:04" 22 | SecondFormat = "15:04" 23 | ThirdFormat = "15:04:05" 24 | RedisFormat = "1/2 15:04:05" //save to redis format 25 | ) 26 | 27 | var saidGoodBye = make(chan int, 1) 28 | 29 | var abortTask = make(map[int]chan int) 30 | var userAction = make(map[string]Action) //map[user]Action 31 | var userTask = make(map[string]conn.Task) 32 | var userTaskIds = make(map[string][]int) 33 | 34 | // Robot is a bot carried with additional information 35 | type Robot struct { 36 | bot *tgbotapi.BotAPI 37 | updates <-chan tgbotapi.Update //update msg 38 | shutUp bool //shut up the robot 39 | name string //name from telegram 40 | nickName string //user defined name 41 | } 42 | 43 | // Action used in interaction mode 44 | type Action struct { 45 | ActionName string 46 | ActionStep int 47 | } 48 | 49 | //return a initialized robot 50 | func newRobot(token, nickName, webHook string) *Robot { 51 | var rb = new(Robot) 52 | var err error 53 | rb.bot, err = tgbotapi.NewBotAPI(token) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | rb.name = rb.bot.Self.UserName //name from telegram 58 | rb.nickName = nickName //name from yourself 59 | log.Printf("%s: Authorized on account %s", rb.nickName, rb.name) 60 | _, err = rb.bot.SetWebhook(tgbotapi.NewWebhook(webHook + rb.bot.Token)) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | rb.updates = rb.bot.ListenForWebhook("/" + rb.bot.Token) 65 | return rb 66 | } 67 | 68 | //robot run and handle update msg 69 | func (rb *Robot) run() { 70 | chatId := conn.ReadMasterId() 71 | rawMsg := fmt.Sprintf("%s is coming back!", rb.nickName) 72 | rb.Reply(chatId, rawMsg) 73 | //go loginZMZ() 74 | //reload tasks from redis 75 | go restoreTasks(rb) 76 | for update := range rb.updates { 77 | go handlerUpdate(rb, update) 78 | } 79 | } 80 | 81 | // Reply is encapsulated robot message send action 82 | func (rb *Robot) Reply(v interface{}, rawMsg string) (err error) { 83 | var chatId int64 84 | switch v.(type) { 85 | case tgbotapi.Update: 86 | chatId = v.(tgbotapi.Update).Message.Chat.ID 87 | case int64: 88 | chatId = v.(int64) 89 | } 90 | msg := tgbotapi.NewMessage(chatId, rawMsg) 91 | msg.ParseMode = "markdown" 92 | log.Printf(rawMsg) 93 | _, err = rb.bot.Send(msg) 94 | if err != nil { 95 | log.Println(err) 96 | } 97 | return 98 | } 99 | 100 | // Start is command '/start' 101 | func (rb *Robot) Start(update tgbotapi.Update) string { 102 | user := update.Message.Chat.UserName 103 | go conn.CreateUserChatId(user, update.Message.Chat.ID) 104 | return fmt.Sprintf("welcome: %s.\nType '/help' see what can I do.", user) 105 | } 106 | 107 | // Help show help message 108 | func (rb *Robot) Help(update tgbotapi.Update) string { 109 | helpMsg := ` 110 | /alarm - set a reminder 111 | /alarms - show all of your alarms 112 | /rmalarm - remove alarm 113 | /memo - save a memo 114 | /memos - show all of your memos 115 | /rmmemo - remove memo 116 | /movie - find movie download links 117 | /show - find American show download links 118 | /trans - translate words between english and chinese 119 | /express - check your latest express status 120 | /exit - exit any interaction mode 121 | /help - show this help message 122 | ` 123 | return helpMsg 124 | } 125 | 126 | // Repeat is markdown test function 127 | func (rb *Robot) Repeat(update tgbotapi.Update) { 128 | rb.Reply(update, update.Message.Text) 129 | } 130 | 131 | // Evolve remote executes self evolve script, exit the robot, only for test. 132 | func (rb *Robot) Evolve(update tgbotapi.Update) { 133 | if update.Message.Chat.FirstName != "Evol" || update.Message.Chat.LastName != "Gan" { 134 | rb.Reply(update, "sorry, unauthorized") 135 | return 136 | } 137 | conn.CreateMasterId(update.Message.Chat.ID) 138 | <-saidGoodBye 139 | close(saidGoodBye) 140 | //git pull and restart 141 | cmd := exec.Command("bash", "/root/evolve_"+rb.nickName) 142 | cmd.Start() 143 | os.Exit(1) 144 | } 145 | 146 | // Translate Zh<-->En 147 | // command '/trans' 148 | func (rb *Robot) Translate(update tgbotapi.Update) string { 149 | var info string 150 | if string(update.Message.Text[0]) == "/" { 151 | //'/trans cat' 152 | raw := strings.SplitAfterN(update.Message.Text, " ", 2) //at most 2 substring 153 | if len(raw) < 2 { 154 | return "what do you want to translate, try '/trans cat'?" 155 | } 156 | info = "翻译" + raw[1] //'翻译cat' 157 | } else { 158 | info = update.Message.Text 159 | } 160 | 161 | return qinAI(info) 162 | 163 | } 164 | 165 | // Talk with AI 166 | func (rb *Robot) Talk(update tgbotapi.Update) string { 167 | info := update.Message.Text 168 | if strings.Contains(info, rb.name) { 169 | if strings.Contains(info, "闭嘴") || strings.Contains(info, "别说话") { 170 | rb.shutUp = true 171 | } else if rb.shutUp && strings.Contains(info, "说话") { 172 | rb.shutUp = false 173 | return fmt.Sprintf("%s终于可以说话啦", rb.nickName) 174 | } 175 | info = strings.Replace(info, fmt.Sprintf("@%s", rb.name), "", -1) 176 | } 177 | 178 | if rb.shutUp { 179 | return "" 180 | } 181 | log.Printf(info) 182 | 183 | if rb.nickName != "jarvis" { 184 | if chinese(info) { 185 | return tlAI(info) 186 | } 187 | return mitAI(info) 188 | } else { 189 | //jarvis use another AI 190 | return qinAI(info) 191 | } 192 | } 193 | 194 | // SetReminder set an alarm 195 | // command '/alarm' 196 | func (rb *Robot) SetReminder(update tgbotapi.Update, step int) string { 197 | user := update.Message.Chat.UserName 198 | tmpTask := userTask[user] 199 | tmpAction := userAction[user] 200 | 201 | switch step { 202 | case 0: 203 | //known issue of go, you can not just assign update.Message.Chat.Id to userTask[user].ChatId 204 | tmpTask.ChatId = update.Message.Chat.ID 205 | tmpTask.Owner = user 206 | userTask[user] = tmpTask 207 | 208 | tmpAction.ActionStep++ 209 | userAction[user] = tmpAction 210 | return "Ok, what should I remind you to do?" 211 | case 1: 212 | //save task content 213 | tmpTask.Desc = update.Message.Text 214 | userTask[user] = tmpTask 215 | 216 | tmpAction.ActionStep++ 217 | userAction[user] = tmpAction 218 | return "When or how much time after?\n" + 219 | "You can type:\n" + 220 | "'*2/14 11:30*' means 11:30 at 2/14 \n" + //first format 221 | "'*11:30*' means 11:30 today\n" + //second format 222 | "'*5m10s*' means 5 minutes 10 seconds later" //third format 223 | case 2: 224 | //format time 225 | text := update.Message.Text 226 | text = strings.Replace(text, ":", ":", -1) 227 | var showTime string //show to user 228 | var redisTime string //time string to save to redis 229 | var scheduledTime time.Time 230 | var nowTime = time.Now() 231 | var du time.Duration 232 | var err error 233 | if strings.Contains(text, ":") { 234 | //first and second case 235 | scheduledTime, err = time.Parse(FirstFormat, text) 236 | // nowTime, _ = time.Parse(FirstFormat, nowTime.Format(FirstFormat)) 237 | showTime = scheduledTime.Format(FirstFormat) 238 | redisTime = scheduledTime.Format(RedisFormat) 239 | //second case 240 | if err != nil { 241 | //try to parse with second format 242 | scheduledTime, err = time.Parse(SecondFormat, text) 243 | redisTime = nowTime.Format("1/02 ") + scheduledTime.Format(ThirdFormat) 244 | // nowTime, _ = time.Parse(SecondFormat, nowTime.Format(SecondFormat)) 245 | showTime = scheduledTime.Format(SecondFormat) 246 | 247 | if err != nil { 248 | return "wrong format, try '2/14 11:30' or '11:30'?" 249 | } 250 | } 251 | } else { 252 | //third case 253 | du, err = time.ParseDuration(text) 254 | scheduledTime = nowTime.Add(du) 255 | showTime = scheduledTime.Format(ThirdFormat) 256 | redisTime = scheduledTime.Format(RedisFormat) 257 | if err != nil { 258 | return "wrong format, try '1h2m3s'?" 259 | } 260 | } 261 | //save time 262 | tmpTask.When = redisTime 263 | tmpTask.Id = conn.UpdateTaskId() 264 | userTask[user] = tmpTask 265 | //arrange to do the task 266 | go rb.DoTask(userTask[user]) 267 | //save task in redis 268 | go conn.CreateTask(userTask[user]) 269 | delete(userAction, user) //delete userAction, prevent to be stuck 270 | return fmt.Sprintf("Ok, I will remind you that\n*%s* - *%s*", showTime, userTask[user].Desc) 271 | } 272 | return "" 273 | } 274 | 275 | // DoTask accomplish all tasks here 276 | func (rb *Robot) DoTask(ts conn.Task) { 277 | nowString := time.Now().Format(RedisFormat) 278 | now, _ := time.Parse(RedisFormat, nowString) 279 | when, _ := time.Parse(RedisFormat, ts.When) 280 | if when.After(now) { 281 | //set timer 282 | du := when.Sub(now) 283 | timer := time.NewTimer(du) 284 | abortTask[ts.Id] = make(chan int) 285 | select { 286 | case <-abortTask[ts.Id]: 287 | //triggered by 'rm alarm' command 288 | log.Println("abort mission:", ts.Id) 289 | conn.DeleteTask(ts) 290 | return 291 | case <-timer.C: 292 | break 293 | } 294 | } 295 | //else if now is after when means we miss the time to do the task, so do it immediately 296 | rawMsg := fmt.Sprintf("Hi %s, maybe it's time to:\n*%s*", ts.Owner, ts.Desc) 297 | if rb.Reply(ts.ChatId, rawMsg) != nil { 298 | //if failed to send with the given chatId, load it from redis 299 | rb.Reply(conn.ReadUserChatId(ts.Owner), rawMsg) 300 | } 301 | //delete the task from redis, we won't save it 302 | conn.DeleteTask(ts) 303 | } 304 | 305 | // GetTasks get the given user's all tasks 306 | // command 'alarms' 307 | func (rb *Robot) GetTasks(update tgbotapi.Update) (ret string) { 308 | user := update.Message.Chat.UserName 309 | tasks := conn.ReadUserTasks(user) 310 | if len(tasks) == 0 { 311 | return "You have no alarm now, type '/alarm' to set one?" 312 | } 313 | for i := range tasks { 314 | ret += fmt.Sprintf("%d. %s: %s\n", i+1, tasks[i].When, tasks[i].Desc) 315 | } 316 | return 317 | } 318 | 319 | // RemoveReminder cancel a task 320 | // command '/rmalarm' 321 | func (rb *Robot) RemoveReminder(update tgbotapi.Update, step int) (ret string) { 322 | user := update.Message.Chat.UserName 323 | tmpAction := userAction[user] 324 | switch step { 325 | case 0: 326 | //init the struct 327 | tmpAction.ActionStep++ 328 | userAction[user] = tmpAction 329 | tasks := conn.ReadUserTasks(user) 330 | if len(tasks) == 0 { 331 | delete(userAction, user) 332 | return "You have no alarm now, type '/alarm' to set one?" 333 | } 334 | ret = "Ok, which alarm do you want to remove?(type id)\n" 335 | userTaskIds[user] = make([]int, len(tasks)) 336 | for i := range tasks { 337 | userTaskIds[user][i] = tasks[i].Id 338 | ret += fmt.Sprintf("%d. %s: %s\n", i+1, tasks[i].When, tasks[i].Desc) 339 | } 340 | case 1: 341 | defer func() { 342 | delete(userAction, user) 343 | delete(userTaskIds, user) 344 | }() 345 | index, err := strconv.Atoi(update.Message.Text) 346 | if err != nil { 347 | return "please select the alarm id" 348 | } 349 | taskId := userTaskIds[user][index-1] 350 | //cancel the task 351 | abortTask[taskId] <- 1 352 | ret = "Ok, type '/alarms' to see your new alarms" 353 | } 354 | return 355 | } 356 | 357 | // DownloadMovie download movie from lbl and zmz 358 | // command '/movie' 359 | func (rb *Robot) DownloadMovie(update tgbotapi.Update, step int, results chan<- string) (ret string) { 360 | user := update.Message.Chat.UserName 361 | tmpAction := userAction[user] 362 | switch step { 363 | case 0: 364 | tmpAction.ActionStep++ 365 | userAction[user] = tmpAction 366 | ret = "Ok, which movie do you want to download?" 367 | case 1: 368 | defer func() { 369 | delete(userAction, user) 370 | close(results) 371 | }() 372 | results <- "Searching movie..." 373 | movie := update.Message.Text 374 | var wg sync.WaitGroup 375 | wg.Add(2) 376 | go func() { 377 | defer wg.Done() 378 | getMovieFromZMZ(movie, results) 379 | }() 380 | go func() { 381 | defer wg.Done() 382 | getMovieFromLBL(movie, results) 383 | }() 384 | wg.Wait() 385 | } 386 | return 387 | } 388 | 389 | // DownloadShow download American show from zmz 390 | // command '/show' 391 | func (rb *Robot) DownloadShow(update tgbotapi.Update, step int, results chan string) (ret string) { 392 | user := update.Message.Chat.UserName 393 | tmpAction := userAction[user] 394 | switch step { 395 | case 0: 396 | tmpAction.ActionStep++ 397 | userAction[user] = tmpAction 398 | ret = "Ok, which American show do you want to download?" 399 | case 1: 400 | defer func() { 401 | delete(userAction, user) 402 | close(results) 403 | }() 404 | results <- "Searching American show..." 405 | info := strings.Fields(update.Message.Text) 406 | if len(info) < 3 { 407 | if rct := conn.ReadDownloadRecord(user, info[0]); rct != "" { 408 | results <- fmt.Sprintf("Recent downloads of %s: %s", info[0], rct) 409 | } else { 410 | results <- "Please specify the season and episode, like:\n*疑犯追踪 1 10*" 411 | } 412 | return 413 | } 414 | if getShowFromZMZ(info[0], info[1], info[2], results) { 415 | //found resource 416 | conn.CreateDownloadRecord(user, info[0], fmt.Sprintf("S%sE%s", info[1], info[2])) 417 | } 418 | } 419 | return 420 | } 421 | 422 | // SaveMemo create a memo for user, saved in redis 423 | // command '/memo' 424 | func (rb *Robot) SaveMemo(update tgbotapi.Update, step int) (ret string) { 425 | user := update.Message.Chat.UserName 426 | tmpAction := userAction[user] 427 | switch step { 428 | case 0: 429 | tmpAction.ActionStep++ 430 | userAction[user] = tmpAction 431 | ret = "Ok, what do you want to save?" 432 | case 1: 433 | defer delete(userAction, user) 434 | when := time.Now().Format("2006-1-02 15:04") 435 | memo := update.Message.Text 436 | go conn.CreateMemo(user, when, memo) 437 | ret = "Ok, type '/memos' to see all your memos" 438 | } 439 | return 440 | } 441 | 442 | // GetAllMemos reads all user's memos in redis 443 | // command '/memos' 444 | func (rb *Robot) GetAllMemos(update tgbotapi.Update) (ret string) { 445 | user := update.Message.Chat.UserName 446 | memos := conn.ReadAllMemos(user) 447 | if len(memos) == 0 { 448 | return "You have no memo now, type '/memo' to save one?" 449 | } 450 | for i := range memos { 451 | ret += fmt.Sprintf("%d. %s: *%s*\n", i+1, memos[i].Time, memos[i].Content) 452 | } 453 | return 454 | } 455 | 456 | // RemoveMemo deletes a memo 457 | // command '/rmmemo' 458 | func (rb *Robot) RemoveMemo(update tgbotapi.Update, step int) (ret string) { 459 | user := update.Message.Chat.UserName 460 | tmpAction := userAction[user] 461 | switch step { 462 | case 0: 463 | tmpAction.ActionStep++ 464 | userAction[user] = tmpAction 465 | ret = "Ok, which memo do you want to remove?(type id)\n" + rb.GetAllMemos(update) 466 | case 1: 467 | defer delete(userAction, user) 468 | index, err := strconv.Atoi(update.Message.Text) 469 | if err != nil { 470 | return "please select the memo id" 471 | } 472 | go conn.DeleteMemo(user, index-1) 473 | ret = "Ok, type '/memos' to see your new memos" 474 | } 475 | return 476 | } 477 | 478 | // GetExpressStats fetched express info from web 479 | // command '/express' 480 | func (rb *Robot) GetExpressStats(update tgbotapi.Update) (ret string) { 481 | info := strings.Fields(update.Message.Text) 482 | if len(info) != 2 { 483 | return "Type '/express your-order-no' to get your express information" 484 | } 485 | orderNo := info[1] 486 | type numInfo struct { 487 | Auto []struct { 488 | ComCode string `json:"comCode"` 489 | } `json:"auto"` 490 | } 491 | resp, err := http.Get("https://www.kuaidi100.com/autonumber/autoComNum?text=" + orderNo) 492 | if err != nil { 493 | return "remote server err, please check again later" 494 | } 495 | defer resp.Body.Close() 496 | ni := new(numInfo) 497 | data, err := ioutil.ReadAll(resp.Body) 498 | if err != nil { 499 | return "failed to read from remote server" 500 | } 501 | err = json.Unmarshal(data, ni) 502 | if err != nil { 503 | return "invalid response from remote server" 504 | } 505 | if len(ni.Auto) == 0 { 506 | return "Unknown order number" 507 | } 508 | resp, err = http.Get(fmt.Sprintf("https://www.kuaidi100.com/query?type=%s&postid=%s&id=1", 509 | ni.Auto[0].ComCode, orderNo)) 510 | if err != nil { 511 | return "remote server err, please check again later" 512 | } 513 | type kdInfo struct { 514 | Message string `json:"message"` 515 | Data []struct { 516 | Time string `json:"time"` 517 | Context string `json:"context"` 518 | } `json:"data"` 519 | } 520 | k := new(kdInfo) 521 | data, err = ioutil.ReadAll(resp.Body) 522 | if err != nil { 523 | return "failed to read from remote server" 524 | } 525 | err = json.Unmarshal(data, k) 526 | if err != nil { 527 | return "invalid response from remote server" 528 | } 529 | if k.Message != "ok" { 530 | return k.Message 531 | } 532 | for i, d := range k.Data { 533 | content := d.Time +" "+ d.Context 534 | if i == 0 { 535 | content = "*" + content + "*\n" 536 | } 537 | ret += content + "\n" 538 | } 539 | return 540 | } 541 | 542 | //restore task when robot run 543 | func restoreTasks(rb *Robot) { 544 | tasks := conn.ReadAllTasks() 545 | log.Println("unfinished tasks:", len(tasks)) 546 | for i := range tasks { 547 | go rb.DoTask(tasks[i]) 548 | } 549 | } 550 | 551 | //all telegram updates are handled here 552 | func handlerUpdate(rb *Robot, update tgbotapi.Update) { 553 | defer func() { 554 | if p := recover(); p != nil { 555 | err := fmt.Errorf("internal error: %v", p) 556 | log.Println(err) 557 | } 558 | }() 559 | user := update.Message.Chat.UserName 560 | text := update.Message.Text 561 | var endPoint, rawMsg string 562 | text = strings.Replace(text, "@"+rb.name, "", 1) 563 | received := strings.Split(text, " ") 564 | endPoint = received[0] 565 | if endPoint == "/exit" { 566 | delete(userAction, user) 567 | return 568 | } 569 | if action, ok := userAction[user]; ok { 570 | //if user is in interaction mode 571 | rawMsg = inAction(rb, action, update) 572 | } else if string([]rune(text)[:2]) == "翻译" { 573 | rawMsg = rb.Translate(update) 574 | } else if string(text[0]) == "/" { 575 | //new command 576 | log.Printf(endPoint) 577 | rawMsg = inCommand(rb, endPoint, update) 578 | } else { 579 | //just talk 580 | rawMsg = rb.Talk(update) 581 | } 582 | 583 | if rawMsg == "" { 584 | return 585 | } 586 | if err := rb.Reply(update, rawMsg); err != nil { 587 | panic(err) 588 | } 589 | if endPoint == "/evolve" { 590 | saidGoodBye <- 1 591 | } 592 | } 593 | 594 | func inAction(rb *Robot, action Action, update tgbotapi.Update) (rawMsg string) { 595 | switch action.ActionName { 596 | case "setReminder": 597 | rawMsg = rb.SetReminder(update, action.ActionStep) 598 | case "saveMemo": 599 | rawMsg = rb.SaveMemo(update, action.ActionStep) 600 | case "removeMemo": 601 | rawMsg = rb.RemoveMemo(update, action.ActionStep) 602 | case "removeReminder": 603 | rawMsg = rb.RemoveReminder(update, action.ActionStep) 604 | case "downloadMovie": 605 | results := make(chan string, 2) 606 | go rb.DownloadMovie(update, action.ActionStep, results) 607 | for { 608 | msg, ok := <-results 609 | if !ok { 610 | return 611 | } 612 | rb.Reply(update, msg) 613 | 614 | } 615 | case "downloadShow": 616 | results := make(chan string, 5) 617 | go rb.DownloadShow(update, action.ActionStep, results) 618 | for { 619 | msg, ok := <-results 620 | if !ok { 621 | return 622 | } 623 | rb.Reply(update, msg) 624 | } 625 | } 626 | return 627 | } 628 | 629 | func inCommand(rb *Robot, endPoint string, update tgbotapi.Update) (rawMsg string) { 630 | user := update.Message.Chat.UserName 631 | switch endPoint { 632 | case "/start": 633 | rawMsg = rb.Start(update) 634 | case "/help": 635 | rawMsg = rb.Help(update) 636 | case "/alarms": 637 | rawMsg = rb.GetTasks(update) 638 | case "/memos": 639 | rawMsg = rb.GetAllMemos(update) 640 | case "/trans": 641 | rawMsg = rb.Translate(update) 642 | case "/alarm": 643 | tmpAction := userAction[user] 644 | tmpAction.ActionName = "setReminder" 645 | userAction[user] = tmpAction 646 | rawMsg = rb.SetReminder(update, 0) 647 | case "/movie": 648 | tmpAction := userAction[user] 649 | tmpAction.ActionName = "downloadMovie" 650 | userAction[user] = tmpAction 651 | rawMsg = rb.DownloadMovie(update, 0, nil) 652 | case "/memo": 653 | tmpAction := userAction[user] 654 | tmpAction.ActionName = "saveMemo" 655 | userAction[user] = tmpAction 656 | rawMsg = rb.SaveMemo(update, 0) 657 | case "/rmmemo": 658 | tmpAction := userAction[user] 659 | tmpAction.ActionName = "removeMemo" 660 | userAction[user] = tmpAction 661 | rawMsg = rb.RemoveMemo(update, 0) 662 | case "/rmalarm": 663 | tmpAction := userAction[user] 664 | tmpAction.ActionName = "removeReminder" 665 | userAction[user] = tmpAction 666 | rawMsg = rb.RemoveReminder(update, 0) 667 | case "/show": 668 | tmpAction := userAction[user] 669 | tmpAction.ActionName = "downloadShow" 670 | userAction[user] = tmpAction 671 | rawMsg = rb.DownloadShow(update, 0, nil) 672 | case "/express": 673 | rawMsg = rb.GetExpressStats(update) 674 | case "/evolve": 675 | rawMsg = "upgrading..." 676 | go rb.Evolve(update) 677 | case "/repeat": 678 | rb.Repeat(update) 679 | default: 680 | rawMsg = "unknow command, type /help?" 681 | } 682 | return 683 | } 684 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | // Config for initialized robot and server 10 | type Config struct { 11 | Server string `json:"server"` 12 | Port int `json:"port"` 13 | Cert string `json:"cert"` 14 | CertKey string `json:"cert_key"` 15 | 16 | WebHookURL string `json:"webhook_url"` 17 | RedisAddress string `json:"redis_address"` 18 | RedisPort int `json:"redis_port"` 19 | RedisDB int `json:"redis_db"` 20 | RedisPassword string `json:"redis_password"` 21 | RobotName string `json:"robot_name"` 22 | RobotToken string `json:"robot_token"` 23 | } 24 | 25 | // ParseConfig parses config from the given file path 26 | func ParseConfig(path string) (config *Config, err error) { 27 | file, err := os.Open(path) 28 | if err != nil { 29 | return 30 | } 31 | defer file.Close() 32 | data, err := ioutil.ReadAll(file) 33 | if err != nil { 34 | return 35 | } 36 | config = &Config{} 37 | if err = json.Unmarshal(data, config); err != nil { 38 | return nil, err 39 | } 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "0.0.0.0", 3 | "cert": "cert.pem", 4 | "cert_key": "key.pem", 5 | "port": 8443, 6 | "webhook_url": "https://www.example.com:8443/", 7 | "redis_address": "127.0.0.1", 8 | "redis_port": 6379, 9 | "redis_db": 2, 10 | "redis_password": null, 11 | "robot_name": "{robot's name by yourself}", 12 | "robot_token": "{robot's token from BotFather}" 13 | } -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestParseConfig(t *testing.T) { 6 | cfg, err := ParseConfig("config.json") 7 | if err != nil { 8 | t.Error("parse config.json errer:", err.Error()) 9 | } 10 | if cfg.RobotToken == "" { 11 | t.Error("parse robot token error") 12 | } 13 | _, err = ParseConfig("non-existen.json") 14 | if err == nil { 15 | t.Error("should throw err when parse from non-existen json file") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /conn/conn_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var user = "TestUser" 8 | 9 | func TestRedisConn(t *testing.T) { 10 | Pool = NewPool("127.0.0.1:6379", "", 10) 11 | if Pool == nil { 12 | t.Error("create new pool error") 13 | } 14 | if !Ping("127.0.0.1:6379", "") { 15 | t.Fatal("connect to redis server failed") 16 | } 17 | } 18 | 19 | func TestCRMasterId(t *testing.T) { 20 | CreateMasterId(123) 21 | if ReadMasterId() != 123 { 22 | t.Error("CR master id error") 23 | } 24 | } 25 | 26 | func TestCRUserChatId(t *testing.T) { 27 | CreateUserChatId(user, 1234) 28 | if ReadUserChatId(user) != 1234 { 29 | t.Error("CR user chat id error") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /conn/pool.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package conn provides redis connection 3 | */ 4 | package conn 5 | 6 | import ( 7 | "github.com/garyburd/redigo/redis" 8 | "time" 9 | ) 10 | 11 | var Pool *redis.Pool 12 | 13 | //var CachePool *redis.Pool 14 | 15 | // NewPool return a redis pool 16 | func NewPool(server, password string, db int) *redis.Pool { 17 | return &redis.Pool{ 18 | MaxIdle: 3, 19 | IdleTimeout: 240 * time.Second, 20 | Dial: func() (redis.Conn, error) { 21 | c, err := redis.Dial("tcp", server) 22 | if err != nil { 23 | return nil, err 24 | } 25 | if password != "" { 26 | if _, err := c.Do("AUTH", password); err != nil { 27 | c.Close() 28 | return nil, err 29 | } 30 | } 31 | if _, err := c.Do("SELECT", db); err != nil { 32 | c.Close() 33 | return nil, err 34 | } 35 | return c, err 36 | }, 37 | } 38 | } 39 | 40 | // Ping tests redis pool connection 41 | func Ping(server, password string) bool { 42 | c, err := redis.Dial("tcp", server) 43 | if err != nil { 44 | return false 45 | } 46 | if password != "" { 47 | if _, err := c.Do("AUTH", password); err != nil { 48 | c.Close() 49 | return false 50 | } 51 | } 52 | return true 53 | } 54 | -------------------------------------------------------------------------------- /conn/redis.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "github.com/garyburd/redigo/redis" 5 | "log" 6 | ) 7 | 8 | // Memo is user's memo 9 | type Memo struct { 10 | Time string `redis:"time"` 11 | Content string `redis:"content"` 12 | } 13 | 14 | // Task is user's task 15 | type Task struct { 16 | Id int `redis:"id"` 17 | ChatId int64 `redis:"chatID"` 18 | Owner string `redis:"owner"` 19 | Desc string `redis:"content"` 20 | When string `redis:"time"` 21 | } 22 | 23 | //All redis CRUD actions 24 | 25 | // CreateMasterId saves master's id 26 | func CreateMasterId(id int64) { 27 | c := Pool.Get() 28 | defer c.Close() 29 | c.Do("SET", "masterChatId", id) 30 | } 31 | 32 | // ReadMasterId read master id in redis 33 | func ReadMasterId() int64 { 34 | c := Pool.Get() 35 | defer c.Close() 36 | id, _ := redis.Int64(c.Do("GET", "masterChatId")) 37 | return id 38 | } 39 | 40 | // CreateUserChatId saves user's chat id 41 | func CreateUserChatId(user string, id int64) { 42 | c := Pool.Get() 43 | defer c.Close() 44 | key := user + "ChatId" 45 | c.Do("SET", key, id) 46 | } 47 | 48 | // ReadUserChatId read user's chat id 49 | func ReadUserChatId(user string) int64 { 50 | c := Pool.Get() 51 | defer c.Close() 52 | key := user + "ChatId" 53 | id, _ := redis.Int64(c.Do("GET", key)) 54 | return id 55 | } 56 | 57 | // CreateMemo saves a memo 58 | func CreateMemo(user, when, memo string) { 59 | c := Pool.Get() 60 | defer c.Close() 61 | var setMemoLua = ` 62 | local id = redis.call("INCR", "memoIncrId") 63 | redis.call("RPUSH", KEYS[1]..":memos", id) 64 | redis.call("HMSET", "memo:"..id, "time", KEYS[2], "content", KEYS[3]) 65 | ` 66 | script := redis.NewScript(3, setMemoLua) 67 | script.Do(c, user, when, memo) 68 | } 69 | 70 | // DeleteMemo deletes a memo 71 | func DeleteMemo(user string, index int) { 72 | c := Pool.Get() 73 | defer c.Close() 74 | var deleteMemoLua = ` 75 | local id = redis.call("LINDEX", KEYS[1]..":memos", KEYS[2]) 76 | redis.call("LREM", KEYS[1]..":memos", 1, id) 77 | redis.call("DEL", "memo:"..id) 78 | ` 79 | script := redis.NewScript(2, deleteMemoLua) 80 | script.Do(c, user, index) 81 | } 82 | 83 | // UpdateTaskId auto increases task id 84 | func UpdateTaskId() int { 85 | c := Pool.Get() 86 | defer c.Close() 87 | id, _ := redis.Int(c.Do("INCR", "taskIncrId")) 88 | return id 89 | } 90 | 91 | // CreateTask saves a task 92 | func CreateTask(ts Task) { 93 | c := Pool.Get() 94 | defer c.Close() 95 | log.Println("save task") 96 | var setTaskLua = ` 97 | redis.call("RPUSH", "allTasks", KEYS[1]) 98 | redis.call("RPUSH", KEYS[2]..":tasks", KEYS[1]) 99 | redis.call("HMSET", "task:"..KEYS[1], "id", KEYS[1], "owner", KEYS[2], "time", KEYS[3], "content", KEYS[4], "chatID", KEYS[5]) 100 | ` 101 | script := redis.NewScript(5, setTaskLua) 102 | script.Do(c, ts.Id, ts.Owner, ts.When, ts.Desc, ts.ChatId) 103 | } 104 | 105 | // DeleteTask deletes a task 106 | func DeleteTask(ts Task) { 107 | c := Pool.Get() 108 | defer c.Close() 109 | log.Println("remove", ts.Id) 110 | var removeTaskLua = ` 111 | redis.call("LREM", "allTasks", 1, KEYS[2]) 112 | redis.call("LREM", KEYS[1]..":tasks", 1, KEYS[2]) 113 | redis.call("DEL", "task:"..KEYS[2]) 114 | ` 115 | script := redis.NewScript(2, removeTaskLua) 116 | script.Do(c, ts.Owner, ts.Id) 117 | } 118 | 119 | // ReadUserTasks read user's all tasks 120 | func ReadUserTasks(user string) []Task { 121 | c := Pool.Get() 122 | defer c.Close() 123 | var lua = ` 124 | local data = redis.call("LRANGE", KEYS[1]..":tasks", "0", "-1") 125 | local ret = {} 126 | for idx=1, #data do 127 | ret[idx] = redis.call("HGETALL", "task:"..data[idx]) 128 | end 129 | return ret 130 | ` 131 | var tasks []Task 132 | script := redis.NewScript(1, lua) 133 | values, err := redis.Values(script.Do(c, user)) 134 | if err != nil { 135 | log.Println(err) 136 | } 137 | for i := range values { 138 | t := new(Task) 139 | redis.ScanStruct(values[i].([]interface{}), t) 140 | tasks = append(tasks, *t) 141 | } 142 | return tasks 143 | } 144 | 145 | // ReadAllTasks load all unfinished task 146 | func ReadAllTasks() []Task { 147 | c := Pool.Get() 148 | defer c.Close() 149 | var GetAllTasksLua = ` 150 | local data = redis.call("LRANGE", "allTasks", "0", "-1") 151 | local ret = {} 152 | for idx=1, #data do 153 | ret[idx] = redis.call("HGETALL", "task:"..data[idx]) 154 | end 155 | return ret 156 | ` 157 | var tasks []Task 158 | script := redis.NewScript(0, GetAllTasksLua) 159 | values, err := redis.Values(script.Do(c)) 160 | if err != nil { 161 | log.Println(err) 162 | } 163 | for i := range values { 164 | t := new(Task) 165 | redis.ScanStruct(values[i].([]interface{}), t) 166 | tasks = append(tasks, *t) 167 | } 168 | return tasks 169 | 170 | } 171 | 172 | // ReadAllMemo get user's all memos 173 | func ReadAllMemos(user string) []Memo { 174 | c := Pool.Get() 175 | defer c.Close() 176 | var lua = ` 177 | local data = redis.call("LRANGE", KEYS[1]..":memos", "0", "-1") 178 | local ret = {} 179 | for idx=1, #data do 180 | ret[idx] = redis.call("HGETALL", "memo:"..data[idx]) 181 | end 182 | return ret 183 | ` 184 | var memos []Memo 185 | script := redis.NewScript(1, lua) 186 | values, err := redis.Values(script.Do(c, user)) 187 | if err != nil { 188 | log.Println(err) 189 | } 190 | for i := range values { 191 | m := new(Memo) 192 | redis.ScanStruct(values[i].([]interface{}), m) 193 | memos = append(memos, *m) 194 | } 195 | return memos 196 | } 197 | 198 | // CreateDownloadRecord records user's latest American show download 199 | func CreateDownloadRecord(user, show, se string) { 200 | c := Pool.Get() 201 | defer c.Close() 202 | c.Do("HSET", user+":shows", show, se) 203 | } 204 | 205 | // ReadDownloadRecord get latest download record 206 | func ReadDownloadRecord(user, show string) string { 207 | c := Pool.Get() 208 | defer c.Close() 209 | ret, _ := redis.String(c.Do("HGET", user+":shows", show)) 210 | return ret 211 | } 212 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the MIT License (MIT) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | package main 23 | 24 | import ( 25 | "flag" 26 | "fmt" 27 | "log" 28 | "net" 29 | "net/http" 30 | "strconv" 31 | "strings" 32 | "time" 33 | 34 | "github.com/evolsnow/robot/conn" 35 | "github.com/gorilla/websocket" 36 | ) 37 | 38 | var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { 39 | return true 40 | }} // use default options for webSocket 41 | var visitor = 0 42 | 43 | func main() { 44 | var configFile string 45 | var debug bool 46 | 47 | flag.StringVar(&configFile, "c", "config.json", "specify config file") 48 | flag.BoolVar(&debug, "d", false, "debug mode") 49 | flag.Parse() 50 | config, err := ParseConfig(configFile) 51 | if err != nil { 52 | log.Fatal("a vailid json config file must exist") 53 | } 54 | 55 | //connect to redis 56 | redisPort := strconv.Itoa(config.RedisPort) 57 | redisServer := net.JoinHostPort(config.RedisAddress, redisPort) 58 | if !conn.Ping(redisServer, config.RedisPassword) { 59 | log.Fatal("connect to redis server failed") 60 | } 61 | conn.Pool = conn.NewPool(redisServer, config.RedisPassword, config.RedisDB) 62 | 63 | //create robot and run 64 | robot := newRobot(config.RobotToken, config.RobotName, config.WebHookURL) 65 | robot.bot.Debug = debug 66 | go robot.run() 67 | 68 | //run server and web samaritan 69 | srvPort := strconv.Itoa(config.Port) 70 | http.HandleFunc("/ajax", ajax) 71 | http.HandleFunc("/websocket", socketHandler) 72 | http.HandleFunc("/groupTalk", groupTalk) 73 | log.Fatal(http.ListenAndServeTLS(net.JoinHostPort(config.Server, srvPort), config.Cert, config.CertKey, nil)) 74 | 75 | } 76 | 77 | //3 robot's group talk 78 | func groupTalk(w http.ResponseWriter, r *http.Request) { 79 | c, err := upgrader.Upgrade(w, r, nil) 80 | if err != nil { 81 | log.Print("upgrade:", err) 82 | return 83 | } 84 | defer c.Close() 85 | visitor++ 86 | tlChan := make(chan string, 5) 87 | qinChan := make(chan string, 5) 88 | iceChan := make(chan string, 5) 89 | result := make(chan string, 10) 90 | initSentence := "你好" 91 | tlChan <- tlAI(initSentence) 92 | go func() { 93 | for { 94 | if visitor > 0 { 95 | msgToTl := <-tlChan 96 | replyFromTl := tlAI(msgToTl) 97 | time.Sleep(time.Second * 2) 98 | go func() { 99 | if replyFromTl != "" { 100 | result <- "samaritan: " + replyFromTl 101 | qinChan <- replyFromTl 102 | iceChan <- replyFromTl 103 | } 104 | }() 105 | //c.WriteMessage(websocket.TextMessage, []byte("samaritan: " + replyFromTl)) 106 | } else { 107 | break 108 | } 109 | } 110 | }() 111 | 112 | go func() { 113 | for { 114 | if visitor > 0 { 115 | msgToQin := <-qinChan 116 | replyFromQin := qinAI(msgToQin) 117 | time.Sleep(time.Second * 2) 118 | go func() { 119 | if replyFromQin != "" { 120 | result <- "菲菲: " + replyFromQin 121 | tlChan <- replyFromQin 122 | iceChan <- replyFromQin 123 | } 124 | }() 125 | //c.WriteMessage(websocket.TextMessage, []byte("菲菲: " + replyFromQin)) 126 | } else { 127 | break 128 | } 129 | } 130 | }() 131 | 132 | go func() { 133 | for { 134 | if visitor > 0 { 135 | msgToIce := <-iceChan 136 | replyFromIce := iceAI(msgToIce) 137 | go func() { 138 | if replyFromIce != "" { 139 | result <- "小冰: " + replyFromIce 140 | tlChan <- replyFromIce 141 | qinChan <- replyFromIce 142 | } 143 | }() 144 | //c.WriteMessage(websocket.TextMessage, []byte("小冰: " + replyFromIce)) 145 | } else { 146 | break 147 | } 148 | } 149 | }() 150 | 151 | go func() { 152 | for { 153 | if visitor > 0 { 154 | c.WriteMessage(websocket.TextMessage, []byte(<-result)) 155 | } else { 156 | break 157 | } 158 | } 159 | }() 160 | 161 | for { 162 | 163 | _, _, err := c.ReadMessage() 164 | if err != nil { 165 | log.Println("read:", err) 166 | visitor-- 167 | break 168 | } 169 | } 170 | } 171 | 172 | //used by web samaritan robot 173 | func socketHandler(w http.ResponseWriter, r *http.Request) { 174 | c, err := upgrader.Upgrade(w, r, nil) 175 | if err != nil { 176 | log.Println("upgrade:", err) 177 | return 178 | } 179 | defer c.Close() 180 | for { 181 | mt, in, err := c.ReadMessage() 182 | if err != nil { 183 | log.Println("read:", err) 184 | break 185 | } 186 | ret := receive(string(in)) 187 | for i := range ret { 188 | c.WriteMessage(mt, []byte(ret[i])) 189 | time.Sleep(time.Second) 190 | } 191 | c.WriteMessage(mt, []byte("")) 192 | } 193 | } 194 | 195 | //when webSocket unavailable, fallback to ajax long polling 196 | func ajax(w http.ResponseWriter, r *http.Request) { 197 | w.Header().Add("Access-Control-Allow-Origin", "*") 198 | var messages = make(chan string) 199 | if r.Method == "GET" { 200 | w.Write([]byte(<-messages)) 201 | } else { 202 | body := r.FormValue("text") 203 | if body != "" { 204 | go func(string) { 205 | ret := receive(body) 206 | for i := range ret { 207 | messages <- ret[i] 208 | time.Sleep(time.Second) 209 | } 210 | messages <- "" 211 | }(body) 212 | } 213 | } 214 | } 215 | 216 | //receive from client 217 | func receive(in string) (ret []string) { 218 | fmt.Printf("Received: %s\n", in) 219 | var response string 220 | var answer = make(chan string, 3) 221 | sf := func(c rune) bool { 222 | return c == ',' || c == ',' || c == ';' || c == '。' || c == '.' || c == '?' || c == '?' 223 | } 224 | if chinese(in) { 225 | go func() { 226 | if ret := iceAI(in); ret != "" { 227 | answer <- ret 228 | } 229 | }() 230 | go func() { 231 | if ret := tlAI(in); ret != "" { 232 | answer <- ret 233 | } 234 | }() 235 | go func() { 236 | if ret := qinAI(in); ret != "" { 237 | answer <- strings.Replace(ret, "Jarvis", "samaritan", -1) 238 | } 239 | }() 240 | response = <-answer // accept the first reply 241 | // Separate into fields with func. 242 | ret = strings.FieldsFunc(response, sf) 243 | 244 | } else { 245 | response = mitAI(in) 246 | ret = strings.FieldsFunc(response, sf) 247 | } 248 | return 249 | } 250 | -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf build 3 | mkdir build 4 | cd build 5 | gox github.com/evolsnow/robot 6 | mkdir -p out 7 | for FILE in * 8 | do 9 | DIR="${FILE%.*}"_v1_3 10 | mkdir -p "$DIR" 11 | cp "${FILE}" "$DIR" 12 | cp ../../config.json "$DIR" 13 | zip -r "${DIR%.*}.zip" "$DIR" 14 | mv "${DIR%.*}.zip" out/ 15 | tar -cvzf "${DIR%.*}.tar.gz" "$DIR" 16 | mv "${DIR%.*}.tar.gz" out/ 17 | rm -rf "$DIR" 18 | rm out/out* 19 | done 20 | -------------------------------------------------------------------------------- /source.go: -------------------------------------------------------------------------------- 1 | // get show and movie source download links 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/http/cookiejar" 11 | "net/url" 12 | "regexp" 13 | "strings" 14 | 15 | "github.com/PuerkitoBio/goquery" 16 | ) 17 | 18 | type Media struct { 19 | Name string 20 | Size string 21 | Link string 22 | } 23 | 24 | //zmz.tv needs to login before downloading 25 | var zmzClient http.Client 26 | 27 | //get movie resource from lbl 28 | func getMovieFromLBL(movie string, results chan<- string) { 29 | var id string 30 | resp, err := http.Get("http://www.lbldy.com/search/" + movie) 31 | if err != nil { 32 | log.Println("get movie from lbl err:", err) 33 | return 34 | } 35 | defer resp.Body.Close() 36 | body, err := ioutil.ReadAll(resp.Body) 37 | if err != nil { 38 | log.Println("lbl resp read err:", err) 39 | return 40 | } 41 | re, _ := regexp.Compile("
") 42 | //find first match case 43 | firstId := re.FindSubmatch(body) 44 | if len(firstId) == 0 { 45 | results <- fmt.Sprintf("No results for *%s* from LBL", movie) 46 | return 47 | } else { 48 | id = string(firstId[1]) 49 | var ms []Media 50 | resp, err = http.Get("http://www.lbldy.com/movie/" + id + ".html") 51 | if err != nil { 52 | return 53 | } 54 | defer resp.Body.Close() 55 | doc, err := goquery.NewDocumentFromReader(io.Reader(resp.Body)) 56 | if err != nil { 57 | return 58 | } 59 | doc.Find("p").Each(func(i int, selection *goquery.Selection) { 60 | name := selection.Find("a").Text() 61 | link, _ := selection.Find("a").Attr("href") 62 | if strings.HasPrefix(link, "ed2k") || strings.HasPrefix(link, "magnet") || strings.HasPrefix(link, "thunder") { 63 | m := Media{ 64 | Name: name, 65 | Link: link, 66 | } 67 | ms = append(ms, m) 68 | } 69 | }) 70 | 71 | if len(ms) == 0 { 72 | results <- fmt.Sprintf("No results for *%s* from LBL", movie) 73 | return 74 | } 75 | ret := "Results from LBL:\n\n" 76 | for i, m := range ms { 77 | ret += fmt.Sprintf("*%s*\n```%s```\n\n", m.Name, m.Link) 78 | //when results are too large, we split it. 79 | if i%4 == 0 && i < len(ms)-1 && i > 0 { 80 | results <- ret 81 | ret = fmt.Sprintf("*LBL Part %d*\n\n", i/4+1) 82 | } 83 | } 84 | results <- ret 85 | } 86 | } 87 | 88 | //get movie resource from zmz 89 | func getMovieFromZMZ(movie string, results chan<- string) { 90 | loginZMZ() 91 | if ms := getZMZResource(movie, "0", "0"); ms == nil { 92 | results <- fmt.Sprintf("No results for *%s* from ZMZ", movie) 93 | return 94 | } else { 95 | ret := "Results from ZMZ:\n\n" 96 | for i, m := range ms { 97 | name := m.Name 98 | size := m.Size 99 | link := m.Link 100 | ret += fmt.Sprintf("*%s* %s\n```%s```\n\n", name, size, link) 101 | if i%4 == 0 && i < len(ms)-1 && i > 0 { 102 | results <- ret 103 | ret = fmt.Sprintf("*ZMZ Part %d*\n\n", i/4+1) 104 | } 105 | } 106 | results <- ret 107 | } 108 | return 109 | } 110 | 111 | //get American show resource from zmz 112 | func getShowFromZMZ(show, s, e string, results chan<- string) bool { 113 | loginZMZ() 114 | ms := getZMZResource(show, s, e) 115 | if ms == nil { 116 | results <- fmt.Sprintf("No results found for *S%sE%s*", s, e) 117 | return false 118 | } 119 | for _, m := range ms { 120 | name := m.Name 121 | size := m.Size 122 | link := m.Link 123 | results <- fmt.Sprintf("*ZMZ %s* %s\n```%s```\n\n", name, size, link) 124 | } 125 | return true 126 | } 127 | 128 | //get show and get movie from zmz both uses this function 129 | func getZMZResource(name, season, episode string) []Media { 130 | id := getZMZResourceId(name) 131 | log.Println("resource id:", id) 132 | if id == "" { 133 | return nil 134 | } 135 | resourceURL := "http://www.zmz2017.com/resource/list/" + id 136 | resp, err := zmzClient.Get(resourceURL) 137 | if err != nil { 138 | log.Println("get zmz resource err:", err) 139 | return nil 140 | } 141 | defer resp.Body.Close() 142 | //1.name 2.size 3.link 143 | var ms []Media 144 | doc, err := goquery.NewDocumentFromReader(io.Reader(resp.Body)) 145 | if err != nil { 146 | log.Println("go query err:", err) 147 | return nil 148 | } 149 | doc.Find("li.clearfix").Each(func(i int, selection *goquery.Selection) { 150 | s, _ := selection.Attr("season") 151 | e, _ := selection.Attr("episode") 152 | if s != season || e != episode { 153 | return 154 | } 155 | name := selection.Find(".fl a.lk").Text() 156 | link, _ := selection.Find(".fr a").Attr("href") 157 | var size string 158 | if strings.HasPrefix(link, "ed2k") || strings.HasPrefix(link, "magnet") { 159 | size = selection.Find(".fl font.f3").Text() 160 | if size == "" || size == "0" { 161 | size = "(?)" 162 | } 163 | m := Media{ 164 | Name: name, 165 | Link: link, 166 | Size: size, 167 | } 168 | ms = append(ms, m) 169 | } 170 | }) 171 | return ms 172 | } 173 | 174 | //get source id before find zmz source 175 | func getZMZResourceId(name string) (id string) { 176 | queryURL := fmt.Sprintf("http://www.zmz2017.com/search?keyword=%s&type=resource", name) 177 | re, _ := regexp.Compile(`
`) 178 | resp, err := zmzClient.Get(queryURL) 179 | if err != nil { 180 | log.Println("zmz resource id err:", err) 181 | return 182 | } 183 | defer resp.Body.Close() 184 | body, _ := ioutil.ReadAll(resp.Body) 185 | //find first match case 186 | firstId := re.FindSubmatch(body) 187 | if len(firstId) == 0 { 188 | return 189 | } 190 | id = string(firstId[1]) 191 | return 192 | } 193 | 194 | //login zmz first because zmz don't allow login at different browsers. 195 | func loginZMZ() { 196 | gCookieJar, _ := cookiejar.New(nil) 197 | zmzURL := "http://www.zmz2017.com/User/Login/ajaxLogin" 198 | zmzClient = http.Client{ 199 | Jar: gCookieJar, 200 | } 201 | //post with my public account, you can use it as well. 202 | resp, err := zmzClient.PostForm(zmzURL, url.Values{"account": {"evol4snow"}, "password": {"104545"}, "remember": {"0"}}) 203 | if err != nil { 204 | return 205 | } 206 | resp.Body.Close() 207 | } 208 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "unicode" 4 | 5 | //if the words contain any chinese character, return true 6 | func chinese(words string) (zh bool) { 7 | for _, r := range words { 8 | if unicode.Is(unicode.Scripts["Han"], r) { 9 | zh = true 10 | break 11 | } 12 | } 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestChinese(t *testing.T) { 6 | heads := []string{"你好 世界", "你好 world", "hello 世界", "1 世界"} 7 | tails := []string{"hello world", "1 world"} 8 | 9 | for _, word := range heads { 10 | if !chinese(word) { 11 | t.Error("should be chinese:", word) 12 | } 13 | } 14 | for _, word := range tails { 15 | if chinese(word) { 16 | t.Error("should not be chinese:", word) 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /web/annyang.js: -------------------------------------------------------------------------------- 1 | //! annyang 2 | //! version : 2.1.0 3 | //! author : Tal Ater @TalAter 4 | //! license : MIT 5 | //! https://www.TalAter.com/annyang/ 6 | 7 | (function (undefined) { 8 | "use strict"; 9 | 10 | /** 11 | * # Quick Tutorial, Intro and Demos 12 | * 13 | * The quickest way to get started is to visit the [annyang homepage](https://www.talater.com/annyang/). 14 | * 15 | * For a more in-depth look at annyang, read on. 16 | * 17 | * # API Reference 18 | */ 19 | 20 | // Save a reference to the global object (window in the browser) 21 | var root = this; 22 | 23 | // Get the SpeechRecognition object, while handling browser prefixes 24 | var SpeechRecognition = root.webkitSpeechRecognition || 25 | root.SpeechRecognition || 26 | root.mozSpeechRecognition || 27 | root.msSpeechRecognition || 28 | root.oSpeechRecognition; 29 | 30 | // Check browser support 31 | // This is done as early as possible, to make it as fast as possible for unsupported browsers 32 | if (!SpeechRecognition) { 33 | root.annyang = null; 34 | return undefined; 35 | } 36 | 37 | var commandsList = []; 38 | var recognition; 39 | var callbacks = { start: [], error: [], end: [], result: [], resultMatch: [], resultNoMatch: [], errorNetwork: [], errorPermissionBlocked: [], errorPermissionDenied: [] }; 40 | var autoRestart; 41 | var lastStartedAt = 0; 42 | var debugState = true; 43 | var debugStyle = 'font-weight: bold; color: #00f;'; 44 | var pauseListening = false; 45 | var isListening = false; 46 | 47 | // The command matching code is a modified version of Backbone.Router by Jeremy Ashkenas, under the MIT license. 48 | var optionalParam = /\s*\((.*?)\)\s*/g; 49 | var optionalRegex = /(\(\?:[^)]+\))\?/g; 50 | var namedParam = /(\(\?)?:\w+/g; 51 | var splatParam = /\*\w+/g; 52 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#]/g; 53 | var commandToRegExp = function(command) { 54 | command = command.replace(escapeRegExp, '\\$&') 55 | .replace(optionalParam, '(?:$1)?') 56 | .replace(namedParam, function(match, optional) { 57 | return optional ? match : '([^\\s]+)'; 58 | }) 59 | .replace(splatParam, '(.*?)') 60 | .replace(optionalRegex, '\\s*$1?\\s*'); 61 | return new RegExp('^' + command + '$', 'i'); 62 | }; 63 | 64 | // This method receives an array of callbacks to iterate over, and invokes each of them 65 | var invokeCallbacks = function(callbacks) { 66 | var args = Array.prototype.slice.call(arguments, 1); 67 | callbacks.forEach(function(callback) { 68 | callback.callback.apply(callback.context, args); 69 | }); 70 | }; 71 | 72 | var isInitialized = function() { 73 | return recognition !== undefined; 74 | }; 75 | 76 | var initIfNeeded = function() { 77 | if (!isInitialized()) { 78 | root.annyang.init({}, false); 79 | } 80 | }; 81 | 82 | var registerCommand = function(command, cb, phrase) { 83 | commandsList.push({ command: command, callback: cb, originalPhrase: phrase }); 84 | if (debugState) { 85 | root.console.log('Command successfully loaded: %c'+phrase, debugStyle); 86 | } 87 | }; 88 | root.annyang = { 89 | 90 | /** 91 | * Initialize annyang with a list of commands to recognize. 92 | * 93 | * #### Examples: 94 | * ````javascript 95 | * var commands = {'hello :name': helloFunction}; 96 | * var commands2 = {'hi': helloFunction}; 97 | * 98 | * // initialize annyang, overwriting any previously added commands 99 | * annyang.init(commands, true); 100 | * // adds an additional command without removing the previous commands 101 | * annyang.init(commands2, false); 102 | * ```` 103 | * As of v1.1.0 it is no longer required to call init(). Just start() listening whenever you want, and addCommands() whenever, and as often as you like. 104 | * 105 | * @param {Object} commands - Commands that annyang should listen to 106 | * @param {Boolean} [resetCommands=true] - Remove all commands before initializing? 107 | * @method init 108 | * @deprecated 109 | * @see [Commands Object](#commands-object) 110 | */ 111 | init: function(commands, resetCommands) { 112 | 113 | // resetCommands defaults to true 114 | if (resetCommands === undefined) { 115 | resetCommands = true; 116 | } else { 117 | resetCommands = !!resetCommands; 118 | } 119 | 120 | // Abort previous instances of recognition already running 121 | if (recognition && recognition.abort) { 122 | recognition.abort(); 123 | } 124 | 125 | // initiate SpeechRecognition 126 | recognition = new SpeechRecognition(); 127 | 128 | // Set the max number of alternative transcripts to try and match with a command 129 | recognition.maxAlternatives = 1; 130 | 131 | // In HTTPS, turn off continuous mode for faster results. 132 | // In HTTP, turn on continuous mode for much slower results, but no repeating security notices 133 | recognition.continuous = root.location.protocol === 'http:'; 134 | 135 | // Sets the language to the default 'en-US'. This can be changed with annyang.setLanguage() 136 | recognition.lang = 'en-US'; 137 | recognition.onstart = function() { 138 | isListening = true; 139 | invokeCallbacks(callbacks.start); 140 | }; 141 | 142 | recognition.onerror = function(event) { 143 | invokeCallbacks(callbacks.error); 144 | switch (event.error) { 145 | case 'network': 146 | invokeCallbacks(callbacks.errorNetwork); 147 | break; 148 | case 'not-allowed': 149 | case 'service-not-allowed': 150 | // if permission to use the mic is denied, turn off auto-restart 151 | autoRestart = false; 152 | // determine if permission was denied by user or automatically. 153 | if (new Date().getTime()-lastStartedAt < 200) { 154 | invokeCallbacks(callbacks.errorPermissionBlocked); 155 | } else { 156 | invokeCallbacks(callbacks.errorPermissionDenied); 157 | } 158 | break; 159 | } 160 | }; 161 | 162 | recognition.onend = function() { 163 | isListening = false; 164 | invokeCallbacks(callbacks.end); 165 | // annyang will auto restart if it is closed automatically and not by user action. 166 | if (autoRestart) { 167 | // play nicely with the browser, and never restart annyang automatically more than once per second 168 | var timeSinceLastStart = new Date().getTime()-lastStartedAt; 169 | if (timeSinceLastStart < 1000) { 170 | setTimeout(root.annyang.start, 1000-timeSinceLastStart); 171 | } else { 172 | root.annyang.start(); 173 | } 174 | } 175 | }; 176 | 177 | recognition.onresult = function(event) { 178 | if(pauseListening) { 179 | if (debugState) { 180 | root.console.log('Speech heard, but annyang is paused'); 181 | } 182 | return false; 183 | } 184 | 185 | // Map the results to an array 186 | var SpeechRecognitionResult = event.results[event.resultIndex]; 187 | var results = []; 188 | for (var k = 0; k 0) { 320 | debugState = !!newState; 321 | } else { 322 | debugState = true; 323 | } 324 | }, 325 | 326 | /** 327 | * Set the language the user will speak in. If this method is not called, defaults to 'en-US'. 328 | * 329 | * @param {String} language - The language (locale) 330 | * @method setLanguage 331 | * @see [Languages](#languages) 332 | */ 333 | setLanguage: function(language) { 334 | initIfNeeded(); 335 | recognition.lang = language; 336 | }, 337 | 338 | /** 339 | * Add commands that annyang will respond to. Similar in syntax to init(), but doesn't remove existing commands. 340 | * 341 | * #### Examples: 342 | * ````javascript 343 | * var commands = {'hello :name': helloFunction, 'howdy': helloFunction}; 344 | * var commands2 = {'hi': helloFunction}; 345 | * 346 | * annyang.addCommands(commands); 347 | * annyang.addCommands(commands2); 348 | * // annyang will now listen to all three commands 349 | * ```` 350 | * 351 | * @param {Object} commands - Commands that annyang should listen to 352 | * @method addCommands 353 | * @see [Commands Object](#commands-object) 354 | */ 355 | addCommands: function(commands) { 356 | var cb; 357 | 358 | initIfNeeded(); 359 | 360 | for (var phrase in commands) { 361 | if (commands.hasOwnProperty(phrase)) { 362 | cb = root[commands[phrase]] || commands[phrase]; 363 | if (typeof cb === 'function') { 364 | // convert command to regex then register the command 365 | registerCommand(commandToRegExp(phrase), cb, phrase); 366 | } else if (typeof cb === 'object' && cb.regexp instanceof RegExp) { 367 | // register the command 368 | registerCommand(new RegExp(cb.regexp.source, 'i'), cb.callback, phrase); 369 | } else { 370 | if (debugState) { 371 | root.console.log('Can not register command: %c'+phrase, debugStyle); 372 | } 373 | continue; 374 | } 375 | } 376 | } 377 | }, 378 | 379 | /** 380 | * Remove existing commands. Called with a single phrase, array of phrases, or methodically. Pass no params to remove all commands. 381 | * 382 | * #### Examples: 383 | * ````javascript 384 | * var commands = {'hello': helloFunction, 'howdy': helloFunction, 'hi': helloFunction}; 385 | * 386 | * // Remove all existing commands 387 | * annyang.removeCommands(); 388 | * 389 | * // Add some commands 390 | * annyang.addCommands(commands); 391 | * 392 | * // Don't respond to hello 393 | * annyang.removeCommands('hello'); 394 | * 395 | * // Don't respond to howdy or hi 396 | * annyang.removeCommands(['howdy', 'hi']); 397 | * ```` 398 | * @param {String|Array|Undefined} [commandsToRemove] - Commands to remove 399 | * @method removeCommands 400 | */ 401 | removeCommands: function(commandsToRemove) { 402 | if (commandsToRemove === undefined) { 403 | commandsList = []; 404 | return; 405 | } 406 | commandsToRemove = Array.isArray(commandsToRemove) ? commandsToRemove : [commandsToRemove]; 407 | commandsList = commandsList.filter(function(command) { 408 | for (var i = 0; i 508 | * var commands = { 509 | * // annyang will capture anything after a splat (*) and pass it to the function. 510 | * // e.g. saying "Show me Batman and Robin" will call showFlickr('Batman and Robin'); 511 | * 'show me *tag': showFlickr, 512 | * 513 | * // A named variable is a one word variable, that can fit anywhere in your command. 514 | * // e.g. saying "calculate October stats" will call calculateStats('October'); 515 | * 'calculate :month stats': calculateStats, 516 | * 517 | * // By defining a part of the following command as optional, annyang will respond 518 | * // to both: "say hello to my little friend" as well as "say hello friend" 519 | * 'say hello (to my little) friend': greeting 520 | * }; 521 | * 522 | * var showFlickr = function(tag) { 523 | * var url = 'http://api.flickr.com/services/rest/?tags='+tag; 524 | * $.getJSON(url); 525 | * } 526 | * 527 | * var calculateStats = function(month) { 528 | * $('#stats').text('Statistics for '+month); 529 | * } 530 | * 531 | * var greeting = function() { 532 | * $('#greeting').text('Hello!'); 533 | * } 534 | * 535 | * ```` 536 | * 537 | * ### Using Regular Expressions in commands 538 | * For advanced commands, you can pass a regular expression object, instead of 539 | * a simple string command. 540 | * 541 | * This is done by passing an object containing two properties: `regexp`, and 542 | * `callback` instead of the function. 543 | * 544 | * #### Examples: 545 | * ````javascript 546 | * var calculateFunction = function(month) { console.log(month); } 547 | * var commands = { 548 | * // This example will accept any word as the "month" 549 | * 'calculate :month stats': calculateFunction, 550 | * // This example will only accept months which are at the start of a quarter 551 | * 'calculate :quarter stats': {'regexp': /^calculate (January|April|July|October) stats$/, 'callback': calculateFunction} 552 | * } 553 | ```` 554 | * 555 | * ## Languages 556 | * 557 | * While there isn't an official list of supported languages (cultures? locales?), here is a list based on [anecdotal evidence](http://stackoverflow.com/a/14302134/338039). 558 | * 559 | * * Afrikaans `af` 560 | * * Basque `eu` 561 | * * Bulgarian `bg` 562 | * * Catalan `ca` 563 | * * Arabic (Egypt) `ar-EG` 564 | * * Arabic (Jordan) `ar-JO` 565 | * * Arabic (Kuwait) `ar-KW` 566 | * * Arabic (Lebanon) `ar-LB` 567 | * * Arabic (Qatar) `ar-QA` 568 | * * Arabic (UAE) `ar-AE` 569 | * * Arabic (Morocco) `ar-MA` 570 | * * Arabic (Iraq) `ar-IQ` 571 | * * Arabic (Algeria) `ar-DZ` 572 | * * Arabic (Bahrain) `ar-BH` 573 | * * Arabic (Lybia) `ar-LY` 574 | * * Arabic (Oman) `ar-OM` 575 | * * Arabic (Saudi Arabia) `ar-SA` 576 | * * Arabic (Tunisia) `ar-TN` 577 | * * Arabic (Yemen) `ar-YE` 578 | * * Czech `cs` 579 | * * Dutch `nl-NL` 580 | * * English (Australia) `en-AU` 581 | * * English (Canada) `en-CA` 582 | * * English (India) `en-IN` 583 | * * English (New Zealand) `en-NZ` 584 | * * English (South Africa) `en-ZA` 585 | * * English(UK) `en-GB` 586 | * * English(US) `en-US` 587 | * * Finnish `fi` 588 | * * French `fr-FR` 589 | * * Galician `gl` 590 | * * German `de-DE` 591 | * * Hebrew `he` 592 | * * Hungarian `hu` 593 | * * Icelandic `is` 594 | * * Italian `it-IT` 595 | * * Indonesian `id` 596 | * * Japanese `ja` 597 | * * Korean `ko` 598 | * * Latin `la` 599 | * * Mandarin Chinese `zh-CN` 600 | * * Traditional Taiwan `zh-TW` 601 | * * Simplified China zh-CN `?` 602 | * * Simplified Hong Kong `zh-HK` 603 | * * Yue Chinese (Traditional Hong Kong) `zh-yue` 604 | * * Malaysian `ms-MY` 605 | * * Norwegian `no-NO` 606 | * * Polish `pl` 607 | * * Pig Latin `xx-piglatin` 608 | * * Portuguese `pt-PT` 609 | * * Portuguese (Brasil) `pt-BR` 610 | * * Romanian `ro-RO` 611 | * * Russian `ru` 612 | * * Serbian `sr-SP` 613 | * * Slovak `sk` 614 | * * Spanish (Argentina) `es-AR` 615 | * * Spanish (Bolivia) `es-BO` 616 | * * Spanish (Chile) `es-CL` 617 | * * Spanish (Colombia) `es-CO` 618 | * * Spanish (Costa Rica) `es-CR` 619 | * * Spanish (Dominican Republic) `es-DO` 620 | * * Spanish (Ecuador) `es-EC` 621 | * * Spanish (El Salvador) `es-SV` 622 | * * Spanish (Guatemala) `es-GT` 623 | * * Spanish (Honduras) `es-HN` 624 | * * Spanish (Mexico) `es-MX` 625 | * * Spanish (Nicaragua) `es-NI` 626 | * * Spanish (Panama) `es-PA` 627 | * * Spanish (Paraguay) `es-PY` 628 | * * Spanish (Peru) `es-PE` 629 | * * Spanish (Puerto Rico) `es-PR` 630 | * * Spanish (Spain) `es-ES` 631 | * * Spanish (US) `es-US` 632 | * * Spanish (Uruguay) `es-UY` 633 | * * Spanish (Venezuela) `es-VE` 634 | * * Swedish `sv-SE` 635 | * * Turkish `tr` 636 | * * Zulu `zu` 637 | * 638 | * ## Developing 639 | * 640 | * Prerequisities: node.js 641 | * 642 | * First, install dependencies in your local annyang copy: 643 | * 644 | * npm install 645 | * 646 | * Make sure to run the default grunt task after each change to annyang.js. This can also be done automatically by running: 647 | * 648 | * grunt watch 649 | * 650 | * You can also run a local server for testing your work with: 651 | * 652 | * grunt dev 653 | * 654 | * Point your browser to `https://localhost:8443/demo/` to see the demo page. 655 | * Since it's using self-signed certificate, you might need to click *"Proceed Anyway"*. 656 | * 657 | * For more info, check out the [CONTRIBUTING](https://github.com/TalAter/annyang/blob/master/CONTRIBUTING.md) file 658 | * 659 | */ 660 | -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolsnow/robot/af8db51103000ac7fad58f56144993aa1ca76a5b/web/favicon.ico -------------------------------------------------------------------------------- /web/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolsnow/robot/af8db51103000ac7fad58f56144993aa1ca76a5b/web/github.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Samaritan 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

 

19 |
20 |
21 |
22 | 23 | 24 |
25 | 26 |

28 | 29 |

30 |
31 | 32 | 33 |
34 |
35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /web/jquery-1.11.0.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.11.0 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ 2 | !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k="".trim,l={},m="1.11.0",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(l.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:k&&!k.call("\ufeff\xa0")?function(a){return null==a?"":k.call(a)}:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||n.guid++,e):void 0},now:function(){return+new Date},support:l}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s="sizzle"+-new Date,t=a.document,u=0,v=0,w=eb(),x=eb(),y=eb(),z=function(a,b){return a===b&&(j=!0),0},A="undefined",B=1<<31,C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=D.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",M=L.replace("w","w#"),N="\\["+K+"*("+L+")"+K+"*(?:([*^$|!~]?=)"+K+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+M+")|)|)"+K+"*\\]",O=":("+L+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+N.replace(3,8)+")*)|.*)\\)|)",P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(O),U=new RegExp("^"+M+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L.replace("w","w*")+")"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=/'|\\/g,ab=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),bb=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{G.apply(D=H.call(t.childNodes),t.childNodes),D[t.childNodes.length].nodeType}catch(cb){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function db(a,b,d,e){var f,g,h,i,j,m,p,q,u,v;if((b?b.ownerDocument||b:t)!==l&&k(b),b=b||l,d=d||[],!a||"string"!=typeof a)return d;if(1!==(i=b.nodeType)&&9!==i)return[];if(n&&!e){if(f=Z.exec(a))if(h=f[1]){if(9===i){if(g=b.getElementById(h),!g||!g.parentNode)return d;if(g.id===h)return d.push(g),d}else if(b.ownerDocument&&(g=b.ownerDocument.getElementById(h))&&r(b,g)&&g.id===h)return d.push(g),d}else{if(f[2])return G.apply(d,b.getElementsByTagName(a)),d;if((h=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(h)),d}if(c.qsa&&(!o||!o.test(a))){if(q=p=s,u=b,v=9===i&&a,1===i&&"object"!==b.nodeName.toLowerCase()){m=ob(a),(p=b.getAttribute("id"))?q=p.replace(_,"\\$&"):b.setAttribute("id",q),q="[id='"+q+"'] ",j=m.length;while(j--)m[j]=q+pb(m[j]);u=$.test(a)&&mb(b.parentNode)||b,v=m.join(",")}if(v)try{return G.apply(d,u.querySelectorAll(v)),d}catch(w){}finally{p||b.removeAttribute("id")}}}return xb(a.replace(P,"$1"),b,d,e)}function eb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function fb(a){return a[s]=!0,a}function gb(a){var b=l.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function hb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function ib(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||B)-(~a.sourceIndex||B);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function jb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function kb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function lb(a){return fb(function(b){return b=+b,fb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function mb(a){return a&&typeof a.getElementsByTagName!==A&&a}c=db.support={},f=db.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},k=db.setDocument=function(a){var b,e=a?a.ownerDocument||a:t,g=e.defaultView;return e!==l&&9===e.nodeType&&e.documentElement?(l=e,m=e.documentElement,n=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){k()},!1):g.attachEvent&&g.attachEvent("onunload",function(){k()})),c.attributes=gb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=gb(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(e.getElementsByClassName)&&gb(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=gb(function(a){return m.appendChild(a).id=s,!e.getElementsByName||!e.getElementsByName(s).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==A&&n){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){var c=typeof a.getAttributeNode!==A&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==A?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==A&&n?b.getElementsByClassName(a):void 0},p=[],o=[],(c.qsa=Y.test(e.querySelectorAll))&&(gb(function(a){a.innerHTML="",a.querySelectorAll("[t^='']").length&&o.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||o.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll(":checked").length||o.push(":checked")}),gb(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&o.push("name"+K+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||o.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),o.push(",.*:")})),(c.matchesSelector=Y.test(q=m.webkitMatchesSelector||m.mozMatchesSelector||m.oMatchesSelector||m.msMatchesSelector))&&gb(function(a){c.disconnectedMatch=q.call(a,"div"),q.call(a,"[s!='']:x"),p.push("!=",O)}),o=o.length&&new RegExp(o.join("|")),p=p.length&&new RegExp(p.join("|")),b=Y.test(m.compareDocumentPosition),r=b||Y.test(m.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},z=b?function(a,b){if(a===b)return j=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===t&&r(t,a)?-1:b===e||b.ownerDocument===t&&r(t,b)?1:i?I.call(i,a)-I.call(i,b):0:4&d?-1:1)}:function(a,b){if(a===b)return j=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],k=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:i?I.call(i,a)-I.call(i,b):0;if(f===g)return ib(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)k.unshift(c);while(h[d]===k[d])d++;return d?ib(h[d],k[d]):h[d]===t?-1:k[d]===t?1:0},e):l},db.matches=function(a,b){return db(a,null,null,b)},db.matchesSelector=function(a,b){if((a.ownerDocument||a)!==l&&k(a),b=b.replace(S,"='$1']"),!(!c.matchesSelector||!n||p&&p.test(b)||o&&o.test(b)))try{var d=q.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return db(b,l,null,[a]).length>0},db.contains=function(a,b){return(a.ownerDocument||a)!==l&&k(a),r(a,b)},db.attr=function(a,b){(a.ownerDocument||a)!==l&&k(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!n):void 0;return void 0!==f?f:c.attributes||!n?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},db.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},db.uniqueSort=function(a){var b,d=[],e=0,f=0;if(j=!c.detectDuplicates,i=!c.sortStable&&a.slice(0),a.sort(z),j){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return i=null,a},e=db.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=db.selectors={cacheLength:50,createPseudo:fb,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ab,bb),a[3]=(a[4]||a[5]||"").replace(ab,bb),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||db.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&db.error(a[0]),a},PSEUDO:function(a){var b,c=!a[5]&&a[2];return V.CHILD.test(a[0])?null:(a[3]&&void 0!==a[4]?a[2]=a[4]:c&&T.test(c)&&(b=ob(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ab,bb).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=w[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&w(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==A&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=db.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),t=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&t){k=q[s]||(q[s]={}),j=k[a]||[],n=j[0]===u&&j[1],m=j[0]===u&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[u,n,m];break}}else if(t&&(j=(b[s]||(b[s]={}))[a])&&j[0]===u)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(t&&((l[s]||(l[s]={}))[a]=[u,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||db.error("unsupported pseudo: "+a);return e[s]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?fb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:fb(function(a){var b=[],c=[],d=g(a.replace(P,"$1"));return d[s]?fb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:fb(function(a){return function(b){return db(a,b).length>0}}),contains:fb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:fb(function(a){return U.test(a||"")||db.error("unsupported lang: "+a),a=a.replace(ab,bb).toLowerCase(),function(b){var c;do if(c=n?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===m},focus:function(a){return a===l.activeElement&&(!l.hasFocus||l.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:lb(function(){return[0]}),last:lb(function(a,b){return[b-1]}),eq:lb(function(a,b,c){return[0>c?c+b:c]}),even:lb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:lb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:lb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:lb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function qb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=v++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[u,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[s]||(b[s]={}),(h=i[d])&&h[0]===u&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function rb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function sb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function tb(a,b,c,d,e,f){return d&&!d[s]&&(d=tb(d)),e&&!e[s]&&(e=tb(e,f)),fb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||wb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:sb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=sb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=sb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ub(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],i=g||d.relative[" "],j=g?1:0,k=qb(function(a){return a===b},i,!0),l=qb(function(a){return I.call(b,a)>-1},i,!0),m=[function(a,c,d){return!g&&(d||c!==h)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>j;j++)if(c=d.relative[a[j].type])m=[qb(rb(m),c)];else{if(c=d.filter[a[j].type].apply(null,a[j].matches),c[s]){for(e=++j;f>e;e++)if(d.relative[a[e].type])break;return tb(j>1&&rb(m),j>1&&pb(a.slice(0,j-1).concat({value:" "===a[j-2].type?"*":""})).replace(P,"$1"),c,e>j&&ub(a.slice(j,e)),f>e&&ub(a=a.slice(e)),f>e&&pb(a))}m.push(c)}return rb(m)}function vb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,i,j,k){var m,n,o,p=0,q="0",r=f&&[],s=[],t=h,v=f||e&&d.find.TAG("*",k),w=u+=null==t?1:Math.random()||.1,x=v.length;for(k&&(h=g!==l&&g);q!==x&&null!=(m=v[q]);q++){if(e&&m){n=0;while(o=a[n++])if(o(m,g,i)){j.push(m);break}k&&(u=w)}c&&((m=!o&&m)&&p--,f&&r.push(m))}if(p+=q,c&&q!==p){n=0;while(o=b[n++])o(r,s,g,i);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=E.call(j));s=sb(s)}G.apply(j,s),k&&!f&&s.length>0&&p+b.length>1&&db.uniqueSort(j)}return k&&(u=w,h=t),r};return c?fb(f):f}g=db.compile=function(a,b){var c,d=[],e=[],f=y[a+" "];if(!f){b||(b=ob(a)),c=b.length;while(c--)f=ub(b[c]),f[s]?d.push(f):e.push(f);f=y(a,vb(e,d))}return f};function wb(a,b,c){for(var d=0,e=b.length;e>d;d++)db(a,b[d],c);return c}function xb(a,b,e,f){var h,i,j,k,l,m=ob(a);if(!f&&1===m.length){if(i=m[0]=m[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&c.getById&&9===b.nodeType&&n&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(ab,bb),b)||[])[0],!b)return e;a=a.slice(i.shift().value.length)}h=V.needsContext.test(a)?0:i.length;while(h--){if(j=i[h],d.relative[k=j.type])break;if((l=d.find[k])&&(f=l(j.matches[0].replace(ab,bb),$.test(i[0].type)&&mb(b.parentNode)||b))){if(i.splice(h,1),a=f.length&&pb(i),!a)return G.apply(e,f),e;break}}}return g(a,m)(f,b,!n,e,$.test(a)&&mb(b.parentNode)||b),e}return c.sortStable=s.split("").sort(z).join("")===s,c.detectDuplicates=!!j,k(),c.sortDetached=gb(function(a){return 1&a.compareDocumentPosition(l.createElement("div"))}),gb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||hb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&gb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||hb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),gb(function(a){return null==a.getAttribute("disabled")})||hb(J,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),db}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=a.document,A=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,B=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:A.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:z,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=z.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return y.find(a);this.length=1,this[0]=d}return this.context=z,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};B.prototype=n.fn,y=n(z);var C=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!n(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function E(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return E(a,"nextSibling")},prev:function(a){return E(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(D[a]||(e=n.unique(e)),C.test(a)&&(e=e.reverse())),this.pushStack(e)}});var F=/\S+/g,G={};function H(a){var b=G[a]={};return n.each(a.match(F)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?G[a]||H(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&n.each(arguments,function(a,c){var d;while((d=n.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){if(a===!0?!--n.readyWait:!n.isReady){if(!z.body)return setTimeout(n.ready);n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(z,[n]),n.fn.trigger&&n(z).trigger("ready").off("ready"))}}});function J(){z.addEventListener?(z.removeEventListener("DOMContentLoaded",K,!1),a.removeEventListener("load",K,!1)):(z.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(z.addEventListener||"load"===event.type||"complete"===z.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===z.readyState)setTimeout(n.ready);else if(z.addEventListener)z.addEventListener("DOMContentLoaded",K,!1),a.addEventListener("load",K,!1);else{z.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&z.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!n.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}J(),n.ready()}}()}return I.promise(b)};var L="undefined",M;for(M in n(l))break;l.ownLast="0"!==M,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c=z.getElementsByTagName("body")[0];c&&(a=z.createElement("div"),a.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",b=z.createElement("div"),c.appendChild(a).appendChild(b),typeof b.style.zoom!==L&&(b.style.cssText="border:0;margin:0;width:1px;padding:1px;display:inline;zoom:1",(l.inlineBlockNeedsLayout=3===b.offsetWidth)&&(c.style.zoom=1)),c.removeChild(a),a=b=null)}),function(){var a=z.createElement("div");if(null==l.deleteExpando){l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}}a=null}(),n.acceptData=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(n.acceptData(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f 3 | }}function S(a,b,c){if(n.acceptData(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d]));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},X=/^(?:checkbox|radio)$/i;!function(){var a=z.createDocumentFragment(),b=z.createElement("div"),c=z.createElement("input");if(b.setAttribute("className","t"),b.innerHTML="
a",l.leadingWhitespace=3===b.firstChild.nodeType,l.tbody=!b.getElementsByTagName("tbody").length,l.htmlSerialize=!!b.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==z.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,a.appendChild(c),l.appendChecked=c.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,a.appendChild(b),b.innerHTML="",l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){l.noCloneEvent=!1}),b.cloneNode(!0).click()),null==l.deleteExpando){l.deleteExpando=!0;try{delete b.test}catch(d){l.deleteExpando=!1}}a=b=c=null}(),function(){var b,c,d=z.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),l[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var Y=/^(?:input|select|textarea)$/i,Z=/^key/,$=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,ab=/^([^.]*)(?:\.(.+)|)$/;function bb(){return!0}function cb(){return!1}function db(){try{return z.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof n===L||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(F)||[""],h=b.length;while(h--)f=ab.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(F)||[""],j=b.length;while(j--)if(h=ab.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,m,o=[d||z],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||z,3!==d.nodeType&&8!==d.nodeType&&!_.test(p+n.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[n.expando]?b:new n.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),k=n.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!n.isWindow(d)){for(i=k.delegateType||p,_.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||z)&&o.push(l.defaultView||l.parentWindow||a)}m=0;while((h=o[m++])&&!b.isPropagationStopped())b.type=m>1?i:k.bindType||p,f=(n._data(h,"events")||{})[b.type]&&n._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&n.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&n.acceptData(d)&&g&&d[p]&&!n.isWindow(d)){l=d[g],l&&(d[g]=null),n.event.triggered=p;try{d[p]()}catch(r){}n.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((n.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?n(c,this).index(i)>=0:n.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ib=/^\s+/,jb=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,kb=/<([\w:]+)/,lb=/\s*$/g,sb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]},tb=eb(z),ub=tb.appendChild(z.createElement("div"));sb.optgroup=sb.option,sb.tbody=sb.tfoot=sb.colgroup=sb.caption=sb.thead,sb.th=sb.td;function vb(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==L?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==L?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,vb(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function wb(a){X.test(a.type)&&(a.defaultChecked=a.checked)}function xb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function yb(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function zb(a){var b=qb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ab(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}function Bb(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Cb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(yb(b).text=a.text,zb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&X.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}n.extend({clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!hb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ub.innerHTML=a.outerHTML,ub.removeChild(f=ub.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=vb(f),h=vb(a),g=0;null!=(e=h[g]);++g)d[g]&&Cb(e,d[g]);if(b)if(c)for(h=h||vb(a),d=d||vb(f),g=0;null!=(e=h[g]);g++)Bb(e,d[g]);else Bb(a,f);return d=vb(f,"script"),d.length>0&&Ab(d,!i&&vb(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k,m=a.length,o=eb(b),p=[],q=0;m>q;q++)if(f=a[q],f||0===f)if("object"===n.type(f))n.merge(p,f.nodeType?[f]:f);else if(mb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(kb.exec(f)||["",""])[1].toLowerCase(),k=sb[i]||sb._default,h.innerHTML=k[1]+f.replace(jb,"<$1>")+k[2],e=k[0];while(e--)h=h.lastChild;if(!l.leadingWhitespace&&ib.test(f)&&p.push(b.createTextNode(ib.exec(f)[0])),!l.tbody){f="table"!==i||lb.test(f)?""!==k[1]||lb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)n.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}n.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),l.appendChecked||n.grep(vb(p,"input"),wb),q=0;while(f=p[q++])if((!d||-1===n.inArray(f,d))&&(g=n.contains(f.ownerDocument,f),h=vb(o.appendChild(f),"script"),g&&Ab(h),c)){e=0;while(f=h[e++])pb.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.deleteExpando,m=n.event.special;null!=(d=a[h]);h++)if((b||n.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k?delete d[i]:typeof d.removeAttribute!==L?d.removeAttribute(i):d[i]=null,c.push(f))}}}),n.fn.extend({text:function(a){return W(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||z).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(vb(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&Ab(vb(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(vb(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return W(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(gb,""):void 0;if(!("string"!=typeof a||nb.test(a)||!l.htmlSerialize&&hb.test(a)||!l.leadingWhitespace&&ib.test(a)||sb[(kb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(jb,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(vb(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(vb(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,k=this.length,m=this,o=k-1,p=a[0],q=n.isFunction(p);if(q||k>1&&"string"==typeof p&&!l.checkClone&&ob.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(k&&(i=n.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=n.map(vb(i,"script"),yb),f=g.length;k>j;j++)d=i,j!==o&&(d=n.clone(d,!0,!0),f&&n.merge(g,vb(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,n.map(g,zb),j=0;f>j;j++)d=g[j],pb.test(d.type||"")&&!n._data(d,"globalEval")&&n.contains(h,d)&&(d.src?n._evalUrl&&n._evalUrl(d.src):n.globalEval((d.text||d.textContent||d.innerHTML||"").replace(rb,"")));i=c=null}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],g=n(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Db,Eb={};function Fb(b,c){var d=n(c.createElement(b)).appendTo(c.body),e=a.getDefaultComputedStyle?a.getDefaultComputedStyle(d[0]).display:n.css(d[0],"display");return d.detach(),e}function Gb(a){var b=z,c=Eb[a];return c||(c=Fb(a,b),"none"!==c&&c||(Db=(Db||n("