diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 7802b3d..f40c0d9 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ -# groupme -GroupMe Library +# Version 1.0 Release Date: TBD +I would like to add common helper functions/features inspired by the package use in the community. So please, especially before Version 1.0 release, let me know what you would like to see added to the package, but bear in mind the main objective to be a simple wrapper for the API exposed by the GroupMe team. + +
+ +# GroupMe API Wrapper +## Description +The design of this package is meant to be super simple. Wrap the exposed API endpoints [documented](https://dev.groupme.com/docs/v3#v3) by the GroupMe team. While you can achieve the core of this package with cURL, there are some small added features, coupled along with a modern language, that should simplify writing GroupMe [bots](https://dev.groupme.com/bots) and [applications](https://dev.groupme.com/applications). + +[*FUTURE*] In addition to the Go package, there is also a CLI application built using this package; all the features are available from the command line. + +## Why? +I enjoy programming, I use GroupMe with friends, and I wanted to write a fun add-on application for our group. I happened to start using Go around this time, so it was good practice. + +## Example +```golang +package main + +import ( + "fmt" + + "github.com/densestvoid/groupme" +) + +// This is not a real token. Please find yours by logging +// into the GroupMe development website: https://dev.groupme.com/ +const authorizationToken = "0123456789ABCDEF" + + +// A short program that gets the gets the first 5 groups +// the user is part of, and then the first 10 messages of +// the first group in that list +func main() { + // Create a new client with your auth token + client := groupme.NewClient(authorizationToken) + + // Get the groups your user is part of + groups, err := client.IndexGroups(&GroupsQuery{ + Page: 0, + PerPage: 5, + Omit: "memberships", + }) + + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(groups) + + // Get first 10 messages of the first group + if len(groups) <= 0 { + fmt.Println("No groups") + } + + messages, err := client.IndexMessages(groups[0].ID, &IndexMessagesQuery{ + Limit: 10, + }) + + if err != nil { + fmt.Println(err) + } + + fmt.Println(messages) +} +``` + +## Installation + +### Go Package +`go get github.com/densestvoid/groupme` + +### [*FUTURE*] CLI + +## Contribute +I find the hours I can spend developing personal projects decreasing every year, so I welcome any help I can get. Feel free to tackle any open issues, or if a feature request catches your eye, feel free to reach out to me and we can discuss adding it to the package. However, once version 1.0 is released, I don't foresee much work happening on this project unless the GroupMe API is updated. + +## Credits +All credits for the actual platform belong to the GroupMe team; I only used the exposed API they wrote. + +## License +GPL-3.0 License © [DensestVoid](https://github.com/densestvoid) \ No newline at end of file diff --git a/blocks_api.go b/blocks_api.go new file mode 100644 index 0000000..b41b79a --- /dev/null +++ b/blocks_api.go @@ -0,0 +1,151 @@ +package groupme + +import ( + "net/http" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#blocks + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + blocksEndpointRoot = "/blocks" + + // Actual Endpoints + indexBlocksEndpoint = blocksEndpointRoot // GET + blockBetweenEndpoint = blocksEndpointRoot + "/between" // GET + createBlockEndpoint = blocksEndpointRoot // POST + unblockEndpoint = blocksEndpointRoot // DELETE +) + +////////// API Requests ////////// + +// Index + +/* +IndexBlock - + +A list of contacts you have blocked. These people cannot DM you + +Parameters: + userID - required, ID(string) +*/ +func (c *Client) IndexBlock(userID ID) ([]*Block, error) { + httpReq, err := http.NewRequest("GET", c.endpointBase+indexBlocksEndpoint, nil) + if err != nil { + return nil, err + } + + URL := httpReq.URL + query := URL.Query() + query.Set("user", userID.String()) + URL.RawQuery = query.Encode() + + var resp struct { + Blocks []*Block `json:"blocks"` + } + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp.Blocks, nil +} + +// Between + +/* +BlockBetween - + +Asks if a block exists between you and another user id + +Parameters: + otherUserID - required, ID(string) +*/ +func (c *Client) BlockBetween(userID, otherUserID ID) (bool, error) { + httpReq, err := http.NewRequest("GET", c.endpointBase+blockBetweenEndpoint, nil) + if err != nil { + return false, err + } + + URL := httpReq.URL + query := URL.Query() + query.Set("user", userID.String()) + query.Set("otherUser", otherUserID.String()) + URL.RawQuery = query.Encode() + + var resp struct { + Between bool `json:"between"` + } + err = c.do(httpReq, &resp) + if err != nil { + return false, err + } + + return resp.Between, nil +} + +// Create + +/* +CreateBlock - + +Creates a block between you and the contact + +Parameters: + userID - required, ID(string) + otherUserID - required, ID(string) +*/ +func (c *Client) CreateBlock(userID, otherUserID ID) (*Block, error) { + httpReq, err := http.NewRequest("POST", c.endpointBase+createBlockEndpoint, nil) + if err != nil { + return nil, err + } + + URL := httpReq.URL + query := URL.Query() + query.Set("user", userID.String()) + query.Set("otherUser", otherUserID.String()) + URL.RawQuery = query.Encode() + + var resp struct { + Block *Block `json:"block"` + } + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp.Block, nil +} + +// Unblock + +/* +Unblock - + +Removes block between you and other user + +Parameters: + userID - required, ID(string) + otherUserID - required, ID(string) +*/ +func (c *Client) Unblock(userID, otherUserID ID) error { + httpReq, err := http.NewRequest("DELETE", c.endpointBase+unblockEndpoint, nil) + if err != nil { + return err + } + + URL := httpReq.URL + query := URL.Query() + query.Set("user", userID.String()) + query.Set("otherUser", otherUserID.String()) + URL.RawQuery = query.Encode() + + err = c.do(httpReq, nil) + if err != nil { + return err + } + + return nil +} diff --git a/blocks_api_test.go b/blocks_api_test.go new file mode 100644 index 0000000..b754a32 --- /dev/null +++ b/blocks_api_test.go @@ -0,0 +1,126 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type BlocksAPISuite struct{ APISuite } + +func (s *BlocksAPISuite) SetupSuite() { + s.handler = blocksTestRouter() + s.setupSuite() +} + +func (s *BlocksAPISuite) TestBlocksIndex() { + blocks, err := s.client.IndexBlock("1") + s.Require().NoError(err) + s.Require().NotZero(blocks) + for _, block := range blocks { + s.Assert().NotZero(block) + } +} + +func (s *BlocksAPISuite) TestBlocksBetween() { + between, err := s.client.BlockBetween("1", "2") + s.Require().NoError(err) + s.Assert().True(between) +} + +func (s *BlocksAPISuite) TestBlocksCreate() { + block, err := s.client.CreateBlock("1", "2") + s.Require().NoError(err) + s.Assert().NotZero(block) +} + +func (s *BlocksAPISuite) TestBlocksUnblock() { + s.Assert().NoError(s.client.Unblock("1", "2")) +} + +func TestBlocksAPISuite(t *testing.T) { + suite.Run(t, new(BlocksAPISuite)) +} + +func blocksTestRouter() *mux.Router { + router := mux.NewRouter() + + // Index + router.Path("/blocks"). + Queries("user", ""). + Methods("GET"). + Name("IndexBlock"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "blocks": [ + { + "user_id": "1234567890", + "blocked_user_id": "1234567890", + "created_at": 1302623328 + } + ] + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Block Between + router.Path("/blocks/between"). + Queries("user", "", "otherUser", ""). + Methods("GET"). + Name("BlockBetween"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "between": true + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Create Block + router.Path("/blocks"). + Queries("user", "", "otherUser", ""). + Methods("POST"). + Name("CreateBlock"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "block": { + "user_id": "1234567890", + "blocked_user_id": "1234567890", + "created_at": 1302623328 + } + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Unblock + router.Path("/blocks"). + Queries("user", "", "otherUser", ""). + Methods("DELETE"). + Name("Unblock"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + }) + + /*// Return test router //*/ + return router +} diff --git a/bots_api.go b/bots_api.go new file mode 100644 index 0000000..384e41e --- /dev/null +++ b/bots_api.go @@ -0,0 +1,142 @@ +package groupme + +import ( + "errors" + "fmt" + "net/http" + "net/url" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#bots + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + botsEndpointRoot = "/bots" + + // Actual Endpoints + createBotEndpoint = botsEndpointRoot // POST + postBotMessageEndpoint = botsEndpointRoot + "/post" // POST + indexBotsEndpoint = botsEndpointRoot // GET + destroyBotEndpoint = botsEndpointRoot + "/destroy" // POST +) + +////////// API Requests ////////// + +// Create + +/* +CreateBot - + +Create a bot. See the Bots Tutorial (https://dev.groupme.com/tutorials/bots) +for a full walkthrough. + +Parameters: + See Bot + Name - required + GroupID - required +*/ +func (c *Client) CreateBot(bot *Bot) (*Bot, error) { + httpReq, err := http.NewRequest("POST", c.endpointBase+createBotEndpoint, nil) + if err != nil { + return nil, err + } + + if bot == nil { + return nil, errors.New("bot cannot be nil") + } + + data := url.Values{} + data.Add("bot", bot.String()) + + httpReq.PostForm = data + + var resp Bot + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +// PostMessage + +/* +PostBotMessage - + +Post a message from a bot + +Parameters: + botID - required, ID(string) + text - required, string + pictureURL - string; image must be processed through image + service (https://dev.groupme.com/docs/image_service) +*/ +func (c *Client) PostBotMessage(botID ID, text string, pictureURL *string) error { + URL := fmt.Sprintf(c.endpointBase + postBotMessageEndpoint) + + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return err + } + + data := url.Values{} + data.Add("bot_id", string(botID)) + data.Add("text", text) + if pictureURL != nil { + data.Add("picture_url", *pictureURL) + } + + httpReq.PostForm = data + + return c.do(httpReq, nil) +} + +// Index + +/* +IndexBots - + +List bots that you have created +*/ +func (c *Client) IndexBots() ([]*Bot, error) { + httpReq, err := http.NewRequest("GET", c.endpointBase+indexBotsEndpoint, nil) + if err != nil { + return nil, err + } + + var resp []*Bot + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +// Destroy + +/* +DestroyBot - + +Remove a bot that you have created + +Parameters: + botID - required, ID(string) +*/ +func (c *Client) DestroyBot(botID ID) error { + URL := fmt.Sprintf(c.endpointBase + destroyBotEndpoint) + + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return err + } + + data := url.Values{} + data.Add("bot_id", string(botID)) + + httpReq.PostForm = data + + return c.do(httpReq, nil) +} diff --git a/bots_api_test.go b/bots_api_test.go new file mode 100644 index 0000000..d3a98fc --- /dev/null +++ b/bots_api_test.go @@ -0,0 +1,120 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type BotsAPISuite struct{ APISuite } + +func (s *BotsAPISuite) SetupSuite() { + s.handler = botsTestRouter() + s.setupSuite() +} + +func (s *BotsAPISuite) TestBotsCreate() { + bot, err := s.client.CreateBot(&Bot{ + Name: "test", + GroupID: "1", + AvatarURL: "url.com", + CallbackURL: "otherURL.com", + DMNotification: true, + }) + s.Require().NoError(err) + s.Require().NotZero(bot) +} + +func (s *BotsAPISuite) TestBotsPostMessage() { + err := s.client.PostBotMessage("1", "test message", nil) + s.Require().NoError(err) +} + +func (s *BotsAPISuite) TestBotsIndex() { + bots, err := s.client.IndexBots() + s.Require().NoError(err) + s.Require().NotZero(bots) + for _, bot := range bots { + s.Assert().NotZero(bot) + } +} + +func (s *BotsAPISuite) TestBotsDestroy() { + s.Require().NoError(s.client.DestroyBot("1")) +} + +func TestBotsAPISuite(t *testing.T) { + suite.Run(t, new(BotsAPISuite)) +} + +func botsTestRouter() *mux.Router { + router := mux.NewRouter() + + // Create + router.Path("/bots"). + Methods("POST"). + Name("CreateBot"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(201) + fmt.Fprint(w, `{ + "response": { + "bot_id": "1234567890", + "group_id": "1234567890", + "name": "hal9000", + "avatar_url": "https://i.groupme.com/123456789", + "callback_url": "https://example.com/bots/callback", + "dm_notification": false + }, + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + // Post Message + router.Path("/bots/post"). + Methods("POST"). + Name("PostBotMessage"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(201) + }) + + // Index + router.Path("/bots"). + Methods("GET"). + Name("IndexBots"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": [ + { + "bot_id": "1234567890", + "group_id": "1234567890", + "name": "hal9000", + "avatar_url": "https://i.groupme.com/123456789", + "callback_url": "https://example.com/bots/callback", + "dm_notification": false + } + ], + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Destroy + router.Path("/bots/destroy"). + Methods("POST"). + Name("DestroyBot"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(201) + }) + + /*// Return test router //*/ + return router +} diff --git a/chats_api.go b/chats_api.go new file mode 100644 index 0000000..5d685c8 --- /dev/null +++ b/chats_api.go @@ -0,0 +1,61 @@ +package groupme + +import ( + "net/http" + "strconv" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#chats + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + chatsEndpointRoot = "/chats" + + indexChatsEndpoint = chatsEndpointRoot // GET +) + +// Index + +// ChatsQuery defines the optional URL parameters for IndexChats +type IndexChatsQuery struct { + // Page Number + Page int `json:"page"` + // Number of chats per page + PerPage int `json:"per_page"` +} + +/* +IndexChats - + +Returns a paginated list of direct message chats, or +conversations, sorted by updated_at descending. + +Parameters: See ChatsQuery +*/ +func (c *Client) IndexChats(req *IndexChatsQuery) ([]*Chat, error) { + httpReq, err := http.NewRequest("GET", c.endpointBase+indexChatsEndpoint, nil) + if err != nil { + return nil, err + } + + URL := httpReq.URL + query := URL.Query() + if req != nil { + if req.Page > 0 { + query.Set("page", strconv.Itoa(req.Page)) + } + if req.PerPage > 0 { + query.Set("per_page", strconv.Itoa(req.PerPage)) + } + } + URL.RawQuery = query.Encode() + + var resp []*Chat + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/chats_api_test.go b/chats_api_test.go new file mode 100644 index 0000000..bcc3faf --- /dev/null +++ b/chats_api_test.go @@ -0,0 +1,87 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type ChatsAPISuite struct{ APISuite } + +func (s *ChatsAPISuite) SetupSuite() { + s.handler = chatsTestRouter() + s.setupSuite() +} + +func (s *ChatsAPISuite) TestChatsIndex() { + chats, err := s.client.IndexChats( + &IndexChatsQuery{ + Page: 1, + PerPage: 20, + }, + ) + s.Require().NoError(err) + s.Require().NotZero(chats) + for _, chat := range chats { + s.Assert().NotZero(chat) + } +} + +func TestChatsAPISuite(t *testing.T) { + suite.Run(t, new(ChatsAPISuite)) +} + +func chatsTestRouter() *mux.Router { + router := mux.NewRouter() + + // Index + router.Path("/chats"). + Methods("GET"). + Name("IndexChats"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": [ + { + "created_at": 1352299338, + "updated_at": 1352299338, + "last_message": { + "attachments": [ + + ], + "avatar_url": "https://i.groupme.com/200x200.jpeg.abcdef", + "conversation_id": "12345+67890", + "created_at": 1352299338, + "favorited_by": [ + + ], + "id": "1234567890", + "name": "John Doe", + "recipient_id": "67890", + "sender_id": "12345", + "sender_type": "user", + "source_guid": "GUID", + "text": "Hello world", + "user_id": "12345" + }, + "messages_count": 10, + "other_user": { + "avatar_url": "https://i.groupme.com/200x200.jpeg.abcdef", + "id": "12345", + "name": "John Doe" + } + } + ], + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + /*// Return test router //*/ + return router +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..0b382db --- /dev/null +++ b/client.go @@ -0,0 +1,92 @@ +package groupme + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "net/http" +) + +// Endpoints are added on to the GroupMeAPIBase to get the full URI. +// Overridable for testing +const GroupMeAPIBase = "https://api.groupme.com/v3" + +// Client communicates with the GroupMe API to perform actions +// on the basic types, i.e. Listing, Creating, Destroying +type Client struct { + httpClient *http.Client + endpointBase string + authorizationToken string +} + +// NewClient creates a new GroupMe API Client +func NewClient(authToken string) *Client { + return &Client{ + httpClient: new(http.Client), + endpointBase: GroupMeAPIBase, + authorizationToken: authToken, + } +} + +// Close safely shuts down the Client +func (c *Client) Close() error { + c.httpClient.CloseIdleConnections() + return nil +} + +// String returns a json formatted string +func (c Client) String() string { + return marshal(&c) +} + +///// Handle parsing of nested interface type response ///// +type jsonResponse struct { + Response response `json:"response"` + Meta `json:"meta"` +} + +func newJSONResponse(i interface{}) *jsonResponse { + return &jsonResponse{Response: response{i}} +} + +type response struct { + i interface{} +} + +func (r response) UnmarshalJSON(bs []byte) error { + return json.NewDecoder(bytes.NewBuffer(bs)).Decode(r.i) +} + +func (c Client) do(req *http.Request, i interface{}) error { + getResp, err := c.httpClient.Do(req) + if err != nil { + return err + } + + // Check Status Code is 1XX or 2XX + if getResp.StatusCode/100 > 2 { + return errors.New(getResp.Status) + } + + bytes, err := ioutil.ReadAll(getResp.Body) + if err != nil { + return err + } + + if i == nil { + return nil + } + + resp := newJSONResponse(i) + if err := json.Unmarshal(bytes, &resp); err != nil { + return err + } + + // Check Status Code is 1XX or 2XX + if resp.Meta.Code/100 > 2 { + return &resp.Meta + } + + return nil +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..277559b --- /dev/null +++ b/client_test.go @@ -0,0 +1,49 @@ +package groupme + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/suite" +) + +type ClientSuite struct{ APISuite } + +func (s *ClientSuite) SetupSuite() { + serverMux := http.NewServeMux() + serverMux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + _, err := w.Write([]byte("error")) + s.Require().NoError(err) + }) + s.handler = serverMux + s.setupSuite() +} + +func (s *ClientSuite) SetupTest() { + s.client = NewClient("") + s.Require().NotNil(s.client) + + s.client.endpointBase = s.addr +} + +func (s *ClientSuite) TestClient_Close() { + s.Assert().NoError(s.client.Close()) +} + +func (s *ClientSuite) TestClient_do_DoError() { + req, err := http.NewRequest("", "", nil) + s.Require().NoError(err) + + s.Assert().Error(s.client.do(req, struct{}{})) +} + +func (s *ClientSuite) TestClient_do_UnmarshalError() { + req, err := http.NewRequest("GET", s.addr, nil) + s.Require().NoError(err) + + s.Assert().Error(s.client.do(req, struct{}{})) +} + +func TestClientSuite(t *testing.T) { + suite.Run(t, new(ClientSuite)) +} diff --git a/data_types.go b/data_types.go new file mode 100644 index 0000000..903731b --- /dev/null +++ b/data_types.go @@ -0,0 +1,94 @@ +package groupme + +import ( + "regexp" + "time" +) + +// GroupMe documentation: https://dev.groupme.com/docs/responses + +// ID is an unordered alphanumeric string +type ID string + +// Treated as a constant +var alphaNumericRegex = regexp.MustCompile(`^[a-zA-Z0-9]+$`) + +// Valid checks if the ID string is alpha numeric +func (id ID) Valid() bool { + return alphaNumericRegex.MatchString(string(id)) +} + +func (id ID) String() string { + return string(id) +} + +// Timestamp is the number of seconds since the UNIX epoch +type Timestamp uint64 + +// FromTime returns the time.Time as a Timestamp +func FromTime(t time.Time) Timestamp { + return Timestamp(t.Unix()) +} + +// ToTime returns the Timestamp as a UTC Time +func (t Timestamp) ToTime() time.Time { + return time.Unix(int64(t), 0).UTC() +} + +// String returns the Timestamp in the default time.Time string format +func (t Timestamp) String() string { + return t.ToTime().String() +} + +// PhoneNumber is the country code plus the number of the user +type PhoneNumber string + +// Treated as a constant +var phoneNumberRegex = regexp.MustCompile(`^\+[0-9]+ [0-9]{10}$`) + +// Valid checks if the ID string is alpha numeric +func (pn PhoneNumber) Valid() bool { + return phoneNumberRegex.MatchString(string(pn)) +} + +func (pn PhoneNumber) String() string { + return string(pn) +} + +// StatusCodes are returned by HTTP requests in +// the header and the json "meta" field +type HTTPStatusCode int + +// Text used as constant name +const ( + HTTP_Ok HTTPStatusCode = 200 + HTTP_Created HTTPStatusCode = 201 + HTTP_NoContent HTTPStatusCode = 204 + HTTP_NotModified HTTPStatusCode = 304 + HTTP_BadRequest HTTPStatusCode = 400 + HTTP_Unauthorized HTTPStatusCode = 401 + HTTP_Forbidden HTTPStatusCode = 403 + HTTP_NotFound HTTPStatusCode = 404 + HTTP_EnhanceYourCalm HTTPStatusCode = 420 + HTTP_InternalServerError HTTPStatusCode = 500 + HTTP_BadGateway HTTPStatusCode = 502 + HTTP_ServiceUnavailable HTTPStatusCode = 503 +) + +// String returns the description of the status code according to GroupMe +func (c HTTPStatusCode) String() string { + return map[HTTPStatusCode]string{ + HTTP_Ok: "success", + HTTP_Created: "resource was created successfully", + HTTP_NoContent: "resource was deleted successfully", + HTTP_NotModified: "no new data to return", + HTTP_BadRequest: "invalid format or data specified in the request", + HTTP_Unauthorized: "authentication credentials missing or incorrect", + HTTP_Forbidden: "request refused due to update limits", + HTTP_NotFound: "URI is invalid or resource does not exist", + HTTP_EnhanceYourCalm: "application is being rate limited", + HTTP_InternalServerError: "something unexpected occurred", + HTTP_BadGateway: "GroupMe is down or being upgraded", + HTTP_ServiceUnavailable: "servers are overloaded, try again later", + }[c] +} diff --git a/data_types_test.go b/data_types_test.go new file mode 100644 index 0000000..e1c535f --- /dev/null +++ b/data_types_test.go @@ -0,0 +1,60 @@ +package groupme + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type DataTypesSuite struct { + suite.Suite +} + +func (s *DataTypesSuite) TestID_Valid_True() { + var id ID = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + s.Assert().True(id.Valid()) +} + +func (s *DataTypesSuite) TestID_Valid_False() { + var id ID = "`~!@#$%^&*()_-+={[}]:;\"'<,>.?/|\\" + s.Assert().False(id.Valid()) +} + +func (s *DataTypesSuite) TestTimestamp_FromTime() { + t := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) + expected := Timestamp(0) + actual := FromTime(t) + s.Assert().EqualValues(expected, actual) +} + +func (s *DataTypesSuite) TestTimestamp_ToTime() { + t := Timestamp(0) + expected := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) + actual := t.ToTime() + s.Assert().EqualValues(expected, actual) +} + +func (s *DataTypesSuite) TestPhoneNumber_Valid_True() { + var pn PhoneNumber = "+1 0123456789" + s.Assert().True(pn.Valid()) +} + +func (s *DataTypesSuite) TestPhoneNumber_Valid_NoPlus() { + var pn PhoneNumber = "1 0123456789" + s.Assert().False(pn.Valid()) +} + +func (s *DataTypesSuite) TestPhoneNumber_Valid_NoSpace() { + var pn PhoneNumber = "+10123456789" + s.Assert().False(pn.Valid()) +} + +func (s *DataTypesSuite) TestPhoneNumber_Valid_BadLength() { + var pn PhoneNumber = "+1 01234567890" + s.Assert().False(pn.Valid()) +} + +func TestDataTypesSuite(t *testing.T) { + suite.Run(t, new(DataTypesSuite)) +} diff --git a/direct_messages_api.go b/direct_messages_api.go new file mode 100644 index 0000000..090e9fe --- /dev/null +++ b/direct_messages_api.go @@ -0,0 +1,145 @@ +package groupme + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/google/uuid" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#direct_messages + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + directMessagesEndpointRoot = "/direct_messages" + + // Actual Endpoints + indexDirectMessagesEndpoint = directMessagesEndpointRoot // GET + createDirectMessageEndpoint = directMessagesEndpointRoot // POST +) + +////////// API Requests ////////// + +// Index + +// MessagesQuery defines the optional URL parameters for IndexDirectMessages +type IndexDirectMessagesQuery struct { + // Returns 20 messages created before the given message ID + BeforeID ID `json:"before_id"` + // Returns 20 messages created after the given message ID + SinceID ID `json:"since_id"` +} + +func (q IndexDirectMessagesQuery) String() string { + return marshal(&q) +} + +// IndexDirectMessagesResponse contains the count and set of +// messages returned by the IndexDirectMessages API request +type IndexDirectMessagesResponse struct { + Count int `json:"count"` + Messages []*Message `json:"direct_messages"` +} + +func (r IndexDirectMessagesResponse) String() string { + return marshal(&r) +} + +/* +IndexDirectMessages - + +Fetch direct messages between two users. + +DMs are returned in groups of 20, ordered by created_at +descending. + +If no messages are found (e.g. when filtering with since_id) we +return code 304. + +Note that for historical reasons, likes are returned as an array +of user ids in the favorited_by key. + +Parameters: + otherUserID - required, ID(string); the other participant in the conversation. + See IndexDirectMessagesQuery +*/ +func (c *Client) IndexDirectMessages(otherUserID ID, req *IndexDirectMessagesQuery) (IndexDirectMessagesResponse, error) { + httpReq, err := http.NewRequest("GET", c.endpointBase+indexDirectMessagesEndpoint, nil) + if err != nil { + return IndexDirectMessagesResponse{}, err + } + + query := httpReq.URL.Query() + query.Set("other_user_id", otherUserID.String()) + if req != nil { + if req.BeforeID != "" { + query.Add("before_ID", req.BeforeID.String()) + } + if req.SinceID != "" { + query.Add("since_id", req.SinceID.String()) + } + } + + var resp IndexDirectMessagesResponse + err = c.do(httpReq, &resp) + if err != nil { + return IndexDirectMessagesResponse{}, err + } + + return resp, nil +} + +// Create + +/* +CreateDirectMessage - + +Send a DM to another user + +If you want to attach an image, you must first process it +through our image service. + +Attachments of type emoji rely on data from emoji PowerUps. + +Clients use a placeholder character in the message text and +specify a replacement charmap to substitute emoji characters + +The character map is an array of arrays containing rune data +([[{pack_id,offset}],...]). + +Parameters: + See Message. + recipientID - required, ID(string); The GroupMe user ID of the recipient of this message. + text - required, string. Can be ommitted if at least one + attachment is present + attachments - a polymorphic list of attachments (locations, + images, etc). You may have You may have more than + one of any type of attachment, provided clients can + display it. +*/ +func (c *Client) CreateDirectMessage(m *Message) (*Message, error) { + URL := fmt.Sprintf(c.endpointBase + createDirectMessageEndpoint) + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return nil, err + } + + m.SourceGUID = uuid.New().String() + + data := url.Values{} + data.Set("direct_message", m.String()) + + httpReq.PostForm = data + + var resp struct { + *Message `json:"message"` + } + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp.Message, nil +} diff --git a/direct_messages_api_test.go b/direct_messages_api_test.go new file mode 100644 index 0000000..3f54cac --- /dev/null +++ b/direct_messages_api_test.go @@ -0,0 +1,176 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type DirectMessagesAPISuite struct{ APISuite } + +func (s *DirectMessagesAPISuite) SetupSuite() { + s.handler = directMessagesTestRouter() + s.setupSuite() +} + +func (s *DirectMessagesAPISuite) TestDirectMessagesIndex() { + resp, err := s.client.IndexDirectMessages( + ID("123"), + &IndexDirectMessagesQuery{ + BeforeID: "0123456789", + SinceID: "9876543210", + }, + ) + s.Require().NoError(err) + s.Require().NotZero(resp) + for _, message := range resp.Messages { + s.Assert().NotZero(message) + } +} + +func (s *DirectMessagesAPISuite) TestDirectMessagesCreate() { + message, err := s.client.CreateDirectMessage( + &Message{ + RecipientID: ID("123"), + Text: "Test", + }, + ) + s.Require().NoError(err) + s.Require().NotNil(message) + s.Assert().NotZero(*message) +} + +func TestDirectMessagesAPISuite(t *testing.T) { + suite.Run(t, new(DirectMessagesAPISuite)) +} + +func directMessagesTestRouter() *mux.Router { + router := mux.NewRouter() + + // Index + router.Path("/direct_messages"). + Methods("GET"). + Name("IndexDirectMessages"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "count": 123, + "direct_messages": [ + { + "id": "1234567890", + "source_guid": "GUID", + "recipient_id": "20", + "user_id": "1234567890", + "created_at": 1302623328, + "name": "John", + "avatar_url": "https://i.groupme.com/123456789", + "text": "Hello world ☃☃", + "favorited_by": [ + "101" + ], + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + ] + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Create + router.Path("/direct_messages"). + Methods("POST"). + Name("CreateDirectMessage"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(201) + fmt.Fprint(w, `{ + "response": { + "message": { + "id": "1234567890", + "source_guid": "GUID", + "recipient_id": "20", + "user_id": "1234567890", + "created_at": 1302623328, + "name": "John", + "avatar_url": "https://i.groupme.com/123456789", + "text": "Hello world ☃☃", + "favorited_by": [ + "101" + ], + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + }, + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + /*// Return test router //*/ + return router +} diff --git a/go.mod b/go.mod new file mode 100755 index 0000000..bf9a613 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/densestvoid/groupme + +go 1.14 + +require ( + github.com/go-critic/go-critic v0.5.0 // indirect + github.com/go-toolsmith/astinfo v1.0.0 // indirect + github.com/google/go-cmp v0.5.1 // indirect + github.com/google/uuid v1.1.1 + github.com/gorilla/mux v1.7.4 + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/goveralls v0.0.6 // indirect + github.com/quasilyte/go-consistent v0.0.0-20200404105227-766526bf1e96 // indirect + github.com/quasilyte/go-ruleguard v0.1.3 // indirect + github.com/quasilyte/regex/syntax v0.0.0-20200419152657-af9db7f4a3ab // indirect + github.com/stretchr/testify v1.5.1 + golang.org/x/tools v0.0.0-20200728190822-edd3c8e9e279 // indirect +) diff --git a/go.sum b/go.sum new file mode 100755 index 0000000..d003537 --- /dev/null +++ b/go.sum @@ -0,0 +1,105 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-critic/go-critic v0.5.0 h1:Ic2p5UCl5fX/2WX2w8nroPpPhxRNsNTMlJzsu/uqwnM= +github.com/go-critic/go-critic v0.5.0/go.mod h1:4jeRh3ZAVnRYhuWdOEvwzVqLUpxMSoAT0xZ74JsTPlo= +github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= +github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= +github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= +github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= +github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= +github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= +github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= +github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21 h1:wP6mXeB2V/d1P1K7bZ5vDUO3YqEzcvOREOxZPEu3gVI= +github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astinfo v1.0.0 h1:rNuhpyhsnsze/Pe1l/GUHwxo1rmN7Dyb6oAnFcrXh+w= +github.com/go-toolsmith/astinfo v1.0.0/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= +github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= +github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= +github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= +github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg= +github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= +github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYwvlk= +github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mattn/goveralls v0.0.6 h1:cr8Y0VMo/MnEZBjxNN/vh6G90SZ7IMb6lms1dzMoO+Y= +github.com/mattn/goveralls v0.0.6/go.mod h1:h8b4ow6FxSPMQHF6o2ve3qsclnffZjYTNEKmLesRwqw= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c h1:JoUA0uz9U0FVFq5p4LjEq4C0VgQ0El320s3Ms0V4eww= +github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= +github.com/quasilyte/go-consistent v0.0.0-20200404105227-766526bf1e96 h1:6VBkISnfYpPtRvpE9wsVoxX+i0cDQFBPQPYzw259xWY= +github.com/quasilyte/go-consistent v0.0.0-20200404105227-766526bf1e96/go.mod h1:h5ob45vcE3sydtmo0lUDUmG3Y0HXudxMId1w+5G99VI= +github.com/quasilyte/go-ruleguard v0.1.2-0.20200318202121-b00d7a75d3d8 h1:DvnesvLtRPQOvaUbfXfh0tpMHg29by0H7F2U+QIkSu8= +github.com/quasilyte/go-ruleguard v0.1.2-0.20200318202121-b00d7a75d3d8/go.mod h1:CGFX09Ci3pq9QZdj86B+VGIdNj4VyCo2iPOGS9esB/k= +github.com/quasilyte/go-ruleguard v0.1.3 h1:6BU9UaNiSbTRYriG0PTZACIMi1dLHzIWWseSpX4icNM= +github.com/quasilyte/go-ruleguard v0.1.3/go.mod h1:CGFX09Ci3pq9QZdj86B+VGIdNj4VyCo2iPOGS9esB/k= +github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY= +github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/regex/syntax v0.0.0-20200419152657-af9db7f4a3ab h1:rjBjlam2Bbr6Dwp0T8HY2paibXTjMsNQU7vUH8hB+C4= +github.com/quasilyte/regex/syntax v0.0.0-20200419152657-af9db7f4a3ab/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd h1:7E3PabyysDSEjnaANKBgums/hyvMI/HoHQ50qZEzTrg= +golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200728190822-edd3c8e9e279 h1:VUQjqirfpXJk5i+LtIdDjCAqYrCqTarkUCmMVLqMmVQ= +golang.org/x/tools v0.0.0-20200728190822-edd3c8e9e279/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/groups_api.go b/groups_api.go new file mode 100755 index 0000000..b7d9721 --- /dev/null +++ b/groups_api.go @@ -0,0 +1,417 @@ +package groupme + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strconv" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#groups + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + groupsEndpointRoot = "/groups" + groupEndpointRoot = "/groups/%s" + + // Actual Endpoints + indexGroupsEndpoint = groupsEndpointRoot // GET + formerGroupsEndpoint = groupsEndpointRoot + "/former" // GET + showGroupEndpoint = groupEndpointRoot // GET + createGroupEndpoint = groupsEndpointRoot // POST + updateGroupEndpoint = groupEndpointRoot + "/update" // POST + destroyGroupEndpoint = groupEndpointRoot + "/destroy" // POST + joinGroupEndpoint = groupEndpointRoot + "/join/%s" // POST + rejoinGroupEndpoint = groupsEndpointRoot + "/join" // POST + changeGroupOwnerEndpoint = groupsEndpointRoot + "/change_owners" // POST +) + +////////// Common Request Parameters ////////// + +// GroupSettings is the settings for a group, used by CreateGroup and UpdateGroup +type GroupSettings struct { + // Required. Primary name of the group. Maximum 140 characters + Name string `json:"name"` + // A subheading for the group. Maximum 255 characters + Description string `json:"description"` + // GroupMe Image Service URL + ImageURL string `json:"image_url"` + // Defaults false. If true, disables notifications for all members. + // Documented for use only for UpdateGroup + OfficeMode bool `json:"office_mode"` + // Defaults false. If true, generates a share URL. + // Anyone with the URL can join the group + Share bool `json:"share"` +} + +func (gss GroupSettings) String() string { + return marshal(&gss) +} + +////////// API Requests ////////// + +///// Index ///// + +// GroupsQuery defines optional URL parameters for IndexGroups +type GroupsQuery struct { + // Fetch a particular page of results. Defaults to 1. + Page int `json:"page"` + // Define page size. Defaults to 10. + PerPage int `json:"per_page"` + // Comma separated list of data to omit from output. + // Currently supported value is only "memberships". + // If used then response will contain empty (null) members field. + Omit string `json:"omit"` +} + +func (q GroupsQuery) String() string { + return marshal(&q) +} + +/* +IndexGroups - + +List the authenticated user's active groups. + +The response is paginated, with a default of 10 groups per page. + +Please consider using of omit=memberships parameter. Not including +member lists might significantly improve user experience of your +app for users who are participating in huge groups. + +Parameters: See GroupsQuery +*/ +func (c *Client) IndexGroups(req *GroupsQuery) ([]*Group, error) { + httpReq, err := http.NewRequest("GET", c.endpointBase+indexGroupsEndpoint, nil) + if err != nil { + return nil, err + } + + URL := httpReq.URL + query := URL.Query() + if req != nil { + if req.Page != 0 { + query.Set("page", strconv.Itoa(req.Page)) + } + if req.PerPage != 0 { + query.Set("per_page", strconv.Itoa(req.PerPage)) + } + if req.Omit != "" { + query.Set("omit", req.Omit) + } + } + URL.RawQuery = query.Encode() + + var resp []*Group + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +///// Former ///// + +/* +FormerGroups - + +List they groups you have left but can rejoin. +*/ +func (c *Client) FormerGroups() ([]*Group, error) { + httpReq, err := http.NewRequest("GET", c.endpointBase+formerGroupsEndpoint, nil) + if err != nil { + return nil, err + } + + var resp []*Group + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +///// Show ///// + +/* +ShowGroup - + +Loads a specific group. + +Parameters: + groupID - required, ID(string) +*/ +func (c *Client) ShowGroup(groupID ID) (*Group, error) { + URL := fmt.Sprintf(c.endpointBase+showGroupEndpoint, groupID) + + httpReq, err := http.NewRequest("GET", URL, nil) + if err != nil { + return nil, err + } + + var resp Group + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +///// Create ///// + +/* +CreateGroup - + +Create a new group + +Parameters: See GroupSettings +*/ +func (c *Client) CreateGroup(gs GroupSettings) (*Group, error) { + httpReq, err := http.NewRequest("POST", c.endpointBase+createGroupEndpoint, nil) + if err != nil { + return nil, err + } + + data := url.Values{} + if gs.Name == "" { + return nil, fmt.Errorf("GroupsCreateRequest Name field is required") + } + data.Set("name", gs.Name) + + if gs.Description != "" { + data.Set("description", gs.Description) + } + if gs.ImageURL != "" { + data.Set("image_url", gs.ImageURL) + } + if gs.Share { + data.Set("share", "true") + } + httpReq.PostForm = data + + var resp Group + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +///// Update ///// + +/* +UpdateGroup - + +Update a group after creation + +Parameters: + groupID - required, ID(string) + See GroupSettings +*/ +func (c *Client) UpdateGroup(groupID ID, gs GroupSettings) (*Group, error) { + URL := fmt.Sprintf(c.endpointBase+updateGroupEndpoint, groupID) + + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return nil, err + } + + data := url.Values{} + if gs.Name != "" { + data.Set("name", gs.Name) + } + if gs.Description != "" { + data.Set("description", gs.Description) + } + if gs.ImageURL != "" { + data.Set("image_url", gs.ImageURL) + } + if gs.OfficeMode { + data.Set("office_mode", "true") + } + if gs.Share { + data.Set("share", "true") + } + httpReq.PostForm = data + + var resp Group + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +///// Destroy ///// + +/* +DestroyGroup - + +Disband a group + +This action is only available to the group creator + +Parameters: + groupID - required, ID(string) +*/ +func (c *Client) DestroyGroup(groupID ID) error { + url := fmt.Sprintf(c.endpointBase+destroyGroupEndpoint, groupID) + + httpReq, err := http.NewRequest("POST", url, nil) + if err != nil { + return err + } + + return c.do(httpReq, nil) +} + +///// Join ///// + +/* +JoinGroup - + +Join a shared group + +Parameters: + groupID - required, ID(string) + shareToken - required, string +*/ +func (c *Client) JoinGroup(groupID ID, shareToken string) (*Group, error) { + URL := fmt.Sprintf(c.endpointBase+joinGroupEndpoint, groupID, shareToken) + + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return nil, err + } + + var resp Group + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +///// Rejoin ///// + +/* +RejoinGroup - + +Rejoin a group. Only works if you previously removed yourself. + +Parameters: + groupID - required, ID(string) +*/ +func (c *Client) RejoinGroup(groupID ID) (*Group, error) { + httpReq, err := http.NewRequest("POST", c.endpointBase+rejoinGroupEndpoint, nil) + if err != nil { + return nil, err + } + + data := url.Values{} + data.Set("group_id", string(groupID)) + httpReq.PostForm = data + + var resp Group + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +///// Change Owner ///// + +/* +ChangeGroupOwner- + +Change owner of requested groups. + +This action is only available to the group creator. + +Response is a result object which contain status field, +the result of change owner action for the request + +Parameters: See ChangeOwnerRequest +*/ +func (c *Client) ChangeGroupOwner(reqs ChangeOwnerRequest) (ChangeOwnerResult, error) { + httpReq, err := http.NewRequest("POST", c.endpointBase+changeGroupOwnerEndpoint, nil) + if err != nil { + return ChangeOwnerResult{}, err + } + + data := url.Values{} + data.Set("requests", marshal([]ChangeOwnerRequest{reqs})) + httpReq.PostForm = data + + var resp struct { + Results []ChangeOwnerResult `json:"results"` + } + + err = c.do(httpReq, &resp) + if err != nil { + return ChangeOwnerResult{}, err + } + + if len(resp.Results) < 1 { + return ChangeOwnerResult{}, errors.New("failed to parse results") + } + + return resp.Results[0], nil +} + +type ChangeOwnerStatusCode string + +const ( + ChangeOwner_Ok ChangeOwnerStatusCode = "200" + ChangeOwner_RequesterNewOwner ChangeOwnerStatusCode = "400" + ChangeOwner_NotOwner ChangeOwnerStatusCode = "403" + ChangeOwner_BadGroupOrOwner ChangeOwnerStatusCode = "404" + ChangeOwner_BadRequest ChangeOwnerStatusCode = "405" +) + +// String returns the description of the status code according to GroupMe +func (c ChangeOwnerStatusCode) String() string { + return map[ChangeOwnerStatusCode]string{ + ChangeOwner_Ok: "success", + ChangeOwner_RequesterNewOwner: "requester is also a new owner", + ChangeOwner_NotOwner: "requester is not the owner of the group", + ChangeOwner_BadGroupOrOwner: "group or new owner not found or new owner is not memeber of the group", + ChangeOwner_BadRequest: "request object is missing required field or any of the required fields is not an ID", + }[c] +} + +// ChangeOwnerRequest defines the new owner of a group +type ChangeOwnerRequest struct { + // Required + GroupID string `json:"group_id"` + // Required. UserId of the new owner of the group + // who must be an active member of the group + OwnerID string `json:"owner_id"` +} + +func (r ChangeOwnerRequest) String() string { + return marshal(&r) +} + +// ChangeOwnerResult holds the status of the group owner change +type ChangeOwnerResult struct { + GroupID string `json:"group_id"` + // UserId of the new owner of the group who is + // an active member of the group + OwnerID string `json:"owner_id"` + Status ChangeOwnerStatusCode `json:"status"` +} + +func (r ChangeOwnerResult) String() string { + return marshal(&r) +} diff --git a/groups_api_test.go b/groups_api_test.go new file mode 100755 index 0000000..9e213fc --- /dev/null +++ b/groups_api_test.go @@ -0,0 +1,698 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type GroupsAPISuite struct{ APISuite } + +func (s *GroupsAPISuite) SetupSuite() { + s.handler = groupsTestRouter() + s.setupSuite() +} + +func (s *GroupsAPISuite) TestGroupsIndex() { + groups, err := s.client.IndexGroups(&GroupsQuery{ + Page: 5, + PerPage: 20, + Omit: "memberships", + }) + s.Require().NoError(err) + s.Require().NotZero(groups) + for _, group := range groups { + s.Assert().NotZero(group) + } +} + +func (s *GroupsAPISuite) TestGroupsFormer() { + groups, err := s.client.FormerGroups() + s.Require().NoError(err) + s.Require().NotZero(groups) + for _, group := range groups { + s.Assert().NotZero(group) + } +} + +func (s *GroupsAPISuite) TestGroupsShow() { + group, err := s.client.ShowGroup("1") + s.Require().NoError(err) + s.Assert().NotZero(group) +} + +func (s *GroupsAPISuite) TestGroupsCreate() { + group, err := s.client.CreateGroup(GroupSettings{ + "Test", + "This is a test group", + "www.blank.com/image", + false, + true, + }) + s.Require().NoError(err) + s.Assert().NotZero(group) +} + +func (s *GroupsAPISuite) TestGroupsCreate_EmptyName() { + group, err := s.client.CreateGroup(GroupSettings{ + Name: "", + }) + s.Require().Error(err) + s.Assert().Zero(group) +} + +func (s *GroupsAPISuite) TestGroupsUpdate() { + group, err := s.client.UpdateGroup("1", GroupSettings{ + "Test", + "This is a test group", + "www.blank.com/image", + true, + true, + }) + s.Require().NoError(err) + s.Assert().NotZero(group) +} + +func (s *GroupsAPISuite) TestGroupsDestroy() { + err := s.client.DestroyGroup("1") + s.Require().NoError(err) +} + +func (s *GroupsAPISuite) TestGroupsJoin() { + group, err := s.client.JoinGroup("1", "please") + s.Require().NoError(err) + s.Assert().NotZero(group) +} + +func (s *GroupsAPISuite) TestGroupsRejoin() { + group, err := s.client.RejoinGroup("1") + s.Require().NoError(err) + s.Assert().NotZero(group) +} + +func (s *GroupsAPISuite) TestGroupsChangeOwner() { + result, err := s.client.ChangeGroupOwner( + ChangeOwnerRequest{ + "1", + "123", + }, + ) + s.Require().NoError(err) + s.Assert().NotZero(result) +} +func TestGroupsAPISuite(t *testing.T) { + suite.Run(t, new(GroupsAPISuite)) +} + +////////// Test Groups Router ////////// + +func groupsTestRouter() *mux.Router { + router := mux.NewRouter() + + // Index + router.Path("/groups"). + Methods("GET"). + Name("IndexGroups"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": [ + { + "id": "1234567890", + "name": "Family", + "type": "private", + "description": "Coolest Family Ever", + "image_url": "https://i.groupme.com/123456789", + "creator_user_id": "1234567890", + "created_at": 1302623328, + "updated_at": 1302623328, + "members": [ + { + "user_id": "1234567890", + "nickname": "Jane", + "muted": false, + "image_url": "https://i.groupme.com/123456789" + } + ], + "share_url": "https://groupme.com/join_group/1234567890/SHARE_TOKEN", + "messages": { + "count": 100, + "last_message_id": "1234567890", + "last_message_created_at": 1302623328, + "preview": { + "nickname": "Jane", + "text": "Hello world", + "image_url": "https://i.groupme.com/123456789", + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + } + } + ], + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + // Former + router.Path("/groups/former"). + Methods("GET"). + Name("FormerGroups"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": [ + { + "id": "1234567890", + "name": "Family", + "type": "private", + "description": "Coolest Family Ever", + "image_url": "https://i.groupme.com/123456789", + "creator_user_id": "1234567890", + "created_at": 1302623328, + "updated_at": 1302623328, + "members": [ + { + "user_id": "1234567890", + "nickname": "Jane", + "muted": false, + "image_url": "https://i.groupme.com/123456789" + } + ], + "share_url": "https://groupme.com/join_group/1234567890/SHARE_TOKEN", + "messages": { + "count": 100, + "last_message_id": "1234567890", + "last_message_created_at": 1302623328, + "preview": { + "nickname": "Jane", + "text": "Hello world", + "image_url": "https://i.groupme.com/123456789", + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + } + } + ], + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Show + router.Path("/groups/{id:[0-9]+}"). + Methods("GET"). + Name("ShowGroup"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "id": "1234567890", + "name": "Family", + "type": "private", + "description": "Coolest Family Ever", + "image_url": "https://i.groupme.com/123456789", + "creator_user_id": "1234567890", + "created_at": 1302623328, + "updated_at": 1302623328, + "members": [ + { + "user_id": "1234567890", + "nickname": "Jane", + "muted": false, + "image_url": "https://i.groupme.com/123456789" + } + ], + "share_url": "https://groupme.com/join_group/1234567890/SHARE_TOKEN", + "messages": { + "count": 100, + "last_message_id": "1234567890", + "last_message_created_at": 1302623328, + "preview": { + "nickname": "Jane", + "text": "Hello world", + "image_url": "https://i.groupme.com/123456789", + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + } + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Create + router.Path("/groups"). + Methods("POST"). + Name("CreateGroup"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(201) + fmt.Fprint(w, `{ + "response": { + "id": "1234567890", + "name": "Family", + "type": "private", + "description": "Coolest Family Ever", + "image_url": "https://i.groupme.com/123456789", + "creator_user_id": "1234567890", + "created_at": 1302623328, + "updated_at": 1302623328, + "members": [ + { + "user_id": "1234567890", + "nickname": "Jane", + "muted": false, + "image_url": "https://i.groupme.com/123456789" + } + ], + "share_url": "https://groupme.com/join_group/1234567890/SHARE_TOKEN", + "messages": { + "count": 100, + "last_message_id": "1234567890", + "last_message_created_at": 1302623328, + "preview": { + "nickname": "Jane", + "text": "Hello world", + "image_url": "https://i.groupme.com/123456789", + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + } + }, + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + // Update + router.Path("/groups/{id:[0-9]+}/update"). + Methods("POST"). + Name("UpdateGroup"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "id": "1234567890", + "name": "Family", + "type": "private", + "description": "Coolest Family Ever", + "image_url": "https://i.groupme.com/123456789", + "creator_user_id": "1234567890", + "created_at": 1302623328, + "updated_at": 1302623328, + "members": [ + { + "user_id": "1234567890", + "nickname": "Jane", + "muted": false, + "image_url": "https://i.groupme.com/123456789" + } + ], + "share_url": "https://groupme.com/join_group/1234567890/SHARE_TOKEN", + "messages": { + "count": 100, + "last_message_id": "1234567890", + "last_message_created_at": 1302623328, + "preview": { + "nickname": "Jane", + "text": "Hello world", + "image_url": "https://i.groupme.com/123456789", + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + } + }, + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + // Destroy + router.Path("/groups/{id:[0-9]+}/destroy"). + Methods("POST"). + Name("DestroyGroup"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + }) + + // Join + router.Path("/groups/{id:[0-9]+}/join/{share_token}"). + Methods("POST"). + Name("JoinGroup"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "group": { + "id": "1234567890", + "name": "Family", + "type": "private", + "description": "Coolest Family Ever", + "image_url": "https://i.groupme.com/123456789", + "creator_user_id": "1234567890", + "created_at": 1302623328, + "updated_at": 1302623328, + "members": [ + { + "user_id": "1234567890", + "nickname": "Jane", + "muted": false, + "image_url": "https://i.groupme.com/123456789" + } + ], + "share_url": "https://groupme.com/join_group/1234567890/SHARE_TOKEN", + "messages": { + "count": 100, + "last_message_id": "1234567890", + "last_message_created_at": 1302623328, + "preview": { + "nickname": "Jane", + "text": "Hello world", + "image_url": "https://i.groupme.com/123456789", + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + } + } + }, + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + // Rejoin + router.Path("/groups/join"). + Methods("POST"). + Name("RejoinGroup"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "id": "1234567890", + "name": "Family", + "type": "private", + "description": "Coolest Family Ever", + "image_url": "https://i.groupme.com/123456789", + "creator_user_id": "1234567890", + "created_at": 1302623328, + "updated_at": 1302623328, + "members": [ + { + "user_id": "1234567890", + "nickname": "Jane", + "muted": false, + "image_url": "https://i.groupme.com/123456789" + } + ], + "share_url": "https://groupme.com/join_group/1234567890/SHARE_TOKEN", + "messages": { + "count": 100, + "last_message_id": "1234567890", + "last_message_created_at": 1302623328, + "preview": { + "nickname": "Jane", + "text": "Hello world", + "image_url": "https://i.groupme.com/123456789", + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + } + }, + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + // Change Owner + router.Path("/groups/change_owners"). + Methods("POST"). + Name("ChangeGroupOwner"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "results": [ + { + "group_id": "1234567890", + "owner_id": "1234567890", + "status": "200" + }, + { + "group_id": "1234567890", + "owner_id": "1234567890", + "status": "400" + } + ] + }, + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + /*// Return test router //*/ + return router +} diff --git a/json.go b/json.go new file mode 100755 index 0000000..4e1f257 --- /dev/null +++ b/json.go @@ -0,0 +1,223 @@ +package groupme + +import ( + "encoding/json" + "fmt" +) + +// Meta is the error type returned in the GroupMe response. +// Meant for clients that can't read HTTP status codes +type Meta struct { + Code HTTPStatusCode `json:"code"` + Errors []string `json:"errors"` +} + +// Error returns the code and the error list as a string. +// Satisfies the error interface +func (m Meta) Error() string { + return fmt.Sprintf("Error Code %d: %v", m.Code, m.Errors) +} + +// Group is a GroupMe group, returned in JSON API responses +type Group struct { + ID ID `json:"id"` + Name string `json:"name"` + // Type of group (private|public) + Type string `json:"type"` + Description string `json:"description"` + ImageURL string `json:"image_url"` + CreatorUserID ID `json:"creator_user_id"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` + Members []*Member `json:"members"` + ShareURL string `json:"share_url"` + Messages GroupMessages `json:"messages"` +} + +// GroupMessages is a Group field, only returned in Group JSON API responses +type GroupMessages struct { + Count uint `json:"count"` + LastMessageID ID `json:"last_message_id"` + LastMessageCreatedAt Timestamp `json:"last_message_created_at"` + Preview MessagePreview `json:"preview"` +} + +// MessagePreview is a GroupMessages field, only returned in Group JSON API responses. +// Abbreviated form of Message type +type MessagePreview struct { + Nickname string `json:"nickname"` + Text string `json:"text"` + ImageURL string `json:"image_url"` + Attachments []*Attachment `json:"attachments"` +} + +// GetMemberByUserID gets the group member by their UserID, +// nil if no member matches +func (g Group) GetMemberByUserID(userID ID) *Member { + for _, member := range g.Members { + if member.UserID == userID { + return member + } + } + + return nil +} + +// GetMemberByNickname gets the group member by their Nickname, +// nil if no member matches +func (g Group) GetMemberByNickname(nickname string) *Member { + for _, member := range g.Members { + if member.Nickname == nickname { + return member + } + } + + return nil +} + +func (g Group) String() string { + return marshal(&g) +} + +// Member is a GroupMe group member, returned in JSON API responses +type Member struct { + ID ID `json:"id"` + UserID ID `json:"user_id"` + Nickname string `json:"nickname"` + Muted bool `json:"muted"` + ImageURL string `json:"image_url"` + AutoKicked bool `json:"autokicked"` + AppInstalled bool `json:"app_installed"` + GUID string `json:"guid"` +} + +func (m Member) String() string { + return marshal(&m) +} + +// Message is a GroupMe group message, returned in JSON API responses +type Message struct { + ID ID `json:"id"` + SourceGUID string `json:"source_guid"` + CreatedAt Timestamp `json:"created_at"` + GroupID ID `json:"group_id"` + UserID ID `json:"user_id"` + BotID ID `json:"bot_id"` + SenderID ID `json:"sender_id"` + SenderType SenderType `json:"sender_type"` + System bool `json:"system"` + Name string `json:"name"` + RecipientID ID `json:"recipient_id"` + ConversationID ID `json:"conversation_id"` + AvatarURL string `json:"avatar_url"` + // Maximum length of 1000 characters + Text string `json:"text"` + // Must be an image service URL (i.groupme.com) + ImageURL string `json:"image_url"` + FavoritedBy []string `json:"favorited_by"` + Attachments []*Attachment `json:"attachments"` +} + +func (m Message) String() string { + return marshal(&m) +} + +type SenderType string + +// SenderType constants +const ( + SenderType_User SenderType = "user" + SenderType_Bot SenderType = "bot" + SenderType_System SenderType = "system" +) + +type AttachmentType string + +// AttachmentType constants +const ( + Mentions AttachmentType = "mentions" + Image AttachmentType = "image" + Location AttachmentType = "location" + Emoji AttachmentType = "emoji" +) + +// Attachment is a GroupMe message attachment, returned in JSON API responses +type Attachment struct { + Type AttachmentType `json:"type"` + Loci [][]int `json:"loci"` + UserIDs []ID `json:"user_ids"` + URL string `json:"url"` + Name string `json:"name"` + Latitude string `json:"lat"` + Longitude string `json:"lng"` + Placeholder string `json:"placeholder"` + Charmap [][]int `json:"charmap"` +} + +func (a Attachment) String() string { + return marshal(&a) +} + +// User is a GroupMe user, returned in JSON API responses +type User struct { + ID ID `json:"id"` + PhoneNumber PhoneNumber `json:"phone_number"` + ImageURL string `json:"image_url"` + Name string `json:"name"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` + AvatarURL string `json:"avatar_url"` + Email string `json:"email"` + SMS bool `json:"sms"` +} + +func (u User) String() string { + return marshal(&u) +} + +// Chat is a GroupMe direct message conversation between two users, +// returned in JSON API responses +type Chat struct { + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` + LastMessage *Message `json:"last_message"` + MessagesCount int `json:"messages_count"` + OtherUser User `json:"other_user"` +} + +func (c Chat) String() string { + return marshal(&c) +} + +type Bot struct { + BotID ID `json:"bot_id"` + GroupID ID `json:"group_id"` + Name string `json:"name"` + AvatarURL string `json:"avatar_url"` + CallbackURL string `json:"callback_url"` + DMNotification bool `json:"dm_notification"` +} + +func (b Bot) String() string { + return marshal(&b) +} + +type Block struct { + UserID ID `json:"user_id"` + BlockedUserID ID `json:"blocked_user_id"` + CreatedAT Timestamp `json:"created_at"` +} + +func (b Block) String() string { + return marshal(&b) +} + +// Superficially increases test coverage +func marshal(i interface{}) string { + bytes, err := json.MarshalIndent(i, "", "\t") + if err != nil { + return "" + } + + return string(bytes) +} diff --git a/json_test.go b/json_test.go new file mode 100755 index 0000000..841e596 --- /dev/null +++ b/json_test.go @@ -0,0 +1,74 @@ +package groupme + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type JSONSuite struct { + suite.Suite +} + +func (s *JSONSuite) TestGroup_GetMemberByUserID_Match() { + m := Member{ + UserID: "123", + } + + g := Group{ + Members: []*Member{&m}, + } + + actual := g.GetMemberByUserID("123") + + s.Require().NotNil(actual) + s.Assert().Equal(m, *actual) +} + +func (s *JSONSuite) TestGroup_GetMemberByUserID_NoMatch() { + g := Group{ + Members: []*Member{}, + } + + actual := g.GetMemberByUserID("123") + + s.Require().Nil(actual) +} + +func (s *JSONSuite) TestGroup_GetMemberByNickname_Match() { + m := Member{ + Nickname: "Test User", + } + + g := Group{ + Members: []*Member{&m}, + } + + actual := g.GetMemberByNickname("Test User") + + s.Require().NotNil(actual) + s.Assert().Equal(m, *actual) +} + +func (s *JSONSuite) TestGroup_GetMemberByNickname_NoMatch() { + g := Group{ + Members: []*Member{}, + } + + actual := g.GetMemberByNickname("Test User") + + s.Require().Nil(actual) +} + +func (s *JSONSuite) TestMarshal_NoError() { + s.Assert().Equal("{}", marshal(&struct{}{})) +} + +func (s *JSONSuite) TestMarshal_Error() { + var c chan struct{} + s.Assert().Equal("", marshal(c)) +} + +func TestJSONSuite(t *testing.T) { + suite.Run(t, new(JSONSuite)) +} diff --git a/leaderboard_api.go b/leaderboard_api.go new file mode 100644 index 0000000..bb534e6 --- /dev/null +++ b/leaderboard_api.go @@ -0,0 +1,129 @@ +package groupme + +import ( + "fmt" + "net/http" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#leaderboard + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + leaderboardEndpointRoot = groupEndpointRoot + "/likes" + + // Actual Endpoints + indexLeaderboardEndpoint = leaderboardEndpointRoot // GET + myLikesLeaderboardEndpoint = leaderboardEndpointRoot + "/mine" // GET + myHitsLeaderboardEndpoint = leaderboardEndpointRoot + "/for_me" // GET +) + +////////// API Requests ////////// + +// Index + +type period string + +func (p period) String() string { + return string(p) +} + +// Define acceptable period values +const ( + Period_Day = "day" + Period_Week = "week" + Period_Month = "month" +) + +/* +IndexLeaderboard - + +A list of the liked messages in the group for a given period of +time. Messages are ranked in order of number of likes. + +Parameters: + groupID - required, ID(string) + p - required, period(string) +*/ +func (c *Client) IndexLeaderboard(groupID ID, p period) ([]*Message, error) { + url := fmt.Sprintf(c.endpointBase+indexLeaderboardEndpoint, groupID) + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + URL := httpReq.URL + query := URL.Query() + query.Set("period", p.String()) + URL.RawQuery = query.Encode() + + var resp struct { + Messages []*Message `json:"messages"` + } + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp.Messages, nil +} + +// My Likes + +/* +MyLikesLeaderboard - + +A list of messages you have liked. Messages are returned in +reverse chrono-order. Note that the payload includes a liked_at +timestamp in ISO-8601 format. + +Parameters: + groupID - required, ID(string) +*/ +func (c *Client) MyLikesLeaderboard(groupID ID) ([]*Message, error) { + url := fmt.Sprintf(c.endpointBase+myLikesLeaderboardEndpoint, groupID) + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + var resp struct { + Messages []*Message `json:"messages"` + } + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp.Messages, nil +} + +// My Hits + +/* +MyHitsLeaderboard - + +A list of messages you have liked. Messages are returned in +reverse chrono-order. Note that the payload includes a liked_at +timestamp in ISO-8601 format. + +Parameters: + groupID - required, ID(string) +*/ +func (c *Client) MyHitsLeaderboard(groupID ID) ([]*Message, error) { + url := fmt.Sprintf(c.endpointBase+myHitsLeaderboardEndpoint, groupID) + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + var resp struct { + Messages []*Message `json:"messages"` + } + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp.Messages, nil +} diff --git a/leaderboard_api_test.go b/leaderboard_api_test.go new file mode 100644 index 0000000..3d3ed71 --- /dev/null +++ b/leaderboard_api_test.go @@ -0,0 +1,310 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type LeaderboardAPISuite struct{ APISuite } + +func (s *LeaderboardAPISuite) SetupSuite() { + s.handler = leaderboardTestRouter() + s.setupSuite() +} + +func (s *LeaderboardAPISuite) TestLeaderboardIndex() { + messages, err := s.client.IndexLeaderboard("1", Period_Day) + s.Require().NoError(err) + s.Require().NotZero(messages) + for _, message := range messages { + s.Assert().NotZero(message) + } +} + +func (s *LeaderboardAPISuite) TestLeaderboardMyLikes() { + messages, err := s.client.MyLikesLeaderboard("1") + s.Require().NoError(err) + s.Require().NotZero(messages) + for _, message := range messages { + s.Assert().NotZero(message) + } +} + +func (s *LeaderboardAPISuite) TestLeaderboardMyHits() { + messages, err := s.client.MyHitsLeaderboard("1") + s.Require().NoError(err) + s.Require().NotZero(messages) + for _, message := range messages { + s.Assert().NotZero(message) + } +} + +func TestLeaderboardAPISuite(t *testing.T) { + suite.Run(t, new(LeaderboardAPISuite)) +} + +func leaderboardTestRouter() *mux.Router { + router := mux.NewRouter() + + // Index + router.Path("/groups/{id:[0-9]+}/likes"). + Queries("period", "{period:day|week|month}"). + Methods("GET"). + Name("IndexLeaderboard"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "messages": [ + { + "id": "1234567890", + "source_guid": "GUID", + "created_at": 1302623328, + "user_id": "1234567890", + "group_id": "1234567890", + "name": "John", + "avatar_url": "https://i.groupme.com/123456789", + "text": "Hello world ☃☃", + "system": true, + "favorited_by": [ + "101", + "66", + "1234567890" + ], + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + }, + { + "id": "1234567890", + "source_guid": "GUID", + "created_at": 1302623328, + "user_id": "1234567890", + "group_id": "1234567890", + "name": "John", + "avatar_url": "https://i.groupme.com/123456789", + "text": "Hello world ☃☃", + "system": true, + "favorited_by": [ + "1", + "2" + ], + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + ] + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // My Likes + router.Path("/groups/{id:[0-9]+}/likes/mine"). + Methods("GET"). + Name("MyLikesLeaderboard"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "messages": [ + { + "id": "1234567890", + "source_guid": "GUID", + "created_at": 1302623328, + "user_id": "1234567890", + "group_id": "1234567890", + "name": "John", + "avatar_url": "https://i.groupme.com/123456789", + "text": "Hello world ☃☃", + "system": true, + "favorited_by": [ + "101", + "66", + "1234567890" + ], + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ], + "liked_at": "2014-05-08T18:30:31.6617Z" + } + ] + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // My Hits + router.Path("/groups/{id:[0-9]+}/likes/for_me"). + Methods("GET"). + Name("MyHitsLeaderboard"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "messages": [ + { + "id": "1234567890", + "source_guid": "GUID", + "created_at": 1302623328, + "user_id": "1234567890", + "group_id": "1234567890", + "name": "John", + "avatar_url": "https://i.groupme.com/123456789", + "text": "Hello world ☃☃", + "system": true, + "favorited_by": [ + "101", + "66", + "1234567890" + ], + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + ] + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + /*// Return test router //*/ + return router +} diff --git a/likes_api.go b/likes_api.go new file mode 100644 index 0000000..d712e7d --- /dev/null +++ b/likes_api.go @@ -0,0 +1,63 @@ +package groupme + +import ( + "fmt" + "net/http" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#likes + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + likesEndpointRoot = "/messages/%s/%s" + + createLikeEndpoint = likesEndpointRoot + "/like" // POST + destroyLikeEndpoint = likesEndpointRoot + "/unlike" // POST +) + +////////// API Requests ///////// + +// Create + +/* +CreateLike - + +Like a message. + +Parameters: + conversationID - required, ID(string) + messageID - required, ID(string) +*/ +func (c *Client) CreateLike(conversationID, messageID ID) error { + url := fmt.Sprintf(c.endpointBase+createLikeEndpoint, conversationID, messageID) + + httpReq, err := http.NewRequest("POST", url, nil) + if err != nil { + return err + } + + return c.do(httpReq, nil) +} + +// Destroy + +/* +DestroyLike - + +Unlike a message. + +Parameters: + conversationID - required, ID(string) + messageID - required, ID(string) +*/ +func (c *Client) DestroyLike(conversationID, messageID ID) error { + url := fmt.Sprintf(c.endpointBase+destroyLikeEndpoint, conversationID, messageID) + + httpReq, err := http.NewRequest("POST", url, nil) + if err != nil { + return err + } + + return c.do(httpReq, nil) +} diff --git a/likes_api_test.go b/likes_api_test.go new file mode 100644 index 0000000..b85ace8 --- /dev/null +++ b/likes_api_test.go @@ -0,0 +1,52 @@ +package groupme + +import ( + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type LikesAPISuite struct{ APISuite } + +func (s *LikesAPISuite) SetupSuite() { + s.handler = likesTestRouter() + s.setupSuite() +} + +func (s *LikesAPISuite) TestLikesCreate() { + err := s.client.CreateLike("1", "1") + s.Require().NoError(err) +} + +func (s *LikesAPISuite) TestLikesDestroy() { + err := s.client.DestroyLike("1", "1") + s.Require().NoError(err) +} + +func TestLikesAPISuite(t *testing.T) { + suite.Run(t, new(LikesAPISuite)) +} +func likesTestRouter() *mux.Router { + router := mux.NewRouter() + + // Create + router.Path(`/messages/{conversation_id}/{message_id}/like`). + Methods("POST"). + Name("CreateLike"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + }) + + // Destroy + router.Path(`/messages/{conversation_id}/{message_id}/unlike`). + Methods("POST"). + Name("DestroyLike"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + }) + + /*// Return test router //*/ + return router +} diff --git a/main_test.go b/main_test.go new file mode 100755 index 0000000..6499827 --- /dev/null +++ b/main_test.go @@ -0,0 +1,85 @@ +package groupme + +import ( + "fmt" + "log" + "math/rand" + "net/http" + "os" + "strconv" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +////////// Base API Suite ////////// +type APISuite struct { + // Base attributes + suite.Suite + client *Client + server *http.Server + wg sync.WaitGroup + + // Overriden by child Suite + addr string + handler http.Handler +} + +func (s *APISuite) setupSuite() { + s.addr = "localhost:" + s.generatePort() + + s.client = NewClient("") + s.client.endpointBase = "http://" + s.addr + + s.server = s.startServer(s.addr, s.handler) +} + +func (s *APISuite) TearDownSuite() { + s.client.Close() + s.server.Close() + s.wg.Wait() +} + +///// Start Server ///// +func (s *APISuite) startServer(addr string, handler http.Handler) *http.Server { + server := &http.Server{ + Addr: addr, + Handler: handler, + ErrorLog: log.New(os.Stdout, "SERVER", log.Ltime), + } + + s.wg.Add(1) + go func() { + defer s.wg.Done() + if err := server.ListenAndServe(); err.Error() != "http: Server closed" { + s.Assert().NoError(err) + } + }() + + // Wait until server has started listening + url := fmt.Sprintf("http://%s", addr) + for _, err := http.Get(url); err != nil; _, err = http.Get(url) { + continue + } + + return server +} + +///// Generate Ephemeral Port ///// +const ( + portMin = 49152 + portMax = 65535 + portRange = portMax - portMin +) + +func (s *APISuite) generatePort() string { + rand.Seed(time.Now().UnixNano()) + return strconv.Itoa((rand.Intn(portRange) + portMin)) +} + +////////// Test Main ////////// +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} diff --git a/members_api.go b/members_api.go new file mode 100644 index 0000000..da864d8 --- /dev/null +++ b/members_api.go @@ -0,0 +1,174 @@ +package groupme + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#members + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + membersEndpointRoot = groupEndpointRoot + "/members" + + // Actual Endpoints + addMembersEndpoint = membersEndpointRoot + "/add" // POST + addMembersResultsEndpoint = membersEndpointRoot + "/results/%s" // GET + removeMemberEndpoint = membersEndpointRoot + "/%s/remove" // POST + updateMemberEndpoint = groupEndpointRoot + "/memberships/update" // POST +) + +///// Add ///// + +/* +AddMembers - + +Add members to a group. + +Multiple members can be added in a single request, and results +are fetchedwith a separate call (since memberships are processed +asynchronously). The response includes a results_id that's used +in the results request. + +In order to correlate request params with resulting memberships, +GUIDs can be added to the members parameters. These GUIDs will +be reflected in the membership JSON objects. + +Parameters: + groupID - required, ID(string) + See Member. + Nickname - required + One of the following identifiers must be used: + UserID - ID(string) + PhoneNumber - PhoneNumber(string) + Email - string +*/ +func (c *Client) AddMembers(groupID ID, members ...*Member) (string, error) { + URL := fmt.Sprintf(c.endpointBase+addMembersEndpoint, groupID) + + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return "", err + } + + data := url.Values{} + bytes, err := json.Marshal(members) + if err != nil { + return "", err + } + data.Set("members", string(bytes)) + httpReq.PostForm = data + + var resp struct { + ResultsID string `json:"result_id"` + } + + err = c.do(httpReq, &resp) + if err != nil { + return "", err + } + + return resp.ResultsID, nil +} + +///// Results ///// + +/* +AddMembersResults - +Get the membership results from an add call. + +Successfully created memberships will be returned, including +any GUIDs that were sent up in the add request. If GUIDs were +absent, they are filled in automatically. Failed memberships +and invites are omitted. + +Keep in mind that results are temporary -- they will only be +available for 1 hour after the add request. + +Parameters: + groupID - required, ID(string) + resultID - required, string +*/ +func (c *Client) AddMembersResults(groupID ID, resultID string) ([]*Member, error) { + URL := fmt.Sprintf(c.endpointBase+addMembersResultsEndpoint, groupID, resultID) + + httpReq, err := http.NewRequest("GET", URL, nil) + if err != nil { + return nil, err + } + + var resp struct { + Members []*Member `json:"members"` + } + + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp.Members, nil +} + +///// Remove ///// + +/* +RemoveMember - + +Remove a member (or yourself) from a group. + +Note: The creator of the group cannot be removed or exit. + +Parameters: + groupID - required, ID(string) + membershipID - required, ID(string). Not the same as userID +*/ +func (c *Client) RemoveMember(groupID, membershipID ID) error { + URL := fmt.Sprintf(c.endpointBase+removeMemberEndpoint, groupID, membershipID) + + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return err + } + + return c.do(httpReq, nil) +} + +///// Update ///// + +/* +UpdateMember - + +Update your nickname in a group. The nickname must be +between 1 and 50 characters. +*/ +func (c *Client) UpdateMember(groupID ID, nickname string) (*Member, error) { + URL := fmt.Sprintf(c.endpointBase+updateMemberEndpoint, groupID) + + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return nil, err + } + + type membership struct { + Nickname string `json:"nickname"` + } + + data := url.Values{} + bytes, err := json.Marshal(membership{nickname}) + if err != nil { + return nil, err + } + data.Add("membership", string(bytes)) + + var resp Member + + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} diff --git a/members_api_test.go b/members_api_test.go new file mode 100644 index 0000000..4ada37c --- /dev/null +++ b/members_api_test.go @@ -0,0 +1,137 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type MembersAPISuite struct{ APISuite } + +func (s *MembersAPISuite) SetupSuite() { + s.handler = membersTestRouter() + s.setupSuite() +} + +func (s *MembersAPISuite) TestMembersAdd() { + _, err := s.client.AddMembers( + "1", + &Member{Nickname: "test"}, + ) + s.Require().NoError(err) +} + +func (s *MembersAPISuite) TestMembersResults() { + _, err := s.client.AddMembersResults("1", "123") + s.Require().NoError(err) +} + +func (s *MembersAPISuite) TestMembersRemove() { + err := s.client.RemoveMember("1", "123") + s.Require().NoError(err) +} + +func (s *MembersAPISuite) TestMembersUpdate() { + _, err := s.client.UpdateMember("1", "nickname") + s.Require().NoError(err) +} + +func TestMembersAPISuite(t *testing.T) { + suite.Run(t, new(MembersAPISuite)) +} + +func membersTestRouter() *mux.Router { + router := mux.NewRouter() + + // Add + router.Path("/groups/{id:[0-9]+}/members/add"). + Methods("POST"). + Name("AddMembers"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(202) + fmt.Fprint(w, `{ + "response": { + "results_id": "GUID" + }, + "meta": { + "code": 202, + "errors": [] + } + }`) + }) + + // Results + router.Path("/groups/{id:[0-9]+}/members/results/{result_id}"). + Methods("GET"). + Name("AddMembersResults"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "members": [ + { + "id": "1000", + "user_id": "10000", + "nickname": "John", + "muted": false, + "image_url": "https://i.groupme.com/AVATAR", + "autokicked": false, + "app_installed": true, + "guid": "GUID-1" + }, + { + "id": "2000", + "user_id": "20000", + "nickname": "Anne", + "muted": false, + "image_url": "https://i.groupme.com/AVATAR", + "autokicked": false, + "app_installed": true, + "guid": "GUID-2" + } + ] + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Remove + router.Path("/groups/{id:[0-9]+}/members/{membership_id}/remove"). + Methods("POST"). + Name("RemoveMember"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + }) + + // Update + router.Path("/groups/{id:[0-9]+}/memberships/update"). + Methods("POST"). + Name("UpdateMember"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "id": "MEMBERSHIP ID", + "user_id": "USER ID", + "nickname": "NEW NICKNAME", + "muted": false, + "image_url": "AVATAR URL", + "autokicked": false, + "app_installed": true + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + /*// Return test router //*/ + return router +} diff --git a/messages_api.go b/messages_api.go new file mode 100644 index 0000000..21c42fe --- /dev/null +++ b/messages_api.go @@ -0,0 +1,174 @@ +package groupme + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/google/uuid" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#messages + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + messagesEndpointRoot = groupEndpointRoot + "/messages" + + indexMessagesEndpoint = messagesEndpointRoot // GET + createMessagesEndpoint = messagesEndpointRoot // POST +) + +// Index + +// MessagesQuery defines the optional URL parameters for IndexMessages +type IndexMessagesQuery struct { + // Returns messages created before the given message ID + BeforeID ID + // Returns most recent messages created after the given message ID + SinceID ID + // Returns messages created immediately after the given message ID + AfterID ID + // Number of messages returned. Default is 20. Max is 100. + Limit int +} + +func (q IndexMessagesQuery) String() string { + return marshal(&q) +} + +// MessagesIndexResponse contains the count and set of +// messages returned by the IndexMessages API request +type IndexMessagesResponse struct { + Count int `json:"count"` + Messages []*Message `json:"messages"` +} + +func (r IndexMessagesResponse) String() string { + return marshal(&r) +} + +/* +IndexMessages - + +Retrieve messages for a group. + +By default, messages are returned in groups of 20, ordered by +created_at descending. This can be raised or lowered by passing +a limit parameter, up to a maximum of 100 messages. + +Messages can be scanned by providing a message ID as either the +before_id, since_id, or after_id parameter. If before_id is +provided, then messages immediately preceding the given message +will be returned, in descending order. This can be used to +continually page back through a group's messages. + +The after_id parameter will return messages that immediately +follow a given message, this time in ascending order (which +makes it easy to pick off the last result for continued +pagination). + +Finally, the since_id parameter also returns messages created +after the given message, but it retrieves the most recent +messages. For example, if more than twenty messages are created +after the since_id message, using this parameter will omit the +messages that immediately follow the given message. This is a +bit counterintuitive, so take care. + +If no messages are found (e.g. when filtering with before_id) +we return code 304. + +Note that for historical reasons, likes are returned as an +array of user ids in the favorited_by key. + +Parameters: See MessageQuery +*/ +func (c *Client) IndexMessages(groupID ID, req *IndexMessagesQuery) (IndexMessagesResponse, error) { + url := fmt.Sprintf(c.endpointBase+indexMessagesEndpoint, groupID) + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return IndexMessagesResponse{}, err + } + + URL := httpReq.URL + query := URL.Query() + if req != nil { + if req.BeforeID != "" { + query.Add("before_id", req.BeforeID.String()) + } + if req.SinceID != "" { + query.Add("since_id", req.SinceID.String()) + } + if req.AfterID != "" { + query.Add("after_id", req.AfterID.String()) + } + if req.Limit != 0 { + query.Add("limit", strconv.Itoa(req.Limit)) + } + } + URL.RawQuery = query.Encode() + + var resp IndexMessagesResponse + err = c.do(httpReq, &resp) + if err != nil { + return IndexMessagesResponse{}, err + } + + return resp, nil +} + +// Create + +/* +CreateMessage - +Send a message to a group + +If you want to attach an image, you must first process it +through our image service. + +Attachments of type emoji rely on data from emoji PowerUps. + +Clients use a placeholder character in the message text and +specify a replacement charmap to substitute emoji characters + +The character map is an array of arrays containing rune data +([[{pack_id,offset}],...]). + +The placeholder should be a high-point/invisible UTF-8 character. + +Parameters: + groupID - required, ID(String) + See Message. + text - required, string. Can be ommitted if at least one + attachment is present + attachments - a polymorphic list of attachments (locations, + images, etc). You may have You may have more than + one of any type of attachment, provided clients can + display it. + +*/ +func (c *Client) CreateMessage(groupID ID, m *Message) (*Message, error) { + URL := fmt.Sprintf(c.endpointBase+createMessagesEndpoint, groupID) + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return nil, err + } + + m.SourceGUID = uuid.New().String() + + data := url.Values{} + data.Set("message", m.String()) + + httpReq.PostForm = data + + var resp struct { + *Message `json:"message"` + } + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return resp.Message, nil +} diff --git a/messages_api_test.go b/messages_api_test.go new file mode 100644 index 0000000..6a81dad --- /dev/null +++ b/messages_api_test.go @@ -0,0 +1,192 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type MessagesAPISuite struct{ APISuite } + +func (s *MessagesAPISuite) SetupSuite() { + s.handler = messagesTestRouter() + s.setupSuite() +} + +func (s *MessagesAPISuite) TestMessagesIndex() { + resp, err := s.client.IndexMessages( + ID("123"), + &IndexMessagesQuery{ + BeforeID: "0123456789", + SinceID: "9876543210", + AfterID: "0246813579", + Limit: 20, + }, + ) + s.Require().NoError(err) + s.Require().NotZero(resp) + for _, message := range resp.Messages { + s.Assert().NotZero(message) + } +} + +func (s *MessagesAPISuite) TestMessagesCreate() { + message, err := s.client.CreateMessage( + ID("123"), + &Message{ + Text: "Test", + }, + ) + s.Require().NoError(err) + s.Require().NotNil(message) + s.Assert().NotZero(*message) +} + +func TestMessagesAPISuite(t *testing.T) { + suite.Run(t, new(MessagesAPISuite)) +} + +func messagesTestRouter() *mux.Router { + router := mux.NewRouter() + + // Index + router.Path("/groups/{id:[0-9]+}/messages"). + Methods("GET"). + Name("IndexMessages"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "count": 123, + "messages": [ + { + "id": "1234567890", + "source_guid": "GUID", + "created_at": 1302623328, + "user_id": "1234567890", + "group_id": "1234567890", + "name": "John", + "avatar_url": "https://i.groupme.com/123456789", + "text": "Hello world ☃☃", + "system": true, + "favorited_by": [ + "101", + "66", + "1234567890" + ], + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + ] + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Create + router.Path("/groups/{id:[0-9]+}/messages"). + Methods("POST"). + Name("CreateMessages"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(201) + fmt.Fprint(w, `{ + "response": { + "message": { + "id": "1234567890", + "source_guid": "GUID", + "created_at": 1302623328, + "user_id": "1234567890", + "group_id": "1234567890", + "name": "John", + "avatar_url": "https://i.groupme.com/123456789", + "text": "Hello world ☃☃", + "system": true, + "favorited_by": [ + "101", + "66", + "1234567890" + ], + "attachments": [ + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "image", + "url": "https://i.groupme.com/123456789" + }, + { + "type": "location", + "lat": "40.738206", + "lng": "-73.993285", + "name": "GroupMe HQ" + }, + { + "type": "split", + "token": "SPLIT_TOKEN" + }, + { + "type": "emoji", + "placeholder": "☃", + "charmap": [ + [ + 1, + 42 + ], + [ + 2, + 34 + ] + ] + } + ] + } + }, + "meta": { + "code": 201, + "errors": [] + } + }`) + }) + + /*// Return test router //*/ + return router +} diff --git a/sms_mode_api.go b/sms_mode_api.go new file mode 100644 index 0000000..c05d4f5 --- /dev/null +++ b/sms_mode_api.go @@ -0,0 +1,77 @@ +package groupme + +import ( + "fmt" + "net/http" + "net/url" + "strconv" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#sms_mode + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + smsModeEndpointRoot = usersEndpointRoot + "/sms_mode" + + // Actual Endpoints + createSMSModeEndpoint = smsModeEndpointRoot // POST + deleteSMSModeEndpoint = smsModeEndpointRoot + "/delete" // POST +) + +////////// API Requests ////////// + +// Create + +/* +CreateSMSMode - +Enables SMS mode for N hours, where N is at most 48. After N +hours have elapsed, user will receive push notfications. + +Parameters: + duration - required, integer + registration_id - string; The push notification ID/token + that should be suppressed during SMS mode. If this is + omitted, both SMS and push notifications will be + delivered to the device. +*/ +func (c *Client) CreateSMSMode(duration int, registrationID *ID) error { + httpReq, err := http.NewRequest("POST", c.endpointBase+createSMSModeEndpoint, nil) + if err != nil { + return err + } + + data := url.Values{} + data.Add("duration", strconv.Itoa(duration)) + + if registrationID != nil { + data.Add("registration_id", registrationID.String()) + } + + httpReq.PostForm = data + + err = c.do(httpReq, nil) + if err != nil { + return err + } + + return nil +} + +// Delete + +/* +DeleteSMSMode - + +Disables SMS mode +*/ +func (c *Client) DeleteSMSMode() error { + url := fmt.Sprintf(c.endpointBase + deleteSMSModeEndpoint) + + httpReq, err := http.NewRequest("POST", url, nil) + if err != nil { + return err + } + + return c.do(httpReq, nil) +} diff --git a/sms_mode_api_test.go b/sms_mode_api_test.go new file mode 100644 index 0000000..d4be18b --- /dev/null +++ b/sms_mode_api_test.go @@ -0,0 +1,50 @@ +package groupme + +import ( + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type SMSModeAPISuite struct{ APISuite } + +func (s *SMSModeAPISuite) SetupSuite() { + s.handler = smsModeTestRouter() + s.setupSuite() +} + +func (s *SMSModeAPISuite) TestSMSModeCreate() { + s.Assert().NoError(s.client.CreateSMSMode(10, nil)) +} + +func (s *SMSModeAPISuite) TestSMSModeDelete() { + s.Assert().NoError(s.client.DeleteSMSMode()) +} +func TestSMSModeAPISuite(t *testing.T) { + suite.Run(t, new(SMSModeAPISuite)) +} + +func smsModeTestRouter() *mux.Router { + router := mux.NewRouter() + + // Create + router.Path("/users/sms_mode"). + Methods("POST"). + Name("CreateSMSMode"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(201) + }) + + // Delete + router.Path("/users/sms_mode/delete"). + Methods("POST"). + Name("DeleteSMSMode"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + }) + + /*// Return test router //*/ + return router +} diff --git a/users_api.go b/users_api.go new file mode 100644 index 0000000..b9a87fb --- /dev/null +++ b/users_api.go @@ -0,0 +1,105 @@ +package groupme + +import ( + "fmt" + "net/http" + "net/url" +) + +// GroupMe documentation: https://dev.groupme.com/docs/v3#users + +////////// Endpoints ////////// +const ( + // Used to build other endpoints + usersEndpointRoot = "/users" + + // Actual Endpoints + myUserEndpoint = usersEndpointRoot + "/me" // GET + updateMyUserEndpoint = usersEndpointRoot + "/update" // POST +) + +////////// API Requests ////////// + +// Me + +/* +MyUser - + +Loads a specific group. + +Parameters: + groupID - required, ID(string) +*/ +func (c *Client) MyUser() (*User, error) { + URL := fmt.Sprintf(c.endpointBase + myUserEndpoint) + + httpReq, err := http.NewRequest("GET", URL, nil) + if err != nil { + return nil, err + } + + var resp User + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +// Update + +type UserSettings struct { + // URL to valid JPG/PNG/GIF image. URL will be converted into + // an image service link (https://i.groupme.com/....) + AvatarURL string `json:"avatar_url"` + // Name must be of the form FirstName LastName + Name string `json:"name"` + // Email address. Must be in name@domain.com form + Email string `json:"email"` + ZipCode string `json:"zip_code"` +} + +/* +UpdateMyUser - + +Update attributes about your own account + +Parameters: See UserSettings +*/ +func (c *Client) UpdateMyUser(us UserSettings) (*User, error) { + URL := fmt.Sprintf(c.endpointBase + updateMyUserEndpoint) + + httpReq, err := http.NewRequest("POST", URL, nil) + if err != nil { + return nil, err + } + + data := url.Values{} + + if us.AvatarURL != "" { + data.Add("avatar_url", us.AvatarURL) + } + + if us.Name != "" { + data.Add("name", us.Name) + } + + if us.Email != "" { + data.Add("email", us.Email) + } + + if us.ZipCode != "" { + data.Add("zip_code", us.ZipCode) + } + + httpReq.PostForm = data + + var resp User + err = c.do(httpReq, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} diff --git a/users_api_test.go b/users_api_test.go new file mode 100644 index 0000000..19ebad3 --- /dev/null +++ b/users_api_test.go @@ -0,0 +1,87 @@ +package groupme + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" +) + +type UsersAPISuite struct{ APISuite } + +func (s *UsersAPISuite) SetupSuite() { + s.handler = usersTestRouter() + s.setupSuite() +} + +func (s *UsersAPISuite) TestUsersMe() { + user, err := s.client.MyUser() + s.Require().NoError(err) + s.Assert().NotZero(user) +} + +func (s *UsersAPISuite) TestUsersUpdate() { + user, err := s.client.UpdateMyUser(UserSettings{}) + s.Require().NoError(err) + s.Assert().NotZero(user) +} + +func TestUsersAPISuite(t *testing.T) { + suite.Run(t, new(UsersAPISuite)) +} +func usersTestRouter() *mux.Router { + router := mux.NewRouter() + + // Me + router.Path("/users/me"). + Methods("GET"). + Name("MyUser"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "id": "1234567890", + "phone_number": "+1 2123001234", + "image_url": "https://i.groupme.com/123456789", + "name": "Ronald Swanson", + "created_at": 1302623328, + "updated_at": 1302623328, + "email": "me@example.com", + "sms": false + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + // Update + router.Path("/users/update"). + Methods("POST"). + Name("UpdateMyUser"). + HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + fmt.Fprint(w, `{ + "response": { + "id": "1234567890", + "phone_number": "+1 2123001234", + "image_url": "https://i.groupme.com/123456789", + "name": "Ronald Swanson", + "created_at": 1302623328, + "updated_at": 1302623328, + "email": "me@example.com", + "sms": false + }, + "meta": { + "code": 200, + "errors": [] + } + }`) + }) + + /*// Return test router //*/ + return router +}