Compare commits

...

54 Commits

Author SHA1 Message Date
f9c1b24194 Bump fayec 2023-09-26 13:03:49 -05:00
a58e4100e1 Upgrading fayec 2023-09-25 15:29:44 -05:00
7b9e2a8d29 Fixing bug where nil handler was called 2023-09-24 15:36:17 -04:00
d81730e3e7 Handlers are now fired for individual users 2023-09-22 16:10:44 -05:00
08bfe83ba4 Updating API to re-use client for multiple users 2023-09-20 16:47:47 -05:00
c0f3da8060 Using updated fayec client 2023-09-19 16:28:22 -05:00
5727a20506 Updating package name 2023-09-18 21:37:41 -05:00
3e9851b2c0 Usinc fayec client instead of wray 2023-09-18 21:35:00 -05:00
Sumner Evans
8f23e04eea
namespace under beeper
Signed-off-by: Sumner Evans <sumner@beeper.com>
2022-10-21 15:59:45 -05:00
Karmanyaah Malhotra
c2dbe7021e fix 2021-05-08 16:01:24 -04:00
Karmanyaah Malhotra
6eba33b3be relations api 2021-05-08 15:54:12 -04:00
Karmanyaah Malhotra
7961d30a51 Dependencies 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
12931fb275 ChatID name different over push 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
65299d9606 Update example to use new real-time api 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
aa216ed4af Reformat real_time 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
28b2d85660 logging 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
8dd5993481 Fix panic 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
042fb9a951 fix golangcilint issues 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
b6715c2375 added more conditions 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
c5d6c8a29c replace not required in production version 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
d6a8b0818f Add many types of handling
fix listen loop
improve example
2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
da7bc977a3 Add membership 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
eeb2f88b97 Use interfaces and callbacks 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
784cfe93c1 real time push basics 2021-05-08 15:53:38 -04:00
Karmanyaah Malhotra
6d42a230d1 Fix bugs with direct message api 2021-05-08 15:44:18 -04:00
Karmanyaah Malhotra
7166300503 Add more attachment values 2021-05-08 15:44:18 -04:00
densestvoid
f92a8a7a86 Introducing Contexts
Contexts have been added to all API requests, in addition to other linting fixes
2021-01-22 16:47:22 -05:00
Densest Void
646bec0e27
Update README.md 2021-01-22 14:43:05 -05:00
Densest Void
b9317e3e37
Update and rename go.yml to go-test.yml 2021-01-22 14:39:15 -05:00
Densest Void
05eb619c40
Update golangci-lint.yml 2021-01-22 14:36:24 -05:00
Densest Void
055fdc7126
Delete go-test.yml 2021-01-22 14:35:10 -05:00
Densest Void
3bac8c4f92
Update go.yml 2021-01-22 14:34:56 -05:00
Densest Void
8fca7782ec
Create go.yml 2021-01-22 14:30:44 -05:00
Densest Void
8628d37bef
Update README.md 2021-01-22 13:56:31 -05:00
Densest Void
6f453ff6cc
Merge pull request #10 from tekkamanendless/return-meta-on-error
Return Meta on error
2020-12-29 15:52:53 -05:00
Douglas Danger Manley
73586d4b4c Ensure that we return the correct error code even if we can't read the body 2020-12-20 13:35:13 -05:00
Douglas Danger Manley
7f8d829ff7 Return the Meta on error
Previously, this would never actually return the Meta data structure
since by the time that it got parsed, we already knew that the request
was good.  Now, we do a special parse when we know that it failed so
that we can return the structured data (in particular, we want to be
able to use the HTTP status code).
2020-12-20 13:28:12 -05:00
Densest Void
d657643538
Merge pull request #8 from tekkamanendless/add-member-by-search
Add the email and phone_number fields to the Member structure
2020-12-14 21:31:17 -05:00
Densest Void
3e663f8615
Update golangci-lint.yml
Trying a different marketplace action
2020-12-14 21:20:36 -05:00
Densest Void
eea12a3b50
Update golangci-lint.yml
Update version again
2020-12-14 21:17:56 -05:00
Densest Void
e6f29c4e73
Update golangci-lint.yml
Updated version
2020-12-14 21:07:14 -05:00
Douglas Danger Manley
cd2551461d Add the email and phone_number fields to the Member structure
These fields aren't _returned_ with any member-related calls; however,
they are used when _adding_ a member to a group.  For example, you can
add a member to a group by email address, but you need to set the "email"
property.  Ditto for "phone_number".
2020-12-13 14:00:44 -05:00
densestvoid
2ff9a03a8c Updated JSON tags to omit if empty 2020-08-28 22:04:52 -04:00
densestvoid
f900b99dac Changed POST API content-type to application/json
Fixed AddMember response parsing
added bot post message example
2020-08-24 22:44:28 -04:00
densestvoid
d8cdcf4ef2 API POST Calls Update
POST calls were not getting their JSON data written
2020-08-23 21:25:02 -04:00
densestvoid
010fd832ac Updated Auth Token usage
PostBotMessage no longer requires/uses an auth token
2020-08-23 19:28:50 -04:00
Densest Void
bd76de2ffe
Update go-test.yml 2020-07-31 08:36:18 -04:00
Densest Void
537a97ebe7
Update README.md 2020-07-31 08:34:57 -04:00
Densest Void
9ee368f10d
Create go-test.yml 2020-07-31 08:31:32 -04:00
Densest Void
d9dec16e27
Create golangci-lint.yml 2020-07-31 08:17:25 -04:00
Densest Void
755d681f86
Delete main.yml 2020-07-31 08:16:38 -04:00
Densest Void
f0aa4a6d73
Added workflow 2020-07-31 08:07:58 -04:00
densestvoid
c6f6857cfa v0.1.0 release
Updated tests and client to use the authorization token
Fixed readme and added the example to the examples folder
2020-07-31 07:29:08 -04:00
densestvoid
31885d2726 First major code push
Preparing for version 0.1.0
Contains a real README, the test framework, and the API implementation
2020-07-29 22:20:57 -04:00
39 changed files with 5273 additions and 2 deletions

21
.github/workflows/go-test.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Go Test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.15
- name: Test
run: go test -v ./...

27
.github/workflows/golangci-lint.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: golangci-lint
on:
push:
pull_request:
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.29
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
# Optional: if set to true then the action will use pre-installed Go
# skip-go-installation: true

88
README.md Normal file → Executable file
View File

