Skip to content

Commit 34dfbfb

Browse files
committed
Add webhook management API's
1 parent ffd1e89 commit 34dfbfb

22 files changed

Lines changed: 1016 additions & 104 deletions

api/cmd/migration/main.go

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,14 @@
11
package main
22

33
import (
4-
"context"
5-
"fmt"
64
"log"
75

8-
"github.com/NdoleStudio/httpsms/pkg/di"
9-
"github.com/NdoleStudio/httpsms/pkg/events"
10-
"github.com/NdoleStudio/httpsms/pkg/repositories"
11-
"github.com/NdoleStudio/httpsms/pkg/telemetry"
12-
"github.com/cheggaaa/pb/v3"
13-
cloudevents "github.com/cloudevents/sdk-go/v2"
14-
"github.com/google/uuid"
156
"github.com/joho/godotenv"
16-
"github.com/palantir/stacktrace"
177
)
188

199
func main() {
2010
err := godotenv.Load("../../.env")
2111
if err != nil {
2212
log.Fatal("Error loading .env file")
2313
}
24-
25-
container := di.NewContainer("http-sms")
26-
eventRepo := container.EventRepository()
27-
logger := container.Logger()
28-
29-
cloudEvents, err := eventRepo.FetchAll(context.Background())
30-
if err != nil {
31-
logger.Fatal(stacktrace.Propagate(err, "cannot fetch all cloudEvents"))
32-
}
33-
logger.Info(fmt.Sprintf("fetched %d cloudevents", len(*cloudEvents)))
34-
35-
// create and start new bar
36-
bar := pb.StartNew(len(*cloudEvents))
37-
for _, event := range *cloudEvents {
38-
if event.Type() == events.EventTypeMessageAPISent {
39-
migrateSentEvent(logger, eventRepo, event)
40-
}
41-
if event.Type() == events.EventTypeMessagePhoneReceived {
42-
migrateReceivedEvent(logger, eventRepo, event)
43-
}
44-
bar.Increment()
45-
}
46-
47-
bar.Finish()
48-
}
49-
50-
func migrateReceivedEvent(logger telemetry.Logger, repository repositories.EventRepository, event cloudevents.Event) {
51-
var payload events.MessagePhoneReceivedPayloadV1
52-
if err := event.DataAs(&payload); err != nil {
53-
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
54-
logger.Fatal(stacktrace.Propagate(err, msg))
55-
}
56-
57-
if payload.MessageID != uuid.Nil {
58-
payloadV2 := events.MessagePhoneReceivedPayload{
59-
MessageID: payload.MessageID,
60-
UserID: payload.UserID,
61-
Owner: payload.Owner,
62-
Contact: payload.Contact,
63-
Timestamp: payload.Timestamp,
64-
Content: payload.Content,
65-
}
66-
67-
if err := event.SetData(cloudevents.ApplicationJSON, payloadV2); err != nil {
68-
logger.Fatal(stacktrace.Propagate(err, "cannot set payload v2 data"))
69-
}
70-
71-
if err := repository.Save(context.Background(), event); err != nil {
72-
logger.Fatal(stacktrace.Propagate(err, "cannot save event after updating"))
73-
}
74-
}
75-
}
76-
77-
func migrateSentEvent(logger telemetry.Logger, repository repositories.EventRepository, event cloudevents.Event) {
78-
var payload events.MessageAPISentPayloadV1
79-
if err := event.DataAs(&payload); err != nil {
80-
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
81-
logger.Fatal(stacktrace.Propagate(err, msg))
82-
}
83-
84-
if payload.MessageID != uuid.Nil {
85-
payloadV2 := events.MessageAPISentPayload{
86-
MessageID: payload.MessageID,
87-
UserID: payload.UserID,
88-
Owner: payload.Owner,
89-
MaxSendAttempts: 2,
90-
Contact: payload.Contact,
91-
RequestReceivedAt: payload.RequestReceivedAt,
92-
Content: payload.Content,
93-
}
94-
95-
if err := event.SetData(cloudevents.ApplicationJSON, payloadV2); err != nil {
96-
logger.Fatal(stacktrace.Propagate(err, "cannot set payloadv2 data"))
97-
}
98-
99-
if err := repository.Save(context.Background(), event); err != nil {
100-
logger.Fatal(stacktrace.Propagate(err, "cannot save event after updating"))
101-
}
102-
}
10314
}

