-
Notifications
You must be signed in to change notification settings - Fork 0
Don't hold back #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: ricky-review
Are you sure you want to change the base?
Changes from all commits
6bdc1a9
6bb38b6
ba181c3
2619108
171ec66
36afcd8
9dce13c
97e5915
66bfc3a
8fc08cc
1bb493e
d977f03
271e055
30556a7
e1c1c40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "geoffrey/commands" | ||
|
|
||
| "os" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. look into goimports for info on how go likes its stuff imported. The rule is top group is stdlib so stuff like |
||
| ) | ||
|
|
||
| func main() { | ||
| commands.RegisterCommand(&commands.ServerCommand{}) | ||
| commands.RegisterCommand(&commands.HelpCommand{}) | ||
|
|
||
| commands.RunCommand(os.Args[1:]) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| web: geoffrey server |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # GeoffreyBot | ||
| Geoffrey is a guinea pig/GroupMe bot with a new lease on life |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "errors" | ||
| "strings" | ||
| ) | ||
|
|
||
| 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() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. something I started seeing at work is people putting typical then instead of
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just want your opinion on it. I think it does nothing but help, even if you don't have all the context you normally would have when logging errors |
||
|
|
||
| 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 sanitizeCatFact(fact), nil | ||
| } else { | ||
| return "", errors.New("Could not parse fact out of response body!") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. errors shouldn't start with a capital letter |
||
| } | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is more of an opinion piece: what would you think of doing this instead. I consider it since you have a comment explaining Also its very clear what is happening given the named return values the different |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "io/ioutil" | ||
| "bytes" | ||
| "net/http" | ||
| "fmt" | ||
| "os" | ||
| "strconv" | ||
| "geoffrey/types" | ||
| ) | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| body := fmt.Sprintf(`{ | ||
| "bot_id": "%v", | ||
| "text": "%v", | ||
| "picture_url": "%v" | ||
| }`, id, message, imageUrl) | ||
|
|
||
| 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)) | ||
|
|
||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is familiar. I forget why its familiar but this is a pattern ive seen before. Go likes sourcing all environment variables up front and in one spot if possible. Practically this is very useful because you dont want to find out youre missing an env var only when Having it in one place makes it clear to someone else what is required. Thats why people either source env vars in
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah actually dont use |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when i better understand the overall project I'll have better input. But I have a feeling there is a better way to represent the body than
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the right link. Its for |
||
| bodyBytes, err := ioutil.ReadAll(response.Body) | ||
|
|
||
| if (err != nil) { | ||
| return nil, err | ||
| } | ||
|
|
||
| bodyMap := make(map[string] string) | ||
|
|
||
| json.Unmarshal(bodyBytes, &bodyMap) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. anything that returns an error should be handled. it gets annoying cause its so boiler plate but having is proper because it provides for more robust code. |
||
|
|
||
| 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) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "io/ioutil" | ||
| "net/http" | ||
| "net/url" | ||
| "fmt" | ||
| "time" | ||
| "math/rand" | ||
| "os" | ||
| ) | ||
|
|
||
| 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(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 getTemporizeUrl() string { | ||
| if temporizeUrl == "" { | ||
| temporizeUrl = os.Getenv(temporizeURLEnvironmentVar) | ||
| } | ||
|
|
||
| return temporizeUrl | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how this is working. I don't see a package named
geoffrey. On my IDE it is happy when I do./commandsbut thats still not proper form.go project structures love the long form that you see in repository urls. So
github.com/mmoghaddam385/GeoffreyBot/commandswould be the proper way.Of course for this to work properly on your machine, or any machine that is running this, the
GOPATHwould need to be set and this code would need to live in$GOPATH/github.com/mmoghaddam385/