mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
292 lines
10 KiB
Go
292 lines
10 KiB
Go
package messages
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/android-sms-gateway/client-go/smsgateway"
|
|
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
|
|
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/converters"
|
|
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
|
|
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
|
|
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices"
|
|
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages"
|
|
"github.com/capcom6/go-helpers/slices"
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/gofiber/fiber/v2"
|
|
"go.uber.org/fx"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
route3rdPartyGetMessage = "3rdparty.get.message"
|
|
)
|
|
|
|
type thirdPartyControllerParams struct {
|
|
fx.In
|
|
|
|
MessagesSvc *messages.Service
|
|
DevicesSvc *devices.Service
|
|
|
|
Validator *validator.Validate
|
|
Logger *zap.Logger
|
|
}
|
|
|
|
type ThirdPartyController struct {
|
|
base.Handler
|
|
|
|
messagesSvc *messages.Service
|
|
devicesSvc *devices.Service
|
|
}
|
|
|
|
// @Summary Enqueue message
|
|
// @Description Enqueues a message for sending. If `deviceId` is set, the specified device is used; otherwise a random registered device is chosen.
|
|
// @Security ApiAuth
|
|
// @Tags User, Messages
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param skipPhoneValidation query bool false "Skip phone validation"
|
|
// @Param deviceActiveWithin query int false "Filter devices active within the specified number of hours" default(0) minimum(0)
|
|
// @Param request body smsgateway.Message true "Send message request"
|
|
// @Success 202 {object} smsgateway.GetMessageResponse "Message enqueued"
|
|
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
|
|
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
|
// @Failure 409 {object} smsgateway.ErrorResponse "Message with such ID already exists"
|
|
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
|
// @Header 202 {string} Location "Get message state URL"
|
|
// @Router /3rdparty/v1/messages [post]
|
|
//
|
|
// Enqueue message
|
|
func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error {
|
|
var params thirdPartyPostQueryParams
|
|
if err := h.QueryParserValidator(c, ¶ms); err != nil {
|
|
return err
|
|
}
|
|
|
|
var req smsgateway.Message
|
|
if err := h.BodyParserValidator(c, &req); err != nil {
|
|
return err
|
|
}
|
|
|
|
var device models.Device
|
|
var err error
|
|
var filters []devices.SelectFilter
|
|
|
|
if params.DeviceActiveWithin > 0 {
|
|
filters = append(filters, devices.ActiveWithin(time.Duration(params.DeviceActiveWithin)*time.Hour))
|
|
}
|
|
|
|
// Check if device_id is provided
|
|
if req.DeviceID != "" {
|
|
|
|
device, err = h.devicesSvc.Get(user.ID, append(filters, devices.WithID(req.DeviceID))...)
|
|
if err != nil {
|
|
if errors.Is(err, devices.ErrNotFound) {
|
|
return fiber.NewError(fiber.StatusBadRequest, "No active device with such ID found")
|
|
}
|
|
h.Logger.Error("Failed to get device", zap.Error(err), zap.String("user_id", user.ID), zap.String("device_id", req.DeviceID))
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Can't select device. Please contact support")
|
|
}
|
|
} else {
|
|
// Fallback to random selection
|
|
devices, err := h.devicesSvc.Select(user.ID, filters...)
|
|
if err != nil {
|
|
h.Logger.Error("Failed to select devices", zap.Error(err), zap.String("user_id", user.ID))
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Can't select devices. Please contact support")
|
|
}
|
|
|
|
if len(devices) < 1 {
|
|
return fiber.NewError(fiber.StatusBadRequest, "No active devices found")
|
|
}
|
|
|
|
device, err = slices.Random(devices)
|
|
if err != nil {
|
|
return fmt.Errorf("can't get random device: %w", err)
|
|
}
|
|
}
|
|
|
|
var textContent *messages.TextMessageContent
|
|
var dataContent *messages.DataMessageContent
|
|
if text := req.GetTextMessage(); text != nil {
|
|
textContent = &messages.TextMessageContent{
|
|
Text: text.Text,
|
|
}
|
|
} else if data := req.GetDataMessage(); data != nil {
|
|
dataContent = &messages.DataMessageContent{
|
|
Data: data.Data,
|
|
Port: data.Port,
|
|
}
|
|
} else {
|
|
return fiber.NewError(fiber.StatusBadRequest, "No message content provided")
|
|
}
|
|
|
|
msg := messages.MessageIn{
|
|
ID: req.ID,
|
|
|
|
TextContent: textContent,
|
|
DataContent: dataContent,
|
|
|
|
PhoneNumbers: req.PhoneNumbers,
|
|
IsEncrypted: req.IsEncrypted,
|
|
|
|
SimNumber: req.SimNumber,
|
|
WithDeliveryReport: req.WithDeliveryReport,
|
|
TTL: req.TTL,
|
|
ValidUntil: req.ValidUntil,
|
|
Priority: req.Priority,
|
|
}
|
|
state, err := h.messagesSvc.Enqueue(device, msg, messages.EnqueueOptions{SkipPhoneValidation: params.SkipPhoneValidation})
|
|
if err != nil {
|
|
var errValidation messages.ErrValidation
|
|
if isBadRequest := errors.As(err, &errValidation); isBadRequest {
|
|
return fiber.NewError(fiber.StatusBadRequest, errValidation.Error())
|
|
}
|
|
if isConflict := errors.Is(err, messages.ErrMessageAlreadyExists); isConflict {
|
|
return fiber.NewError(fiber.StatusConflict, err.Error())
|
|
}
|
|
|
|
return fmt.Errorf("can't enqueue message: %w", err)
|
|
}
|
|
|
|
location, err := c.GetRouteURL(route3rdPartyGetMessage, fiber.Map{
|
|
"id": state.ID,
|
|
})
|
|
if err != nil {
|
|
h.Logger.Warn("Failed to get route URL", zap.String("route", route3rdPartyGetMessage), zap.Error(err))
|
|
} else {
|
|
c.Location(location)
|
|
}
|
|
|
|
return c.Status(fiber.StatusAccepted).
|
|
JSON(smsgateway.GetMessageResponse{
|
|
ID: state.ID,
|
|
DeviceID: state.DeviceID,
|
|
State: smsgateway.ProcessingState(state.State),
|
|
IsHashed: state.IsHashed,
|
|
IsEncrypted: state.IsEncrypted,
|
|
Recipients: state.Recipients,
|
|
States: state.States,
|
|
})
|
|
}
|
|
|
|
// @Summary Get messages
|
|
// @Description Retrieves a list of messages with filtering and pagination
|
|
// @Security ApiAuth
|
|
// @Tags User, Messages
|
|
// @Produce json
|
|
// @Param from query string false "Start date in RFC3339 format" Format(date-time)
|
|
// @Param to query string false "End date in RFC3339 format" Format(date-time)
|
|
// @Param state query string false "Filter messages by processing state" Enum(Pending, Processed, Sent, Delivered, Failed)
|
|
// @Param deviceId query string false "Filter by device ID" min(21) max(21)
|
|
// @Param limit query int false "Pagination limit" default(50) min(1) max(100)
|
|
// @Param offset query int false "Pagination offset" default(0)
|
|
// @Success 200 {object} smsgateway.GetMessagesResponse "A list of messages"
|
|
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
|
|
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
|
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
|
// @Router /3rdparty/v1/messages [get]
|
|
//
|
|
// Get message history
|
|
func (h *ThirdPartyController) list(user models.User, c *fiber.Ctx) error {
|
|
params := thirdPartyGetQueryParams{}
|
|
if err := h.QueryParserValidator(c, ¶ms); err != nil {
|
|
return err
|
|
}
|
|
|
|
messages, total, err := h.messagesSvc.SelectStates(user, params.ToFilter(), params.ToOptions())
|
|
if err != nil {
|
|
h.Logger.Error("Failed to get message history", zap.Error(err), zap.String("user_id", user.ID))
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve message history")
|
|
}
|
|
|
|
c.Set("X-Total-Count", strconv.Itoa(int(total)))
|
|
return c.JSON(
|
|
slices.Map(messages, converters.MessageStateToDTO),
|
|
)
|
|
}
|
|
|
|
// @Summary Get message state
|
|
// @Description Returns message state by ID
|
|
// @Security ApiAuth
|
|
// @Tags User, Messages
|
|
// @Produce json
|
|
// @Param id path string true "Message ID"
|
|
// @Success 200 {object} smsgateway.GetMessageResponse "Message state"
|
|
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
|
|
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
|
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
|
// @Router /3rdparty/v1/messages/{id} [get]
|
|
//
|
|
// Get message state
|
|
func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
|
|
state, err := h.messagesSvc.GetState(user, id)
|
|
if err != nil {
|
|
if errors.Is(err, messages.ErrMessageNotFound) {
|
|
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return c.JSON(converters.MessageStateToDTO(state))
|
|
}
|
|
|
|
// @Summary Request inbox messages export
|
|
// @Description Initiates process of inbox messages export via webhooks. For each message the `sms:received` webhook will be triggered. The webhooks will be triggered without specific order.
|
|
// @Security ApiAuth
|
|
// @Tags User, Messages
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body smsgateway.MessagesExportRequest true "Export inbox request"
|
|
// @Success 202 {object} object "Inbox export request accepted"
|
|
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
|
|
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
|
|
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
|
|
// @Router /3rdparty/v1/inbox/export [post]
|
|
//
|
|
// Export inbox
|
|
func (h *ThirdPartyController) postInboxExport(user models.User, c *fiber.Ctx) error {
|
|
req := smsgateway.MessagesExportRequest{}
|
|
if err := h.BodyParserValidator(c, &req); err != nil {
|
|
return err
|
|
}
|
|
|
|
device, err := h.devicesSvc.Get(user.ID, devices.WithID(req.DeviceID))
|
|
if err != nil {
|
|
if errors.Is(err, devices.ErrNotFound) {
|
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid device ID")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
if err := h.messagesSvc.ExportInbox(device, req.Since, req.Until); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.SendStatus(fiber.StatusAccepted)
|
|
}
|
|
|
|
func (h *ThirdPartyController) Register(router fiber.Router) {
|
|
router.Get("", userauth.WithUser(h.list))
|
|
router.Post("", userauth.WithUser(h.post))
|
|
router.Get(":id", userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
|
|
|
|
router.Post("inbox/export", userauth.WithUser(h.postInboxExport))
|
|
}
|
|
|
|
func NewThirdPartyController(params thirdPartyControllerParams) *ThirdPartyController {
|
|
return &ThirdPartyController{
|
|
Handler: base.Handler{
|
|
Logger: params.Logger.Named("messages"),
|
|
Validator: params.Validator,
|
|
},
|
|
messagesSvc: params.MessagesSvc,
|
|
devicesSvc: params.DevicesSvc,
|
|
}
|
|
}
|