Skip to content

Commit 5c21681

Browse files
committed
Create 3cx integration
1 parent 361da9e commit 5c21681

7 files changed

Lines changed: 266 additions & 0 deletions

File tree

‎api/pkg/di/container.go‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func NewContainer(projectID string, version string) (container *Container) {
129129
container.RegisterWebhookListeners()
130130

131131
container.RegisterLemonsqueezyRoutes()
132+
container.RegisterIntegration3CXRoutes()
132133

133134
container.RegisterDiscordRoutes()
134135
container.RegisterDiscordListeners()
@@ -165,6 +166,12 @@ func (container *Container) App() (app *fiber.App) {
165166
return app
166167
}
167168

169+
// BearerAPIKeyMiddleware creates a new instance of middlewares.BearerAPIKeyAuth
170+
func (container *Container) BearerAPIKeyMiddleware() fiber.Handler {
171+
container.logger.Debug("creating middlewares.BearerAPIKeyAuth")
172+
return middlewares.BearerAPIKeyAuth(container.Logger(), container.Tracer(), container.UserRepository())
173+
}
174+
168175
// AuthenticatedMiddleware creates a new instance of middlewares.Authenticated
169176
func (container *Container) AuthenticatedMiddleware() fiber.Handler {
170177
container.logger.Debug("creating middlewares.Authenticated")
@@ -268,6 +275,10 @@ func (container *Container) DB() (db *gorm.DB) {
268275
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Discord{})))
269276
}
270277

278+
if err = db.AutoMigrate(&entities.Integration3CX{}); err != nil {
279+
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Integration3CX{})))
280+
}
281+
271282
return container.db
272283
}
273284

@@ -883,6 +894,18 @@ func (container *Container) LemonsqueezyHandler() (handler *handlers.Lemonsqueez
883894
)
884895
}
885896

