From 6bdc1a92d3d8b8b63a300c513a60553f81d9aaf5 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sat, 13 Oct 2018 10:42:52 -0700 Subject: [PATCH 01/14] Create README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fde939 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# GeoffreyBot +Geoffrey is a guinea pig/GroupMe bot with a new lease on life From 6bb38b65f29d99ada77235431f60a2fbf4ec7dbe Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sat, 13 Oct 2018 12:34:04 -0700 Subject: [PATCH 02/14] Hello World test --- Procfile | 1 + main.go | 34 ++++++++++++++++++++++++++++++++++ vendor/vendor.json | 6 ++++++ 3 files changed, 41 insertions(+) create mode 100644 Procfile create mode 100644 main.go create mode 100644 vendor/vendor.json diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..8dbac67 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: geoffrey \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..81f18e3 --- /dev/null +++ b/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + "fmt" + "net/http" + "os" +) + +func determineListenAddress() (string, error) { + port := os.Getenv("PORT") + if port == "" { + return "", fmt.Errorf("$PORT not set") + } + return ":" + port, nil +} + +func hello(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request recieved!\n") + fmt.Fprintln(w, "Hello World") +} + +func main() { + addr, err := determineListenAddress() + if err != nil { + log.Fatal(err) + } + + http.HandleFunc("/", hello) + log.Printf("Listening on %s...\n", addr) + if err := http.ListenAndServe(addr, nil); err != nil { + panic(err) + } +} \ No newline at end of file diff --git a/vendor/vendor.json b/vendor/vendor.json new file mode 100644 index 0000000..8fff69a --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,6 @@ +{ + "comment": "", + "ignore": "test", + "package": [], + "rootPath": "geoffrey" +} From ba181c3e87dbf6d85991a33422fad1b23e5f92e6 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sat, 13 Oct 2018 12:59:37 -0700 Subject: [PATCH 03/14] Simple message reciever endpoint --- main.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/main.go b/main.go index 81f18e3..0af2674 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "encoding/json" + "io/ioutil" "log" "fmt" "net/http" @@ -20,6 +22,22 @@ func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello World") } +func recieveMessage(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Message Recieved!!\n") + body, err := ioutil.ReadAll(r.Body) + + if (err != nil) { + fmt.Printf("Error reading request body: %v", err) + return + } + + bodyMap := make(map[string] string) + + json.Unmarshal(body, &bodyMap) + + fmt.Printf("Body: %v", bodyMap) +} + func main() { addr, err := determineListenAddress() if err != nil { @@ -27,6 +45,7 @@ func main() { } http.HandleFunc("/", hello) + http.HandleFunc("/message", recieveMessage) log.Printf("Listening on %s...\n", addr) if err := http.ListenAndServe(addr, nil); err != nil { panic(err) From 26191089d8ae0afa51abb5bdfa252231f6f01207 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sat, 13 Oct 2018 20:28:25 -0700 Subject: [PATCH 04/14] Make skill structure and add yesOrNoSkill, GenericQuestionSkill, and CatFacts skill --- Main.go | 42 ++++++++++++++++++ api/CatFacts.go | 42 ++++++++++++++++++ api/GroupMe.go | 78 ++++++++++++++++++++++++++++++++++ api/NetUtils.go | 34 +++++++++++++++ api/YesOrNo.go | 67 +++++++++++++++++++++++++++++ endpoints/EndpointRegistry.go | 18 ++++++++ endpoints/FallbackHandler.go | 12 ++++++ endpoints/MessageRecieved.go | 53 +++++++++++++++++++++++ main.go | 53 ----------------------- skills/CatFactSkill.go | 71 +++++++++++++++++++++++++++++++ skills/GenericQuestionSkill.go | 43 +++++++++++++++++++ skills/SkillUtils.go | 75 ++++++++++++++++++++++++++++++++ skills/SkillsRegistry.go | 18 ++++++++ skills/YesOrNoSkill.go | 63 +++++++++++++++++++++++++++ 14 files changed, 616 insertions(+), 53 deletions(-) create mode 100644 Main.go create mode 100644 api/CatFacts.go create mode 100644 api/GroupMe.go create mode 100644 api/NetUtils.go create mode 100644 api/YesOrNo.go create mode 100644 endpoints/EndpointRegistry.go create mode 100644 endpoints/FallbackHandler.go create mode 100644 endpoints/MessageRecieved.go delete mode 100644 main.go create mode 100644 skills/CatFactSkill.go create mode 100644 skills/GenericQuestionSkill.go create mode 100644 skills/SkillUtils.go create mode 100644 skills/SkillsRegistry.go create mode 100644 skills/YesOrNoSkill.go diff --git a/Main.go b/Main.go new file mode 100644 index 0000000..04a4097 --- /dev/null +++ b/Main.go @@ -0,0 +1,42 @@ +package main + +import ( + "geoffrey/endpoints" + "log" + "fmt" + "net/http" + "os" +) + +func determineListenAddress() (string, error) { + port := os.Getenv("PORT") + if port == "" { + return "", fmt.Errorf("$PORT not set") + } + return ":" + port, nil +} + +// registerHandlers loops over all endpoints in the registry and tells +// the server to handle them +func registerHandlers() { + fmt.Println("Registering Endpoints...") + + for path, handler := range endpoints.GetAllEndpoints() { + fmt.Printf("Registering Handler for %v\n", path) + http.HandleFunc(path, handler) + } +} + +func main() { + addr, err := determineListenAddress() + if err != nil { + log.Fatal(err) + } + + registerHandlers() + + log.Printf("Listening on %s...\n", addr) + if err := http.ListenAndServe(addr, nil); err != nil { + panic(err) + } +} \ No newline at end of file diff --git a/api/CatFacts.go b/api/CatFacts.go new file mode 100644 index 0000000..c31720f --- /dev/null +++ b/api/CatFacts.go @@ -0,0 +1,42 @@ +package api + +import ( + "net/http" + "errors" +) + +const catFactsBaseUrl = "https://catfact.ninja" + +const catFactEndpoint = catFactsBaseUrl + "/fact" +const catFactFactKey = "fact" + +func GetCatFact() (string, error) { + resp, err := http.Get(catFactEndpoint) + + if (err != nil) { + return "", err + } + + defer resp.Body.Close() + + if (resp.StatusCode != 200) { + return "", buildStatusError("Error recieved from cat facts endpoint", resp) + } + + body, err := getBodyJson(resp) + + if (err != nil) { + return "", err + } + + if fact, exists := body[catFactFactKey]; exists { + return fact, nil + } else { + return "", errors.New("Could not parse fact out of response body!") + } +} + +func GetCatFactAsync(channel chan StringResult) { + fact, err := GetCatFact() + channel <- StringResult{fact, err} +} \ No newline at end of file diff --git a/api/GroupMe.go b/api/GroupMe.go new file mode 100644 index 0000000..90de5e5 --- /dev/null +++ b/api/GroupMe.go @@ -0,0 +1,78 @@ +package api + +import ( + "io/ioutil" + "bytes" + "net/http" + "fmt" + "os" +) + +const groupMeBaseUrl = "https://api.groupme.com/v3" +const botPostMessageUrl = groupMeBaseUrl + "/bots/post" + +const jsonContentType = "application/json" + +const botIdEnvironmentVar = "BOT_ID" + +var botId string = "" + +func PostGroupMeMessage(message string) { + id := getBotId() + if (id == "") { + fmt.Printf("Cannot send message; $%v environment variable not set\n", botIdEnvironmentVar) + return + } + + body := fmt.Sprintf(`{ + "bot_id": "%v", + "text": "%v" + }`, id, message) + + postGroupMeMessage(body) +} + +func PostGroupMeMessageWithPicture(message string, imageUrl string) { + id := getBotId() + if (id == "") { + fmt.Printf("Cannot send message; $%v environment variable not set\n", botIdEnvironmentVar) + return + } + + fmt.Printf("sending gm message w/ picture: %v\n%v\n", message, imageUrl) + + body := fmt.Sprintf(`{ + "bot_id": "%v", + "text": "%v", + "picture_url": "%v" + }`, id, message, imageUrl) + + fmt.Printf("post bost: %v\n", body) + + postGroupMeMessage(body) +} + +// postGroupMeMessage is the internal function that both public post functions delegate to +func postGroupMeMessage(postBody string) { + resp, err := http.Post(botPostMessageUrl, jsonContentType, bytes.NewBufferString(postBody)) + + if (err != nil) { + fmt.Printf("Error posting message; %v", err) + return + } + + if (resp.StatusCode != 202) { + respBody, _ := ioutil.ReadAll(resp.Body) + fmt.Printf("Post failed; response code: %v: %v;\n\tbody: %v\n", resp.StatusCode, resp.Status, string(respBody)) + } + + resp.Body.Close() +} + +func getBotId() string { + if botId == "" { + botId = os.Getenv(botIdEnvironmentVar) + } + + return botId +} \ No newline at end of file diff --git a/api/NetUtils.go b/api/NetUtils.go new file mode 100644 index 0000000..a0a8f6a --- /dev/null +++ b/api/NetUtils.go @@ -0,0 +1,34 @@ +package api + +import ( + "strconv" + "encoding/json" + "net/http" + "io/ioutil" + "errors" +) + +type StringResult struct { + Result string + Err error +} + +// getBodyJson takes an http.Response pointer and reads all of its data into a map +func getBodyJson(response *http.Response) (map[string] string, error) { + bodyBytes, err := ioutil.ReadAll(response.Body) + + if (err != nil) { + return nil, err + } + + bodyMap := make(map[string] string) + + json.Unmarshal(bodyBytes, &bodyMap) + + return bodyMap, nil +} + +// buildStatusError takes an http response and builds an error with the status code and message in it +func buildStatusError(message string, response *http.Response) error { + return errors.New(message + "; Status " + strconv.Itoa(response.StatusCode) + ": " + response.Status) +} \ No newline at end of file diff --git a/api/YesOrNo.go b/api/YesOrNo.go new file mode 100644 index 0000000..4035fba --- /dev/null +++ b/api/YesOrNo.go @@ -0,0 +1,67 @@ +package api + +import ( + "time" + "math/rand" + "net/http" +) + +type YesOrNoAnswer int +type YesOrNoResponse struct { + Answer YesOrNoAnswer + ImageUrl string +} + +const ( + YES YesOrNoAnswer = iota + NO + MAYBE +) + +const yesOrNoUrl = "https://yesno.wtf/api" +const forceMaybeUrl = yesOrNoUrl + "/?force=maybe" + +func GetYesOrNo() (YesOrNoResponse, error) { + var retval YesOrNoResponse + url := yesOrNoUrl + + if shouldForceMaybe() { + url = forceMaybeUrl + } + + resp, err := http.Get(url) + + if (err != nil) { + return retval, err + } + + defer resp.Body.Close() + + if (resp.StatusCode != 200) { + return retval, buildStatusError("Error recieved from cat facts endpoint", resp) + } + + // Get the body and parse it into a map + body, err := getBodyJson(resp) + + if (err != nil) { + return retval, err + } + + // Everything has gone well, read the response now + switch body["answer"] { + case "yes": retval.Answer = YES + case "no": retval.Answer = NO + default: retval.Answer = MAYBE + } + + retval.ImageUrl = body["image"] + + return retval, nil +} + +// Force maybe ~1/15 times to make things more interesting +func shouldForceMaybe() bool { + rand.Seed(time.Now().Unix()) + return rand.Intn(15) == 1 +} \ No newline at end of file diff --git a/endpoints/EndpointRegistry.go b/endpoints/EndpointRegistry.go new file mode 100644 index 0000000..0f2948f --- /dev/null +++ b/endpoints/EndpointRegistry.go @@ -0,0 +1,18 @@ +package endpoints + +import ( + "net/http" +) + +type HandlerFunc func(http.ResponseWriter, *http.Request) + +var endpoints = map[string] HandlerFunc { + "/" : fallbackHandler, + "/message" : messageRecieved, +} + +// GetAllEndpoints returns all registered endpoints in a map +// The map contains the String path of the endpoint mapped to its handler func +func GetAllEndpoints() map[string] HandlerFunc { + return endpoints +} \ No newline at end of file diff --git a/endpoints/FallbackHandler.go b/endpoints/FallbackHandler.go new file mode 100644 index 0000000..42081bb --- /dev/null +++ b/endpoints/FallbackHandler.go @@ -0,0 +1,12 @@ +package endpoints + +import ( + "fmt" + "net/http" +) + +func fallbackHandler(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Request handled by fallback handler; path - %v\n", r.URL.Path) + + fmt.Fprintln(w, "You've reached Geoffrey, please leave a message after the beep...") +} \ No newline at end of file diff --git a/endpoints/MessageRecieved.go b/endpoints/MessageRecieved.go new file mode 100644 index 0000000..0ab0726 --- /dev/null +++ b/endpoints/MessageRecieved.go @@ -0,0 +1,53 @@ +package endpoints + +import ( + "geoffrey/skills" + "encoding/json" + "io/ioutil" + "fmt" + "net/http" +) + +const postBodyMessageId = "id" +const postBodyGroupId = "group_id" +const postBodySenderName = "name" +const postBodySenderType = "sender_type" + +// messageRecieved handles POST requests from GroupMe that get sent for each message sent +// in the group that the bot is in +func messageRecieved(w http.ResponseWriter, r *http.Request) { + fmt.Printf("Message Recieved!!\n") + + defer r.Body.Close() + + body, err := ioutil.ReadAll(r.Body) + + if (err != nil) { + fmt.Printf("Error reading request body: %v\n", err) + return + } + + fmt.Printf("Super raw: %v", string(body)) + + bodyMap := make(map[string] string) + json.Unmarshal(body, &bodyMap) + + logMessage(bodyMap) + + // Only process messages from humans + if (bodyMap[postBodySenderType] != "user") { + return + } + + for _, skill := range skills.GetActiveSkills() { + if (skill(bodyMap)) { + break + } + } +} + +func logMessage(body map[string] string) { + fmt.Printf("raw: %v", body) + fmt.Println("Message: " + body["text"]) + fmt.Printf("Message (id: %v) from %v in group %v\n", body[postBodyMessageId], body[postBodySenderName], body[postBodyGroupId]) +} diff --git a/main.go b/main.go deleted file mode 100644 index 0af2674..0000000 --- a/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "encoding/json" - "io/ioutil" - "log" - "fmt" - "net/http" - "os" -) - -func determineListenAddress() (string, error) { - port := os.Getenv("PORT") - if port == "" { - return "", fmt.Errorf("$PORT not set") - } - return ":" + port, nil -} - -func hello(w http.ResponseWriter, r *http.Request) { - fmt.Printf("Request recieved!\n") - fmt.Fprintln(w, "Hello World") -} - -func recieveMessage(w http.ResponseWriter, r *http.Request) { - fmt.Printf("Message Recieved!!\n") - body, err := ioutil.ReadAll(r.Body) - - if (err != nil) { - fmt.Printf("Error reading request body: %v", err) - return - } - - bodyMap := make(map[string] string) - - json.Unmarshal(body, &bodyMap) - - fmt.Printf("Body: %v", bodyMap) -} - -func main() { - addr, err := determineListenAddress() - if err != nil { - log.Fatal(err) - } - - http.HandleFunc("/", hello) - http.HandleFunc("/message", recieveMessage) - log.Printf("Listening on %s...\n", addr) - if err := http.ListenAndServe(addr, nil); err != nil { - panic(err) - } -} \ No newline at end of file diff --git a/skills/CatFactSkill.go b/skills/CatFactSkill.go new file mode 100644 index 0000000..e0a87fa --- /dev/null +++ b/skills/CatFactSkill.go @@ -0,0 +1,71 @@ +package skills + +import ( + "fmt" + "strings" + "geoffrey/api" +) + +var greetingOptions = []string { + "Hi %v!", + "Hi there %v!", + "Hey %v!", + "What's crackin' %v?!", + "Hello there %v.", +} + +var factPrefixOptions = []string { + "Did you know %v?", + "Have you heard that %v?", + "Can you believe that %v?", + "Wanna hear a fun cat fact?? Here ya go: %v.", + "Guess what! Ahh you'll never guess I'll just tell you: %v!", +} + +// catFactSkill sends the group a cat fact if geoffrey is +// mentioned in a message +func catFactSkill(bodyMap map[string] string) bool { + if (!isGeoffreyMentioned(bodyMap[postBodyMessageText])) { + return false + } + + // Go get the cat fact while we figure out who to @ + catFactChannel := make(chan api.StringResult) + go api.GetCatFactAsync(catFactChannel) + + // Get only the first name of the sender + sender := bodyMap[postBodySenderName] + indexOfSpace := strings.Index(sender, " ") + if indexOfSpace > 0 { + sender = sender[:indexOfSpace] + } + + greeting := fmt.Sprintf(pickRandomFromStringArray(greetingOptions), sender) + factFormat := string(pickRandomFromStringArray(factPrefixOptions)) + + catFactResult := <-catFactChannel + + if (catFactResult.Err != nil) { + fmt.Printf("Error getting cat fact for cat fact skill; %v", catFactResult.Err) + return false + } + + fact := formatCatFact(catFactResult.Result) + + finalMessage := greeting + " " + fmt.Sprintf(factFormat, fact) + api.PostGroupMeMessage(finalMessage) + + return true +} + +func formatCatFact(fact string) string { + firstLetter := strings.ToLower(fact[:1]) + lastLetter := fact[len(fact) - 1] + + // Chop off punctuation if it's there + if (lastLetter == '.' || lastLetter == '?' || lastLetter == '!') { + return firstLetter + fact[1:len(fact) - 1] + } else { + return firstLetter + fact[1:] + } +} diff --git a/skills/GenericQuestionSkill.go b/skills/GenericQuestionSkill.go new file mode 100644 index 0000000..52baa01 --- /dev/null +++ b/skills/GenericQuestionSkill.go @@ -0,0 +1,43 @@ +package skills + +import ( + "geoffrey/api" + "fmt" + "strings" + "net/url" +) + +var answerOptions = []string { + "Uhh I don't really know how to answer that...", + "Hey listen idk alright leave me out of this.", + "Believe me if I knew I'd tell you!", + "I'm not even going to justify that question with an answer...", + "I'm just a guinea pig I don't know these things!", + "Ugh why do I have to do everything around here... http://lmgtfy.com/?q=%v", + "Listen that's not my problem buddy good luck figuring it out though.", +} + +func genericQuestionSkill(bodyMap map[string] string) bool { + + // First check if geoffrey is mentioned + if (!isGeoffreyMentioned(bodyMap[postBodyMessageText])) { + return false + } + + messageTextWithoutMention := stripGeoffreyMentions(bodyMap[postBodyMessageText]) + + // Next check if it's a question + if (!isQuestion(messageTextWithoutMention)) { + return false + } + + response := pickRandomFromStringArray(answerOptions) + + // If it's the lmgtfy response, throw in the query string + if (strings.Contains(response, "lmgtfy")) { + response = fmt.Sprintf(response, url.QueryEscape(messageTextWithoutMention)) + } + + api.PostGroupMeMessage(response) + return true +} \ No newline at end of file diff --git a/skills/SkillUtils.go b/skills/SkillUtils.go new file mode 100644 index 0000000..1eca3bd --- /dev/null +++ b/skills/SkillUtils.go @@ -0,0 +1,75 @@ +package skills + +import ( + "math/rand" + "time" + "strings" +) + +var aliases = []string { + "geoff", + "geoffrey", +} + +const postBodyMessageText = "text" +const postBodySenderName = "sender" + +// isGeoffreyMentioned checks the given messageText for any instances of geoffrey's aliases +// preceeded by an '@' +func isGeoffreyMentioned(messageText string) bool { + // Case doesn't matter! + var lowerCaseMessage = strings.ToLower(messageText) + + // Loop over each of Geoffrey's aliases to see if he's been mentioned in the message + for _, alias := range aliases { + if strings.Contains(lowerCaseMessage, "@" + alias) { + return true + } + } + + return false +} + +// stripGeoffreyMentions removes all @aliases from the given string +// Note the returned string also is all lower case +func stripGeoffreyMentions(message string) string { + var lowerCaseMessage = strings.ToLower(message) + + for _, alias := range aliases { + lowerCaseMessage = strings.Replace(lowerCaseMessage, "@" + alias, "", -1) + } + + return strings.TrimSpace(lowerCaseMessage) +} + +// isQuestion checks if the given message text is a question or not +func isQuestion(message string) bool { + // Naively just check if there's a question mark in the string :\ + return strings.Contains(message, "?") +} + +// isYesOrNoQuestion checks if the given message text is a yes or no question +func isYesOrNoQuestion(message string) bool { + // First confirm it's a generic question + if (!isQuestion(message)) { + return false + } + + // Case does't matter! + message = strings.ToLower(message) + + // Now check if it starts with a yes/no question starter + for _, starter := range []string { "do", "should", "will", "am", "is" } { + if (strings.Contains(message, starter)) { + return true + } + } + + return false +} + +// pickRandomFromArray returns a random string from the given array +func pickRandomFromStringArray(arr []string) string { + rand.Seed(time.Now().Unix()) + return arr[rand.Int() % len(arr)] +} \ No newline at end of file diff --git a/skills/SkillsRegistry.go b/skills/SkillsRegistry.go new file mode 100644 index 0000000..c0b24dd --- /dev/null +++ b/skills/SkillsRegistry.go @@ -0,0 +1,18 @@ +package skills + +// A Skill is a function that takes a map containing the POST body of a GroupMe message POST +// and may or may not perform an action based on it + +// ActiveSkills return true/false depending on whether or not they consumed the event +type ActiveSkill func(map[string] string) bool + +var activeSkills = []ActiveSkill { + yesOrNoSkill, + genericQuestionSkill, + catFactSkill, +} + +// GetActiveSkills returns all registered active skills in order or priority +func GetActiveSkills() []ActiveSkill { + return activeSkills +} \ No newline at end of file diff --git a/skills/YesOrNoSkill.go b/skills/YesOrNoSkill.go new file mode 100644 index 0000000..4bb401f --- /dev/null +++ b/skills/YesOrNoSkill.go @@ -0,0 +1,63 @@ +package skills + +import ( + "fmt" + "geoffrey/api" +) + +var responseOptionMap = map[api.YesOrNoAnswer] []string { + api.YES: []string { + "Yes!", + "Yea sure why not.", + "I don't see why not!", + "Go for it!", + "Absolutely!", + }, + api.NO: []string { + "No!", + "Nah I don't thinkg so.", + "I'm gonna go with...no", + "Ehhh maybe some other time pal.", + "Not a chance.", + "Just...no.", + "Nope.", + }, + api.MAYBE: []string { + "Hmm that's a tough one...", + "idk", + "Meh...maybe.", + "Ahhh who cares?", + "Just do what you want it doesn't really matter", + }, +} + +func yesOrNoSkill(bodyMap map[string] string) bool { + // First check if geoffrey is mentioned + if (!isGeoffreyMentioned(bodyMap[postBodyMessageText])) { + return false + } + + messageTextWithoutMention := stripGeoffreyMentions(bodyMap[postBodyMessageText]) + + fmt.Printf("yes or no; text: %v\n", messageTextWithoutMention) + + // Next check if it's a yes or no question + if (!isYesOrNoQuestion(messageTextWithoutMention)) { + return false + } + + fmt.Println("passed") + + response, err := api.GetYesOrNo() + + if (err != nil) { + fmt.Printf("Error getting Yes or No response: %v", err) + return false + } + + messageText := pickRandomFromStringArray(responseOptionMap[response.Answer]) + + api.PostGroupMeMessageWithPicture(messageText, response.ImageUrl) + + return true +} \ No newline at end of file From 171ec6620ff5b9b77e95eaf1c9023b487eeed9dc Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sat, 13 Oct 2018 21:00:11 -0700 Subject: [PATCH 05/14] Fix json unmarshalling and remove excess logging --- api/GroupMe.go | 4 ---- endpoints/MessageRecieved.go | 31 +++++++++++++++++++++++-------- skills/CatFactSkill.go | 7 ++++--- skills/GenericQuestionSkill.go | 7 ++++--- skills/SkillUtils.go | 2 +- skills/SkillsRegistry.go | 4 +++- skills/YesOrNoSkill.go | 11 ++++------- types/Types.go | 10 ++++++++++ 8 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 types/Types.go diff --git a/api/GroupMe.go b/api/GroupMe.go index 90de5e5..5c5a37f 100644 --- a/api/GroupMe.go +++ b/api/GroupMe.go @@ -39,16 +39,12 @@ func PostGroupMeMessageWithPicture(message string, imageUrl string) { return } - fmt.Printf("sending gm message w/ picture: %v\n%v\n", message, imageUrl) - body := fmt.Sprintf(`{ "bot_id": "%v", "text": "%v", "picture_url": "%v" }`, id, message, imageUrl) - fmt.Printf("post bost: %v\n", body) - postGroupMeMessage(body) } diff --git a/endpoints/MessageRecieved.go b/endpoints/MessageRecieved.go index 0ab0726..8a30b2c 100644 --- a/endpoints/MessageRecieved.go +++ b/endpoints/MessageRecieved.go @@ -2,6 +2,7 @@ package endpoints import ( "geoffrey/skills" + "geoffrey/types" "encoding/json" "io/ioutil" "fmt" @@ -9,6 +10,7 @@ import ( ) const postBodyMessageId = "id" +const postBodyMessageText = "text" const postBodyGroupId = "group_id" const postBodySenderName = "name" const postBodySenderType = "sender_type" @@ -27,27 +29,40 @@ func messageRecieved(w http.ResponseWriter, r *http.Request) { return } - fmt.Printf("Super raw: %v", string(body)) + bodyMap := make(map[string] interface{}) + err = json.Unmarshal(body, &bodyMap) - bodyMap := make(map[string] string) - json.Unmarshal(body, &bodyMap) + if (err != nil) { + fmt.Printf("Error unmarshalling body into map; %v", err) + return + } logMessage(bodyMap) + message := buildMessageStruct(bodyMap) // Only process messages from humans - if (bodyMap[postBodySenderType] != "user") { + if (message.SenderType != "user") { return } for _, skill := range skills.GetActiveSkills() { - if (skill(bodyMap)) { + if (skill(message)) { break } } } -func logMessage(body map[string] string) { - fmt.Printf("raw: %v", body) - fmt.Println("Message: " + body["text"]) +func buildMessageStruct(bodyMap map[string] interface{}) types.GroupMeMessagePost { + var msg types.GroupMeMessagePost + msg.Id = bodyMap[postBodyMessageId].(string) + msg.GroupId = bodyMap[postBodyGroupId].(string) + msg.Sender = bodyMap[postBodySenderName].(string) + msg.SenderType = bodyMap[postBodySenderType].(string) + msg.MessageText = bodyMap[postBodyMessageText].(string) + + return msg +} + +func logMessage(body map[string] interface{}) { fmt.Printf("Message (id: %v) from %v in group %v\n", body[postBodyMessageId], body[postBodySenderName], body[postBodyGroupId]) } diff --git a/skills/CatFactSkill.go b/skills/CatFactSkill.go index e0a87fa..9bc9722 100644 --- a/skills/CatFactSkill.go +++ b/skills/CatFactSkill.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "geoffrey/api" + "geoffrey/types" ) var greetingOptions = []string { @@ -24,8 +25,8 @@ var factPrefixOptions = []string { // catFactSkill sends the group a cat fact if geoffrey is // mentioned in a message -func catFactSkill(bodyMap map[string] string) bool { - if (!isGeoffreyMentioned(bodyMap[postBodyMessageText])) { +func catFactSkill(message types.GroupMeMessagePost) bool { + if (!isGeoffreyMentioned(message.MessageText)) { return false } @@ -34,7 +35,7 @@ func catFactSkill(bodyMap map[string] string) bool { go api.GetCatFactAsync(catFactChannel) // Get only the first name of the sender - sender := bodyMap[postBodySenderName] + sender := message.Sender indexOfSpace := strings.Index(sender, " ") if indexOfSpace > 0 { sender = sender[:indexOfSpace] diff --git a/skills/GenericQuestionSkill.go b/skills/GenericQuestionSkill.go index 52baa01..290d4ae 100644 --- a/skills/GenericQuestionSkill.go +++ b/skills/GenericQuestionSkill.go @@ -1,6 +1,7 @@ package skills import ( + "geoffrey/types" "geoffrey/api" "fmt" "strings" @@ -17,14 +18,14 @@ var answerOptions = []string { "Listen that's not my problem buddy good luck figuring it out though.", } -func genericQuestionSkill(bodyMap map[string] string) bool { +func genericQuestionSkill(message types.GroupMeMessagePost) bool { // First check if geoffrey is mentioned - if (!isGeoffreyMentioned(bodyMap[postBodyMessageText])) { + if (!isGeoffreyMentioned(message.MessageText)) { return false } - messageTextWithoutMention := stripGeoffreyMentions(bodyMap[postBodyMessageText]) + messageTextWithoutMention := stripGeoffreyMentions(message.MessageText) // Next check if it's a question if (!isQuestion(messageTextWithoutMention)) { diff --git a/skills/SkillUtils.go b/skills/SkillUtils.go index 1eca3bd..1685e01 100644 --- a/skills/SkillUtils.go +++ b/skills/SkillUtils.go @@ -7,8 +7,8 @@ import ( ) var aliases = []string { - "geoff", "geoffrey", + "geoff", } const postBodyMessageText = "text" diff --git a/skills/SkillsRegistry.go b/skills/SkillsRegistry.go index c0b24dd..a421ed0 100644 --- a/skills/SkillsRegistry.go +++ b/skills/SkillsRegistry.go @@ -1,10 +1,12 @@ package skills +import "geoffrey/types" + // A Skill is a function that takes a map containing the POST body of a GroupMe message POST // and may or may not perform an action based on it // ActiveSkills return true/false depending on whether or not they consumed the event -type ActiveSkill func(map[string] string) bool +type ActiveSkill func(types.GroupMeMessagePost) bool var activeSkills = []ActiveSkill { yesOrNoSkill, diff --git a/skills/YesOrNoSkill.go b/skills/YesOrNoSkill.go index 4bb401f..249ac5e 100644 --- a/skills/YesOrNoSkill.go +++ b/skills/YesOrNoSkill.go @@ -3,6 +3,7 @@ package skills import ( "fmt" "geoffrey/api" + "geoffrey/types" ) var responseOptionMap = map[api.YesOrNoAnswer] []string { @@ -31,23 +32,19 @@ var responseOptionMap = map[api.YesOrNoAnswer] []string { }, } -func yesOrNoSkill(bodyMap map[string] string) bool { +func yesOrNoSkill(message types.GroupMeMessagePost) bool { // First check if geoffrey is mentioned - if (!isGeoffreyMentioned(bodyMap[postBodyMessageText])) { + if (!isGeoffreyMentioned(message.MessageText)) { return false } - messageTextWithoutMention := stripGeoffreyMentions(bodyMap[postBodyMessageText]) - - fmt.Printf("yes or no; text: %v\n", messageTextWithoutMention) + messageTextWithoutMention := stripGeoffreyMentions(message.MessageText) // Next check if it's a yes or no question if (!isYesOrNoQuestion(messageTextWithoutMention)) { return false } - fmt.Println("passed") - response, err := api.GetYesOrNo() if (err != nil) { diff --git a/types/Types.go b/types/Types.go new file mode 100644 index 0000000..9ed44ce --- /dev/null +++ b/types/Types.go @@ -0,0 +1,10 @@ +package types + +// This struct encapsulates the information in a group me message post request +type GroupMeMessagePost struct { + Id string + GroupId string + Sender string + SenderType string + MessageText string +} \ No newline at end of file From 36afcd83ff9bc3d1dff3b068df64f3a0041fa359 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Fri, 26 Oct 2018 19:02:40 -0700 Subject: [PATCH 06/14] Fix typos --- skills/SkillUtils.go | 2 +- skills/YesOrNoSkill.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/skills/SkillUtils.go b/skills/SkillUtils.go index 1685e01..a89722b 100644 --- a/skills/SkillUtils.go +++ b/skills/SkillUtils.go @@ -59,7 +59,7 @@ func isYesOrNoQuestion(message string) bool { message = strings.ToLower(message) // Now check if it starts with a yes/no question starter - for _, starter := range []string { "do", "should", "will", "am", "is" } { + for _, starter := range []string { "do", "should", "will", "am", "is", "are" } { if (strings.Contains(message, starter)) { return true } diff --git a/skills/YesOrNoSkill.go b/skills/YesOrNoSkill.go index 249ac5e..0bcef3c 100644 --- a/skills/YesOrNoSkill.go +++ b/skills/YesOrNoSkill.go @@ -11,12 +11,14 @@ var responseOptionMap = map[api.YesOrNoAnswer] []string { "Yes!", "Yea sure why not.", "I don't see why not!", - "Go for it!", + "Most definitely!", + "mhmm", + "Yaaaasssssss", "Absolutely!", }, api.NO: []string { "No!", - "Nah I don't thinkg so.", + "Nah I don't think so.", "I'm gonna go with...no", "Ehhh maybe some other time pal.", "Not a chance.", From 9dce13cb2cbd0b67d9dfd780673e8f74ac23a3a3 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sun, 28 Oct 2018 15:56:47 -0700 Subject: [PATCH 07/14] Switch to commands based architecture --- Main.go | 40 ++++------------------- Procfile | 2 +- commands/ConsoleCommand.go | 67 ++++++++++++++++++++++++++++++++++++++ commands/Server.go | 51 +++++++++++++++++++++++++++++ commands/hello_world.go | 17 ++++++++++ types/Types.go | 13 ++++++++ 6 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 commands/ConsoleCommand.go create mode 100644 commands/Server.go create mode 100644 commands/hello_world.go diff --git a/Main.go b/Main.go index 04a4097..aa6dfae 100644 --- a/Main.go +++ b/Main.go @@ -1,42 +1,14 @@ package main import ( - "geoffrey/endpoints" - "log" - "fmt" - "net/http" + "geoffrey/commands" + "os" ) -func determineListenAddress() (string, error) { - port := os.Getenv("PORT") - if port == "" { - return "", fmt.Errorf("$PORT not set") - } - return ":" + port, nil -} - -// registerHandlers loops over all endpoints in the registry and tells -// the server to handle them -func registerHandlers() { - fmt.Println("Registering Endpoints...") - - for path, handler := range endpoints.GetAllEndpoints() { - fmt.Printf("Registering Handler for %v\n", path) - http.HandleFunc(path, handler) - } -} - func main() { - addr, err := determineListenAddress() - if err != nil { - log.Fatal(err) - } + commands.RegisterCommand(&commands.ServerCommand{}) + commands.RegisterCommand(&commands.HelpCommand{}) - registerHandlers() - - log.Printf("Listening on %s...\n", addr) - if err := http.ListenAndServe(addr, nil); err != nil { - panic(err) - } -} \ No newline at end of file + commands.RunCommand(os.Args[1:]) +} diff --git a/Procfile b/Procfile index 8dbac67..6b67261 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: geoffrey \ No newline at end of file +web: geoffrey server \ No newline at end of file diff --git a/commands/ConsoleCommand.go b/commands/ConsoleCommand.go new file mode 100644 index 0000000..a376ad9 --- /dev/null +++ b/commands/ConsoleCommand.go @@ -0,0 +1,67 @@ +package commands + +import ( + "fmt" + "geoffrey/types" +) + +const CommandNotFoundCode = -100 + +var commands []types.ConsoleCommand + +func RegisterCommand(cmd types.ConsoleCommand) { + commands = append(commands, cmd) +} + +// RunCommand takes an slice of strings and attempts to run the command associated with it. +// The first string in the slice is used to determine the command name, the rest are pass into +// the command as arguments +func RunCommand(commandStrings []string) int { + if len(commandStrings) > 0 { + for _, cmd := range commands { + if cmd.Name() == commandStrings[0] { + return cmd.Execute(commandStrings[1:]) + } + } + + fmt.Printf("Command not found! (%v)\n", commandStrings[0]) + } else { + fmt.Println("Please specify a command:") + } + + (&HelpCommand{}).Execute(nil) + + return CommandNotFoundCode +} + +// ********** HELP COMMAND + +type HelpCommand struct{} + +func (*HelpCommand) Name() string { return "help" } +func (*HelpCommand) Usage() string { return "help - List all commands and their usage text\n" + + "\thelp ... - Print usage text for the given cmds" } + +func (*HelpCommand) Execute(args []string) int { + var cmdsToPrint []types.ConsoleCommand + + if len(args) == 0 { + // No args? print help for all commands! + cmdsToPrint = commands + } else { + // Some args? print help only for the given commands + for _, cmd := range commands { + for _, arg := range args { + if cmd.Name() == arg { + cmdsToPrint = append(cmdsToPrint, cmd) + } + } + } + } + + for _, cmd := range cmdsToPrint { + fmt.Printf("%v usage:\n\t%v\n\n", cmd.Name(), cmd.Usage()) + } + + return 0 +} \ No newline at end of file diff --git a/commands/Server.go b/commands/Server.go new file mode 100644 index 0000000..d0f6bfc --- /dev/null +++ b/commands/Server.go @@ -0,0 +1,51 @@ +package commands + +import ( + "geoffrey/endpoints" + + "log" + "fmt" + "net/http" + "os" +) + +type ServerCommand struct{} + +func (*ServerCommand) Name() string { return "server" } +func (*ServerCommand) Usage() string { return "server - Start the Geoffrey server on port $PORT"} + +func (*ServerCommand) Execute(args []string) int { + addr, err := determineListenAddress() + if err != nil { + log.Fatal(err) + } + + registerHandlers() + + log.Printf("Listening on %s...\n", addr) + if err := http.ListenAndServe(addr, nil); err != nil { + panic(err) + } + + return 0 +} + +func determineListenAddress() (string, error) { + port := os.Getenv("PORT") + if port == "" { + return "", fmt.Errorf("$PORT not set") + } + + return ":" + port, nil +} + +// registerHandlers loops over all endpoints in the registry and tells +// the server to handle them +func registerHandlers() { + fmt.Println("Registering Endpoints...") + + for path, handler := range endpoints.GetAllEndpoints() { + fmt.Printf("Registering Handler for %v\n", path) + http.HandleFunc(path, handler) + } +} diff --git a/commands/hello_world.go b/commands/hello_world.go new file mode 100644 index 0000000..cc982db --- /dev/null +++ b/commands/hello_world.go @@ -0,0 +1,17 @@ +package commands + +import ( + "fmt" +) + +// HelloCommands is a types.ConsoleCommand +type HelloCommand struct {} + +func (*HelloCommand) Name() string { return "hello" } +func (*HelloCommand) Usage() string {return "Just run it"} + +func (*HelloCommand) Execute(args []string) int { + fmt.Println("Hello, World") + + return 0 +} diff --git a/types/Types.go b/types/Types.go index 9ed44ce..fddabfc 100644 --- a/types/Types.go +++ b/types/Types.go @@ -7,4 +7,17 @@ type GroupMeMessagePost struct { Sender string SenderType string MessageText string +} + +type ConsoleCommand interface { + // Name returns the name of this command + Name() string + + // Usage returns a short help string to be displayed when the user asks + // for help + Usage() string + + // Execute should run the command and return a result code. + // 0 for success, anything else for error + Execute(args []string) int } \ No newline at end of file From 97e5915819230e4ffae13ab0abe00fb5366bf305 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sun, 28 Oct 2018 16:20:37 -0700 Subject: [PATCH 08/14] Fix catfact punctuation; delete hello_world command --- commands/hello_world.go | 17 ----------------- skills/CatFactSkill.go | 23 +++++++++++++---------- 2 files changed, 13 insertions(+), 27 deletions(-) delete mode 100644 commands/hello_world.go diff --git a/commands/hello_world.go b/commands/hello_world.go deleted file mode 100644 index cc982db..0000000 --- a/commands/hello_world.go +++ /dev/null @@ -1,17 +0,0 @@ -package commands - -import ( - "fmt" -) - -// HelloCommands is a types.ConsoleCommand -type HelloCommand struct {} - -func (*HelloCommand) Name() string { return "hello" } -func (*HelloCommand) Usage() string {return "Just run it"} - -func (*HelloCommand) Execute(args []string) int { - fmt.Println("Hello, World") - - return 0 -} diff --git a/skills/CatFactSkill.go b/skills/CatFactSkill.go index 9bc9722..7855ffa 100644 --- a/skills/CatFactSkill.go +++ b/skills/CatFactSkill.go @@ -43,6 +43,8 @@ func catFactSkill(message types.GroupMeMessagePost) bool { greeting := fmt.Sprintf(pickRandomFromStringArray(greetingOptions), sender) factFormat := string(pickRandomFromStringArray(factPrefixOptions)) + factWithoutPunctuation := factFormat[:len(factFormat) - 1] + factPunctuation := string(factFormat[len(factFormat) - 1]) catFactResult := <-catFactChannel @@ -51,22 +53,23 @@ func catFactSkill(message types.GroupMeMessagePost) bool { return false } - fact := formatCatFact(catFactResult.Result) + fact := formatCatFact(catFactResult.Result, factPunctuation) - finalMessage := greeting + " " + fmt.Sprintf(factFormat, fact) + finalMessage := greeting + " " + fmt.Sprintf(factWithoutPunctuation, fact) api.PostGroupMeMessage(finalMessage) return true } -func formatCatFact(fact string) string { +func formatCatFact(fact string, punctuation string) string { + // Some cat facts are multiple sentences, find the first punctuation so we can replace it later + var puncuationIndex = strings.IndexAny(fact, ".?!") + if puncuationIndex == -1 { + puncuationIndex = len(fact) - 1 + } + + // Force the first letter to be lower case firstLetter := strings.ToLower(fact[:1]) - lastLetter := fact[len(fact) - 1] - // Chop off punctuation if it's there - if (lastLetter == '.' || lastLetter == '?' || lastLetter == '!') { - return firstLetter + fact[1:len(fact) - 1] - } else { - return firstLetter + fact[1:] - } + return firstLetter + fact[1:puncuationIndex] + punctuation + fact[puncuationIndex+1:] } From 66bfc3a218b98ac4a393f28750bc25dff71b5163 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Wed, 28 Nov 2018 20:52:37 -0800 Subject: [PATCH 09/14] Add ability to mention people in bot message --- api/GroupMe.go | 36 ++++++++++++++++++++++++++++++++++++ skills/SkillsRegistry.go | 1 + skills/SummonSkill.go | 20 ++++++++++++++++++++ types/Types.go | 9 +++++++++ 4 files changed, 66 insertions(+) create mode 100644 skills/SummonSkill.go diff --git a/api/GroupMe.go b/api/GroupMe.go index 5c5a37f..8e1d535 100644 --- a/api/GroupMe.go +++ b/api/GroupMe.go @@ -6,6 +6,8 @@ import ( "net/http" "fmt" "os" + "strconv" + "geoffrey/types" ) const groupMeBaseUrl = "https://api.groupme.com/v3" @@ -48,6 +50,40 @@ func PostGroupMeMessageWithPicture(message string, imageUrl string) { postGroupMeMessage(body) } +func PostGroupMeMessageWithMentions(message string, mentions ...types.GroupMeMessageMention) { + id := getBotId() + if (id == "") { + fmt.Printf("Cannot send message; $%v environment variable not set\n", botIdEnvironmentVar) + return + } + + var lociArr = "" + var userIdArr = "" + + if (len(mentions) > 0) { + for _, mention := range(mentions) { + lociArr += "[" + strconv.Itoa(mention.StartIndex) + "," + strconv.Itoa(mention.Length) + "]," + userIdArr += `"` + mention.UserId + `",` + } + + // Cut off extra commas + lociArr = lociArr[:len(lociArr)-1] + userIdArr = userIdArr[:len(userIdArr)-1] + } + + body := fmt.Sprintf(`{ + "bot_id": "%v", + "text": "%v", + "attachments": [{ + "type": "mentions", + "loci": [%v], + "user_ids": [%v] + }] + }`, botId, message, lociArr, userIdArr) + + postGroupMeMessage(body) +} + // postGroupMeMessage is the internal function that both public post functions delegate to func postGroupMeMessage(postBody string) { resp, err := http.Post(botPostMessageUrl, jsonContentType, bytes.NewBufferString(postBody)) diff --git a/skills/SkillsRegistry.go b/skills/SkillsRegistry.go index a421ed0..ae67fc5 100644 --- a/skills/SkillsRegistry.go +++ b/skills/SkillsRegistry.go @@ -11,6 +11,7 @@ type ActiveSkill func(types.GroupMeMessagePost) bool var activeSkills = []ActiveSkill { yesOrNoSkill, genericQuestionSkill, + summonSkill, catFactSkill, } diff --git a/skills/SummonSkill.go b/skills/SummonSkill.go new file mode 100644 index 0000000..a5031fc --- /dev/null +++ b/skills/SummonSkill.go @@ -0,0 +1,20 @@ +package skills + +import ( + "geoffrey/api" + "geoffrey/types" +) + +func summonSkill(message types.GroupMeMessagePost) bool { + + var messageText = "@Kaie Westmaas" + var mention = types.GroupMeMessageMention { + UserId: "1234", + StartIndex: 0, + Length: len(messageText), + } + + api.PostGroupMeMessageWithMentions(messageText, mention) + + return true; +} diff --git a/types/Types.go b/types/Types.go index fddabfc..19b41ac 100644 --- a/types/Types.go +++ b/types/Types.go @@ -9,6 +9,15 @@ type GroupMeMessagePost struct { MessageText string } +// This struct represents a mention in a group me message +// StartIndex refers to the start index of the mention in the message text +// Length refers to the length of the substring mention text +type GroupMeMessageMention struct { + UserId string + StartIndex int + Length int +} + type ConsoleCommand interface { // Name returns the name of this command Name() string From 8fc08cc4d60460a536c8fe20d8022a4d2012b8da Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sat, 1 Dec 2018 17:36:05 -0800 Subject: [PATCH 10/14] Make passive skill structure, start Temporize integration --- api/Temporize.go | 12 +++++++ endpoints/EndpointRegistry.go | 1 + endpoints/PassiveSkillRequest.go | 54 ++++++++++++++++++++++++++++++++ skills/SkillsRegistry.go | 16 ++++++++-- skills/SummonSkill.go | 9 +++--- 5 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 api/Temporize.go create mode 100644 endpoints/PassiveSkillRequest.go diff --git a/api/Temporize.go b/api/Temporize.go new file mode 100644 index 0000000..930064b --- /dev/null +++ b/api/Temporize.go @@ -0,0 +1,12 @@ +package api + + +// ScheduleSingleEventInAWeek will schedule a cron job for approximately (+/- 2 days) a week from today +// at a random time around noon +func ScheduleSingleEventInAWeek(url string) { + // TODO: THIS +} + +func ScheduleSingleEvent(cron string, url string) { + // TODO: THIS +} \ No newline at end of file diff --git a/endpoints/EndpointRegistry.go b/endpoints/EndpointRegistry.go index 0f2948f..ad52a1f 100644 --- a/endpoints/EndpointRegistry.go +++ b/endpoints/EndpointRegistry.go @@ -9,6 +9,7 @@ type HandlerFunc func(http.ResponseWriter, *http.Request) var endpoints = map[string] HandlerFunc { "/" : fallbackHandler, "/message" : messageRecieved, + "/skill/passive" : passiveSkillRequest, } // GetAllEndpoints returns all registered endpoints in a map diff --git a/endpoints/PassiveSkillRequest.go b/endpoints/PassiveSkillRequest.go new file mode 100644 index 0000000..a951e4b --- /dev/null +++ b/endpoints/PassiveSkillRequest.go @@ -0,0 +1,54 @@ +package endpoints + +import ( + "net/http" + "fmt" + + "geoffrey/api" + "geoffrey/skills" +) + +const skillNameQueryParam = "skill" + +const rescheduleQueryParam = "reschedule" +const rescheduleWeekQueryParam = "week" +const rescheduleURL = "TODO" + +func passiveSkillRequest(w http.ResponseWriter, r *http.Request) { + fmt.Println("Passive Skill Request recieved!!"); + + skillsParam, ok := r.URL.Query()[skillNameQueryParam] + + if !ok || len(skillsParam) < 1 { + fmt.Printf("Missing %v query param!\n", skillNameQueryParam) + w.WriteHeader(400) + return + } + + // Get the first skill in the array + skill := skills.GetPassiveSkillByName(skillsParam[0]) + + if skill == nil { + fmt.Printf("Skill (%v) not found!\n", skillsParam[0]) + w.WriteHeader(400) + return + } + + fmt.Printf("Running passive skill %v...\n", skillsParam[0]) + skill() + + rescheduleParams, ok := r.URL.Query()[rescheduleQueryParam] + + if ok && len(rescheduleParams) >= 1 { + if rescheduleParams[0] == rescheduleWeekQueryParam { + fmt.Println("Rescheduling event for ~1 week") + api.ScheduleSingleEventInAWeek(rescheduleURL) + } else { + fmt.Printf("Unexpected reschedule duration: %t\n", rescheduleParams[0]) + w.WriteHeader(400) + return + } + } + + w.WriteHeader(200) +} diff --git a/skills/SkillsRegistry.go b/skills/SkillsRegistry.go index ae67fc5..ea4c3cf 100644 --- a/skills/SkillsRegistry.go +++ b/skills/SkillsRegistry.go @@ -1,6 +1,8 @@ package skills -import "geoffrey/types" +import ( + "geoffrey/types" +) // A Skill is a function that takes a map containing the POST body of a GroupMe message POST // and may or may not perform an action based on it @@ -8,14 +10,24 @@ import "geoffrey/types" // ActiveSkills return true/false depending on whether or not they consumed the event type ActiveSkill func(types.GroupMeMessagePost) bool +// Passive skills just do their thing man +type PassiveSkill func() + var activeSkills = []ActiveSkill { yesOrNoSkill, genericQuestionSkill, - summonSkill, catFactSkill, } // GetActiveSkills returns all registered active skills in order or priority func GetActiveSkills() []ActiveSkill { return activeSkills +} + +var passiveSkills = map[string] PassiveSkill { + "summon-skill": summonSkill, +} + +func GetPassiveSkillByName(name string) PassiveSkill { + return passiveSkills[name] } \ No newline at end of file diff --git a/skills/SummonSkill.go b/skills/SummonSkill.go index a5031fc..dbbfbb0 100644 --- a/skills/SummonSkill.go +++ b/skills/SummonSkill.go @@ -1,20 +1,19 @@ package skills import ( + "os" "geoffrey/api" "geoffrey/types" ) -func summonSkill(message types.GroupMeMessagePost) bool { - +// summons the beast... +func summonSkill() { var messageText = "@Kaie Westmaas" var mention = types.GroupMeMessageMention { - UserId: "1234", + UserId: os.Getenv("SUMMON_USER_ID"), StartIndex: 0, Length: len(messageText), } api.PostGroupMeMessageWithMentions(messageText, mention) - - return true; } From 1bb493e593ce472a95b9ffa508b98af0559603aa Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sun, 9 Dec 2018 13:28:52 -0800 Subject: [PATCH 11/14] Add passive skill infrastructure and event scheduling --- api/Temporize.go | 62 +++++++++++++++++++++++++++++--- endpoints/PassiveSkillRequest.go | 9 +++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/api/Temporize.go b/api/Temporize.go index 930064b..a3fdf90 100644 --- a/api/Temporize.go +++ b/api/Temporize.go @@ -1,12 +1,64 @@ package api +import ( + "io/ioutil" + "net/http" + "net/url" + "fmt" + "time" + "math/rand" + "os" +) -// ScheduleSingleEventInAWeek will schedule a cron job for approximately (+/- 2 days) a week from today +const temporizeURLEnvironmentVar = "TEMPORIZE_URL" + +var temporizeUrl = "" + +// ScheduleSingleEventInAWeek will schedule an event for approximately a week (4-7 days) from today // at a random time around noon -func ScheduleSingleEventInAWeek(url string) { - // TODO: THIS +func ScheduleSingleEventInAWeek(callbackUrl string) { + now := time.Now() + rand.Seed(now.Unix()) + + later := now.AddDate(0, 0, 4 + rand.Intn(3)) + later = time.Date(later.Year(), + later.Month(), + later.Day(), + rand.Intn(24), + rand.Intn(60), + 0, 0, time.UTC) + + ScheduleSingleEvent(later, callbackUrl) +} + +func ScheduleSingleEvent(t time.Time, callbackUrl string) { + baseUrl := getTemporizeUrl() + if baseUrl == "" { + fmt.Printf("Cannot schedule event; $%v environment variable not set\n", temporizeURLEnvironmentVar) + return + } + + url := fmt.Sprintf("%v/v1/events/%v/%v", baseUrl, t.Format(time.RFC3339), url.QueryEscape(callbackUrl)) + + resp, err := http.Post(url, "text/plain", nil) + + if (err != nil) { + fmt.Printf("Error scheduling event; %v", err) + return + } + + if (resp.StatusCode != 200) { + respBody, _ := ioutil.ReadAll(resp.Body) + fmt.Printf("Error scheduling event; Post failed; response code: %v: %v;\n\tbody: %v\n", resp.StatusCode, resp.Status, string(respBody)) + } + + resp.Body.Close() } -func ScheduleSingleEvent(cron string, url string) { - // TODO: THIS +func getTemporizeUrl() string { + if temporizeUrl == "" { + temporizeUrl = os.Getenv(temporizeURLEnvironmentVar) + } + + return temporizeUrl } \ No newline at end of file diff --git a/endpoints/PassiveSkillRequest.go b/endpoints/PassiveSkillRequest.go index a951e4b..c049ad5 100644 --- a/endpoints/PassiveSkillRequest.go +++ b/endpoints/PassiveSkillRequest.go @@ -3,6 +3,7 @@ package endpoints import ( "net/http" "fmt" + "os" "geoffrey/api" "geoffrey/skills" @@ -12,7 +13,6 @@ const skillNameQueryParam = "skill" const rescheduleQueryParam = "reschedule" const rescheduleWeekQueryParam = "week" -const rescheduleURL = "TODO" func passiveSkillRequest(w http.ResponseWriter, r *http.Request) { fmt.Println("Passive Skill Request recieved!!"); @@ -42,7 +42,8 @@ func passiveSkillRequest(w http.ResponseWriter, r *http.Request) { if ok && len(rescheduleParams) >= 1 { if rescheduleParams[0] == rescheduleWeekQueryParam { fmt.Println("Rescheduling event for ~1 week") - api.ScheduleSingleEventInAWeek(rescheduleURL) + fmt.Printf("rescheuling at: %v\n", getServerName() + r.URL.RequestURI()) + api.ScheduleSingleEventInAWeek(getServerName() + r.URL.RequestURI()) } else { fmt.Printf("Unexpected reschedule duration: %t\n", rescheduleParams[0]) w.WriteHeader(400) @@ -52,3 +53,7 @@ func passiveSkillRequest(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) } + +func getServerName() string { + return os.Getenv("SELF_URL") +} From d977f036b7a16426c147300dd0606cd24d7932e0 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Tue, 11 Dec 2018 22:30:05 -0800 Subject: [PATCH 12/14] Add passive cat fact skill and roaster skill --- skills/CatFactSkill.go | 54 ++++++++++++--- skills/RoasterSkill.go | 140 +++++++++++++++++++++++++++++++++++++++ skills/SkillsRegistry.go | 27 +++++++- 3 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 skills/RoasterSkill.go diff --git a/skills/CatFactSkill.go b/skills/CatFactSkill.go index 7855ffa..2dc9024 100644 --- a/skills/CatFactSkill.go +++ b/skills/CatFactSkill.go @@ -15,6 +15,15 @@ var greetingOptions = []string { "Hello there %v.", } +var passiveGreetingSubjectOptions = []string { + "y'all", + "guys", + "everyone", + "fam", + "friends", + "losers", +} + var factPrefixOptions = []string { "Did you know %v?", "Have you heard that %v?", @@ -23,17 +32,13 @@ var factPrefixOptions = []string { "Guess what! Ahh you'll never guess I'll just tell you: %v!", } -// catFactSkill sends the group a cat fact if geoffrey is +// catFactActiveSkill sends the group a cat fact if geoffrey is // mentioned in a message -func catFactSkill(message types.GroupMeMessagePost) bool { +func catFactActiveSkill(message types.GroupMeMessagePost) bool { if (!isGeoffreyMentioned(message.MessageText)) { return false } - // Go get the cat fact while we figure out who to @ - catFactChannel := make(chan api.StringResult) - go api.GetCatFactAsync(catFactChannel) - // Get only the first name of the sender sender := message.Sender indexOfSpace := strings.Index(sender, " ") @@ -41,7 +46,36 @@ func catFactSkill(message types.GroupMeMessagePost) bool { sender = sender[:indexOfSpace] } - greeting := fmt.Sprintf(pickRandomFromStringArray(greetingOptions), sender) + finalMessage, err := getCatFact(sender) + if err != nil { + fmt.Printf("Error getting cat fact for cat fact skill; %v", err) + return false + } + + api.PostGroupMeMessage(finalMessage) + + return true +} + +// catFactPassiveSkill sends a random cat fact to the group un prompted! +func catFactPassiveSkill() { + finalMessage, err := getCatFact(pickRandomFromStringArray(passiveGreetingSubjectOptions)) + if err != nil { + fmt.Printf("Error getting cat fact for cat fact skill; %v", err) + return + } + + api.PostGroupMeMessage(finalMessage) +} + +// getCatFact gets the cat fact from the API and formats the message while its waiting for a response +func getCatFact(name string) (string, error) { + // Go get the cat fact while we figure out who to @ + catFactChannel := make(chan api.StringResult) + go api.GetCatFactAsync(catFactChannel) + + + greeting := fmt.Sprintf(pickRandomFromStringArray(greetingOptions), name) factFormat := string(pickRandomFromStringArray(factPrefixOptions)) factWithoutPunctuation := factFormat[:len(factFormat) - 1] factPunctuation := string(factFormat[len(factFormat) - 1]) @@ -49,16 +83,14 @@ func catFactSkill(message types.GroupMeMessagePost) bool { catFactResult := <-catFactChannel if (catFactResult.Err != nil) { - fmt.Printf("Error getting cat fact for cat fact skill; %v", catFactResult.Err) - return false + return "", catFactResult.Err } fact := formatCatFact(catFactResult.Result, factPunctuation) finalMessage := greeting + " " + fmt.Sprintf(factWithoutPunctuation, fact) - api.PostGroupMeMessage(finalMessage) - return true + return finalMessage, nil } func formatCatFact(fact string, punctuation string) string { diff --git a/skills/RoasterSkill.go b/skills/RoasterSkill.go new file mode 100644 index 0000000..8328057 --- /dev/null +++ b/skills/RoasterSkill.go @@ -0,0 +1,140 @@ +package skills + +import ( + "time" + "math/rand" + "geoffrey/api" + "geoffrey/types" +) + +type Roast struct { + text string + mention types.GroupMeMessageMention +} + +// map of string (user id) to potential roasts +// user id of "" can apply to anyone +var roasts = map[string] []Roast { + "21004947": { // Johnny + { + text: "@Johnny Bollash when're you gonna drop the whole 'Plutarch' thing and admit your middle name is Francis?", + mention: types.GroupMeMessageMention { + UserId: "21004947", + StartIndex: 0, + Length: len("@Johnny Bollash"), + }, + }, + { + text: "@Johnny Bollash Connecticut is small. Boom roasted", + mention: types.GroupMeMessageMention { + UserId: "21004947", + StartIndex: 0, + Length: len("@Johnny Bollash"), + }, + }, + }, + "20596690": { // Anokhi + { + text: "@Anohki Patel I was going through some of the old messages I missed while I was gone and...I'm a f**king guinea pig alright??", + mention: types.GroupMeMessageMention { + UserId: "20596690", + StartIndex: 0, + Length: len("@Anohki Patel"), + }, + }, + }, + "18172472": { // Apurva + { + text: "@Apurva Kasam you live in Missouri. Boom roasted", + mention: types.GroupMeMessageMention { + UserId: "18172472", + StartIndex: 0, + Length: len("@Apurva Kasam"), + }, + }, + }, + "20626795": { // Michael + { + text: "@Michael Moghaddam you write shitty bots", + mention: types.GroupMeMessageMention { + UserId: "20626795", + StartIndex: 0, + Length: len("@Michael Moghaddam"), + }, + }, + { + text: "@Michael Moghaddam D.C. isn't part of Maryland. Get over yourself.", + mention: types.GroupMeMessageMention { + UserId: "20626795", + StartIndex: 0, + Length: len("@Michael Moghaddam"), + }, + }, + }, + "17123786": { // Heman + { + text: "@Hemanth Koralla...do I know you?", + mention: types.GroupMeMessageMention { + UserId: "17123786", + StartIndex: 0, + Length: len("@Hemanth Koralla"), + }, + }, + }, + "20868132": { // David + { + text: "@David morrison why didn't you capitalize the 'm' in your last name?", + mention: types.GroupMeMessageMention { + UserId: "20868132", + StartIndex: 0, + Length: len("@David morrison"), + }, + }, + }, + "22602314": { // Kaie + { + text: "@Kaie Westmaas have you moved to Japan yet?", + mention: types.GroupMeMessageMention { + UserId: "22602314", + StartIndex: 0, + Length: len("@Kaie Westmaas"), + }, + }, + }, + "21405378": { // Ryan + { + text: "@Ryan Miller You dress like a dad. Boom roasted.", + mention: types.GroupMeMessageMention { + UserId: "21405378", + StartIndex: 0, + Length: len("@Ryan Miller"), + }, + }, + }, + "21498740": { // Vicki + { + text: "@Victoria Kravets got any new stalkers recently?", + mention: types.GroupMeMessageMention { + UserId: "21498740", + StartIndex: 0, + Length: len("@Victoria Kravets"), + }, + }, + }, +} + +func roasterPassiveSkill() { + roast := getRandomRoast() + + api.PostGroupMeMessageWithMentions(roast.text, roast.mention) +} + +func getRandomRoast() Roast { + var allRoasts []Roast + for _, roastList := range roasts { + allRoasts = append(allRoasts, roastList...) + } + + rand.Seed(time.Now().Unix()) + return allRoasts[rand.Intn(len(allRoasts))] +} \ No newline at end of file diff --git a/skills/SkillsRegistry.go b/skills/SkillsRegistry.go index ea4c3cf..1c55e7e 100644 --- a/skills/SkillsRegistry.go +++ b/skills/SkillsRegistry.go @@ -1,6 +1,9 @@ package skills import ( + "fmt" + "time" + "math/rand" "geoffrey/types" ) @@ -16,7 +19,7 @@ type PassiveSkill func() var activeSkills = []ActiveSkill { yesOrNoSkill, genericQuestionSkill, - catFactSkill, + catFactActiveSkill, } // GetActiveSkills returns all registered active skills in order or priority @@ -25,9 +28,29 @@ func GetActiveSkills() []ActiveSkill { } var passiveSkills = map[string] PassiveSkill { - "summon-skill": summonSkill, + "summon": summonSkill, + "cat-fact": catFactPassiveSkill, + "roast": roasterPassiveSkill, } func GetPassiveSkillByName(name string) PassiveSkill { + if (name == "random") { + rand.Seed(time.Now().Unix()) + + // Calculate the random index to use + var target = rand.Intn(len(passiveSkills)) + var idx = 0 + // Iterate over the map until we hit target, then return the skill + // Side note: I can't believe there isn't a better way to do this :\ + for name, skill := range passiveSkills { + if (idx == target) { + fmt.Printf("Getting random skill...%v\n", name) + return skill + } + + idx++ + } + } + return passiveSkills[name] } \ No newline at end of file From 271e055b386b319b6ed0946146fafaa181821eb1 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Sun, 3 Feb 2019 18:38:03 -0500 Subject: [PATCH 13/14] Add nostalgia passive skill --- api/CatFacts.go | 8 ++++- api/DropBox.go | 50 ++++++++++++++++++++++++++ api/GroupMeImages.go | 61 ++++++++++++++++++++++++++++++++ skills/NostalgiaSkill.go | 76 ++++++++++++++++++++++++++++++++++++++++ skills/SkillUtils.go | 22 ++++++++++++ skills/SkillsRegistry.go | 2 ++ 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 api/DropBox.go create mode 100644 api/GroupMeImages.go create mode 100644 skills/NostalgiaSkill.go diff --git a/api/CatFacts.go b/api/CatFacts.go index c31720f..2cf5b89 100644 --- a/api/CatFacts.go +++ b/api/CatFacts.go @@ -3,6 +3,7 @@ package api import ( "net/http" "errors" + "strings" ) const catFactsBaseUrl = "https://catfact.ninja" @@ -30,7 +31,7 @@ func GetCatFact() (string, error) { } if fact, exists := body[catFactFactKey]; exists { - return fact, nil + return sanitizeCatFact(fact), nil } else { return "", errors.New("Could not parse fact out of response body!") } @@ -39,4 +40,9 @@ func GetCatFact() (string, error) { func GetCatFactAsync(channel chan StringResult) { fact, err := GetCatFact() channel <- StringResult{fact, err} +} + +func sanitizeCatFact(fact string) string { + fact = strings.Replace(fact, "\"", "'", -1) // Remove quote marks + return strings.Replace(fact, "\\", "", -1) // Remove backslashes } \ No newline at end of file diff --git a/api/DropBox.go b/api/DropBox.go new file mode 100644 index 0000000..3a0228c --- /dev/null +++ b/api/DropBox.go @@ -0,0 +1,50 @@ +package api + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "errors" + "os" +) + +const dropboxContentBaseUrl = "https://content.dropboxapi.com/2/" +const dropboxFileDownloadUrl = dropboxContentBaseUrl + "files/download" + +const dropboxAccessTokenEnvVar = "DROPBOX_ACCESS_TOKEN" + +// DownloadFile retrieves a file from dropbox at the given path +// If is the caller's responsibility to close the file stream +func DownloadFile(filePath string) (io.ReadCloser, error) { + token := os.Getenv(dropboxAccessTokenEnvVar) + if token == "" { + return nil, errors.New("Could not download file; " + dropboxAccessTokenEnvVar + " env var not set") + } + + client := &http.Client{} + request, err := http.NewRequest("POST", dropboxFileDownloadUrl, nil) + addAuthHeader(token, request) + + arg := fmt.Sprintf(`{"path":"%v"}`, filePath) + + request.Header.Add("Dropbox-API-Arg", arg) + + resp, err := client.Do(request) + + if err != nil { + return nil, err + } + + if (resp.StatusCode != 200) { + respBody, _ := ioutil.ReadAll(resp.Body) + resp.Body.Close() + return nil, errors.New("Error downloading file: " + string(respBody)) + } + + return resp.Body, nil +} + +func addAuthHeader(token string, request *http.Request) { + request.Header.Add("Authorization", "Bearer " + token) +} \ No newline at end of file diff --git a/api/GroupMeImages.go b/api/GroupMeImages.go new file mode 100644 index 0000000..39f649e --- /dev/null +++ b/api/GroupMeImages.go @@ -0,0 +1,61 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" +) + +type responseData struct { + Data payload `json:"payload"` +} + +type payload struct { + Url string `json:"url"` + PictureUrl string `json:"picture_url"` +} + +const groupMeImageServiceUrl = "https://image.groupme.com/pictures?access_token=%v" + +const groupMeAccessTokenEnvVar = "GM_ACCESS_TOKEN" + +// Process Image posts the given image to GroupMe's image service +// and returns the group me url of the image +func ProcessImage(image io.Reader) string { + token := os.Getenv(groupMeAccessTokenEnvVar) + if token == "" { + fmt.Println("Cannot process image; GM_ACCESS_TOKEN env var not set") + return "" + } + + url := fmt.Sprintf(groupMeImageServiceUrl, token) + + resp, err := http.Post(url, "image/jpeg", image) + + defer resp.Body.Close() + + if err != nil { + fmt.Printf("Error from GroupMe image servce: %v", err) + return "" + } + + respBody, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != 200 { + return string(respBody) + } + + respPayload := responseData{} + + err = json.Unmarshal(respBody, &respPayload) + + if err != nil { + fmt.Printf("Error converting response body to JSON: %v\n", err) + return "" + } + + return respPayload.Data.PictureUrl +} diff --git a/skills/NostalgiaSkill.go b/skills/NostalgiaSkill.go new file mode 100644 index 0000000..4ee973c --- /dev/null +++ b/skills/NostalgiaSkill.go @@ -0,0 +1,76 @@ +package skills + +import ( + "fmt" + "math/rand" + "time" + "strconv" + "os" + + "geoffrey/api" +) + +const nostalgiaPicFilePath = "/pics/%v.jpg" +const numNostalgiaPicsEnvVar = "NUM_NOSTALGIA_PICS" + +var passiveGreetingOptions = []string { + "Let's go on a stroll down memory lane...", + "Hahahaha remember this?? (I don't...ya know cuz you guys kicked me out and all...)", + "Woah check out this blast from the past:", + "Ahhh those were the days...", + "How come no one ever invites me to cool stuff like this?", + "Woah when did this happen?", + "You guys sure had a good time without me...#FOMO", + "What's the story behind this beauty??", +} + +func nostalgiaPassiveSkill() { + pictureUrl := getRandomNostalgiaPicUrl() + + if pictureUrl != "" { + fmt.Printf("Random nostalgia pic url: %v\n", pictureUrl) + + messageText := pickRandomFromStringArray(passiveGreetingOptions) + + api.PostGroupMeMessageWithPicture(messageText, pictureUrl) + } else { + postErrorMessage() + } +} + +func getRandomNostalgiaPicUrl() string { + numNostalgiaPicsStr := os.Getenv(numNostalgiaPicsEnvVar) + if numNostalgiaPicsStr == "" { + fmt.Printf("Could not retrieve random nostalgia pic; %v env var not set!\n", numNostalgiaPicsEnvVar) + return "" + } + + numNostalgiaPics, _ := strconv.Atoi(numNostalgiaPicsStr) + + rand.Seed(time.Now().Unix()) + // I started the pics at 1 instead of 0, sue me + randIndex := 1 + (rand.Int() % numNostalgiaPics) + filePath := fmt.Sprintf(nostalgiaPicFilePath, randIndex) + + fmt.Printf("Random nostalgia pic index: %v\n", randIndex) + fmt.Printf("Downloading dropbox file %v...\n", filePath) + + file, err := api.DownloadFile(filePath) + + if err != nil { + fmt.Printf("Error downloading file: %v\n", err) + return "" + } + + defer file.Close() + + fmt.Printf("Processing %v in GroupMe Image service...\n", filePath) + + processedUrl := api.ProcessImage(file) + + if processedUrl == "" { + fmt.Printf("Error processing image with GroupMe Image service :(\n") + } + + return processedUrl +} diff --git a/skills/SkillUtils.go b/skills/SkillUtils.go index a89722b..4d5db61 100644 --- a/skills/SkillUtils.go +++ b/skills/SkillUtils.go @@ -4,6 +4,9 @@ import ( "math/rand" "time" "strings" + + "geoffrey/api" + "geoffrey/types" ) var aliases = []string { @@ -72,4 +75,23 @@ func isYesOrNoQuestion(message string) bool { func pickRandomFromStringArray(arr []string) string { rand.Seed(time.Now().Unix()) return arr[rand.Int() % len(arr)] +} + +var errorMessageOptions = []string { + "I don't feel so good...", + "Maybe try checking my logs every once in a while :/", + "I've got a cyber stomach ache pls help", +} + +// postErrorMessage sends a message to the groupme indicating something went wrong +func postErrorMessage() { + mention := types.GroupMeMessageMention { + UserId: "20626795", + StartIndex: 0, + Length: len("@Michael Moghaddam"), + } + + message := "@Michael Moghaddam " + pickRandomFromStringArray(errorMessageOptions) + + api.PostGroupMeMessageWithMentions(message, mention) } \ No newline at end of file diff --git a/skills/SkillsRegistry.go b/skills/SkillsRegistry.go index 1c55e7e..98a8292 100644 --- a/skills/SkillsRegistry.go +++ b/skills/SkillsRegistry.go @@ -31,6 +31,8 @@ var passiveSkills = map[string] PassiveSkill { "summon": summonSkill, "cat-fact": catFactPassiveSkill, "roast": roasterPassiveSkill, + "nostalgia": nostalgiaPassiveSkill, + "nostalgia-rand-bump": nostalgiaPassiveSkill, // Bump up the likelihood of this skill getting picked randomly } func GetPassiveSkillByName(name string) PassiveSkill { From e1c1c40155bb92a161ff835655ec71b3bc62d921 Mon Sep 17 00:00:00 2001 From: Michael Moghaddam Date: Mon, 27 May 2019 16:13:02 -0700 Subject: [PATCH 14/14] Add "did" as yes or no question starter --- skills/SkillUtils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/SkillUtils.go b/skills/SkillUtils.go index 4d5db61..b3edc7c 100644 --- a/skills/SkillUtils.go +++ b/skills/SkillUtils.go @@ -62,7 +62,7 @@ func isYesOrNoQuestion(message string) bool { message = strings.ToLower(message) // Now check if it starts with a yes/no question starter - for _, starter := range []string { "do", "should", "will", "am", "is", "are" } { + for _, starter := range []string { "do", "did", "should", "will", "am", "is", "are" } { if (strings.Contains(message, starter)) { return true } @@ -94,4 +94,4 @@ func postErrorMessage() { message := "@Michael Moghaddam " + pickRandomFromStringArray(errorMessageOptions) api.PostGroupMeMessageWithMentions(message, mention) -} \ No newline at end of file +}