Skip to content

Commit

Permalink
Add implementation for heartbeat and message thread API's
Browse files Browse the repository at this point in the history
  • Loading branch information
AchoArnold committed Jul 25, 2023
1 parent 0f0403a commit 5f05f17
Show file tree
Hide file tree
Showing 16 changed files with 324 additions and 83 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
repos:
- repo: https://github.com/tekwizely/pre-commit-golang
rev: master
rev: v1.0.0-rc.1
hooks:
- id: go-fumpt
- id: go-mod-tidy
- id: go-lint
- id: go-imports
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
rev: v4.4.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ import "github.com/NdoleStudio/httpsms-go"

## Implemented

- [Messages](#messages)
- `POST /v1/messages/send`: Send a new SMS Message
- [x] **[Messages](#messages)**
- [x] `POST /v1/messages/send`: Send a new SMS
- [x] `GET /v1/messages`: Get list of messages which are exchanged between 2 phone numbers.
- [x] **Heartbeats**
- [x] `GET /v1/heartbeats`: Get the heartbeats of an Android Phone
- [x] **Message Threads**
- [x] `GET /v1/message-threads`: Get the message threads of a phone number


## Usage

Expand Down
13 changes: 8 additions & 5 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
)

Expand All @@ -22,7 +21,9 @@ type Client struct {
baseURL string
apiKey string

Messages *messagesService
MessageThreads *MessageThreadService
Heartbeats *HeartbeatService
Messages *MessageService
}

// New creates and returns a new campay.Client from a slice of campay.ClientOption.
Expand All @@ -40,7 +41,9 @@ func New(options ...Option) *Client {
}

client.common.client = client
client.Messages = (*messagesService)(&client.common)
client.Messages = (*MessageService)(&client.common)
client.Heartbeats = (*HeartbeatService)(&client.common)
client.MessageThreads = (*MessageThreadService)(&client.common)
return client
}

Expand Down Expand Up @@ -89,7 +92,7 @@ func (client *Client) do(req *http.Request) (*Response, error) {
return resp, err
}