897+
// Integration3CXHandler creates a new instance of handlers.Integration3CXHandler
898+
func (container *Container) Integration3CXHandler() (handler *handlers.Integration3CXHandler) {
899+
container.logger.Debug(fmt.Sprintf("creating %T", handler))
900+
901+
return handlers.NewIntegration3CxHandler(
902+
container.Logger(),
903+
container.Tracer(),
904+
container.MessageService(),
905+
container.BillingService(),
906+
)
907+
}
908+
886909
// DiscordHandler creates a new instance of handlers.DiscordHandler
887910
func (container *Container) DiscordHandler() (handler *handlers.DiscordHandler) {
888911
container.logger.Debug(fmt.Sprintf("creating %T", handler))
@@ -934,6 +957,12 @@ func (container *Container) RegisterLemonsqueezyRoutes() {
934957
container.LemonsqueezyHandler().RegisterRoutes(container.App())
935958
}
936959

960+
// RegisterIntegration3CXRoutes registers routes for the /integration/3cx prefix
961+
func (container *Container) RegisterIntegration3CXRoutes() {
962+
container.logger.Debug(fmt.Sprintf("registering [%T] routes", &handlers.Integration3CXHandler{}))
963+
container.Integration3CXHandler().RegisterRoutes(container.App(), container.BearerAPIKeyMiddleware(), container.AuthenticatedMiddleware())
964+
}
965+
937966
// RegisterDiscordRoutes registers routes for the /discord prefix
938967
func (container *Container) RegisterDiscordRoutes() {
939968
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.DiscordHandler{}))
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package entities
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
// Integration3CX stores the discord integration of a user
10+
type Integration3CX struct {
11+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
12+
UserID UserID `json:"user_id" gorm:"index" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
13+
WebhookURL string `json:"webhook_url" example:"https://org.3cx.com.au/sms/generic/123"`
14+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
15+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
16+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package handlers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/requests"
7+
"github.com/davecgh/go-spew/spew"
8+
9+
"github.com/NdoleStudio/httpsms/pkg/services"
10+
"github.com/NdoleStudio/httpsms/pkg/telemetry"
11+
"github.com/gofiber/fiber/v2"
12+
"github.com/palantir/stacktrace"
13+
)
14+
15+
// Integration3CXHandler handles 3CX events
16+
type Integration3CXHandler struct {
17+
handler
18+
logger telemetry.Logger
19+
tracer telemetry.Tracer
20+
messageService *services.MessageService
21+
billingService *services.BillingService
22+
}
23+
24+
// NewIntegration3CxHandler creates a new Integration3CXHandler
25+
func NewIntegration3CxHandler(
26+
logger telemetry.Logger,
27+
tracer telemetry.Tracer,
28+
messageService *services.MessageService,
29+
billingService *services.BillingService,
30+
) (h *Integration3CXHandler) {
31+
return &Integration3CXHandler{
32+
logger: logger.WithService(fmt.Sprintf("%T", h)),
33+
tracer: tracer,
34+
messageService: messageService,
35+
billingService: billingService,
36+
}
37+
}
38+
39+
// RegisterRoutes registers the routes for the MessageHandler
40+
func (h *Integration3CXHandler) RegisterRoutes(app *fiber.App, middlewares ...fiber.Handler) {
41+
router := app.Group("integration/3cx/")
42+
router.Post("/messages", h.computeRoute(middlewares, h.Messages)...)
43+
}
44+
45+
// Messages consumes a 3cx event
46+
// @Summary Sends a 3CX SMS message
47+
// @Description Sends an SMS message from the 3CX platform
48+
// @Tags 3CXIntegration
49+
// @Accept json
50+
// @Produce json
51+
// @Success 204 {object} responses.NoContent
52+
// @Failure 400 {object} responses.BadRequest
53+
// @Failure 401 {object} responses.Unauthorized
54+
// @Failure 422 {object} responses.UnprocessableEntity
55+
// @Failure 500 {object} responses.InternalServerError
56+
// @Router /integration/3cx/messages [post]
57+
func (h *Integration3CXHandler) Messages(c *fiber.Ctx) error {
58+
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
59+
defer span.End()
60+
61+
spew.Dump(string(c.Body()))
62+
63+
var request requests.Integration3CXMessage
64+
if err := c.BodyParser(&request); err != nil {
65+
msg := fmt.Sprintf("cannot marshall [%s] into %T", c.Body(), request)
66+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
67+
return h.responseBadRequest(c, err)
68+
}
69+
70+
if msg := h.billingService.IsEntitled(ctx, h.userIDFomContext(c)); msg != nil {
71+
ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("user with ID [%s] can't send a [3cx] message", h.userIDFomContext(c))))
72+
return h.responsePaymentRequired(c, *msg)
73+
}
74+
75+
message, err := h.messageService.SendMessage(ctx, request.ToMessageSendParams(h.userIDFomContext(c), c.OriginalURL()))
76+
if err != nil {
77+
msg := fmt.Sprintf("cannot send [3cx] message with paylod [%s]", c.Body())
78+
ctxLogger.Error(stacktrace.Propagate(err, msg))
79+
return h.responseInternalServerError(c)
80+
}
81+
82+
return h.responseOK(c, "message added to queue", message)
83+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package middlewares
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/NdoleStudio/httpsms/pkg/repositories"
8+
"github.com/NdoleStudio/httpsms/pkg/telemetry"
9+
"github.com/gofiber/fiber/v2"
10+
"github.com/palantir/stacktrace"
11+
)
12+
13+
// BearerAPIKeyAuth authenticates an API key using the Bearer header
14+
func BearerAPIKeyAuth(logger telemetry.Logger, tracer telemetry.Tracer, userRepository repositories.UserRepository) fiber.Handler {
15+
logger = logger.WithService("middlewares.APIKeyAuth")
16+
17+
return func(c *fiber.Ctx) error {
18+
ctx, span := tracer.StartFromFiberCtx(c, "middlewares.APIKeyAuth")
19+
defer span.End()
20+
21+
ctxLogger := tracer.CtxLogger(logger, span)
22+
23+
apiKey := strings.TrimSpace(strings.Replace(c.Get(authHeaderBearer), bearerScheme, "", 1))
24+
if len(apiKey) == 0 {
25+
span.AddEvent(fmt.Sprintf("the request header has no [%s] api key", authHeaderAPIKey))
26+
return c.Next()
27+
}
28+
29+
authUser, err := userRepository.LoadAuthUser(ctx, apiKey)
30+
if err != nil {
31+
ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot load user with api key [%s] using header [%s]", apiKey, c.Get(authHeaderBearer))))
32+
return c.Next()
33+
}
34+
35+
c.Locals(ContextKeyAuthUserID, authUser)
36+
37+
ctxLogger.Info(fmt.Sprintf("[%T] set successfully for user with ID [%s]", authUser, authUser.ID))
38+
39+
return c.Next()
40+
}
41+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/NdoleStudio/httpsms/pkg/entities"
8+
"github.com/NdoleStudio/httpsms/pkg/telemetry"
9+
"github.com/palantir/stacktrace"
10+
"gorm.io/gorm"
11+
)
12+
13+
// gormIntegration3CxRepository is responsible for persisting entities.Integration3CX
14+
type gormIntegration3CxRepository struct {
15+
logger telemetry.Logger
16+
tracer telemetry.Tracer
17+
db *gorm.DB
18+
}
19+
20+
// NewGormIntegration3CXRepository creates the GORM version of the Integration3CxRepository
21+
func NewGormIntegration3CXRepository(
22+
logger telemetry.Logger,
23+
tracer telemetry.Tracer,
24+
db *gorm.DB,
25+
) Integration3CxRepository {
26+
return &gormIntegration3CxRepository{
27+
logger: logger.WithService(fmt.Sprintf("%T", &gormIntegration3CxRepository{})),
28+
tracer: tracer,
29+
db: db,
30+
}
31+
}
32+
33+
// Save an entities.Integration3CX
34+
func (repository *gormIntegration3CxRepository) Save(ctx context.Context, integration *entities.Integration3CX) error {
35+
ctx, span := repository.tracer.Start(ctx)
36+
defer span.End()
37+
38+
if err := repository.db.WithContext(ctx).Save(integration).Error; err != nil {
39+
msg := fmt.Sprintf("cannot save [%T] with ID [%s]", integration, integration.ID)
40+
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
41+
}
42+
43+
return nil
44+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
)
8+
9+
// Integration3CxRepository loads and persists an entities.Integration3CX
10+
type Integration3CxRepository interface {
11+
// Save an entities.Integration3CX
12+
Save(ctx context.Context, heartbeat *entities.Integration3CX) error
13+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package requests
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
8+
"github.com/nyaruka/phonenumbers"
9+
10+
"github.com/NdoleStudio/httpsms/pkg/services"
11+
)
12+
13+
// Integration3CXMessage is the payload for sending and SMS message via 3CX
14+
type Integration3CXMessage struct {
15+
request
16+
From string `json:"from" example:"+18005550199"`
17+
To string `json:"to" example:"+18005550100"`
18+
Text string `json:"text" example:"This is a sample text message"`
19+
}
20+
21+
// Sanitize sets defaults to MessageReceive
22+
func (input *Integration3CXMessage) Sanitize() Integration3CXMessage {
23+
input.To = input.sanitizeAddress(input.To)
24+
input.From = input.sanitizeAddress(input.From)
25+
return *input
26+
}
27+
28+
// ToMessageSendParams converts Integration3CXMessage to services.MessageSendParams
29+
func (input *Integration3CXMessage) ToMessageSendParams(userID entities.UserID, source string) services.MessageSendParams {
30+
from, _ := phonenumbers.Parse(input.From, phonenumbers.UNKNOWN_REGION)
31+
return services.MessageSendParams{
32+
Source: source,
33+
Owner: *from,
34+
RequestID: input.sanitizeStringPointer("integration-3cx"),
35+
UserID: userID,
36+
RequestReceivedAt: time.Now().UTC(),
37+
Contact: input.sanitizeAddress(input.To),
38+
Content: input.Text,
39+
}
40+
}

0 commit comments

Comments
 (0)