@ -1,2 +1,86 @@
# groupme # Version 1.0 Release Date: TBD
GroupMe Library 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.
<br>
# GroupMe API Wrapper
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/densestvoid/groupme?label=version&logo=version&sort=semver)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/densestvoid/groupme)](https://pkg.go.dev/github.com/densestvoid/groupme)
## 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(&groupme.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, &groupme.IndexMessagesQuery{
Limit: 10,
})
if err != nil {
fmt.Println(err)
}
fmt.Println(messages)
}
```
## Installation
### Go Package
`go get github.com/densestvoid/groupme`
### [*FUTURE*] CLI
## Support
You can join the [GroupMe support group](https://groupme.com/join_group/65686806/il1737tE) (you will need to provide a reason for joining), or the [Discord server](https://discord.gg/raAdxWuKTU).
## 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)

115
blocks_api.go Normal file
View File

@ -0,0 +1,115 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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 ////////*/
// IndexBlock - A list of contacts you have blocked. These people cannot DM you
func (c *Client) IndexBlock(ctx context.Context, userID string, authToken string) ([]*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)
URL.RawQuery = query.Encode()
var resp struct {
Blocks []*Block `json:"blocks"`
}
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp.Blocks, nil
}
// BlockBetween - Asks if a block exists between you and another user id
func (c *Client) BlockBetween(ctx context.Context, userID, otherUserID string, authToken string) (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)
query.Set("otherUser", otherUserID)
URL.RawQuery = query.Encode()
var resp struct {
Between bool `json:"between"`
}
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return false, err
}
return resp.Between, nil
}
// CreateBlock - Creates a block between you and the contact
func (c *Client) CreateBlock(ctx context.Context, userID, otherUserID string, authToken string) (*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)
query.Set("otherUser", otherUserID)
URL.RawQuery = query.Encode()
var resp struct {
Block *Block `json:"block"`
}
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp.Block, nil
}
// Unblock - Removes block between you and other user
func (c *Client) Unblock(ctx context.Context, userID, otherUserID string, authToken string) 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)
query.Set("otherUser", otherUserID)
URL.RawQuery = query.Encode()
err = c.doWithAuthToken(ctx, httpReq, nil, authToken)
if err != nil {
return err
}
return nil
}

128
blocks_api_test.go Normal file
View File

@ -0,0 +1,128 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(context.Background(), "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(context.Background(), "1", "2")
s.Require().NoError(err)
s.Assert().True(between)
}
func (s *BlocksAPISuite) TestBlocksCreate() {
block, err := s.client.CreateBlock(context.Background(), "1", "2")
s.Require().NoError(err)
s.Assert().NotZero(block)
}
func (s *BlocksAPISuite) TestBlocksUnblock() {
s.Assert().NoError(s.client.Unblock(context.Background(), "1", "2"))
}
func TestBlocksAPISuite(t *testing.T) {
suite.Run(t, new(BlocksAPISuite))
}
func blocksTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}

123
bots_api.go Normal file
View File

@ -0,0 +1,123 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
// 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 ////////*/
// CreateBot - Create a bot. See the Bots Tutorial (https://dev.groupme.com/tutorials/bots)
// for a full walkthrough.
func (c *Client) CreateBot(ctx context.Context, bot *Bot, authToken string) (*Bot, error) {
URL := c.endpointBase + createBotEndpoint
var data = struct {
Bot *Bot `json:"bot,omitempty"`
}{
bot,
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}
var resp Bot
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return &resp, nil
}
// PostBotMessage - Post a message from a bot
// TODO: Move PostBotMessage to bot object, since it doesn't require access token
func (c *Client) PostBotMessage(ctx context.Context, botID ID, text string, pictureURL *string) error {
URL := fmt.Sprintf(c.endpointBase + postBotMessageEndpoint)
var data = struct {
BotID ID `json:"bot_id"`
Text string `json:"text"`
PictureURL *string `json:",omitempty"`
}{
botID,
text,
pictureURL,
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return err
}
return c.do(ctx, httpReq, nil)
}
// IndexBots - list bots that you have created
func (c *Client) IndexBots(ctx context.Context, authToken string) ([]*Bot, error) {
httpReq, err := http.NewRequest("GET", c.endpointBase+indexBotsEndpoint, nil)
if err != nil {
return nil, err
}
var resp []*Bot
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp, nil
}
// DestroyBot - Remove a bot that you have created
func (c *Client) DestroyBot(ctx context.Context, botID ID, authToken string) error {
URL := fmt.Sprintf(c.endpointBase + destroyBotEndpoint)
var data = struct {
BotID ID `json:"bot_id"`
}{
botID,
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return err
}
return c.doWithAuthToken(ctx, httpReq, nil, authToken)
}

123
bots_api_test.go Normal file
View File

@ -0,0 +1,123 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(context.Background(), &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(context.Background(), "1", "test message", nil)
s.Require().NoError(err)
}
func (s *BotsAPISuite) TestBotsIndex() {
bots, err := s.client.IndexBots(context.Background())
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(context.Background(), "1"))
}
func TestBotsAPISuite(t *testing.T) {
suite.Run(t, new(BotsAPISuite))
}
func botsTestRouter() *mux.Router {
router := mux.NewRouter()
authRouter := router.Queries("token", "").Subrouter()
// Create
authRouter.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
authRouter.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
authRouter.Path("/bots/destroy").
Methods("POST").
Name("DestroyBot").
HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(201)
})
/*// Return test router //*/
return router
}

75
chats_api.go Normal file
View File

@ -0,0 +1,75 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"net/http"
"strconv"
)
// GroupMe documentation: https://dev.groupme.com/docs/v3#chats
/*//////// Endpoints ////////*/
const (
// Used to build other endpoints
chatsEndpointRoot = "/chats"
indexChatsEndpoint = chatsEndpointRoot // GET
)
// IndexChatsQuery 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.
func (c *Client) IndexChats(ctx context.Context, req *IndexChatsQuery, authToken string) ([]*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.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp, nil
}
func (c *Client) IndexRelations(ctx context.Context, authToken string) ([]*User, error) {
httpReq, err := http.NewRequest("GET", "https://api.groupme.com/v4"+"/relationships", nil)
if err != nil {
return nil, err
}
URL := httpReq.URL
query := URL.Query()
query.Set("include_blocked", "true")
URL.RawQuery = query.Encode()
var resp []*User
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp, nil
}

90
chats_api_test.go Normal file
View File

@ -0,0 +1,90 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(
context.Background(),
&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().Queries("token", "").Subrouter()
// 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
}

120
client.go Normal file
View File

@ -0,0 +1,120 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
)
// GroupMeAPIBase - Endpoints are added on to this 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
}
// NewClient creates a new GroupMe API Client
func NewClient() *Client {
return &Client{
// TODO: enable transport information passing in
httpClient: &http.Client{},
endpointBase: GroupMeAPIBase,
}
}
// 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)
}
const errorStatusCodeMin = 300
func (c Client) do(ctx context.Context, req *http.Request, i interface{}) error {
req = req.WithContext(ctx)
if req.Method == "POST" {
req.Header.Set("Content-Type", "application/json")
}
getResp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer getResp.Body.Close()
var readBytes []byte
// Check Status Code is 1XX or 2XX
if getResp.StatusCode >= errorStatusCodeMin {
readBytes, err = ioutil.ReadAll(getResp.Body)
if err != nil {
// We couldn't read the output. Oh well; generate the appropriate error type anyway.
return &Meta{
Code: HTTPStatusCode(getResp.StatusCode),
}
}
resp := newJSONResponse(nil)
if err = json.Unmarshal(readBytes, &resp); err != nil {
// We couldn't parse the output. Oh well; generate the appropriate error type anyway.
return &Meta{
Code: HTTPStatusCode(getResp.StatusCode),
}
}
return &resp.Meta
}
if i == nil {
return nil
}
readBytes, err = ioutil.ReadAll(getResp.Body)
if err != nil {
return err
}
resp := newJSONResponse(i)
if err := json.Unmarshal(readBytes, &resp); err != nil {
return err
}
return nil
}
func (c Client) doWithAuthToken(ctx context.Context, req *http.Request, i interface{}, authToken string) error {
URL := req.URL
query := URL.Query()
query.Set("token", authToken)
URL.RawQuery = query.Encode()
return c.do(ctx, req, i)
}

59
client_test.go Normal file
View File

@ -0,0 +1,59 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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_PostContentType() {
req, err := http.NewRequest("POST", "", nil)
s.Require().NoError(err)
s.Assert().Error(s.client.do(context.Background(), req, struct{}{}))
s.Assert().EqualValues(req.Header.Get("Content-Type"), "application/json")
}
func (s *ClientSuite) TestClient_do_DoError() {
req, err := http.NewRequest("", "", nil)
s.Require().NoError(err)
s.Assert().Error(s.client.do(context.Background(), 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(context.Background(), req, struct{}{}))
}
func TestClientSuite(t *testing.T) {
suite.Run(t, new(ClientSuite))
}

95
data_types.go Normal file
View File

@ -0,0 +1,95 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
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(`^\+\d+ \d{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)
}
// HTTPStatusCode are returned by HTTP requests in
// the header and the json "meta" field
type HTTPStatusCode int
// Text used as constant name
const (
HTTPOk HTTPStatusCode = 200
HTTPCreated HTTPStatusCode = 201
HTTPNoContent HTTPStatusCode = 204
HTTPNotModified HTTPStatusCode = 304
HTTPBadRequest HTTPStatusCode = 400
HTTPUnauthorized HTTPStatusCode = 401
HTTPForbidden HTTPStatusCode = 403
HTTPNotFound HTTPStatusCode = 404
HTTPEnhanceYourCalm HTTPStatusCode = 420
HTTPInternalServerError HTTPStatusCode = 500
HTTPBadGateway HTTPStatusCode = 502
HTTPServiceUnavailable HTTPStatusCode = 503
)
// String returns the description of the status code according to GroupMe
func (c HTTPStatusCode) String() string {
return map[HTTPStatusCode]string{
HTTPOk: "success",
HTTPCreated: "resource was created successfully",
HTTPNoContent: "resource was deleted successfully",
HTTPNotModified: "no new data to return",
HTTPBadRequest: "invalid format or data specified in the request",
HTTPUnauthorized: "authentication credentials missing or incorrect",
HTTPForbidden: "request refused due to update limits",
HTTPNotFound: "URI is invalid or resource does not exist",
HTTPEnhanceYourCalm: "application is being rate limited",
HTTPInternalServerError: "something unexpected occurred",
HTTPBadGateway: "GroupMe is down or being upgraded",
HTTPServiceUnavailable: "servers are overloaded, try again later",
}[c]
}

