Added: ttl support

This commit is contained in:
Aleksandr Soloshenko 2023-10-21 00:33:29 +07:00
parent a9a92b15bb
commit e473400bdf
15 changed files with 128 additions and 29 deletions

View File

@ -16,6 +16,7 @@ Authorization: Basic IUGFXE:v4ejpbeydvjo1h
{
"message": "Test",
"ttl": {{$randomInt 0 86400}},
"phoneNumbers": [
"79990001234"
]
@ -24,3 +25,7 @@ Authorization: Basic IUGFXE:v4ejpbeydvjo1h
###
GET {{baseUrl}}/api/3rdparty/v1/message/pRl1wf_yez5TikJb_xemU HTTP/1.1
Authorization: Basic IUGFXE:v4ejpbeydvjo1h
###
GET {{baseUrl}}/api/mobile/v1/message HTTP/1.1
Authorization: Bearer KuvE4LBXzvy8QO2ZXDDMP

View File

@ -357,6 +357,12 @@
"example": [
"79990001234"
]
},
"ttl": {
"description": "Время жизни сообщения в секундах",
"type": "integer",
"minimum": 5,
"example": 86400
}
}
},

View File

@ -33,6 +33,11 @@ definitions:
maxItems: 100
minItems: 1
type: array
ttl:
description: Время жизни сообщения в секундах
example: 86400
minimum: 5
type: integer
required:
- message
- phoneNumbers

View File