_, err = io.Copy(ioutil.Discard, httpResponse.Body)
_, err = io.Copy(io.Discard, httpResponse.Body)
if err != nil {
return resp, err
}
Expand All @@ -106,7 +109,7 @@ func (client *Client) newResponse(httpResponse *http.Response) (*Response, error
resp := new(Response)
resp.HTTPResponse = httpResponse

buf, err := ioutil.ReadAll(resp.HTTPResponse.Body)
buf, err := io.ReadAll(resp.HTTPResponse.Body)
if err != nil {
return nil, err
}
Expand Down
7 changes: 7 additions & 0 deletions contracts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package httpsms

type ApiResponse[T any] struct {
Data T `json:"data"`
Message string `json:"message"`
Status string `json:"status"`
}
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
module github.com/NdoleStudio/httpsms-go

go 1.17
go 1.18

require github.com/stretchr/testify v1.7.0
require (
github.com/google/uuid v1.3.0
github.com/stretchr/testify v1.7.0
)

require (
github.com/davecgh/go-spew v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
26 changes: 26 additions & 0 deletions heartbeat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package httpsms

import (
"time"

"github.com/google/uuid"
)

// Heartbeat represents is a pulse from an active phone
type Heartbeat struct {
ID uuid.UUID `json:"id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
Owner string `json:"owner" gorm:"index:idx_heartbeats_owner_timestamp" example:"+18005550199"`
UserID string `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
Timestamp time.Time `json:"timestamp" example:"2022-06-05T14:26:01.520828+03:00"`
}

// HeartbeatIndexParams is the payload for fetching entities.Heartbeat of a phone number
type HeartbeatIndexParams struct {
Skip int `json:"skip"`
Owner string `json:"owner"`
Query *string `json:"query"`
Limit int `json:"limit"`
}

// HeartbeatsResponse is the response gotten with a message content
type HeartbeatsResponse ApiResponse[[]Heartbeat]
44 changes: 44 additions & 0 deletions heartbeat_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package httpsms

import (
"context"
"encoding/json"
"net/http"
"strconv"
)

// HeartbeatService is the API client for the `/heartbeats` endpoint
type HeartbeatService service

// Index returns a list of heartbeats from an android phone. It will be sorted by timestamp in descending order.
//
// API Docs: https://api.httpsms.com/index.html#/Heartbeats/get_heartbeats
func (service *HeartbeatService) Index(ctx context.Context, params *HeartbeatIndexParams) (*HeartbeatsResponse, *Response, error) {
request, err := service.client.newRequest(ctx, http.MethodGet, "/v1/heartbeats", nil)
if err != nil {
return nil, nil, err
}

q := request.URL.Query()
q.Add("skip", strconv.Itoa(params.Skip))
q.Add("owner", params.Owner)
q.Add("limit", strconv.Itoa(params.Limit))

if params.Query != nil {
q.Add("query", *params.Query)
}

request.URL.RawQuery = q.Encode()

response, err := service.client.do(request)
if err != nil {
return nil, response, err
}

heartbeats := new(HeartbeatsResponse)
if err = json.Unmarshal(*response.Body, heartbeats); err != nil {
return nil, response, err
}

return heartbeats, response, nil
}
15 changes: 12 additions & 3 deletions internal/stubs/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,35 @@ func MessagesSendResponse() []byte {
return []byte(`
{
"data": {
"can_be_polled": false,
"contact": "+18005550100",
"content": "This is a sample text message",
"created_at": "2022-06-05T14:26:02.302718+03:00",
"failure_reason": "",
"delivered_at": "2022-06-05T14:26:09.527976+03:00",
"expired_at": "2022-06-05T14:26:09.527976+03:00",
"failed_at": "2022-06-05T14:26:09.527976+03:00",
"failure_reason": null,
"id": "32343a19-da5e-4b1b-a767-3298a73703cb",
"last_attempted_at": "2022-06-05T14:26:09.527976+03:00",
"max_send_attempts": 1,
"order_timestamp": "2022-06-05T14:26:09.527976+03:00",
"owner": "+18005550199",
"received_at": "2022-06-05T14:26:09.527976+03:00",
"request_id": "153554b5-ae44-44a0-8f4f-7bbac5657ad4",
"request_received_at": "2022-06-05T14:26:01.520828+03:00",
"scheduled_at": "2022-06-05T14:26:09.527976+03:00",
"send_attempt_count": 0,
"send_time": 133414,
"sent_at": "2022-06-05T14:26:09.527976+03:00",
"sim": "SIM1",
"status": "pending",
"type": "mobile-terminated",
"updated_at": "2022-06-05T14:26:10.303278+03:00",
"user_id": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC"
},
"message": "message created successfully",
"message": "item created successfully",
"status": "success"
}
}
`)
}

Expand Down
65 changes: 65 additions & 0 deletions message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package httpsms

import (
"time"

"github.com/google/uuid"
)

// MessageSendParams is the request payload for sending a message
type MessageSendParams struct {
Content string `json:"content"`
From string `json:"from"`
RequestID string `json:"request_id,omitempty"`
To string `json:"to"`
}

// MessageIndexParams is the payload fetching entities.Message sent between 2 numbers
type MessageIndexParams struct {
Skip int `json:"skip"`
Contact string `json:"contact"`
Owner string `json:"owner"`
Query *string `json:"query"`
Limit int `json:"limit"`
}

// MessageResponse is the response gotten with a message content
type MessageResponse ApiResponse[Message]

// MessagesResponse is the response with multiple messages
type MessagesResponse ApiResponse[[]Message]

// Message represents and incoming or outgoing SMS message
type Message struct {
ID uuid.UUID `json:"id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
RequestID *string `json:"request_id" example:"153554b5-ae44-44a0-8f4f-7bbac5657ad4"`
Owner string `json:"owner" example:"+18005550199"`
UserID string `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
Contact string `json:"contact" example:"+18005550100"`
Content string `json:"content" example:"This is a sample text message"`
Type string `json:"type" example:"mobile-terminated"`
Status string `json:"status" example:"pending"`
// SIM is the SIM card to use to send the message
// * SMS1: use the SIM card in slot 1
// * SMS2: use the SIM card in slot 2
SIM string `json:"sim" example:"SIM1"`

// SendDuration is the number of nanoseconds from when the request was received until when the mobile phone send the message
SendDuration *int64 `json:"send_time" example:"133414"`

RequestReceivedAt time.Time `json:"request_received_at" example:"2022-06-05T14:26:01.520828+03:00"`
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
OrderTimestamp time.Time `json:"order_timestamp" gorm:"index:idx_messages_order_timestamp" example:"2022-06-05T14:26:09.527976+03:00"`
LastAttemptedAt *time.Time `json:"last_attempted_at" example:"2022-06-05T14:26:09.527976+03:00"`
NotificationScheduledAt *time.Time `json:"scheduled_at" example:"2022-06-05T14:26:09.527976+03:00"`
SentAt *time.Time `json:"sent_at" example:"2022-06-05T14:26:09.527976+03:00"`
DeliveredAt *time.Time `json:"delivered_at" example:"2022-06-05T14:26:09.527976+03:00"`
ExpiredAt *time.Time `json:"expired_at" example:"2022-06-05T14:26:09.527976+03:00"`
FailedAt *time.Time `json:"failed_at" example:"2022-06-05T14:26:09.527976+03:00"`
CanBePolled bool `json:"can_be_polled" example:"false"`
SendAttemptCount uint `json:"send_attempt_count" example:"0"`
MaxSendAttempts uint `json:"max_send_attempts" example:"1"`
ReceivedAt *time.Time `json:"received_at" example:"2022-06-05T14:26:09.527976+03:00"`
FailureReason *string `json:"failure_reason" example:"UNKNOWN"`
}
67 changes: 67 additions & 0 deletions message_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package httpsms

import (
"context"
"encoding/json"
"net/http"
"strconv"
)

// MessageService is the API client for the `/` endpoint
type MessageService service

// Send adds a new SMS message to be sent by the android phone
//
// API Docs: https://api.httpsms.com/index.html#/Messages/post_messages_send
func (service *MessageService) Send(ctx context.Context, params *MessageSendParams) (*MessageResponse, *Response, error) {
request, err := service.client.newRequest(ctx, http.MethodPost, "/v1/messages/send", params)
if err != nil {
return nil, nil, err
}

response, err := service.client.do(request)
if err != nil {
return nil, response, err
}

message := new(MessageResponse)
if err = json.Unmarshal(*response.Body, message); err != nil {
return nil, response, err
}

return message, response, nil
}

// Index returns a list of messages which are sent between 2 phone numbers. It will be sorted by timestamp in descending order.
//
// API Docs: https://api.httpsms.com/index.html#/Messages/get_messages
func (service *MessageService) Index(ctx context.Context, params *MessageIndexParams) (*MessagesResponse, *Response, error) {
request, err := service.client.newRequest(ctx, http.MethodGet, "/v1/messages", nil)
if err != nil {
return nil, nil, err
}

q := request.URL.Query()
q.Add("skip", strconv.Itoa(params.Skip))
q.Add("owner", params.Owner)
q.Add("contact", params.Contact)
q.Add("limit", strconv.Itoa(params.Limit))

if params.Query != nil {
q.Add("query", *params.Query)
}

request.URL.RawQuery = q.Encode()

response, err := service.client.do(request)
if err != nil {
return nil, response, err
}

messages := new(MessagesResponse)
if err = json.Unmarshal(*response.Body, messages); err != nil {
return nil, response, err
}

return messages, response, nil
}
File renamed without changes.
34 changes: 34 additions & 0 deletions message_thread.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package httpsms

import (
"time"

"github.com/google/uuid"
)

// MessageThreadIndexParams is the payload fetching entities.MessageThread sent between 2 numbers
type MessageThreadIndexParams struct {
IsArchived bool `json:"is_archived"`
Skip int `json:"skip"`
Query *string `json:"query"`
Limit int `json:"limit"`
Owner string `json:"owner"`
}

// MessageThread represents a message thread between 2 phone numbers
type MessageThread struct {
ID uuid.UUID `json:"id" example:"32343a19-da5e-4b1b-a767-3298a73703ca"`
Owner string `json:"owner" example:"+18005550199"`
Contact string `json:"contact" example:"+18005550100"`
IsArchived bool `json:"is_archived" example:"false"`
UserID string `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
Color string `json:"color" example:"indigo"`
LastMessageContent string `json:"last_message_content" example:"This is a sample message content"`
LastMessageID uuid.UUID `json:"last_message_id" example:"32343a19-da5e-4b1b-a767-3298a73703ca"`
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:09.527976+03:00"`
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:09.527976+03:00"`
OrderTimestamp time.Time `json:"order_timestamp" example:"2022-06-05T14:26:09.527976+03:00"`
}

// MessageThreadsResponse is the response gotten with a message content
type MessageThreadsResponse ApiResponse[[]MessageThread]
Loading

0 comments on commit 5f05f17

Please sign in to comment.