61
data_types_test.go Normal file
View File

@ -0,0 +1,61 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
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))
}

140
direct_messages_api.go Normal file
View File

@ -0,0 +1,140 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"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 ////////*/
// IndexDirectMessagesQuery 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(ctx context.Context, otherUserID string, req *IndexDirectMessagesQuery, authToken string) (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)
if req != nil {
if req.BeforeID != "" {
query.Add("before_ID", req.BeforeID.String())
}
if req.SinceID != "" {
query.Add("since_id", req.SinceID.String())
}
}
httpReq.URL.RawQuery = query.Encode()
var resp IndexDirectMessagesResponse
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return IndexDirectMessagesResponse{}, err
}
return resp, nil
}
/*
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}],...]).
*/
func (c *Client) CreateDirectMessage(ctx context.Context, m *Message, authToken string) (*Message, error) {
URL := fmt.Sprintf(c.endpointBase + createDirectMessageEndpoint)
m.SourceGUID = uuid.New().String()
var data = struct {
DirectMessage *Message `json:"direct_message,omitempty"`
}{
m,
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}
var resp struct {
*Message `json:"direct_message"`
}
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp.Message, nil
}

181
direct_messages_api_test.go Normal file
View File

@ -0,0 +1,181 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(
context.Background(),
"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(
context.Background(),
&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))
}
// nolint // not duplicate code
func directMessagesTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}

View File

@ -0,0 +1,50 @@
package main
import (
"context"
"fmt"
)
// 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(
context.Background(),
&groupme.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(context.Background(), groups[0].ID, &groupme.IndexMessagesQuery{
Limit: 10,
})
if err != nil {
fmt.Println(err)
}
fmt.Println(messages)
}

View File

@ -0,0 +1,19 @@
package main
import (
"context"
"fmt"
)
// This is not a real Bot ID. Please find yours by logging
// into the GroupMe development website: https://dev.groupme.com/bots
const botID = "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("")
fmt.Println(client.PostBotMessage(context.Background(), botID, "Your message here!", nil))
}

View File

@ -0,0 +1,177 @@
package main
import (
"context"
"fmt"
"log"
"gitea.watsonlabs.net/watsonb8/groupme-lib"
)
// This is not a real token. Please find yours by logging
// into the GroupMe development website: https://dev.groupme.com/
var authorizationToken = "ASD"
var authorizationToken2 = "ASDF"
// A short program that subscribes to 2 groups and 2 direct chats
// and prints out all recognized events in those
func main() {
//create push subscription and start listening
p := groupme.NewPushSubscription(context.Background())
err := p.Connect(context.TODO())
if err != nil {
return
}
// Create a new client with your auth token
client := groupme.NewClient()
User, _ := client.MyUser(context.Background(), authorizationToken)
User2, _ := client.MyUser(context.Background(), authorizationToken2)
//handles (in this case prints) all messages
p.AddFullHandler(Handler{User: User}, authorizationToken)
p.AddHandler(Handler{User: User2}, authorizationToken2)
//Subscribe to get messages and events for the specific user
p.SubscribeToUser(context.Background(), User.ID, authorizationToken)
p.SubscribeToUser(context.Background(), User2.ID, authorizationToken2)
if err != nil {
log.Fatal(err)
}
// Get the groups your user is part of
groups, err := client.IndexGroups(
context.Background(),
&groupme.GroupsQuery{
Page: 0,
PerPage: 2,
Omit: "memberships",
}, authorizationToken)
groups2, err := client.IndexGroups(
context.Background(),
&groupme.GroupsQuery{
Page: 0,
PerPage: 2,
Omit: "memberships",
}, authorizationToken2)
if err != nil {
fmt.Println(err)
return
}
//Subscribe to those groups
for _, j := range groups {
err = p.SubscribeToGroup(context.TODO(), j.ID, authorizationToken)
if err != nil {
log.Fatal(err)
}
}
for _, j := range groups2 {
err = p.SubscribeToGroup(context.TODO(), j.ID, authorizationToken2)
if err != nil {
log.Fatal(err)
}
}
//get chats your user is part of
chats, err := client.IndexChats(context.Background(),
&groupme.IndexChatsQuery{
Page: 0,
PerPage: 2,
}, authorizationToken)
chats2, err := client.IndexChats(context.Background(),
&groupme.IndexChatsQuery{
Page: 0,
PerPage: 2,
}, authorizationToken2)
//subscribe to all those chats
for _, j := range chats {
go func() {
err := p.SubscribeToDM(context.TODO(), j.LastMessage.ConversationID, authorizationToken)
if err != nil {
log.Fatal(err)
}
}()
}
for _, j := range chats2 {
go func() {
err := p.SubscribeToDM(context.TODO(), j.LastMessage.ConversationID, authorizationToken2)
if err != nil {
log.Fatal(err)
}
}()
}
//blocking
<-make(chan (struct{}))
}
// Following example handlers print out all data
type Handler struct {
User *groupme.User
}
func (h Handler) HandleError(e error) {
fmt.Println(e)
}
func (h Handler) HandleTextMessage(msg groupme.Message) {
fmt.Println(msg.Text, msg.Name, msg.Attachments)
}
func (h Handler) HandleJoin(group groupme.ID) {
fmt.Println("User joined group with id", group.String())
}
func (h Handler) HandleLike(msg groupme.Message) {
fmt.Println(msg.ID, "liked by", msg.FavoritedBy)
}
func (h Handler) HandlerMembership(i groupme.ID) {
fmt.Println("Membership event on", i.String())
}
func (h Handler) HandleGroupTopic(group groupme.ID, newTopic string) {
fmt.Println(group.String(), "has new topic of", newTopic)
}
func (h Handler) HandleGroupName(group groupme.ID, newName string) {
fmt.Println(group.String(), "has new name of", newName)
}
func (h Handler) HandleGroupAvatar(group groupme.ID, newAvatar string) {
fmt.Println(group.String(), "has new avatar url of", newAvatar)
}
func (h Handler) HandleLikeIcon(group groupme.ID, PackID, PackIndex int, Type string) {
//Not sure how to use without groupme icon packs
if len(Type) == 0 {
fmt.Println("Default like icon set")
return
}
fmt.Println(group.String(), "has new like icon of", PackID, PackIndex, Type)
}
func (h Handler) HandleNewNickname(group groupme.ID, user groupme.ID, newName string) {
fmt.Printf("In group %s, user %s has new nickname %s\n", group.String(), user.String(), newName)
}
func (h Handler) HandleNewAvatarInGroup(group groupme.ID, user groupme.ID, avatarURL string) {
if avatarURL == "" {
//get default avatar
avatarURL = h.User.ImageURL
}
fmt.Printf("In group %s, user %s has new avatar with url %s\n", group.String(), user.String(), avatarURL)
}
func (h Handler) HandleMembers(group groupme.ID, members []groupme.Member, added bool) {
action := "removed"
if added {
action = "added"
}
fmt.Printf("In group %s, users %v %s\n", group.String(), members, action)
}