api/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ require (
1414
github.com/davecgh/go-spew v1.1.1
1515
github.com/gofiber/fiber/v2 v2.34.0
1616
github.com/gofiber/swagger v0.0.1
17+
github.com/golang-jwt/jwt v3.2.2+incompatible
1718
github.com/google/uuid v1.3.0
19+
github.com/hashicorp/go-retryablehttp v0.7.2
1820
github.com/hirosassa/zerodriver v0.1.2
1921
github.com/jinzhu/now v1.1.4
2022
github.com/joho/godotenv v1.4.0
2123
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
24+
github.com/lib/pq v1.10.6
2225
github.com/matcornic/hermes/v2 v2.1.0
2326
github.com/nyaruka/phonenumbers v1.1.0
2427
github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177
@@ -72,6 +75,7 @@ require (
7275
github.com/googleapis/go-type-adapters v1.0.0 // indirect
7376
github.com/gorilla/css v1.0.0 // indirect
7477
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.1 // indirect
78+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
7579
github.com/huandu/xstrings v1.2.0 // indirect
7680
github.com/imdario/mergo v0.3.6 // indirect
7781
github.com/jackc/chunkreader/v2 v2.0.1 // indirect

api/go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
193193
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
194194
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
195195
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
196+
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
197+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
196198
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
197199
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
198200
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -299,6 +301,12 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb
299301
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
300302
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.1 h1:/sDbPb60SusIXjiJGYLUoS/rAQurQmvGWmwn2bBPM9c=
301303
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.1/go.mod h1:G+WkljZi4mflcqVxYSgvt8MNctRQHjEH8ubKtt1Ka3w=
304+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
305+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
306+
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
307+
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
308+
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
309+
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
302310
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
303311
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
304312
github.com/hirosassa/zerodriver v0.1.2 h1:tOBstLRxuUs8bol1Ho+r3xTs5Y8DKv3dqA7DnA+pjo0=

api/pkg/di/container.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package di
33
import (
44
"context"
55
"fmt"
6+
"net/http"
67
"os"
78
"strconv"
89
"time"
910

1011
lemonsqueezy "github.com/NdoleStudio/lemonsqueezy-go"
12+
"github.com/hashicorp/go-retryablehttp"
1113

1214
"github.com/jinzhu/now"
1315

@@ -96,6 +98,9 @@ func NewContainer(projectID string) (container *Container) {
9698
container.RegisterBillingRoutes()
9799
container.RegisterBillingListeners()
98100

101+
container.RegisterWebhookRoutes()
102+
container.RegisterWebhookListeners()
103+
99104
container.RegisterLemonsqueezyRoutes()
100105

101106
// this has to be last since it registers the /* route
@@ -340,6 +345,17 @@ func (container *Container) BillingHandler() (h *handlers.BillingHandler) {
340345
)
341346
}
342347

348+
// WebhookHandler creates a new instance of handlers.WebhookHandler
349+
func (container *Container) WebhookHandler() (h *handlers.WebhookHandler) {
350+
container.logger.Debug(fmt.Sprintf("creating %T", h))
351+
return handlers.NewWebhookHandler(
352+
container.Logger(),
353+
container.Tracer(),
354+
container.WebhookService(),
355+
container.WebhookHandlerValidator(),
356+
)
357+
}
358+
343359
// HeartbeatHandlerValidator creates a new instance of validators.HeartbeatHandlerValidator
344360
func (container *Container) HeartbeatHandlerValidator() (validator *validators.HeartbeatHandlerValidator) {
345361
container.logger.Debug(fmt.Sprintf("creating %T", validator))
@@ -358,6 +374,15 @@ func (container *Container) BillingHandlerValidator() (validator *validators.Bil
358374
)
359375
}
360376

377+
// WebhookHandlerValidator creates a new instance of validators.WebhookHandlerValidator
378+
func (container *Container) WebhookHandlerValidator() (validator *validators.WebhookHandlerValidator) {
379+
container.logger.Debug(fmt.Sprintf("creating %T", validator))
380+
return validators.NewWebhookHandlerValidator(
381+
container.Logger(),
382+
container.Tracer(),
383+
)
384+
}
385+
361386
// MessageThreadHandler creates a new instance of handlers.MessageThreadHandler
362387
func (container *Container) MessageThreadHandler() (h *handlers.MessageThreadHandler) {
363388
container.logger.Debug(fmt.Sprintf("creating %T", h))
@@ -445,6 +470,16 @@ func (container *Container) BillingUsageRepository() (repository repositories.Bi
445470
)
446471
}
447472

473+
// WebhookRepository creates a new instance of repositories.WebhookRepository
474+
func (container *Container) WebhookRepository() (repository repositories.WebhookRepository) {
475+
container.logger.Debug("creating GORM repositories.WebhookRepository")
476+
return repositories.NewGormWebhookRepository(
477+
container.Logger(),
478+
container.Tracer(),
479+
container.DB(),
480+
)
481+
}
482+
448483
// PhoneNotificationRepository creates a new instance of repositories.PhoneNotificationRepository
449484
func (container *Container) PhoneNotificationRepository() (repository repositories.PhoneNotificationRepository) {
450485
container.logger.Debug("creating GORM repositories.PhoneNotificationRepository")
@@ -517,6 +552,44 @@ func (container *Container) BillingService() (service *services.BillingService)
517552
)
518553
}
519554

555+
// WebhookService creates a new instance of services.WebhookService
556+
func (container *Container) WebhookService() (service *services.WebhookService) {
557+
container.logger.Debug(fmt.Sprintf("creating %T", service))
558+
return services.NewWebhookService(
559+
container.Logger(),
560+
container.Tracer(),
561+
container.HTTPClient("webhook"),
562+
container.WebhookRepository(),
563+
)
564+
}
565+
566+
// HTTPClient creates a new http.Client
567+
func (container *Container) HTTPClient(name string) *http.Client {
568+
container.logger.Debug(fmt.Sprintf("creating %s %T", name, http.DefaultClient))
569+
return &http.Client{
570+
Timeout: 60 * time.Second,
571+
Transport: container.RetryHTTPRoundTripper(),
572+
}
573+
}
574+
575+
//func (container *Container) HTTPRoundTripper(name string) http.RoundTripper {
576+
// container.logger.Debug(fmt.Sprintf("Debug: initializing %s %T", name, http.DefaultTransport))
577+
// return otelroundtripper.New(
578+
// otelroundtripper.WithName(name),
579+
// otelroundtripper.WithParent(container.RetryHTTPRoundTripper()),
580+
// otelroundtripper.WithMeter(global.Meter(os.Getenv("NAMESPACE"))),
581+
// otelroundtripper.WithAttributes(initializers.InitializeOtelResources(container.Version, container.Namespace).Attributes()...),
582+
// )
583+
//}
584+
585+
// RetryHTTPRoundTripper creates a retryable http.RoundTripper
586+
func (container *Container) RetryHTTPRoundTripper() http.RoundTripper {
587+
container.logger.Debug(fmt.Sprintf("initializing retry %T", http.DefaultTransport))
588+
retryClient := retryablehttp.NewClient()
589+
retryClient.Logger = container.Logger()
590+
return retryClient.StandardClient().Transport
591+
}
592+
520593
// PhoneService creates a new instance of services.PhoneService
521594
func (container *Container) PhoneService() (service *services.PhoneService) {
522595
container.logger.Debug(fmt.Sprintf("creating %T", service))
@@ -665,7 +738,7 @@ func (container *Container) LemonsqueezyService() (service *services.Lemonsqueez
665738
func (container *Container) LemonsqueezyHandler() (handler *handlers.LemonsqueezyHandler) {
666739
container.logger.Debug(fmt.Sprintf("creating %T", handler))
667740

668-
return handlers.NewLemonsqueezyHandlerHandler(
741+
return handlers.NewLemonsqueezyHandler(
669742
container.Logger(),
670743
container.Tracer(),
671744
container.LemonsqueezyService(),
@@ -687,6 +760,7 @@ func (container *Container) LemonsqueezyHandlerValidator() (validator *validator
687760
func (container *Container) LemonsqueezyClient() (client *lemonsqueezy.Client) {
688761
container.logger.Debug(fmt.Sprintf("creating %T", client))
689762
return lemonsqueezy.New(
763+
lemonsqueezy.WithHTTPClient(container.HTTPClient("lemonsqueezy")),
690764
lemonsqueezy.WithAPIKey(os.Getenv("LEMONSQUEEZY_API_KEY")),
691765
lemonsqueezy.WithSigningSecret(os.Getenv("LEMONSQUEEZY_SIGNING_SECRET")),
692766
)
@@ -769,6 +843,20 @@ func (container *Container) RegisterBillingListeners() {
769843
}
770844
}
771845

846+
// RegisterWebhookListeners registers event listeners for listeners.WebhookListener
847+
func (container *Container) RegisterWebhookListeners() {
848+
container.logger.Debug(fmt.Sprintf("registering listeners for %T", listeners.WebhookListener{}))
849+
_, routes := listeners.NewWebhookListener(
850+
container.Logger(),
851+
container.Tracer(),
852+
container.WebhookService(),
853+
)
854+
855+
for event, handler := range routes {
856+
container.EventDispatcher().Subscribe(event, handler)
857+
}
858+
}
859+
772860
// MessageService creates a new instance of services.MessageService
773861
func (container *Container) MessageService() (service *services.MessageService) {
774862
container.logger.Debug(fmt.Sprintf("creating %T", service))
@@ -818,6 +906,12 @@ func (container *Container) RegisterBillingRoutes() {
818906
container.BillingHandler().RegisterRoutes(container.AuthRouter())
819907
}
820908

909+
// RegisterWebhookRoutes registers routes for the /webhooks prefix
910+
func (container *Container) RegisterWebhookRoutes() {
911+
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.WebhookHandler{}))
912+
container.WebhookHandler().RegisterRoutes(container.App(), container.AuthenticatedMiddleware())
913+
}
914+
821915
// RegisterPhoneRoutes registers routes for the /phone prefix
822916
func (container *Container) RegisterPhoneRoutes() {
823917
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.PhoneHandler{}))

api/pkg/entities/webhook.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package entities
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
"github.com/lib/pq"
8+
)
9+
10+
// Webhook stores the webhooks of a user
11+
type Webhook struct {
12+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
13+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
14+
URL string `json:"url" example:"https://example.com"`
15+
SigningKey string `json:"signing_key" example:"DGW8NwQp7mxKaSZ72Xq9v67SLqSbWQvckzzmK8D6rvd7NywSEkdMJtuxKyEkYnCY"`
16+
Events pq.StringArray `json:"events" example:"[message.phone.received]"`
17+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
18+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
19+
}

api/pkg/events/message_phone_received_event.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,3 @@ type MessagePhoneReceivedPayload struct {
2020
Timestamp time.Time `json:"timestamp"`
2121
Content string `json:"content"`
2222
}
23-
24-
// MessagePhoneReceivedPayloadV1 is the payload of the EventTypeMessagePhoneReceived event
25-
type MessagePhoneReceivedPayloadV1 struct {
26-
MessageID uuid.UUID `json:"id"`
27-
UserID entities.UserID `json:"user_id"`
28-
Owner string `json:"owner"`
29-
Contact string `json:"contact"`
30-
Timestamp time.Time `json:"timestamp"`
31-
Content string `json:"content"`
32-
}

api/pkg/handlers/lemonsqueezy_handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ type LemonsqueezyHandler struct {
2323
validator *validators.LemonsqueezyHandlerValidator
2424
}
2525

26-
// NewLemonsqueezyHandlerHandler creates a new LemonsqueezyHandler
27-
func NewLemonsqueezyHandlerHandler(
26+
// NewLemonsqueezyHandler creates a new LemonsqueezyHandler
27+
func NewLemonsqueezyHandler(
2828
logger telemetry.Logger,
2929
tracer telemetry.Tracer,
3030
service *services.LemonsqueezyService,

0 commit comments

Comments
 (0)