fake-sms/internal/route/powow.go

451 lines
11 KiB
Go

package route
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"strconv"
"strings"
"forge.cadoles.com/Cadoles/fake-sms/internal/command"
"forge.cadoles.com/Cadoles/fake-sms/internal/config"
"forge.cadoles.com/Cadoles/fake-sms/internal/model/powow"
"forge.cadoles.com/Cadoles/fake-sms/internal/storm"
"github.com/davecgh/go-spew/spew"
php "github.com/kovetskiy/go-php-serialize"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/logger"
"gitlab.com/wpetit/goweb/middleware/container"
"gitlab.com/wpetit/goweb/service"
)
type ErrorCode int
const (
ErrorCodeInvalidCommand ErrorCode = 99997
ErrorCodeAuthenticationFailure ErrorCode = 99998
ErrorCodeNotEnoughPrivileges ErrorCode = 99999
ErrorCodeTransactionSMSSendMissingSMSID ErrorCode = 1
ErrorCodeTransactionSMSSendMissingMobilePhoneNumber ErrorCode = 2
ErrorCodeTransactionSMSSendInvalidSMSID ErrorCode = 3
ErrorCodeTransactionSMSSendInvalidMobilePhoneNumber ErrorCode = 4
ErrorCodeTransactionSMSSendInvalidCustomData ErrorCode = 5
ErrorCodeTransactionSMSSendInvalidTimeToSend ErrorCode = 6
ErrorCodeTransactionSMSSendAccountSubscribersLimitExceeded ErrorCode = 7
ErrorCodeTransactionSMSSendMobilePhoneNumberCannotBeSaved ErrorCode = 7
ErrorCodeTransactionSMSSendTransactionalIDCannotBeCreated ErrorCode = 9
ErrorCodeTransactionSMSSendSMSSentLimitExceeded ErrorCode = 10
ErrorCodeTransactionSMSUpdateMissingSMSID ErrorCode = 1
ErrorCodeTransactionSMSUpdateInvalidSMSID ErrorCode = 3
ErrorCodeTransactionSMSUpdateInvalidFromName ErrorCode = 4
ErrorCodeTransactionSMSUpdateInvalidLanguage ErrorCode = 7
)
type Command string
const (
CommandTransactionalSMSSend = "TransactionalSms.Send"
CommandTransactionalSMSCreate = "TransactionalSms.Create"
CommandTransactionalSMSUpdate = "TransactionalSms.Update"
)
type PowowRequest struct {
APIKey string `json:"ApiKey"`
Command Command
ResponseFormat string
Payload map[string]interface{}
}
type PowowResponse struct {
Success bool
ErrorCode map[int]ErrorCode
}
type PowowResponseSuccess struct {
Success bool
ErrorCode ErrorCode
}
func handlePowowEntrypoint(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctn := container.Must(ctx)
conf := config.Must(ctn)
err := r.ParseForm()
if err != nil {
logger.Error(ctx, "could not parse form", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
pr := &PowowRequest{
Payload: make(map[string]interface{}),
}
pr.APIKey = r.FormValue("APIKey")
pr.Command = Command(r.FormValue("Command"))
pr.ResponseFormat = r.FormValue("ResponseFormat")
if pr.Command == CommandTransactionalSMSUpdate {
smsid, err := strconv.ParseFloat(r.FormValue("SmsID"), 64)
if err != nil {
logger.Error(ctx, "could not convert SmsID to float", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
pr.Payload = map[string]interface{}{
"SmsID": smsid,
"Content": r.FormValue("Content"),
"ShortLink": r.FormValue("ShortLink"),
"Language": r.FormValue("Language"),
"FromName": r.FormValue("FromName"),
"SmsName": r.FormValue("SmsName"),
}
}
if pr.Command == CommandTransactionalSMSSend {
customData, err := php.Decode(r.FormValue("CustomData"))
if err != nil {
logger.Error(ctx, "could not cunserialized custom data", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
var smsid float64
if r.FormValue("SmsID") != "" {
smsid, err = strconv.ParseFloat(r.FormValue("SmsID"), 64)
if err != nil {
logger.Error(ctx, "could not convert SmsID to float", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}
pr.Payload = map[string]interface{}{
"SmsID": smsid,
"MobilePhoneNumber": strings.TrimSpace(r.FormValue("MobilePhoneNumber")),
"TimeToSend": r.FormValue("TimeToSend"),
"CustomData": customData,
}
}
// Authenticate user
if conf.Powow.APIKey != pr.APIKey {
res := &PowowResponse{
Success: false,
ErrorCode: map[int]ErrorCode{0: ErrorCodeAuthenticationFailure},
}
sendPowowResponse(w, res)
return
}
// If phone number must return an error
spew.Dump(conf.Powow.InvalidPhoneNumber)
spew.Dump(pr.Payload["MobilePhoneNumber"])
if conf.Powow.InvalidPhoneNumber == pr.Payload["MobilePhoneNumber"] {
res := &PowowResponse{
Success: false,
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSSendInvalidMobilePhoneNumber},
}
sendPowowResponse(w, res)
return
}
// Handle Powow command
switch pr.Command {
case CommandTransactionalSMSSend:
handleTransactionalSMSSend(ctx, ctn, w, pr)
return
case CommandTransactionalSMSCreate:
handleTransactionalSMSCreate(ctx, ctn, w, pr)
return
case CommandTransactionalSMSUpdate:
handleTransactionalSMSUpdate(ctx, ctn, w, pr)
return
default:
res := &PowowResponse{
Success: false,
ErrorCode: map[int]ErrorCode{0: ErrorCodeInvalidCommand},
}
sendPowowResponse(w, res)
return
}
}
// Mock https://powow4.iroquois.fr/user/docs/api/#send-transactional-sms
func handleTransactionalSMSSend(ctx context.Context, ctn *service.Container, w http.ResponseWriter, req *PowowRequest) {
bus := cqrs.Must(ctn)
db := storm.Must(ctn)
smsID, exists, valid := getPowowSMSID(req)
if !exists {
sendPowowResponse(w, &PowowResponse{
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSSendMissingSMSID},
Success: false,
})
return
}
if !valid {
sendPowowResponse(w, &PowowResponse{
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSSendInvalidSMSID},
Success: false,
})
return
}
smsTmpl := &powow.SMSTemplate{}
if err := db.One("ID", smsID, smsTmpl); err != nil {
if errors.Is(err, storm.ErrNotFound) {
sendPowowResponse(w, &PowowResponse{
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSSendInvalidSMSID},
Success: false,
})
return
}
panic(errors.Wrap(err, "could not retrieve sms template"))
}
customData := make(map[string]interface{})
rawCustomData, exists := req.Payload["CustomData"]
if exists {
cData, ok := rawCustomData.(map[interface{}]interface{})
if !ok {
sendPowowResponse(w, &PowowResponse{
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSSendInvalidCustomData},
Success: false,
})
return
}
for k, v := range cData {
customData[k.(string)] = v
}
req.Payload["CustomData"] = customData
}
body, err := createSMSBody(smsTmpl.Content, customData)
if err != nil {
panic(errors.Wrap(err, "could not generate sms body"))
}
req.Payload["_Template"] = smsTmpl
storeSMS := &command.StoreSMSRequest{
From: smsTmpl.FromName,
Body: body,
Recipient: req.Payload["MobilePhoneNumber"].(string),
Metadata: req.Payload,
}
_, err = bus.Exec(ctx, storeSMS)
if err != nil {
panic(errors.Wrap(err, "could not store sms"))
}
res := &struct {
PowowResponseSuccess
TransactionalID int
}{
PowowResponseSuccess: PowowResponseSuccess{
Success: true,
ErrorCode: 0,
},
TransactionalID: 0,
}
sendPowowResponse(w, res)
}
func createSMSBody(template string, customData map[string]interface{}) (string, error) {
content := template
spew.Dump(customData)
for k, v := range customData {
decoded, err := base64.StdEncoding.DecodeString(v.(string))
if err != nil {
return "", errors.WithStack(err)
}
key := "%Subscriber:" + k + "%"
content = strings.ReplaceAll(content, key, string(decoded))
}
return content, nil
}
// Mock https://powow4.iroquois.fr/user/docs/api/#create-transactional-sms
func handleTransactionalSMSCreate(ctx context.Context, ctn *service.Container, w http.ResponseWriter, req *PowowRequest) {
db := storm.Must(ctn)
smsTemplate := &powow.SMSTemplate{}
if err := db.Save(smsTemplate); err != nil {
panic(errors.Wrap(err, "could not save sms template"))
}
res := &struct {
PowowResponseSuccess
SmsID int
}{
PowowResponseSuccess: PowowResponseSuccess{
Success: true,
ErrorCode: 0,
},
SmsID: smsTemplate.ID,
}
sendPowowResponse(w, res)
}
// Mock https://powow4.iroquois.fr/user/docs/api/#update-transactional-sms
func handleTransactionalSMSUpdate(ctx context.Context, ctn *service.Container, w http.ResponseWriter, req *PowowRequest) {
smsID, exists, valid := getPowowSMSID(req)
if !exists {
sendPowowResponse(w, &PowowResponse{
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSUpdateMissingSMSID},
Success: false,
})
return
}
if !valid {
sendPowowResponse(w, &PowowResponse{
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSUpdateInvalidSMSID},
Success: false,
})
return
}
db := storm.Must(ctn)
smsTmpl := &powow.SMSTemplate{}
if err := db.One("ID", smsID, smsTmpl); err != nil {
if errors.Is(err, storm.ErrNotFound) {
sendPowowResponse(w, &PowowResponse{
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSUpdateInvalidSMSID},
Success: false,
})
return
}
panic(errors.Wrap(err, "could not retrieve sms template"))
}
rawContent, exists := req.Payload["Content"]
if exists {
content, ok := rawContent.(string)
if ok {
smsTmpl.Content = content
}
}
rawLanguage, exists := req.Payload["Language"]
if exists {
language, ok := rawLanguage.(string)
if ok {
if !contains(language, "en", "fr", "it", "es", "de", "pt", "pl", "zh") {
sendPowowResponse(w, &PowowResponse{
ErrorCode: map[int]ErrorCode{0: ErrorCodeTransactionSMSUpdateInvalidLanguage},
Success: false,
})
return
}
smsTmpl.Language = language
}
}
rawFromName, exists := req.Payload["FromName"]
if exists {
fromName, ok := rawFromName.(string)
if ok {
smsTmpl.FromName = fromName
}
}
rawShortLink, exists := req.Payload["ShortLink"]
if exists {
shortLink, ok := rawShortLink.(float64)
if ok {
if shortLink == 1.0 {
smsTmpl.ShortLink = 1
} else {
smsTmpl.ShortLink = 0
}
}
}
rawSMSName, exists := req.Payload["SmsName"]
if exists {
smsName, ok := rawSMSName.(string)
if ok {
smsTmpl.SmsName = smsName
}
}
rawShortLink, exists = req.Payload["ShortLink"]
if exists {
shortLink, ok := rawShortLink.(float64)
if ok {
if shortLink == 1.0 {
smsTmpl.ShortLink = 1
} else {
smsTmpl.ShortLink = 0
}
}
}
if err := db.Save(smsTmpl); err != nil {
panic(errors.Wrap(err, "could not save sms template"))
}
sendPowowResponse(w, &PowowResponseSuccess{
ErrorCode: 0,
Success: true,
})
}
func sendPowowResponse(w http.ResponseWriter, res interface{}) {
w.Header().Add("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
if err := encoder.Encode(res); err != nil {
panic(errors.WithStack(err))
}
}
func getPowowSMSID(req *PowowRequest) (smsID int, exists bool, valid bool) {
rawSMSID, exists := req.Payload["SmsID"]
if !exists {
return -1, false, false
}
smsIDFloat, ok := rawSMSID.(float64)
if !ok {
return -1, true, false
}
return int(smsIDFloat), true, true
}