Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package main

import (
"geoffrey/commands"
Copy link
Collaborator

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 ./commands but thats still not proper form.

go project structures love the long form that you see in repository urls. So github.com/mmoghaddam385/GeoffreyBot/commands would be the proper way.

Of course for this to work properly on your machine, or any machine that is running this, the GOPATH would need to be set and this code would need to live in $GOPATH/github.com/mmoghaddam385/


"os"
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 os. Second group is third party. Last group is stuff thats in this project.

)

func main() {
commands.RegisterCommand(&commands.ServerCommand{})
commands.RegisterCommand(&commands.HelpCommand{})

commands.RunCommand(os.Args[1:])
}
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: geoffrey server
2 changes: 2 additions & 0 deletions README.md
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
48 changes: 48 additions & 0 deletions api/CatFacts.go
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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something I started seeing at work is people putting typical defered functions in a wrapper that catches errors for them. Since Close() can return an error yet its typically defered it causes room for missed bugs. So what i've seen some people do is

func logError(f func () error) {
	err := f()
	if err != nil {
		log15.Error("something happened!", "error", err)
	}
}

then instead of defer resp.Body.Close() they do defer logError(resp.Body.Close)

Copy link
Collaborator

Choose a reason for hiding this comment

The 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!")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errors shouldn't start with a capital letter
https://github.com/golang/go/wiki/CodeReviewComments#error-strings

}
}

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
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

func removeQuotes(old string) (new string) {
    return strings.replace(old, "\"", "'", -1)
}

func removeBackslashes(old string) (new string) {
    return strings.Replace(old, "\\", "",  -1)
}

func sanitzeCatFact(fact string) string {
    return removeBackSlashes(removeQuotes(fact))
}
// or
func sanitzeCatFact(fact string) (sanitizedFact string) {
    sanitizedFact = removeQuotes(fact)
    return removeBackSlashes(sanitizedFact)
}

I consider it since you have a comment explaining what the two lines are doing and im under the opinion that breaking out little things for clarity is worth it, even if its more lines of code.

Also its very clear what is happening given the named return values (new string)

the different sanitizeCatFacts are up to you. I think idiomatic go would prefer the second version Obvious code is important. What you can do in one line you should do in three

50 changes: 50 additions & 0 deletions api/DropBox.go
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)
}
110 changes: 110 additions & 0 deletions api/GroupMe.go
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 x happens.
You do want to find out when you deploy, or practice deploying. cause if you fail a deploy you can rollback and try again later. if you fail only when x happens then you have a bug.

Having it in one place makes it clear to someone else what is required. Thats why people either source env vars in main.go or in an init() per package. theres a link somewhere to what init() does

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah actually dont use init. declare them all in the top level var block and either set a default or exit if it doesnt exist. At work we have utils.GetEnv{type}(envName string, default {type} and utils.MustGetEnv{type}(envName string) which does os.Exit(1) if envName points to an empty value

}
61 changes: 61 additions & 0 deletions api/GroupMeImages.go
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
}
34 changes: 34 additions & 0 deletions api/NetUtils.go
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 map[string]string. A lot of times people make structs with json tags and when you throw that into json.Unmarshal it works easy. Then you have your own custom representation that you can cater towards readability. ill find a link

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right link. Its for marshal but works both ways
https://golang.org/pkg/encoding/json/#Marshal

bodyBytes, err := ioutil.ReadAll(response.Body)

if (err != nil) {
return nil, err
}

bodyMap := make(map[string] string)

json.Unmarshal(bodyBytes, &bodyMap)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

err := f()
if err != nil {
  // do something
 // maybe just return err
}

is proper because it provides for more robust code.
theres been interesting conversation relating to error handling in go and some are proposals for Go 2


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)
}
64 changes: 64 additions & 0 deletions api/Temporize.go
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
}
Loading