17
go.mod Executable file
View File

@ -0,0 +1,17 @@
module gitea.watsonlabs.net/watsonb8/groupme-lib
go 1.21.0
require (
gitea.watsonlabs.net/watsonb8/fayec v0.0.5-0.20230926180210-b375ab3c8c11
github.com/google/uuid v1.2.0
github.com/gorilla/mux v1.8.0
github.com/stretchr/testify v1.7.0
)
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

50
go.sum Executable file
View File

@ -0,0 +1,50 @@
gitea.watsonlabs.net/watsonb8/fayec v0.0.0-20230919020138-8f0db7048755 h1:FEhNSjSNvZ+nVg5Z3ds6X8ys3qjM+mmyLTSqKhCUHuQ=
gitea.watsonlabs.net/watsonb8/fayec v0.0.0-20230919020138-8f0db7048755/go.mod h1:gv8CWMq6dFJQhH30u8bO3u4k2irKlclZktLNYDebQ/0=
gitea.watsonlabs.net/watsonb8/fayec v0.0.0-20230919151904-5ca9ade6f946 h1:loc70tiaFs1U4sqn+lKMSBlo5OfvVfClYnWjfGLXaSg=
gitea.watsonlabs.net/watsonb8/fayec v0.0.0-20230919151904-5ca9ade6f946/go.mod h1:gv8CWMq6dFJQhH30u8bO3u4k2irKlclZktLNYDebQ/0=
gitea.watsonlabs.net/watsonb8/fayec v0.0.1 h1:MNFmTaTyyKKgrw04dGO9C5ojtm1jIvy8oHYqbj0ECeY=
gitea.watsonlabs.net/watsonb8/fayec v0.0.1/go.mod h1:gv8CWMq6dFJQhH30u8bO3u4k2irKlclZktLNYDebQ/0=
gitea.watsonlabs.net/watsonb8/fayec v0.0.2 h1:tqbgr1vRZ6Wq4W81xBg+FTOywSv3EJpK263SAVXXTco=
gitea.watsonlabs.net/watsonb8/fayec v0.0.2/go.mod h1:gv8CWMq6dFJQhH30u8bO3u4k2irKlclZktLNYDebQ/0=
gitea.watsonlabs.net/watsonb8/fayec v0.0.3 h1:YpaZBIee8Ix6uGm1UoEtBix1dEU1TURChAsJGJ3pVRo=
gitea.watsonlabs.net/watsonb8/fayec v0.0.3/go.mod h1:gv8CWMq6dFJQhH30u8bO3u4k2irKlclZktLNYDebQ/0=
gitea.watsonlabs.net/watsonb8/fayec v0.0.4 h1:SLvwip1DQy13QngVsEgoLtN7T6bS+X6348p6PQhUF2A=
gitea.watsonlabs.net/watsonb8/fayec v0.0.4/go.mod h1:gv8CWMq6dFJQhH30u8bO3u4k2irKlclZktLNYDebQ/0=
gitea.watsonlabs.net/watsonb8/fayec v0.0.5-0.20230926180210-b375ab3c8c11 h1:xJ9eSFyIrDA43UVpbxOD1QkA2jhg+vS+eezFKCDV3Dw=
gitea.watsonlabs.net/watsonb8/fayec v0.0.5-0.20230926180210-b375ab3c8c11/go.mod h1:gv8CWMq6dFJQhH30u8bO3u4k2irKlclZktLNYDebQ/0=
gitea.watsonlabs.net/watsonb8/fayec v0.0.5 h1:9+UHzUuEcLuZ5Gx5S/NTBxYshUhsiQ5M3vzUF8RAKxw=
gitea.watsonlabs.net/watsonb8/fayec v0.0.5/go.mod h1:gv8CWMq6dFJQhH30u8bO3u4k2irKlclZktLNYDebQ/0=
github.com/autogrowsystems/wray v0.0.0-20160519030252-f36984f6648c/go.mod h1:druJ8QMeBCUmwJ7ZSFowx77dWxEWF3SYlQlsqZaLZQg=
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/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/karmanyaahm/wray v0.0.0-20210303233435-756d58657c14 h1:NrATjZKvkY+ojL8FXTWa3fQ+wihFrAxLNE6T+wOkIcY=
github.com/karmanyaahm/wray v0.0.0-20210303233435-756d58657c14/go.mod h1:ysD86MIEevmAkdfdg5s6Qt3I07RN6fvMAyna7jCGG2o=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

419
groups_api.go Executable file
View File

