func TestHMain(t *testing.T) { reset(t) user, fid, fidT := seedDBUser(t) req := dt.Request{CMD: "Hi", UserID: user.ID} // Test via a UserID byt, err := json.Marshal(req) if err != nil { t.Fatal(err) } c, b := request("POST", "/", byt) if c != http.StatusOK { log.Info(b) t.Fatal("expected", http.StatusOK, "got", c) } if b == "Something went wrong with my wiring... I'll get that fixed up soon." { t.Fatal(`expected "Hi there :)" but got "Something went wrong..."`) } // Test via a FlexID req.UserID = 0 req.FlexID = fid req.FlexIDType = fidT byt, err = json.Marshal(req) if err != nil { t.Fatal(err) } c, b = request("POST", "/", byt) if c != http.StatusOK { log.Info(b) t.Fatal("expected", http.StatusOK, "got", c) } }
func cleanup() { q := `DELETE FROM messages` _, err := p.DB.Exec(q) if err != nil { log.Info("failed to delete messages.", err) } q = `DELETE FROM states` _, err = p.DB.Exec(q) if err != nil { log.Info("failed to delete messages.", err) } }
func writeError(w http.ResponseWriter, err error) { tmp := strings.Replace(err.Error(), `"`, "'", -1) errS := struct{ Msg string }{Msg: tmp} byt, err := json.Marshal(errS) if err != nil { log.Info("failed to marshal error", err) return } w.Header().Set("Content-Type", "application/json") if _, err = w.Write(byt); err != nil { log.Info("failed to write error", err) } }
func TestPluginSearch(t *testing.T) { query := "weather" var byt []byte var err error if testing.Short() { log.Info("stubbing plugin search results in short mode.") byt = []byte(`[{"Name":"Weather","Valid":true}]`) } else { byt, err = searchItsAbot(query) if err != nil { t.Fatal(err) } } var b []byte buf := bytes.NewBuffer(b) if err = outputPluginResults(buf, byt); err != nil { t.Fatal(err) } tmp := buf.String() if !strings.Contains(tmp, "NAME") { t.Fatal(err) } if !strings.Contains(tmp, "Weather") { t.Fatal(err) } }
// fetchTrainingSentences retrieves training sentences from a remote source // (via ITSABOT_URL, which defaults to itsabot.org). func fetchTrainingSentences(pluginID uint64, name string) ([]tSentence, error) { c := &http.Client{Timeout: 10 * time.Second} pid := strconv.FormatUint(pluginID, 10) u := os.Getenv("ITSABOT_URL") + "/api/plugins/train/" + pid req, err := http.NewRequest("GET", u, nil) if err != nil { return nil, err } resp, err := c.Do(req) if err != nil { return nil, err } defer func() { if err = resp.Body.Close(); err != nil { log.Info("failed to close response body.", err) } }() ss := []tSentence{} // This occurs when the plugin has not been published, which we should // ignore on boot. if resp.StatusCode == http.StatusBadRequest { log.Infof("warn: plugin %s has not been published", name) return ss, nil } err = json.NewDecoder(resp.Body).Decode(&ss) return ss, err }
// hapiAdmins returns a list of all admins with the training and manage team // permissions. func hapiAdmins(w http.ResponseWriter, r *http.Request) { if os.Getenv("ABOT_ENV") != "test" { if !isAdmin(w, r) { return } if !isLoggedIn(w, r) { return } } var admins []struct { ID uint64 Name string Email string } q := `SELECT id, name, email FROM users WHERE admin=TRUE` err := db.Select(&admins, q) if err != nil && err != sql.ErrNoRows { writeErrorInternal(w, err) return } b, err := json.Marshal(admins) if err != nil { writeErrorInternal(w, err) return } _, err = w.Write(b) if err != nil { log.Info("failed to write response.", err) } }
// hapiRemoteTokens returns the final six bytes of each auth token used to // authenticate to the remote service and when. func hapiRemoteTokens(w http.ResponseWriter, r *http.Request) { if os.Getenv("ABOT_ENV") != "test" { if !isAdmin(w, r) { return } if !isLoggedIn(w, r) { return } } // We initialize the variable here because we want empty slices to // marshal to [], not null auths := []struct { Token string Email string CreatedAt time.Time PluginIDs dt.Uint64Slice }{} q := `SELECT token, email, pluginids, createdat FROM remotetokens` err := db.Select(&auths, q) if err != nil && err != sql.ErrNoRows { writeErrorInternal(w, err) return } byt, err := json.Marshal(auths) if err != nil { writeErrorInternal(w, err) return } _, err = w.Write(byt) if err != nil { log.Info("failed to write response.", err) } }
func TestHIndex(t *testing.T) { c, b := request("GET", "/", nil) if c != http.StatusOK { log.Info(b) t.Fatal("expected", http.StatusOK, "got", c) } }
func loadLocation(l string) *time.Location { loc, err := time.LoadLocation(l) if err != nil { log.Info("failed to load location.", l) } return loc }
func generatePlugin(l *log.Logger, name string) error { // Log in to get the maintainer email if os.Getenv("ABOT_ENV") != "test" { p := filepath.Join(os.Getenv("HOME"), ".abot.conf") if _, err := os.Stat(p); os.IsNotExist(err) { login() } } // Ensure the name and path are unique globally var words []string var lastIdx int name = strings.Replace(name, " ", "_", -1) dirName := name for i, letter := range name { if i == 0 { continue } if unicode.IsUpper(letter) { words = append(words, name[lastIdx:i]) lastIdx = i } } words = append(words, name[lastIdx:]) dirName = strings.Join(words, "_") dirName = strings.ToLower(dirName) name = strings.ToLower(name) // Create the directory if err := os.Mkdir(dirName, 0744); err != nil { return err } // Generate a plugin.json file if err := buildPluginJSON(dirName, name); err != nil { log.Info("failed to create plugin.json") return err } // Generate name.go and name_test.go files with starter keywords and // state machines if err := buildPluginScaffoldFile(dirName, name); err != nil { log.Info("failed to create plugin scaffold file") return err } return nil }
// ExtractCities efficiently from a user's message. func ExtractCities(db *sqlx.DB, in *dt.Msg) ([]dt.City, error) { // Interface type is used to expand the args in db.Select below. // Although we're only storing strings, []string{} doesn't work. var args []interface{} // Look for "at", "in", "on" prepositions to signal that locations // follow, skipping everything before var start int for i := range in.Stems { switch in.Stems[i] { case "at", "in", "on": start = i break } } // Prepare sentence for iteration tmp := regexNonWords.ReplaceAllString(in.Sentence, "") words := strings.Fields(strings.Title(tmp)) // Iterate through words and bigrams to assemble a DB query for i := start; i < len(words); i++ { args = append(args, words[i]) } bgs := bigrams(words, start) for i := 0; i < len(bgs); i++ { args = append(args, bgs[i]) } cities := []dt.City{} q := `SELECT name, countrycode FROM cities WHERE countrycode='US' AND name IN (?) ORDER BY LENGTH(name) DESC` query, arguments, err := sqlx.In(q, args) query = db.Rebind(query) rows, err := db.Query(query, arguments...) if err != nil { return nil, err } defer func() { if err = rows.Close(); err != nil { log.Info("failed to close db rows.", err) } }() for rows.Next() { city := dt.City{} if err = rows.Scan(&city.Name, &city.CountryCode); err != nil { return nil, err } cities = append(cities, city) } if err = rows.Err(); err != nil { return nil, err } if len(cities) == 0 { return nil, ErrNotFound } return cities, nil }
// hapiAdminsUpdate adds or removes admin permission from a given user. func hapiAdminsUpdate(w http.ResponseWriter, r *http.Request) { if os.Getenv("ABOT_ENV") != "test" { if !isAdmin(w, r) { return } if !isLoggedIn(w, r) { return } if !isValidCSRF(w, r) { return } } var req struct { ID uint64 Email string Admin bool } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErrorBadRequest(w, err) return } // This is a clever way to update the user using EITHER email or ID // (whatever the client had available). Then we return the ID of the // updated entry to send back to the client for faster future requests. if req.ID > 0 && len(req.Email) > 0 { writeErrorBadRequest(w, errors.New("only one value allowed: ID or Email")) return } q := `UPDATE users SET admin=$1 WHERE id=$2 OR email=$3 RETURNING id` err := db.QueryRow(q, req.Admin, req.ID, req.Email).Scan(&req.ID) if err == sql.ErrNoRows { // This error is frequently user-facing. writeErrorBadRequest(w, errors.New("User not found.")) return } if err != nil { writeErrorInternal(w, err) return } var user struct { ID uint64 Email string Name string } q = `SELECT id, email, name FROM users WHERE id=$1` if err = db.Get(&user, q, req.ID); err != nil { writeErrorInternal(w, err) return } byt, err := json.Marshal(user) if err != nil { writeErrorInternal(w, err) return } _, err = w.Write(byt) if err != nil { log.Info("failed to write response.", err) } }
func TestMain(m *testing.M) { if err := core.LoadEnvVars(); err != nil { log.Info("failed to load env vars", err) } if err := os.Setenv("ABOT_ENV", "test"); err != nil { log.Fatal(err) } os.Exit(m.Run()) }
func TestPluginGenerate(t *testing.T) { defer func() { if err := os.RemoveAll("__test"); err != nil { log.Info("failed to clean up __test dir.", err) } if err := os.RemoveAll("test_here"); err != nil { log.Info("failed to clean up __test dir.", err) } }() // generate plugin l := log.New("") l.SetFlags(0) if err := generatePlugin(l, "__test"); err != nil { t.Fatal(err) } if err := generatePlugin(l, "TestHere"); err != nil { t.Fatal(err) } }
func sendEventsTick(evtChan chan *dt.ScheduledEvent, t time.Time) { // Listen for events that need to be sent. go func(chan *dt.ScheduledEvent) { q := `UPDATE scheduledevents SET sent=TRUE WHERE id=$1` select { case evt := <-evtChan: log.Debug("received event") if smsConn == nil { log.Info("failed to send scheduled event (missing SMS driver). will retry.") return } // Send event. On error, event will be retried next // minute. if err := evt.Send(smsConn); err != nil { log.Info("failed to send scheduled event", err) return } // Update event as sent if _, err := db.Exec(q, evt.ID); err != nil { log.Info("failed to update scheduled event as sent", err) return } } }(evtChan) q := `SELECT id, content, flexid, flexidtype FROM scheduledevents WHERE sent=false AND sendat<=$1` evts := []*dt.ScheduledEvent{} if err := db.Select(&evts, q, time.Now()); err != nil { log.Info("failed to queue scheduled event", err) return } for _, evt := range evts { // Queue the event for sending evtChan <- evt } }
func TestMain(m *testing.M) { if err := LoadEnvVars(); err != nil { log.Info("failed to load env vars", err) } if err := os.Setenv("ABOT_ENV", "test"); err != nil { log.Fatal("failed to set ABOT_ENV", err) } var err error router, err = NewServer() if err != nil { log.Fatal("failed to start server", err) } os.Exit(m.Run()) }
func testPlugin() (int, error) { if err := core.LoadPluginsGo(); err != nil { return 0, err } r := plugin.TestPrepare() plugin.TestCleanup() var count int for _, plg := range core.PluginsGo { log.Info("loading", plg.Name) for _, test := range plg.Tests { log.Info("running", test) count++ for in, exps := range test { err := plugin.TestReq(r, in, exps) if err != nil { return count, err } } } } plugin.TestCleanup() return count, nil }
func TestHAPIProfile(t *testing.T) { reset(t) user, _, _ := seedDBUser(t) seedDBUserSession(t, user) c, b := userRequest("GET", "/api/user/profile.json", nil, user) if c != http.StatusOK { log.Info(b) t.Fatal("expected", http.StatusOK, "got", c) } if !strings.Contains(b, "Name") { t.Fatal(`expected "Name" but got`, b) } }
func TestHSignupSubmit(t *testing.T) { reset(t) u := "http://*****:*****@example.com", "Password": "******", "FID": "+13105555555" }`) c, b := request("POST", u, data) if c != http.StatusOK { log.Info(b) t.Fatal("expected", http.StatusOK, "got", c) } }
// preprocess converts a user input into a Msg that's been persisted to the // database func preprocess(r *http.Request) (*dt.Msg, error) { req := &dt.Request{} err := json.NewDecoder(r.Body).Decode(req) if err != nil { log.Info("could not parse empty body", err) return nil, err } sendPostReceiveEvent(&req.CMD) u, err := dt.GetUser(db, req) if err != nil { return nil, err } sendPreProcessingEvent(&req.CMD, u) // TODO trigger training if needed (see buildInput) return NewMsg(u, req.CMD) }
// hMain is the endpoint to hit when you want a direct response via JSON. // The Abot console uses this endpoint. func hMain(w http.ResponseWriter, r *http.Request) { errMsg := "Something went wrong with my wiring... I'll get that fixed up soon." ret, err := ProcessText(r) if err != nil { if len(ret) > 0 { ret = errMsg } log.Info("failed to process text.", err) // TODO notify plugins listening for errors } w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Access-Control-Allow-Origin") _, err = fmt.Fprint(w, ret) if err != nil { writeErrorInternal(w, err) } }
// hapiAdminExists checks if an admin exists in the database. func hapiAdminExists(w http.ResponseWriter, r *http.Request) { var count int q := `SELECT COUNT(*) FROM users WHERE admin=TRUE LIMIT 1` if err := db.Get(&count, q); err != nil { writeErrorInternal(w, err) return } byt, err := json.Marshal(count > 0) if err != nil { writeErrorInternal(w, err) return } _, err = w.Write(byt) if err != nil { log.Info("failed writing response header.", err) } }
func TestHAPILoginSubmit(t *testing.T) { reset(t) user, _, _ := seedDBUser(t) data := struct { Email string Password string }{ Email: user.Email, Password: user.Password, } byt, err := json.Marshal(data) if err != nil { t.Fatal(err) } u := "http://localhost:" + os.Getenv("PORT") + "/api/login.json" c, b := request("POST", u, byt) if c != http.StatusOK { log.Info(b) t.Fatal("expected", http.StatusOK, "got", c) } }
// hapiConversationsNeedTraining returns a list of all sentences that require a // human response. func hapiConversationsNeedTraining(w http.ResponseWriter, r *http.Request) { if os.Getenv("ABOT_ENV") != "test" { if !isAdmin(w, r) { return } if !isLoggedIn(w, r) { return } } msgs := []struct { Sentence string FlexID *string CreatedAt time.Time UserID uint64 FlexIDType *int }{} q := `SELECT * FROM ( SELECT DISTINCT ON (flexid) userid, flexid, flexidtype, sentence, createdat FROM messages WHERE needstraining=TRUE AND trained=FALSE AND abotsent=FALSE AND sentence<>'' ) t ORDER BY createdat DESC` err := db.Select(&msgs, q) if err == sql.ErrNoRows { w.WriteHeader(http.StatusOK) } if err != nil { writeErrorInternal(w, err) return } byt, err := json.Marshal(msgs) if err != nil { writeErrorInternal(w, err) return } _, err = w.Write(byt) if err != nil { log.Info("failed to write response.", err) } }
func buildPluginScaffoldFile(dirName, name string) error { fi, err := os.Create(filepath.Join(dirName, dirName+".go")) if err != nil { return err } defer func() { err = fi.Close() if err != nil { log.Info("failed to close plugin.json.", err) } }() dir, err := os.Getwd() if err != nil { return err } dir = filepath.Join(dir, name) _, err = fi.WriteString(pluginScaffoldFile(dir, name)) if err != nil { return err } return nil }
func buildPluginJSON(dirName, name string) error { var maintainer string if os.Getenv("ABOT_ENV") == "test" { maintainer = "*****@*****.**" } else { fi, err := os.Open(filepath.Join(os.Getenv("HOME"), ".abot.conf")) if err != nil { return err } defer func() { if err = fi.Close(); err != nil { log.Info("failed to close plugin.json file.", err) } }() scn := bufio.NewScanner(fi) var lineNum int for scn.Scan() { if lineNum < 1 { lineNum++ continue } maintainer = scn.Text() break } if scn.Err() != nil { return err } } b := []byte(`{ "Name": "` + name + `", "Maintainer": "` + maintainer + `", "Usage": ["Show me a demo"], "Tests": [ {"Show me a demo": ["demo"]} ] }`) return ioutil.WriteFile(filepath.Join(dirName, "plugin.json"), b, 0744) }
// GetSetting retrieves a specific setting's value. It throws a fatal error if // the setting has not been declared in the plugin's plugin.json file. func (p *Plugin) GetSetting(name string) string { if p.Config.Settings[name] == nil { pluginName := p.Config.Name if len(pluginName) == 0 { pluginName = "plugin" } m := fmt.Sprintf( "missing setting %s. please declare it in the %s's plugin.json", name, pluginName) log.Fatal(m) } var val string q := `SELECT value FROM settings WHERE name=$1 AND pluginname=$2` err := p.DB.Get(&val, q, name, p.Config.Name) if err == sql.ErrNoRows { return p.Config.Settings[name].Default } if err != nil { log.Info("failed to get plugin setting.", err) return "" } return val }
func publishPlugin(c *cli.Context) error { p := filepath.Join(os.Getenv("HOME"), ".abot.conf") fi, err := os.Open(p) if err != nil { if err.Error() == fmt.Sprintf("open %s: no such file or directory", p) { login() publishPlugin(c) return nil } log.Fatal(err) } defer func() { if err = fi.Close(); err != nil { log.Fatal(err) } }() // Prepare request if len(c.Args().First()) == 0 { log.Fatal("missing plugin's `go get` path") } reqData := struct { Path string Secret string }{ Path: c.Args().First(), Secret: core.RandSeq(24), } byt, err := json.Marshal(reqData) if err != nil { log.Fatal(err) } u := os.Getenv("ITSABOT_URL") + "/api/plugins.json" req, err := http.NewRequest("POST", u, bytes.NewBuffer(byt)) if err != nil { log.Fatal(err) } // Populate req with login credentials from ~/.abot.conf scn := bufio.NewScanner(fi) var lineNum int for scn.Scan() { line := scn.Text() cookie := &http.Cookie{} switch lineNum { case 0: cookie.Name = "iaID" case 1: cookie.Name = "iaEmail" case 2: req.Header.Set("Authorization", "Bearer "+line) case 3: cookie.Name = "iaIssuedAt" default: log.Fatal("unknown line in abot.conf") } if lineNum != 2 { cookie.Value = url.QueryEscape(line) req.AddCookie(cookie) } lineNum++ } if err = scn.Err(); err != nil { log.Fatal(err) } cookie := &http.Cookie{} cookie.Name = "iaScopes" req.AddCookie(cookie) client := &http.Client{} resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer func() { if err = resp.Body.Close(); err != nil { log.Fatal(err) } }() if resp.StatusCode == 401 { login() publishPlugin(c) } else if resp.StatusCode != 202 { log.Info("something went wrong. status code", resp.StatusCode) var msg string if err = json.NewDecoder(resp.Body).Decode(&msg); err != nil { log.Fatal(err) } log.Fatal(msg) } // Make a websocket request to get updates about the publishing process uri, err := url.Parse(os.Getenv("ITSABOT_URL")) if err != nil { log.Fatal(err) } ws, err := websocket.Dial("ws://"+uri.Host+"/api/ws", "", os.Getenv("ABOT_URL")) if err != nil { log.Fatal(err) } if err = websocket.Message.Send(ws, reqData.Secret); err != nil { log.Fatal(err) } var msg socket.Msg l := log.New("") l.SetFlags(0) l.Info("> Establishing connection with server...") var established bool var lastMsg string for { websocket.JSON.Receive(ws, &msg) if !established { l.Info("OK") established = true } if msg.Content == lastMsg { log.Info("server hung up. please try again") if err = ws.Close(); err != nil { log.Info("failed to close socket.", err) } return nil } lastMsg = msg.Content if msg.Type == socket.MsgTypeFinishedSuccess || msg.Type == socket.MsgTypeFinishedFailed { if err = ws.Close(); err != nil { log.Info("failed to close socket.", err) } return nil } if len(msg.Content) < 2 { l.Info(msg.Content) continue } tmp := msg.Content[0:2] if tmp == "> " || tmp == "==" { l.Info("") } l.Info(msg.Content) } }
func login() { reader := bufio.NewReader(os.Stdin) fmt.Print("Email: ") email, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } fmt.Print("Password: "******"ITSABOT_URL") + "/api/users/login.json" resp, err := http.Post(u, "application/json", bytes.NewBuffer(byt)) if err != nil { log.Fatal(err) } defer func() { if err = resp.Body.Close(); err != nil { log.Fatal(err) } }() var data struct { ID uint64 Email string Scopes []string AuthToken string IssuedAt uint64 } if err = json.NewDecoder(resp.Body).Decode(&data); err != nil { log.Fatal(err) } if resp.StatusCode == 401 { log.Fatal(errors.New("invalid email/password combination")) } // Create abot.conf file, truncate if exists fi, err := os.Create(filepath.Join(os.Getenv("HOME"), ".abot.conf")) if err != nil { log.Fatal(err) } defer func() { if err = fi.Close(); err != nil { log.Fatal(err) } }() // Insert auth data s := fmt.Sprintf("%d\n%s\n%s\n%d", data.ID, data.Email, data.AuthToken, data.IssuedAt) _, err = fi.WriteString(s) if err != nil { log.Fatal(err) } log.Info("Success!") }
func updateAnalyticsTick(t time.Time) { if os.Getenv("ABOT_ENV") == "test" { return } log.Info("updating analytics") createdAt := t.Round(24 * time.Hour) // User count var count int q := `SELECT COUNT(*) FROM ( SELECT DISTINCT (flexid, flexidtype) FROM userflexids ) AS t` if err := db.Get(&count, q); err != nil { log.Info("failed to retrieve user count.", err) return } aq := `INSERT INTO analytics (label, value, createdat) VALUES ($1, $2, $3) ON CONFLICT (label, createdat) DO UPDATE SET value=$2` _, err := db.Exec(aq, keyUserCount, count, createdAt) if err != nil { log.Info("failed to update analytics (user count).", err) return } // Message count q = `SELECT COUNT(*) FROM messages` if err = db.Get(&count, q); err != nil { log.Info("failed to retrieve message count.", err) return } _, err = db.Exec(aq, keyMsgCount, count, createdAt) if err != nil { log.Info("failed to update analytics (msg count).", err) return } // Messages needing training q = `SELECT COUNT(*) FROM messages WHERE needstraining=TRUE AND abotsent=FALSE` if err = db.Get(&count, q); err != nil { log.Info("failed to retrieve user count.", err) return } _, err = db.Exec(aq, keyTrainCount, count, createdAt) if err != nil { log.Info("failed to update analytics (msg count).", err) return } // Version number client := &http.Client{Timeout: 15 * time.Minute} u := "https://raw.githubusercontent.com/itsabot/abot/master/base/plugins.json" req, err := http.NewRequest("GET", u, nil) if err != nil { log.Info("failed to retrieve version number.", err) return } reqResp, err := client.Do(req) if err != nil { log.Info("failed to retrieve version number.", err) return } defer func() { if err = reqResp.Body.Close(); err != nil { log.Info("failed to close body.", err) } }() var remoteConf PluginJSON if err = json.NewDecoder(reqResp.Body).Decode(&remoteConf); err != nil { log.Info("failed to retrieve version number.", err) return } _, err = db.Exec(aq, keyVersion, remoteConf.Version, createdAt) if err != nil { log.Info("failed to update analytics (version number).", err) return } }