mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
Merge remote-tracking branch 'origin/master' into feature/device-last-seen
This commit is contained in:
commit
28244692d6
5
.github/workflows/docker-publish.yml
vendored
5
.github/workflows/docker-publish.yml
vendored
@ -1,7 +1,6 @@
|
||||
name: docker-publish
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@ -14,10 +13,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# step 2: set up go
|
||||
- name: Set up Go 1.20
|
||||
- name: Set up Go 1.21
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ">=1.20"
|
||||
go-version: "1.21"
|
||||
|
||||
# step 3: install dependencies
|
||||
- name: Install all Go dependencies
|
||||
|
||||
52
.github/workflows/golangci-lint.yml
vendored
Normal file
52
.github/workflows/golangci-lint.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
# pull-requests: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
cache: false
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
# Require: The version of golangci-lint to use.
|
||||
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
|
||||
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
|
||||
version: latest
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
#
|
||||
# Note: By default, the `.golangci.yml` file should be at the root of the repository.
|
||||
# The location of the configuration file can be changed by using `--config=`
|
||||
# args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0
|
||||
args: --timeout=5m
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
# Optional: if set to true, then all caching functionality will be completely disabled,
|
||||
# takes precedence over all other caching options.
|
||||
# skip-cache: true
|
||||
|
||||
# Optional: if set to true, then the action won't cache or restore ~/go/pkg.
|
||||
# skip-pkg-cache: true
|
||||
|
||||
# Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
|
||||
# skip-build-cache: true
|
||||
|
||||
# Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
|
||||
# install-mode: "goinstall"
|
||||
5
Makefile
5
Makefile
@ -26,8 +26,11 @@ db-upgrade-raw:
|
||||
run:
|
||||
go run cmd/$(project_name)/main.go
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
test:
|
||||
go test -cover ./...
|
||||
go test -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||
|
||||
build:
|
||||
go build -o tmp/$(project_name) ./cmd/$(project_name)
|
||||
|
||||
@ -9,7 +9,7 @@ Authorization: Basic {{localCredentials}}
|
||||
|
||||
{
|
||||
"message": "{{$localDatetime iso8601}}",
|
||||
"ttl": 600,
|
||||
"validUntil": "2024-02-10T12:00:00+00:00",
|
||||
"phoneNumbers": [
|
||||
"{{phone}}"
|
||||
],
|
||||
@ -22,6 +22,20 @@ POST {{localUrl}}/message HTTP/1.1
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
{
|
||||
"message": "{{$localDatetime iso8601}}",
|
||||
"ttl": 86400,
|
||||
"phoneNumbers": [
|
||||
"{{phone}}"
|
||||
],
|
||||
"withDeliveryReport": true
|
||||
}
|
||||
|
||||
###
|
||||
POST {{localUrl}}/message HTTP/1.1
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
{
|
||||
"message": "17wc9/ZRf1l84LHkEK3hgA==.aH1XrMHAeMyF4PeiavV3dk8o2fP0nSo92IqseLQfg14=",
|
||||
"ttl": 600,
|
||||
|
||||
@ -19,11 +19,11 @@ Authorization: Basic {{credentials}}
|
||||
|
||||
{
|
||||
"message": "{{$localDatetime iso8601}}",
|
||||
"ttl": 600,
|
||||
"validUntil": "2024-02-10T12:00:00+07:00",
|
||||
"phoneNumbers": [
|
||||
"{{phone}}"
|
||||
],
|
||||
"simNumber": 1,
|
||||
"simNumber": {{$randomInt 1 2}},
|
||||
"withDeliveryReport": true
|
||||
}
|
||||
|
||||
|
||||
@ -469,6 +469,11 @@
|
||||
"minimum": 5,
|
||||
"example": 86400
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "Время окончания жизни сообщения",
|
||||
"type": "string",
|
||||
"example": "2020-01-01T00:00:00Z"
|
||||
},
|
||||
"withDeliveryReport": {
|
||||
"description": "Запрашивать отчет о доставке",
|
||||
"type": "boolean",
|
||||
|
||||
@ -74,6 +74,10 @@ definitions:
|
||||
example: 86400
|
||||
minimum: 5
|
||||
type: integer
|
||||
validUntil:
|
||||
description: Время окончания жизни сообщения
|
||||
example: "2020-01-01T00:00:00Z"
|
||||
type: string
|
||||
withDeliveryReport:
|
||||
description: Запрашивать отчет о доставке
|
||||
example: true
|
||||
|
||||
4
go.mod
4
go.mod
@ -8,7 +8,7 @@ require (
|
||||
firebase.google.com/go/v4 v4.12.1
|
||||
github.com/capcom6/go-infra-fx v0.0.0-20240104165405-d2c3993a9516
|
||||
github.com/go-playground/validator/v10 v10.16.0
|
||||
github.com/gofiber/fiber/v2 v2.51.0
|
||||
github.com/gofiber/fiber/v2 v2.52.1
|
||||
github.com/jaevor/go-nanoid v1.3.0
|
||||
github.com/nyaruka/phonenumbers v1.3.0
|
||||
go.uber.org/fx v1.20.1
|
||||
@ -37,7 +37,7 @@ require (
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@ -82,8 +82,8 @@ github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrt
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gofiber/contrib/fiberzap/v2 v2.1.2 h1:7Z1BqS1sYK9e9jTwqPcWx9qQt46PI8oeswgAp6YNZC4=
|
||||
github.com/gofiber/contrib/fiberzap/v2 v2.1.2/go.mod h1:ulCCQOdDYABGsOQfbndASmCsCN86hsC96iKoOTNYfy8=
|
||||
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
|
||||
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
|
||||
github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI=
|
||||
github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
@ -123,8 +123,8 @@ github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
|
||||
@ -94,6 +94,9 @@ func (h *thirdPartyHandler) postMessage(user models.User, c *fiber.Ctx) error {
|
||||
if err := h.BodyParserValidator(c, &req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
skipPhoneValidation := c.QueryBool("skipPhoneValidation", false)
|
||||
|
||||
|
||||
@ -8,6 +8,10 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Validatable interface {
|
||||
Validate() error
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
Logger *zap.Logger
|
||||
Validator *validator.Validate
|
||||
@ -18,11 +22,7 @@ func (h *Handler) BodyParserValidator(c *fiber.Ctx, out any) error {
|
||||
return fmt.Errorf("can't parse body: %w", err)
|
||||
}
|
||||
|
||||
if h.Validator == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return h.Validator.Struct(out)
|
||||
return h.validateStruct(out)
|
||||
}
|
||||
|
||||
func (h *Handler) QueryParserValidator(c *fiber.Ctx, out any) error {
|
||||
@ -30,11 +30,7 @@ func (h *Handler) QueryParserValidator(c *fiber.Ctx, out any) error {
|
||||
return fmt.Errorf("can't parse query: %w", err)
|
||||
}
|
||||
|
||||
if h.Validator == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return h.Validator.Struct(out)
|
||||
return h.validateStruct(out)
|
||||
}
|
||||
|
||||
func (h *Handler) ParamsParserValidator(c *fiber.Ctx, out any) error {
|
||||
@ -42,9 +38,21 @@ func (h *Handler) ParamsParserValidator(c *fiber.Ctx, out any) error {
|
||||
return fmt.Errorf("can't parse params: %w", err)
|
||||
}
|
||||
|
||||
if h.Validator == nil {
|
||||
return nil
|
||||
return h.validateStruct(out)
|
||||
}
|
||||
|
||||
func (h *Handler) validateStruct(out any) error {
|
||||
if h.Validator != nil {
|
||||
if err := h.Validator.Struct(out); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return h.Validator.Struct(out)
|
||||
if req, ok := out.(Validatable); ok {
|
||||
if err := req.Validate(); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
205
internal/sms-gateway/handlers/handler_test.go
Normal file
205
internal/sms-gateway/handlers/handler_test.go
Normal file
@ -0,0 +1,205 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
type TestRequestBody struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Age int `json:"age" validate:"required"`
|
||||
}
|
||||
|
||||
type TestRequestBodyNoValidate struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Age int `json:"age" validate:"required"`
|
||||
}
|
||||
|
||||
func (t *TestRequestBody) Validate() error {
|
||||
if t.Age < 18 {
|
||||
return fmt.Errorf("must be at least 18 years old")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestQueryParams struct {
|
||||
Page int `query:"page" validate:"required"`
|
||||
}
|
||||
|
||||
type TestURLParams struct {
|
||||
ID string `params:"id" validate:"required,uuid"`
|
||||
}
|
||||
|
||||
func TestHandler_BodyParserValidator(t *testing.T) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
validate := validator.New()
|
||||
|
||||
handler := &Handler{
|
||||
Logger: logger,
|
||||
Validator: validate,
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Post("/test", func(c *fiber.Ctx) error {
|
||||
var body TestRequestBody
|
||||
return handler.BodyParserValidator(c, &body)
|
||||
})
|
||||
app.Post("/test2", func(c *fiber.Ctx) error {
|
||||
var body TestRequestBodyNoValidate
|
||||
return handler.BodyParserValidator(c, &body)
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
path string
|
||||
payload any
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
description: "Valid request body",
|
||||
path: "/test",
|
||||
payload: &TestRequestBody{Name: "John Doe", Age: 25},
|
||||
expectedStatus: fiber.StatusOK,
|
||||
},
|
||||
{
|
||||
description: "Invalid request body - missing name",
|
||||
path: "/test",
|
||||
payload: &TestRequestBody{Age: 25},
|
||||
expectedStatus: fiber.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
description: "Invalid request body - age too low",
|
||||
path: "/test",
|
||||
payload: &TestRequestBody{Name: "John Doe", Age: 17},
|
||||
expectedStatus: fiber.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
description: "Valid request body - no validation",
|
||||
path: "/test2",
|
||||
payload: &TestRequestBodyNoValidate{Name: "John Doe", Age: 17},
|
||||
expectedStatus: fiber.StatusOK,
|
||||
},
|
||||
{
|
||||
description: "No request body",
|
||||
path: "/test",
|
||||
payload: nil,
|
||||
expectedStatus: fiber.StatusUnprocessableEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
var req *http.Request
|
||||
if test.payload != nil {
|
||||
bodyBytes, _ := json.Marshal(test.payload)
|
||||
req = httptest.NewRequest("POST", test.path, bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req = httptest.NewRequest("POST", test.path, nil)
|
||||
}
|
||||
|
||||
resp, _ := app.Test(req)
|
||||
if test.expectedStatus != resp.StatusCode {
|
||||
t.Errorf("Expected status code %d, got %d", test.expectedStatus, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_QueryParserValidator(t *testing.T) {
|
||||
type fields struct {
|
||||
Logger *zap.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
type args struct {
|
||||
c *fiber.Ctx
|
||||
out any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
Logger: tt.fields.Logger,
|
||||
Validator: tt.fields.Validator,
|
||||
}
|
||||
if err := h.QueryParserValidator(tt.args.c, tt.args.out); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Handler.QueryParserValidator() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_ParamsParserValidator(t *testing.T) {
|
||||
type fields struct {
|
||||
Logger *zap.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
type args struct {
|
||||
c *fiber.Ctx
|
||||
out any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
Logger: tt.fields.Logger,
|
||||
Validator: tt.fields.Validator,
|
||||
}
|
||||
if err := h.ParamsParserValidator(tt.args.c, tt.args.out); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Handler.ParamsParserValidator() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_validateStruct(t *testing.T) {
|
||||
type fields struct {
|
||||
Logger *zap.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
type args struct {
|
||||
out any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
Logger: tt.fields.Logger,
|
||||
Validator: tt.fields.Validator,
|
||||
}
|
||||
if err := h.validateStruct(tt.args.out); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Handler.validateStruct() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -83,11 +83,12 @@ func (s *MessagesService) SelectPending(deviceID string) ([]smsgateway.Message,
|
||||
result[i] = smsgateway.Message{
|
||||
ID: v.ExtID,
|
||||
Message: v.Message,
|
||||
TTL: ttl,
|
||||
SimNumber: v.SimNumber,
|
||||
WithDeliveryReport: types.AsPointer[bool](v.WithDeliveryReport),
|
||||
IsEncrypted: v.IsEncrypted,
|
||||
PhoneNumbers: s.recipientsToDomain(v.Recipients),
|
||||
TTL: ttl,
|
||||
ValidUntil: v.ValidUntil,
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +150,7 @@ func (s *MessagesService) Enqeue(device models.Device, message smsgateway.Messag
|
||||
}
|
||||
}
|
||||
|
||||
var validUntil *time.Time = nil
|
||||
var validUntil *time.Time = message.ValidUntil
|
||||
if message.TTL != nil && *message.TTL > 0 {
|
||||
validUntil = types.AsPointer(time.Now().Add(time.Duration(*message.TTL) * time.Second))
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ func (c *Client) doRequest(ctx context.Context, method, path string, headers map
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
}()
|
||||
|
||||
|
||||
@ -26,12 +26,12 @@ func TestClient_Send(t *testing.T) {
|
||||
|
||||
if string(req) != `{"message":"","phoneNumbers":null}` {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write(req)
|
||||
_, _ = w.Write(req)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(`{}`))
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
package smsgateway
|
||||
|
||||
import "time"
|
||||
|
||||
type ProcessState string
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MessageStatePending ProcessState = "Pending" // В ожидании
|
||||
@ -27,11 +28,21 @@ type Device struct {
|
||||
type Message struct {
|
||||
ID string `json:"id,omitempty" validate:"omitempty,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` // Идентификатор
|
||||
Message string `json:"message" validate:"required,max=65535" example:"Hello World!"` // Текст сообщения
|
||||
TTL *uint64 `json:"ttl,omitempty" validate:"omitempty,min=5" example:"86400"` // Время жизни сообщения в секундах
|
||||
SimNumber *uint8 `json:"simNumber,omitempty" validate:"omitempty,max=3" example:"1"` // Номер сим-карты
|
||||
WithDeliveryReport *bool `json:"withDeliveryReport,omitempty" example:"true"` // Запрашивать отчет о доставке
|
||||
IsEncrypted bool `json:"isEncrypted,omitempty" example:"true"` // Зашифровано
|
||||
PhoneNumbers []string `json:"phoneNumbers" validate:"required,min=1,max=100,dive,required,min=10,max=128" example:"79990001234"` // Получатели
|
||||
|
||||
TTL *uint64 `json:"ttl,omitempty" validate:"omitempty,min=5" example:"86400"` // Время жизни сообщения в секундах
|
||||
ValidUntil *time.Time `json:"validUntil,omitempty" example:"2020-01-01T00:00:00Z"` // Время окончания жизни сообщения
|
||||
}
|
||||
|
||||
func (m Message) Validate() error {
|
||||
if m.TTL != nil && m.ValidUntil != nil {
|
||||
return fmt.Errorf("%w: ttl and validUntil", ErrConflictFields)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Состояние сообщения
|
||||
|
||||
7
pkg/smsgateway/types.go
Normal file
7
pkg/smsgateway/types.go
Normal file
@ -0,0 +1,7 @@
|
||||
package smsgateway
|
||||
|
||||
import "errors"
|
||||
|
||||
type ProcessState string
|
||||
|
||||
var ErrConflictFields = errors.New("conflict fields")
|
||||
@ -52,3 +52,7 @@ To avoid mobile operator restrictions, we introduced a delay feature in version
|
||||
## Can I use long or non-standard phone numbers?
|
||||
|
||||
Yes, starting from [1.6.1](https://github.com/capcom6/android-sms-gateway/releases/tag/v1.6.1) of the app, our system allows the use of long or non-standard phone numbers, which may be common with M2M (machine-to-machine) SIM cards or other special cases. To bypass the standard phone number validation, simply add the query parameter `skipPhoneValidation=true` to your API request. Please note that with validation disabled, you are responsible for ensuring the correctness of the phone numbers. They should still follow the E.164 format, beginning with a '+' and containing only digits.
|
||||
|
||||
## What does the `RESULT_ERROR_LIMIT_EXCEEDED` error mean SMS?
|
||||
|
||||
The `RESULT_ERROR_LIMIT_EXCEEDED` error occurs when you've hit the sending limit imposed by your carrier or the Android operating system. This is a safeguard against spamming and typically happens if you try to send too many messages in a short period. To avoid this, try spacing out your messages or contact your carrier to inquire about their message sending limits. See also [How can I set up delays between sending messages?](#how-can-i-set-up-delays-between-sending-messages)
|
||||
|
||||
@ -175,4 +175,69 @@ class Encryptor {
|
||||
return crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, "sha1");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
Based on [pycryptodome](https://pypi.org/project/pycryptodome/)
|
||||
|
||||
```python
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import SHA1
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
from Crypto.Random import get_random_bytes
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
|
||||
class AESEncryptor(BaseEncryptor):
|
||||
def encrypt(self, cleartext: str) -> str:
|
||||
saltBytes = self._generate_salt()
|
||||
key = self._generate_key(saltBytes, self.iterations)
|
||||
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=saltBytes)
|
||||
|
||||
encrypted_bytes = cipher.encrypt(pad(cleartext.encode(), AES.block_size))
|
||||
|
||||
salt = base64.b64encode(saltBytes).decode("utf-8")
|
||||
encrypted = base64.b64encode(encrypted_bytes).decode("utf-8")
|
||||
|
||||
return f"$aes-256-cbc/pbkdf2-sha1$i={self.iterations}${salt}${encrypted}"
|
||||
|
||||
def decrypt(self, encrypted: str) -> str:
|
||||
chunks = encrypted.split("$")
|
||||
|
||||
if len(chunks) < 5:
|
||||
raise ValueError("Invalid encryption format")
|
||||
|
||||
if chunks[1] != "aes-256-cbc/pbkdf2-sha1":
|
||||
raise ValueError("Unsupported algorithm")
|
||||
|
||||
params = self._parse_params(chunks[2])
|
||||
if "i" not in params:
|
||||
raise ValueError("Missing iteration count")
|
||||
|
||||
iterations = int(params["i"])
|
||||
salt = base64.b64decode(chunks[-2])
|
||||
encrypted_bytes = base64.b64decode(chunks[-1])
|
||||
|
||||
key = self._generate_key(salt, iterations)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=salt)
|
||||
|
||||
decrypted_bytes = unpad(cipher.decrypt(encrypted_bytes), AES.block_size)
|
||||
|
||||
return decrypted_bytes.decode("utf-8")
|
||||
|
||||
def _generate_salt(self) -> bytes:
|
||||
return get_random_bytes(16)
|
||||
|
||||
def _generate_key(self, salt: bytes, iterations: int) -> bytes:
|
||||
return PBKDF2(
|
||||
self.passphrase,
|
||||
salt,
|
||||
count=iterations,
|
||||
dkLen=32,
|
||||
hmac_hash_module=SHA1,
|
||||
)
|
||||
|
||||
def _parse_params(self, params: str) -> t.Dict[str, str]:
|
||||
return {k: v for k, v in [p.split("=") for p in params.split(",")]}
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user