@ -0,0 +1,419 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"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(ctx context.Context, req *GroupsQuery, authToken string) ([]*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.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp, nil
}
/*/// Former ///*/
/*
FormerGroups -
List they groups you have left but can rejoin.
*/
func (c *Client) FormerGroups(ctx context.Context, authToken string) ([]*Group, error) {
httpReq, err := http.NewRequest("GET", c.endpointBase+formerGroupsEndpoint, nil)
if err != nil {
return nil, err
}
var resp []*Group
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp, nil
}
/*/// Show ///*/
/*
ShowGroup -
Loads a specific group.
Parameters:
groupID - required, ID(string)
*/
func (c *Client) ShowGroup(ctx context.Context, groupID ID, authToken string) (*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.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return &resp, nil
}
/*/// Create ///*/
/*
CreateGroup -
# Create a new group
Parameters: See GroupSettings
*/
func (c *Client) CreateGroup(ctx context.Context, gs GroupSettings, authToken string) (*Group, error) {
URL := fmt.Sprintf(c.endpointBase + createGroupEndpoint)
jsonBytes, err := json.Marshal(&gs)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}
var resp Group
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
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(ctx context.Context, groupID ID, gs GroupSettings, authToken string) (*Group, error) {
URL := fmt.Sprintf(c.endpointBase+updateGroupEndpoint, groupID)
jsonBytes, err := json.Marshal(&gs)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}
var resp Group
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
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(ctx context.Context, groupID ID, authToken string) error {
url := fmt.Sprintf(c.endpointBase+destroyGroupEndpoint, groupID)
httpReq, err := http.NewRequest("POST", url, nil)
if err != nil {
return err
}
return c.doWithAuthToken(ctx, httpReq, nil, authToken)
}
/*/// Join ///*/
/*
JoinGroup -
# Join a shared group
Parameters:
groupID - required, ID(string)
shareToken - required, string
*/
func (c *Client) JoinGroup(ctx context.Context, groupID ID, shareToken string, authToken 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.doWithAuthToken(ctx, httpReq, &resp, authToken)
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(ctx context.Context, groupID ID, authToken string) (*Group, error) {
URL := fmt.Sprintf(c.endpointBase + rejoinGroupEndpoint)
var data = struct {
GroupID ID `json:"group_id"`
}{
groupID,
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}
var resp Group
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
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(ctx context.Context, reqs ChangeOwnerRequest, authToken string) (ChangeOwnerResult, error) {
URL := fmt.Sprintf(c.endpointBase + changeGroupOwnerEndpoint)
var data = struct {
Requests []ChangeOwnerRequest `json:"requests"`
}{
[]ChangeOwnerRequest{reqs},
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return ChangeOwnerResult{}, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return ChangeOwnerResult{}, err
}
var resp struct {
Results []ChangeOwnerResult `json:"results"`
}
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
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
// Change owner Status Codes
const (
ChangeOwnerOk changeOwnerStatusCode = "200"
ChangeOwnerRequesterNewOwner changeOwnerStatusCode = "400"
ChangeOwnerNotOwner changeOwnerStatusCode = "403"
ChangeOwnerBadGroupOrOwner changeOwnerStatusCode = "404"
ChangeOwnerBadRequest changeOwnerStatusCode = "405"
)
// String returns the description of the status code according to GroupMe
func (c changeOwnerStatusCode) String() string {
return map[changeOwnerStatusCode]string{
ChangeOwnerOk: "success",
ChangeOwnerRequesterNewOwner: "requester is also a new owner",
ChangeOwnerNotOwner: "requester is not the owner of the group",
ChangeOwnerBadGroupOrOwner: "group or new owner not found or new owner is not member of the group",
ChangeOwnerBadRequest: "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)
}

700
groups_api_test.go Executable file
View File

@ -0,0 +1,700 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(
context.Background(),
&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(context.Background())
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(context.Background(), "1")
s.Require().NoError(err)
s.Assert().NotZero(group)
}
func (s *GroupsAPISuite) TestGroupsCreate() {
group, err := s.client.CreateGroup(
context.Background(),
GroupSettings{
"Test",
"This is a test group",
"www.blank.com/image",
false,
true,
},
)
s.Require().NoError(err)
s.Assert().NotZero(group)
}
func (s *GroupsAPISuite) TestGroupsUpdate() {
group, err := s.client.UpdateGroup(context.Background(), "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(context.Background(), "1")
s.Require().NoError(err)
}
func (s *GroupsAPISuite) TestGroupsJoin() {
group, err := s.client.JoinGroup(context.Background(), "1", "please")
s.Require().NoError(err)
s.Assert().NotZero(group)
}
func (s *GroupsAPISuite) TestGroupsRejoin() {
group, err := s.client.RejoinGroup(context.Background(), "1")
s.Require().NoError(err)
s.Assert().NotZero(group)
}
func (s *GroupsAPISuite) TestGroupsChangeOwner() {
result, err := s.client.ChangeGroupOwner(
context.Background(),
ChangeOwnerRequest{
"1",
"123",
},
)
s.Require().NoError(err)
s.Assert().NotZero(result)
}
func TestGroupsAPISuite(t *testing.T) {
suite.Run(t, new(GroupsAPISuite))
}
/*//////// Test Groups Router ////////*/
// nolint // not duplicate code
func groupsTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}

233
json.go Executable file
View File

@ -0,0 +1,233 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
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,omitempty"`
Errors []string `json:"errors,omitempty"`
}
// 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,omitempty"`
Name string `json:"name,omitempty"`
// Type of group (private|public)
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
ImageURL string `json:"image_url,omitempty"`
CreatorUserID ID `json:"creator_user_id,omitempty"`
CreatedAt Timestamp `json:"created_at,omitempty"`
UpdatedAt Timestamp `json:"updated_at,omitempty"`
Members []*Member `json:"members,omitempty"`
ShareURL string `json:"share_url,omitempty"`
Messages GroupMessages `json:"messages,omitempty"`
}
// GroupMessages is a Group field, only returned in Group JSON API responses
type GroupMessages struct {
Count uint `json:"count,omitempty"`
LastMessageID ID `json:"last_message_id,omitempty"`
LastMessageCreatedAt Timestamp `json:"last_message_created_at,omitempty"`
Preview MessagePreview `json:"preview,omitempty"`
}
// MessagePreview is a GroupMessages field, only returned in Group JSON API responses.
// Abbreviated form of Message type
type MessagePreview struct {
Nickname string `json:"nickname,omitempty"`
Text string `json:"text,omitempty"`
ImageURL string `json:"image_url,omitempty"`
Attachments []*Attachment `json:"attachments,omitempty"`
}
// 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,omitempty"`
UserID ID `json:"user_id,omitempty"`
Nickname string `json:"nickname,omitempty"`
Muted bool `json:"muted,omitempty"`
ImageURL string `json:"image_url,omitempty"`
AutoKicked bool `json:"autokicked,omitempty"`
AppInstalled bool `json:"app_installed,omitempty"`
GUID string `json:"guid,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"` // Only used when searching for the member to add to a group.
Email string `json:"email,omitempty"` // Only used when searching for the member to add to a group.
}
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,omitempty"`
SourceGUID string `json:"source_guid,omitempty"`
CreatedAt Timestamp `json:"created_at,omitempty"`
GroupID ID `json:"group_id,omitempty"`
UserID ID `json:"user_id,omitempty"`
BotID ID `json:"bot_id,omitempty"`
SenderID ID `json:"sender_id,omitempty"`
SenderType senderType `json:"sender_type,omitempty"`
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
RecipientID ID `json:"recipient_id,omitempty"`
//ChatID - over push ConversationID seems to be called ChatID
ChatID ID `json:"chat_id,omitempty"`
ConversationID ID `json:"conversation_id,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
// Maximum length of 1000 characters
Text string `json:"text,omitempty"`
// Must be an image service URL (i.groupme.com)
ImageURL string `json:"image_url,omitempty"`
FavoritedBy []string `json:"favorited_by,omitempty"`
Attachments []*Attachment `json:"attachments,omitempty"`
}
func (m *Message) String() string {
return marshal(m)
}
type senderType string
// SenderType constants
const (
SenderTypeUser senderType = "user"
SenderTypeBot senderType = "bot"
SenderTypeSystem 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,omitempty"`
Loci [][]int `json:"loci,omitempty"`
UserIDs []ID `json:"user_ids,omitempty"`
URL string `json:"url,omitempty"`
FileID string `json:"file_id,omitempty"`
VideoPreviewURL string `json:"preview_url,omitempty"`
Name string `json:"name,omitempty"`
Latitude string `json:"lat,omitempty"`
Longitude string `json:"lng,omitempty"`
Placeholder string `json:"placeholder,omitempty"`
Charmap [][]int `json:"charmap,omitempty"`
ReplyID ID `json:"reply_id,omitempty"`
}
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,omitempty"`
PhoneNumber PhoneNumber `json:"phone_number,omitempty"`
ImageURL string `json:"image_url,omitempty"`
Name string `json:"name,omitempty"`
CreatedAt Timestamp `json:"created_at,omitempty"`
UpdatedAt Timestamp `json:"updated_at,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
Email string `json:"email,omitempty"`
SMS bool `json:"sms,omitempty"`
}
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,omitempty"`
UpdatedAt Timestamp `json:"updated_at,omitempty"`
LastMessage *Message `json:"last_message,omitempty"`
MessagesCount int `json:"messages_count,omitempty"`
OtherUser User `json:"other_user,omitempty"`
}
func (c *Chat) String() string {
return marshal(c)
}
// Bot is a GroupMe bot, it is connected to a specific group which it can send messages to
type Bot struct {
BotID ID `json:"bot_id,omitempty"`
GroupID ID `json:"group_id,omitempty"`
Name string `json:"name,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
CallbackURL string `json:"callback_url,omitempty"`
DMNotification bool `json:"dm_notification,omitempty"`
}
func (b *Bot) String() string {
return marshal(b)
}
// Block is a GroupMe block between two users, direct messages are not allowed
type Block struct {
UserID ID `json:"user_id,omitempty"`
BlockedUserID ID `json:"blocked_user_id,omitempty"`
CreatedAT Timestamp `json:"created_at,omitempty"`
}
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)
}

75
json_test.go Executable file
View File

@ -0,0 +1,75 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
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))
}

121
leaderboard_api.go Normal file
View File

@ -0,0 +1,121 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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
// Define acceptable period values
const (
PeriodDay = "day"
PeriodWeek = "week"
PeriodMonth = "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.
func (c *Client) IndexLeaderboard(ctx context.Context, groupID ID, p period, authToken string) ([]*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", string(p))
URL.RawQuery = query.Encode()
var resp struct {
Messages []*Message `json:"messages"`
}
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
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(ctx context.Context, groupID ID, authToken string) ([]*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.doWithAuthToken(ctx, httpReq, &resp, authToken)
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(ctx context.Context, groupID ID, authToken string) ([]*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.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp.Messages, nil
}

313
leaderboard_api_test.go Normal file
View File

@ -0,0 +1,313 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(context.Background(), "1", PeriodDay)
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(context.Background(), "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(context.Background(), "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))
}
// nolint // not duplicate code
func leaderboardTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}

47
likes_api.go Normal file
View File

@ -0,0 +1,47 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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.
func (c *Client) CreateLike(ctx context.Context, conversationID, messageID ID, authToken string) error {
url := fmt.Sprintf(c.endpointBase+createLikeEndpoint, conversationID, messageID)
httpReq, err := http.NewRequest("POST", url, nil)
if err != nil {
return err
}
return c.doWithAuthToken(ctx, httpReq, nil, authToken)
}
// DestroyLike - Unlike a message.
func (c *Client) DestroyLike(ctx context.Context, conversationID, messageID ID, authToken string) error {
url := fmt.Sprintf(c.endpointBase+destroyLikeEndpoint, conversationID, messageID)
httpReq, err := http.NewRequest("POST", url, nil)
if err != nil {
return err
}
return c.doWithAuthToken(ctx, httpReq, nil, authToken)
}

56
likes_api_test.go Normal file
View File

@ -0,0 +1,56 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(context.Background(), "1", "1")
s.Require().NoError(err)
}
func (s *LikesAPISuite) TestLikesDestroy() {
err := s.client.DestroyLike(context.Background(), "1", "1")
s.Require().NoError(err)
}
func TestLikesAPISuite(t *testing.T) {
suite.Run(t, new(LikesAPISuite))
}
// nolint // not duplicate code
func likesTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}

88
main_test.go Executable file
View File

@ -0,0 +1,88 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
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
// Overridden 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)
// nolint // url is meant to be variable
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())
// nolint // weak random generator is ok for creating port number in a test
return strconv.Itoa((rand.Intn(portRange) + portMin))
}
/*//////// Test Main ////////*/
func TestMain(m *testing.M) {
os.Exit(m.Run())
}

185
members_api.go Normal file
View File

@ -0,0 +1,185 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
// 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(ctx context.Context, groupID ID, authToken string, members ...*Member) (string, error) {
URL := fmt.Sprintf(c.endpointBase+addMembersEndpoint, groupID)
var data = struct {
Members []*Member `json:"members"`
}{
members,
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return "", err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return "", err
}
var resp struct {
ResultsID string `json:"results_id"`
}
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
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(ctx context.Context, groupID ID, resultID string, authToken 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.doWithAuthToken(ctx, httpReq, &resp, authToken)
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(ctx context.Context, groupID, membershipID ID, authToken string) error {
URL := fmt.Sprintf(c.endpointBase+removeMemberEndpoint, groupID, membershipID)
httpReq, err := http.NewRequest("POST", URL, nil)
if err != nil {
return err
}
return c.doWithAuthToken(ctx, httpReq, nil, authToken)
}
/*/// Update ///*/
/*
UpdateMember -
Update your nickname in a group. The nickname must be
between 1 and 50 characters.
*/
func (c *Client) UpdateMember(ctx context.Context, groupID ID, nickname string, authToken string) (*Member, error) {
URL := fmt.Sprintf(c.endpointBase+updateMemberEndpoint, groupID)
type Nickname struct {
Nickname string `json:"nickname"`
}
var data = struct {
Membership Nickname `json:"membership"`
}{
Nickname{nickname},
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}
var resp Member
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return &resp, nil
}

140
members_api_test.go Normal file
View File

@ -0,0 +1,140 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(
context.Background(),
"1",
&Member{Nickname: "test"},
)
s.Require().NoError(err)
}
func (s *MembersAPISuite) TestMembersResults() {
_, err := s.client.AddMembersResults(context.Background(), "1", "123")
s.Require().NoError(err)
}
func (s *MembersAPISuite) TestMembersRemove() {
err := s.client.RemoveMember(context.Background(), "1", "123")
s.Require().NoError(err)
}
func (s *MembersAPISuite) TestMembersUpdate() {
_, err := s.client.UpdateMember(context.Background(), "1", "nickname")
s.Require().NoError(err)
}
func TestMembersAPISuite(t *testing.T) {
suite.Run(t, new(MembersAPISuite))
}
func membersTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}

157
messages_api.go Normal file
View File

@ -0,0 +1,157 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"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
)
// IndexMessagesQuery 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)
}
// IndexMessagesResponse 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 - Retrieves 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.
*/
func (c *Client) IndexMessages(ctx context.Context, groupID ID, req *IndexMessagesQuery, authToken string) (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.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return IndexMessagesResponse{}, err
}
return resp, nil
}
/*
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.
*/
func (c *Client) CreateMessage(ctx context.Context, groupID ID, m *Message, authToken string) (*Message, error) {
URL := fmt.Sprintf(c.endpointBase+createMessagesEndpoint, groupID)
m.SourceGUID = uuid.New().String()
var data = struct {
Message *Message `json:"message"`
}{
m,
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}
var resp struct {
*Message `json:"message"`
}
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return resp.Message, nil
}

197
messages_api_test.go Normal file
View File

@ -0,0 +1,197 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(
context.Background(),
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(
context.Background(),
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))
}
// nolint // not duplicate code
func messagesTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}

196
real_time.go Normal file
View File

@ -0,0 +1,196 @@
package groupme
import (
"context"
"errors"
"gitea.watsonlabs.net/watsonb8/fayec"
"gitea.watsonlabs.net/watsonb8/fayec/message"
"gitea.watsonlabs.net/watsonb8/fayec/subscription"
"log"
"strings"
"sync"
"time"
)
const (
PushServer = "wss://push.groupme.com/faye"
userChannel = "/user/"
groupChannel = "/group/"
dmChannel = "/direct_message/"
)
var (
ErrHandlerNotFound = errors.New("Handler not found")
ErrListenerNotStarted = errors.New("GroupMe listener not started")
)
var concur = sync.Mutex{}
type HandlerAll interface {
Handler
//of self
HandlerText
HandlerLike
HandlerMembership
//of group
HandleGroupTopic
HandleGroupAvatar
HandleGroupName
HandleGroupLikeIcon
//of group members
HandleMemberNewNickname
HandleMemberNewAvatar
HandleMembers
}
type Handler interface {
HandleError(error)
}
type HandlerText interface {
HandleTextMessage(Message)
}
type HandlerLike interface {
HandleLike(Message)
}
type HandlerMembership interface {
HandleJoin(ID)
}
// Group Handlers
type HandleGroupTopic interface {
HandleGroupTopic(group ID, newTopic string)
}
type HandleGroupName interface {
HandleGroupName(group ID, newName string)
}
type HandleGroupAvatar interface {
HandleGroupAvatar(group ID, newAvatar string)
}
type HandleGroupLikeIcon interface {
HandleLikeIcon(group ID, PackID, PackIndex int, Type string)
}
// Group member handlers
type HandleMemberNewNickname interface {
HandleNewNickname(group ID, user ID, newName string)
}
type HandleMemberNewAvatar interface {
HandleNewAvatarInGroup(group ID, user ID, avatarURL string)
}
type HandleMembers interface {
//HandleNewMembers returns only partial member with id and nickname; added is false if removing
HandleMembers(group ID, members []Member, added bool)
}
// PushSubscription manages real time subscription
type PushSubscription struct {
channel chan message.Data
client *fayec.Client
handlers map[string][]Handler // key == token
LastConnected int64
}
// NewPushSubscription creates and returns a push subscription object
func NewPushSubscription(context context.Context) PushSubscription {
r := PushSubscription{
channel: make(chan message.Data),
handlers: make(map[string][]Handler),
}
return r
}
func (r *PushSubscription) AddHandler(h Handler, authToken string) {
if r.handlers[authToken] == nil {
r.handlers[authToken] = []Handler{h}
} else {
r.handlers[authToken] = append(r.handlers[authToken], h)
}
//r.handlers = append(r.handlers, h)
}
// AddFullHandler is the same as AddHandler except it ensures the interface implements everything
func (r *PushSubscription) AddFullHandler(h HandlerAll, authToken string) {
if r.handlers[authToken] == nil {
r.handlers[authToken] = []Handler{h}
} else {
r.handlers[authToken] = append(r.handlers[authToken], h)
}
//r.handlers = append(r.handlers, h)
}
var RealTimeHandlers map[string]func(r *PushSubscription, channel string, authToken string, data ...interface{})
var RealTimeSystemHandlers map[string]func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte)
// Listen connects to GroupMe. Runs in Goroutine.
func (r *PushSubscription) Connect(context context.Context) error {
c, err := fayec.NewClient(PushServer)
if err != nil {
return err
}
r.client = c
return nil
}
// SubscribeToUser to users
func (r *PushSubscription) SubscribeToUser(context context.Context, id ID, authToken string) error {
return r.subscribeWithPrefix(userChannel, context, id, authToken)
}
// SubscribeToGroup to groups for typing notification
func (r *PushSubscription) SubscribeToGroup(context context.Context, id ID, authToken string) error {
return r.subscribeWithPrefix(groupChannel, context, id, authToken)
}
// SubscribeToDM to users
func (r *PushSubscription) SubscribeToDM(context context.Context, id ID, authToken string) error {
id = ID(strings.Replace(id.String(), "+", "_", 1))
return r.subscribeWithPrefix(dmChannel, context, id, authToken)
}
func (r *PushSubscription) subscribeWithPrefix(prefix string, context context.Context, groupID ID, authToken string) error {
concur.Lock()
defer concur.Unlock()
if r.client == nil {
return ErrListenerNotStarted
}
var sub *subscription.Subscription
sub, err := r.client.Subscribe(prefix+groupID.String(), authToken)
if err != nil {
panic(err)
}
err = sub.OnMessage(func(channel string, data message.Data) {
r.LastConnected = time.Now().Unix()
dataMap := data.(map[string]interface{})
content := dataMap["subject"]
contentType := dataMap["type"].(string)
handler, ok := RealTimeHandlers[contentType]
if !ok {
if contentType == "ping" ||
len(contentType) == 0 ||
content == "" {
return
}
log.Println("Unable to handle GroupMe message type", contentType)
} else {
handler(r, channel, authToken, content)
}
})
return nil
}
// Connected check if connected
func (r *PushSubscription) Connected() bool {
return r.LastConnected+30 >= time.Now().Unix()
}

266
real_time_handler.go Normal file
View File

@ -0,0 +1,266 @@
package groupme
import (
"encoding/json"
"fmt"
"log"
"strconv"
)
func init() {
RealTimeHandlers = make(map[string]func(r *PushSubscription, channel string, authToken string, data ...interface{}))
//Base Handlers on user channel
RealTimeHandlers["direct_message.create"] = func(r *PushSubscription, channel string, authToken string, data ...interface{}) {
b, _ := json.Marshal(data[0])
out := Message{}
_ = json.Unmarshal(b, &out)
//maybe something with API versioning
out.ConversationID = out.ChatID
if out.UserID.String() == "system" {
event := struct {
Event struct {
Kind string `json:"type"`
Data interface{}
}
}{}
err := json.Unmarshal(b, &event)
if err != nil {
fmt.Println(err)
}
rawData, _ := json.Marshal(event.Event.Data)
handler, ok := RealTimeSystemHandlers[event.Event.Kind]
if !ok {
log.Println("Unable to handle system message of type", event.Event.Kind)
return
}
id := out.GroupID
if len(id) == 0 {
id = out.ConversationID
}
handler(r, channel, id, authToken, rawData)
return
}
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandlerText); ok {
h.HandleTextMessage(out)
}
}
}
RealTimeHandlers["line.create"] = RealTimeHandlers["direct_message.create"]
RealTimeHandlers["like.create"] = func(r *PushSubscription, channel string, authToken string, data ...interface{}) { //should be an associated chatEvent
}
RealTimeHandlers["membership.create"] = func(r *PushSubscription, channel string, authToken string, data ...interface{}) {
c, _ := data[0].(map[string]interface{})
id, _ := c["id"].(string)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandlerMembership); ok {
h.HandleJoin(ID(id))
}
}
}
//following are for each chat
RealTimeHandlers["favorite"] = func(r *PushSubscription, channel string, authToken string, data ...interface{}) {
c, ok := data[0].(map[string]interface{})
if !ok {
fmt.Println(data, "err")
return
}
e, ok := c["line"]
if !ok {
fmt.Println(data, "err")
return
}
d, _ := json.Marshal(e)
msg := Message{}
_ = json.Unmarshal(d, &msg)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandlerLike); ok {
h.HandleLike(msg)
}
}
}
//following are for messages from system (administrative/settings changes)
RealTimeSystemHandlers = make(map[string]func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte))
RealTimeSystemHandlers["membership.nickname_changed"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
thing := struct {
Name string
User struct {
ID int
}
}{}
_ = json.Unmarshal(rawData, &thing)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleMemberNewNickname); ok {
h.HandleNewNickname(id, ID(strconv.Itoa(thing.User.ID)), thing.Name)
}
}
}
RealTimeSystemHandlers["membership.avatar_changed"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
content := struct {
AvatarURL string `json:"avatar_url"`
User struct {
ID int
}
}{}
_ = json.Unmarshal(rawData, &content)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleMemberNewAvatar); ok {
h.HandleNewAvatarInGroup(id, ID(strconv.Itoa(content.User.ID)), content.AvatarURL)
}
}
}
RealTimeSystemHandlers["membership.announce.added"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
data := struct {
Added []Member `json:"added_users"`
}{}
_ = json.Unmarshal(rawData, &data)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleMembers); ok {
h.HandleMembers(id, data.Added, true)
}
}
}
RealTimeSystemHandlers["membership.notifications.removed"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
data := struct {
Added Member `json:"removed_user"`
}{}
_ = json.Unmarshal(rawData, &data)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleMembers); ok {
h.HandleMembers(id, []Member{data.Added}, false)
}
}
}
RealTimeSystemHandlers["membership.name_change"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
data := struct {
Name string
}{}
_ = json.Unmarshal(rawData, &data)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleGroupName); ok {
h.HandleGroupName(id, data.Name)
}
}
}
RealTimeSystemHandlers["group.name_change"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
data := struct {
Name string
}{}
_ = json.Unmarshal(rawData, &data)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleGroupName); ok {
h.HandleGroupName(id, data.Name)
}
}
}
RealTimeSystemHandlers["group.topic_change"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
data := struct {
Topic string
}{}
_ = json.Unmarshal(rawData, &data)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleGroupTopic); ok {
h.HandleGroupTopic(id, data.Topic)
}
}
}
RealTimeSystemHandlers["group.avatar_change"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
data := struct {
AvatarURL string `json:"avatar_url"`
}{}
_ = json.Unmarshal(rawData, &data)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleGroupAvatar); ok {
h.HandleGroupAvatar(id, data.AvatarURL)
}
}
}
RealTimeSystemHandlers["group.like_icon_set"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
data := struct {
LikeIcon struct {
PackID int `json:"pack_id"`
PackIndex int `json:"pack_index"`
Type string
} `json:"like_icon"`
}{}
_ = json.Unmarshal(rawData, &data)
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleGroupLikeIcon); ok {
h.HandleLikeIcon(id, data.LikeIcon.PackID, data.LikeIcon.PackIndex, data.LikeIcon.Type)
}
}
}
RealTimeSystemHandlers["group.like_icon_removed"] = func(r *PushSubscription, channel string, id ID, authToken string, rawData []byte) {
handlers := r.handlers[authToken]
for _, h := range handlers {
if h, ok := h.(HandleGroupLikeIcon); ok {
h.HandleLikeIcon(id, 0, 0, "")
}
}
}
}

86
sms_mode_api.go Normal file
View File

@ -0,0 +1,86 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
// 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(ctx context.Context, duration int, registrationID *ID, authToken string) error {
URL := fmt.Sprintf(c.endpointBase + createSMSModeEndpoint)
var data = struct {
Duration int `json:"duration"`
RegistrationID *ID `json:"registration_id,omitempty"`
}{
duration,
registrationID,
}
jsonBytes, err := json.Marshal(&data)
if err != nil {
return err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return err
}
err = c.doWithAuthToken(ctx, httpReq, nil, authToken)
if err != nil {
return err
}
return nil
}
// Delete
/*
DeleteSMSMode -
Disables SMS mode
*/
func (c *Client) DeleteSMSMode(ctx context.Context, authToken string) error {
url := fmt.Sprintf(c.endpointBase + deleteSMSModeEndpoint)
httpReq, err := http.NewRequest("POST", url, nil)
if err != nil {
return err
}
return c.doWithAuthToken(ctx, httpReq, nil, authToken)
}

53
sms_mode_api_test.go Normal file
View File

@ -0,0 +1,53 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(context.Background(), 10, nil))
}
func (s *SMSModeAPISuite) TestSMSModeDelete() {
s.Assert().NoError(s.client.DeleteSMSMode(context.Background()))
}
func TestSMSModeAPISuite(t *testing.T) {
suite.Run(t, new(SMSModeAPISuite))
}
// nolint // not duplicate code
func smsModeTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}

93
users_api.go Normal file
View File

@ -0,0 +1,93 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
// 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(ctx context.Context, authToken string) (*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.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return &resp, nil
}
// UserSettings are the settings for a GroupMe user
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(ctx context.Context, us UserSettings, authToken string) (*User, error) {
URL := fmt.Sprintf(c.endpointBase + updateMyUserEndpoint)
jsonBytes, err := json.Marshal(&us)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonBytes))
if err != nil {
return nil, err
}
var resp User
err = c.doWithAuthToken(ctx, httpReq, &resp, authToken)
if err != nil {
return nil, err
}
return &resp, nil
}

91
users_api_test.go Normal file
View File

@ -0,0 +1,91 @@
// Package groupme defines a client capable of executing API commands for the GroupMe chat service
package groupme
import (
"context"
"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(context.Background())
s.Require().NoError(err)
s.Assert().NotZero(user)
}
func (s *UsersAPISuite) TestUsersUpdate() {
user, err := s.client.UpdateMyUser(context.Background(), UserSettings{})
s.Require().NoError(err)
s.Assert().NotZero(user)
}
func TestUsersAPISuite(t *testing.T) {
suite.Run(t, new(UsersAPISuite))
}
// nolint // not duplicate code
func usersTestRouter() *mux.Router {
router := mux.NewRouter().Queries("token", "").Subrouter()
// 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
}