@ -26,6 +26,8 @@ import (
// @host localhost:3000
// @schemes http
// @BasePath /api
//
// SMS-шлюз
func main() {
cfg := config.GetConfig()

View File

@ -0,0 +1,10 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE `messages`
ADD `valid_until` datetime;
-- +goose StatementEnd
---
-- +goose Down
-- +goose StatementBegin
ALTER TABLE `messages` DROP `valid_until`;
-- +goose StatementEnd

4
go.mod
View File

@ -4,12 +4,11 @@ go 1.20
require (
bitbucket.org/soft-c/gohelpers v1.0.3-0.20221007032455-694a304f5909
bitbucket.org/soft-c/gomicrobase v1.1.2-0.20221006080527-7eeddcd13770
bitbucket.org/soft-c/gomicrobase v1.3.1-0.20231020165939-64940c19df05
firebase.google.com/go/v4 v4.12.0
github.com/go-playground/validator/v10 v10.11.0
github.com/gofiber/fiber/v2 v2.38.1
github.com/jaevor/go-nanoid v1.3.0
github.com/joho/godotenv v1.4.0
github.com/nyaruka/phonenumbers v1.1.8
google.golang.org/api v0.114.0
gorm.io/gorm v1.23.8
@ -41,6 +40,7 @@ require (
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.15.11 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect

8
go.sum
View File

@ -1,7 +1,7 @@
bitbucket.org/soft-c/gohelpers v1.0.3-0.20221007032455-694a304f5909 h1:SwG69J+r+YOkcHnOUYFmsdvRyeEnvMs2KHTf+4L+4f4=
bitbucket.org/soft-c/gohelpers v1.0.3-0.20221007032455-694a304f5909/go.mod h1:AlZzQ1xftfS8rWSwjwTj3/MPMFFfgfNCP3b7xLoEeKM=
bitbucket.org/soft-c/gomicrobase v1.1.2-0.20221006080527-7eeddcd13770 h1:IAEOe5Uv9+zQEaOuT8eHJPUfS64Nc0I9ufq7afFXKUk=
bitbucket.org/soft-c/gomicrobase v1.1.2-0.20221006080527-7eeddcd13770/go.mod h1:Pvv07ulIXfDG5us/ZG5Of7vvjY+tq3fkcrY/MmJUEiE=
bitbucket.org/soft-c/gomicrobase v1.3.1-0.20231020165939-64940c19df05 h1:FHJKNG4sy0LYof5LlF8MIUYCcF0/d57Pw+GSXrBPiQc=
bitbucket.org/soft-c/gomicrobase v1.3.1-0.20231020165939-64940c19df05/go.mod h1:/b3RdE4wI1BxzZ/FrJ4c8X3g2h/dL8XRyiXovD9oXzE=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@ -196,8 +196,8 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=

View File

@ -1,13 +1,9 @@
package config
import (
"errors"
"io/fs"
"os"
"sync"
microbase "bitbucket.org/soft-c/gomicrobase"
"github.com/joho/godotenv"
)
type Config struct {
@ -20,21 +16,10 @@ var instance *Config
var once = sync.Once{}
func newConfig() *Config {
if err := godotenv.Load(".env"); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
errorLog.Println(err)
}
}
path := os.Getenv("CONFIG_PATH")
if path == "" {
path = "config.yml"
}
config := Config{}
if err := microbase.LoadConfig(path, &config); err != nil {
errorLog.Fatalf("Can't load config from %s: %s", path, err.Error())
if err := microbase.LoadConfig(&config); err != nil {
errorLog.Fatalf("Can't load config from %s", err.Error())
}
return &config

View File

@ -3,6 +3,7 @@ package smsgateway
import (
"bitbucket.org/capcom6/smsgatewaybackend/internal/smsgateway/handlers"
"bitbucket.org/capcom6/smsgatewaybackend/internal/smsgateway/models"
_ "bitbucket.org/capcom6/smsgatewaybackend/internal/smsgateway/tasks"
microbase "bitbucket.org/soft-c/gomicrobase"
)

View File

@ -32,6 +32,8 @@ type thirdPartyHandler struct {
// @Failure 400 {object} smsgateway.ErrorResponse "Некорректный запрос"
// @Failure 500 {object} smsgateway.ErrorResponse "Внутренняя ошибка сервера"
// @Router /3rdparty/v1/message [post]
//
// Поставить сообщение в очередь
func (h *thirdPartyHandler) postMessage(user models.User, c *fiber.Ctx) error {
req := smsgateway.Message{}
if err := h.BodyParserValidator(c, &req); err != nil {
@ -39,7 +41,7 @@ func (h *thirdPartyHandler) postMessage(user models.User, c *fiber.Ctx) error {
}
if len(user.Devices) < 1 {
return fiber.NewError(fiber.StatusBadRequest, "Нет ни одного устройтсва в учетной записи")
return fiber.NewError(fiber.StatusBadRequest, "Нет ни одного устройства в учетной записи")
}
device := user.Devices[0]
@ -66,6 +68,8 @@ func (h *thirdPartyHandler) postMessage(user models.User, c *fiber.Ctx) error {
// @Failure 400 {object} smsgateway.ErrorResponse "Некорректный запрос"
// @Failure 500 {object} smsgateway.ErrorResponse "Внутренняя ошибка сервера"
// @Router /3rdparty/v1/message [get]
//
// Получить состояние сообщения
func (h *thirdPartyHandler) getMessage(user models.User, c *fiber.Ctx) error {
id := c.Params("id")

View File

@ -35,6 +35,8 @@ type mobileHandler struct {
// @Failure 400 {object} smsgateway.ErrorResponse "Некорректный запрос"
// @Failure 500 {object} smsgateway.ErrorResponse "Внутренняя ошибка сервера"
// @Router /mobile/v1/device [post]
//
// Регистрация устройства
func (h *mobileHandler) postDevice(c *fiber.Ctx) error {
req := smsgateway.MobileRegisterRequest{}
@ -75,6 +77,8 @@ func (h *mobileHandler) postDevice(c *fiber.Ctx) error {
// @Failure 403 {object} smsgateway.ErrorResponse "Операция запрещена"
// @Failure 500 {object} smsgateway.ErrorResponse "Внутренняя ошибка сервера"
// @Router /mobile/v1/device [patch]
//
// Обновление устройства
func (h *mobileHandler) patchDevice(device models.Device, c *fiber.Ctx) error {
req := smsgateway.MobileUpdateRequest{}
@ -102,6 +106,8 @@ func (h *mobileHandler) patchDevice(device models.Device, c *fiber.Ctx) error {
// @Success 200 {array} smsgateway.Message "Список сообщений"
// @Failure 500 {object} smsgateway.ErrorResponse "Внутренняя ошибка сервера"
// @Router /mobile/v1/message [get]
//
// Получить сообщения для отправки
func (h *mobileHandler) getMessage(device models.Device, c *fiber.Ctx) error {
messages, err := h.messagesSvc.SelectPending(device.ID)
if err != nil {
@ -122,6 +128,8 @@ func (h *mobileHandler) getMessage(device models.Device, c *fiber.Ctx) error {
// @Failure 400 {object} smsgateway.ErrorResponse "Некорректный запрос"
// @Failure 500 {object} smsgateway.ErrorResponse "Внутренняя ошибка сервера"
// @Router /mobile/v1/message [patch]
//
// Обновить состояние сообщений
func (h *mobileHandler) patchMessage(device models.Device, c *fiber.Ctx) error {
req := []smsgateway.MessageState{}
if err := c.BodyParser(&req); err != nil {

View File

@ -42,11 +42,12 @@ type Device struct {
}
type Message struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
DeviceID string `gorm:"not null;type:char(21);uniqueIndex:unq_messages_id_device,priority:2;index:idx_messages_device_state"`
ExtID string `gorm:"not null;type:varchar(36);uniqueIndex:unq_messages_id_device,priority:1"`
Message string `gorm:"not null;type:tinytext"`
State MessageState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending;index:idx_messages_device_state"`
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
DeviceID string `gorm:"not null;type:char(21);uniqueIndex:unq_messages_id_device,priority:2;index:idx_messages_device_state"`
ExtID string `gorm:"not null;type:varchar(36);uniqueIndex:unq_messages_id_device,priority:1"`
Message string `gorm:"not null;type:tinytext"`
State MessageState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending;index:idx_messages_device_state"`
ValidUntil time.Time `gorm:"type:datetime"`
Device Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"`
Recipients []MessageRecipient `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"`

View File

@ -40,11 +40,26 @@ func (s *MessagesService) SelectPending(deviceID string) ([]smsgateway.Message,
return nil, err
}
messages = s.filterTimeouted(messages)
result := make([]smsgateway.Message, len(messages))
for i, v := range messages {
var ttl *uint64 = nil
if !v.ValidUntil.IsZero() {
delta := time.Until(v.ValidUntil).Seconds()
if delta > 0 {
deltaInt := uint64(delta)
ttl = &deltaInt
} else {
deltaInt := uint64(0)
ttl = &deltaInt
}
}
result[i] = smsgateway.Message{
ID: v.ExtID,
Message: v.Message,
TTL: ttl,
PhoneNumbers: s.recipientsToDomain(v.Recipients),
}
}
@ -98,10 +113,16 @@ func (s *MessagesService) Enqeue(device models.Device, message smsgateway.Messag
}
}
validUntil := time.Time{}
if message.TTL != nil && *message.TTL > 0 {
validUntil = time.Now().Add(time.Duration(*message.TTL) * time.Second)
}
msg := models.Message{
DeviceID: device.ID,
ExtID: message.ID,
Message: message.Message,
ValidUntil: validUntil,
Recipients: s.recipientsToModel(message.PhoneNumbers),
}
if msg.ExtID == "" {
@ -129,6 +150,22 @@ func (s *MessagesService) Enqeue(device models.Device, message smsgateway.Messag
return state, nil
}
func (s *MessagesService) filterTimeouted(messages []models.Message) []models.Message {
result := make([]models.Message, 0, len(messages))
for _, v := range messages {
if v.ValidUntil.IsZero() || time.Now().Before(v.ValidUntil) {
result = append(result, v)
} else if v.State == models.MessageStatePending {
v.State = models.MessageStateFailed
for i, _ := range v.Recipients {
v.Recipients[i].State = models.MessageStateFailed
}
s.Messages.UpdateState(&v)
}
}
return result
}
func (s *MessagesService) recipientsToDomain(input []models.MessageRecipient) []string {
output := make([]string, len(input))

View File

@ -0,0 +1,34 @@
package tasks
import (
"context"
"fmt"
"time"
microbase "bitbucket.org/soft-c/gomicrobase"
"gorm.io/gorm"
)
func init() {
microbase.RegisterOnStartedListener(task)
}
func task(ctx context.Context, c chan error, d *gorm.DB) error {
ticker := time.NewTicker(60 * time.Second)
go func() {
defer func() {
c <- nil
}()
for {
select {
case <-ticker.C:
fmt.Println("tick")
case <-ctx.Done():
ticker.Stop()
return
}
}
}()
return nil
}

View File

@ -14,6 +14,7 @@ const (
type Message struct {
ID string `json:"id,omitempty" validate:"omitempty,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` // Идентификатор
Message string `json:"message" validate:"required,max=256" example:"Hello World!"` // Текст сообщения
TTL *uint64 `json:"ttl" validate:"omitempty,min=5" example:"86400"` // Время жизни сообщения в секундах
PhoneNumbers []string `json:"phoneNumbers" validate:"required,min=1,max=100,dive,required,min=10" example:"79990001234"` // Получатели
}