github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/worker/sms/sms.go (about)

     1  package sms
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"runtime"
    10  	"time"
    11  
    12  	"github.com/cozy/cozy-stack/model/contact"
    13  	"github.com/cozy/cozy-stack/model/instance"
    14  	"github.com/cozy/cozy-stack/model/job"
    15  	"github.com/cozy/cozy-stack/model/notification/center"
    16  	"github.com/cozy/cozy-stack/pkg/config/config"
    17  	"github.com/cozy/cozy-stack/pkg/logger"
    18  	"github.com/cozy/cozy-stack/pkg/mail"
    19  	"github.com/labstack/echo/v4"
    20  )
    21  
    22  func init() {
    23  	job.AddWorker(&job.WorkerConfig{
    24  		WorkerType:   "sms",
    25  		Concurrency:  runtime.NumCPU(),
    26  		MaxExecCount: 1,
    27  		Timeout:      10 * time.Second,
    28  		Reserved:     true,
    29  		WorkerFunc:   Worker,
    30  	})
    31  }
    32  
    33  // Worker is the worker that send SMS.
    34  func Worker(ctx *job.TaskContext) error {
    35  	var msg center.SMS
    36  	if err := ctx.UnmarshalMessage(&msg); err != nil {
    37  		return err
    38  	}
    39  
    40  	err := sendSMS(ctx, &msg)
    41  	if err != nil {
    42  		ctx.Logger().Warnf("could not send SMS notification: %s", err)
    43  		sendFallbackMail(ctx.Instance, msg.MailFallback)
    44  	}
    45  	return err
    46  }
    47  
    48  func sendSMS(ctx *job.TaskContext, msg *center.SMS) error {
    49  	inst := ctx.Instance
    50  	cfg, err := getConfig(inst)
    51  	if err != nil {
    52  		return err
    53  	}
    54  	number, err := getMyselfPhoneNumber(inst)
    55  	if err != nil {
    56  		return err
    57  	}
    58  	switch cfg.Provider {
    59  	case "api_sen":
    60  		log := ctx.Logger()
    61  		return sendSenAPI(cfg, msg, number, log)
    62  	default:
    63  		return errors.New("Unknown provider for sending SMS")
    64  	}
    65  }
    66  
    67  func sendSenAPI(cfg *config.SMS, msg *center.SMS, number string, log logger.Logger) error {
    68  	payload, err := json.Marshal(map[string]interface{}{
    69  		"content":  msg.Message,
    70  		"receiver": []interface{}{number},
    71  	})
    72  	if err != nil {
    73  		return err
    74  	}
    75  	req, err := http.NewRequest(http.MethodPost, cfg.URL, bytes.NewReader(payload))
    76  	if err != nil {
    77  		return err
    78  	}
    79  	req.Header.Add(echo.HeaderAccept, echo.MIMEApplicationJSON)
    80  	req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON)
    81  	req.Header.Add(echo.HeaderAuthorization, "Bearer "+cfg.Token)
    82  	res, err := http.DefaultClient.Do(req)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	defer res.Body.Close()
    87  	if res.StatusCode == 200 {
    88  		return nil
    89  	}
    90  
    91  	var body map[string]interface{}
    92  	if err := json.NewDecoder(res.Body).Decode(&body); err == nil {
    93  		if t, ok := body["type"].(string); ok {
    94  			log = log.WithField("type", t)
    95  		}
    96  		if detail, ok := body["detail"].(string); ok {
    97  			log = log.WithField("detail", detail)
    98  		}
    99  		log.WithField("status_code", res.StatusCode).Warnf("Cannot send SMS")
   100  	}
   101  	return fmt.Errorf("Unexpected status code: %d", res.StatusCode)
   102  }
   103  
   104  func getMyselfPhoneNumber(inst *instance.Instance) (string, error) {
   105  	myself, err := contact.GetMyself(inst)
   106  	if err != nil {
   107  		return "", err
   108  	}
   109  	number := myself.PrimaryPhoneNumber()
   110  	if number == "" {
   111  		return "", errors.New("No phone number in the myself contact document")
   112  	}
   113  	return number, nil
   114  }
   115  
   116  func getConfig(inst *instance.Instance) (*config.SMS, error) {
   117  	cfg, ok := config.GetConfig().Notifications.Contexts[inst.ContextName]
   118  	if !ok {
   119  		return nil, errors.New("SMS not configured on this context")
   120  	}
   121  	return &cfg, nil
   122  }
   123  
   124  func sendFallbackMail(inst *instance.Instance, email *mail.Options) {
   125  	if inst == nil || email == nil {
   126  		return
   127  	}
   128  	msg, err := job.NewMessage(&email)
   129  	if err != nil {
   130  		return
   131  	}
   132  	_, _ = job.System().PushJob(inst, &job.JobRequest{
   133  		WorkerType: "sendmail",
   134  		Message:    msg,
   135  	})
   136  }