From d2d5f003d8f0533f0f756cd71ff834957ac3e96b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 29 Jan 2026 21:53:49 +0000 Subject: [PATCH] Get Voip.ms working again in the text system Because we need it for the conference. --- api/api.go | 4 +- api/debug.go | 21 +++++---- api/routes.go | 2 + comms/text/text.go | 46 +++---------------- comms/text/twilio.go | 52 +++++++++++++++++++++ comms/text/voipms.go | 103 ++++++++++++++++++++++++++++++++++++++++++ config/config.go | 26 +++++++++++ platform/text/text.go | 24 +++++++--- 8 files changed, 221 insertions(+), 57 deletions(-) create mode 100644 comms/text/twilio.go create mode 100644 comms/text/voipms.go diff --git a/api/api.go b/api/api.go index 3b255c50..eb3ec2d3 100644 --- a/api/api.go +++ b/api/api.go @@ -39,7 +39,7 @@ func apiAudioPost(w http.ResponseWriter, r *http.Request, u *models.User) { return } if err := json.Unmarshal(body, &payload); err != nil { - debugSaveRequest(body, err, "Audio note POST JSON decode error") + //debugSaveRequest(body, err, "Audio note POST JSON decode error") http.Error(w, "Failed to decode the payload", http.StatusBadRequest) return } @@ -136,7 +136,7 @@ func apiImagePost(w http.ResponseWriter, r *http.Request, u *models.User) { return } if err := json.Unmarshal(body, &payload); err != nil { - debugSaveRequest(body, err, "Image note POST JSON decode error") + //debugSaveRequest(body, err, "Image note POST JSON decode error") http.Error(w, "Failed to decode the payload", http.StatusBadRequest) return } diff --git a/api/debug.go b/api/debug.go index 00522a1e..4bc40d78 100644 --- a/api/debug.go +++ b/api/debug.go @@ -1,22 +1,25 @@ package api import ( + "io" + "net/http" "os" "github.com/rs/zerolog/log" ) -func debugSaveRequest(body []byte, err error, message string) { - // TODO(eliribble): avoid using a single static filename and instead securely generate - // this value +func debugSaveRequest(r *http.Request) { + tmpFile, err := os.CreateTemp("/tmp", "request-*.data") if err != nil { - log.Error().Err(err).Msg(message) + log.Error().Err(err).Msg("failed to create temp file for debugSaveRequest") + return } - output, err := os.OpenFile("/tmp/request.body", os.O_RDWR|os.O_CREATE, 0666) + defer tmpFile.Close() + + _, err = io.Copy(tmpFile, r.Body) if err != nil { - log.Info().Msg("Failed to open temp request.bady") + log.Error().Err(err).Msg("failed to copy request body in debugSaveRequest") + return } - defer output.Close() - output.Write(body) - log.Info().Msg("Wrote request to /tmp/request.body") + log.Info().Str("filename", tmpFile.Name()).Msg("Saved request body") } diff --git a/api/routes.go b/api/routes.go index dba03ec4..5527baed 100644 --- a/api/routes.go +++ b/api/routes.go @@ -28,6 +28,8 @@ func AddRoutes(r chi.Router) { r.Post("/twilio/message", twilioMessagePost) r.Post("/twilio/text", twilioTextPost) r.Post("/twilio/text/status", twilioTextStatusPost) + r.Get("/voipms/text", voipmsTextGet) + r.Post("/voipms/text", voipmsTextPost) r.Get("/webhook/fieldseeker", webhookFieldseeker) r.Post("/webhook/fieldseeker", webhookFieldseeker) } diff --git a/comms/text/text.go b/comms/text/text.go index 018cfdd8..6e8c1d99 100644 --- a/comms/text/text.go +++ b/comms/text/text.go @@ -2,51 +2,17 @@ package text import ( "context" - "encoding/json" "fmt" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/rs/zerolog/log" - "github.com/twilio/twilio-go" - twilioApi "github.com/twilio/twilio-go/rest/api/v2010" ) func SendText(ctx context.Context, source string, destination string, message string) (string, error) { - client := twilio.NewRestClient() - - params := &twilioApi.CreateMessageParams{} - params.SetMessagingServiceSid(config.TwilioMessagingServiceSID) - - params.SetBody(message) - params.SetTo(destination) - resp, err := client.Api.CreateMessage(params) - - if err != nil { - return "", fmt.Errorf("Failed to create message to %s: %w", destination, err) + switch config.TextProvider { + case "voipms": + return sendTextVoipms(ctx, destination, message) + case "twilio": + return sendTextTwilio(ctx, source, destination, message) } - if resp.Sid == nil { - log.Warn().Str("src", source).Str("dst", destination).Msg("Text message sid is nil") - return "", nil - } - log.Info().Str("src", source).Str("dst", destination).Str("message", message).Str("sid", *resp.Sid).Msg("Created text message") - return *resp.Sid, nil -} - -func sendSMS(destination, source, message string) error { - client := twilio.NewRestClientWithParams(twilio.ClientParams{ - Username: config.TwilioAccountSID, - Password: config.TwilioAuthToken, - }) - params := &twilioApi.CreateMessageParams{} - params.SetTo("+15558675309") - params.SetFrom("+15017250604") - params.SetBody("Hello from Go!") - - resp, err := client.Api.CreateMessage(params) - if err != nil { - return fmt.Errorf("Error sending SMS message: %w", err) - } - response, _ := json.Marshal(*resp) - log.Debug().Str("response", string(response)).Msg("Send SMS") - return nil + return "", fmt.Errorf("Unsupported provider '%s'", config.TextProvider) } diff --git a/comms/text/twilio.go b/comms/text/twilio.go new file mode 100644 index 00000000..86092b1b --- /dev/null +++ b/comms/text/twilio.go @@ -0,0 +1,52 @@ +package text + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/rs/zerolog/log" + "github.com/twilio/twilio-go" + twilioApi "github.com/twilio/twilio-go/rest/api/v2010" +) + +func sendTextTwilio(ctx context.Context, source string, destination string, message string) (string, error) { + client := twilio.NewRestClient() + + params := &twilioApi.CreateMessageParams{} + params.SetMessagingServiceSid(config.TwilioMessagingServiceSID) + + params.SetBody(message) + params.SetTo(destination) + resp, err := client.Api.CreateMessage(params) + + if err != nil { + return "", fmt.Errorf("Failed to create message to %s: %w", destination, err) + } + if resp.Sid == nil { + log.Warn().Str("src", source).Str("dst", destination).Msg("Text message sid is nil") + return "", nil + } + log.Info().Str("src", source).Str("dst", destination).Str("message", message).Str("sid", *resp.Sid).Msg("Created text message") + return *resp.Sid, nil +} + +func sendSMSTwilio(destination, source, message string) error { + client := twilio.NewRestClientWithParams(twilio.ClientParams{ + Username: config.TwilioAccountSID, + Password: config.TwilioAuthToken, + }) + params := &twilioApi.CreateMessageParams{} + params.SetTo("+15558675309") + params.SetFrom("+15017250604") + params.SetBody("Hello from Go!") + + resp, err := client.Api.CreateMessage(params) + if err != nil { + return fmt.Errorf("Error sending SMS message: %w", err) + } + response, _ := json.Marshal(*resp) + log.Debug().Str("response", string(response)).Msg("Send SMS") + return nil +} diff --git a/comms/text/voipms.go b/comms/text/voipms.go new file mode 100644 index 00000000..967ffb0f --- /dev/null +++ b/comms/text/voipms.go @@ -0,0 +1,103 @@ +package text + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/rs/zerolog/log" +) + +var VOIP_MS_API = "https://voip.ms/api/v1/rest.php" + +type VoipMSResponse struct { + Message string `json:"message"` + Status string `json:"status"` + SMS int `json:"sms"` +} + +func sendTextVoipms(ctx context.Context, to string, content string, media ...string) (string, error) { + if len(content) > 2048 { + return "", errors.New("Message content is more than 160 characters") + } + params := url.Values{} + params.Add("api_password", config.VoipMSPassword) + params.Add("api_username", config.VoipMSUsername) + params.Add("method", "sendMMS") + params.Add("did", config.VoipMSNumber) + params.Add("dst", to) + params.Add("message", content) + /* + for i, med := range media { + // These should be one of: + // 1. A full URL that the service cat GET + // 2. A base64-encoded image starting with "data:image/png;base64,iVBORw0KGgoAAAANSUh..." + params.Add(fmt.Sprintf("media%d", i+1), med) + } + params.Add(fmt.Sprintf("media%d", len(media)+1), "") + */ + + response, err := makeVoipMSRequest(params) + if err != nil { + return "", fmt.Errorf("Failed to send MMS: %w", err) + } + log.Info().Str("status", response.Status).Int("sms", response.SMS).Msg("Sent MMS message") + return strconv.Itoa(response.SMS), nil +} + +func sendSMSVoipms(to string, content string) (string, error) { + if len(content) > 160 { + return "", errors.New("Message content is more than 160 characters") + } + params := url.Values{} + params.Add("api_password", config.VoipMSPassword) + params.Add("api_username", config.VoipMSUsername) + params.Add("method", "sendSMS") + params.Add("did", config.VoipMSNumber) + params.Add("dst", to) + params.Add("message", content) + + response, err := makeVoipMSRequest(params) + if err != nil { + return "", fmt.Errorf("Failed to send SMS: %w", err) + } + log.Info().Str("status", response.Status).Int("sms", response.SMS).Msg("Sent MMS message") + return strconv.Itoa(response.SMS), nil +} + +func makeVoipMSRequest(params url.Values) (VoipMSResponse, error) { + result := VoipMSResponse{} + // Construct the URL with query parameters + full_url := VOIP_MS_API + "?" + params.Encode() + + // Make the HTTP request + log.Debug().Str("full_url", full_url).Msg("Sending command to VoIP.ms") + resp, err := http.Get(full_url) + if err != nil { + log.Warn().Err(err).Str("url", full_url).Msg("Failed to make request to Voip.MS") + return result, fmt.Errorf("Error making request: %w", err) + } + defer resp.Body.Close() + + // Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Warn().Err(err).Str("url", full_url).Msg("Failed to read Voip.MS response body") + return result, fmt.Errorf("Failed to read response: %w", err) + } + log.Info().Str("response", string(body)).Msg("Response from Voip.MS") + + // Parse the JSON response + var response VoipMSResponse + err = json.Unmarshal(body, &response) + if err != nil { + return result, fmt.Errorf("Failed to unmarshal JSON response: %w", err) + } + return response, nil +} diff --git a/config/config.go b/config/config.go index a0342663..978db9d7 100644 --- a/config/config.go +++ b/config/config.go @@ -31,10 +31,14 @@ var ( PhoneNumberSupport phonenumbers.PhoneNumber PhoneNumberSupportStr string SentryDSN string + TextProvider string TwilioAuthToken string TwilioAccountSID string TwilioMessagingServiceSID string TwilioRCSSenderRMO string + VoipMSNumber string + VoipMSPassword string + VoipMSUsername string ) func IsProductionEnvironment() bool { @@ -156,6 +160,16 @@ func Parse() (err error) { if SentryDSN == "" { return fmt.Errorf("You must specify a non-empty SENTRY_DSN") } + TextProvider = os.Getenv("TEXT_PROVIDER") + switch TextProvider { + case "": + return fmt.Errorf("You must specify a non-empty TEXT_PROVIDER") + case "twilio": + case "voipms": + break + default: + return fmt.Errorf("Unrecognized text provider '%s'", TextProvider) + } TwilioAccountSID = os.Getenv("TWILIO_ACCOUNT_SID") if TwilioAccountSID == "" { return fmt.Errorf("You must specify a non-empty TWILIO_ACCOUNT_SID") @@ -172,6 +186,18 @@ func Parse() (err error) { if TwilioRCSSenderRMO == "" { return fmt.Errorf("You must specify a non-empty TWILIO_RCS_SENDER_RMO") } + VoipMSNumber = os.Getenv("VOIPMS_NUMBER") + if VoipMSNumber == "" { + return fmt.Errorf("You must specify a non-empty VOIPMS_NUMBER") + } + VoipMSPassword = os.Getenv("VOIPMS_PASSWORD") + if VoipMSPassword == "" { + return fmt.Errorf("You must specify a non-empty VOIPMS_PASSWORD") + } + VoipMSUsername = os.Getenv("VOIPMS_USERNAME") + if VoipMSPassword == "" { + return fmt.Errorf("You must specify a non-empty VOIPMS_USERNAME") + } return nil } diff --git a/platform/text/text.go b/platform/text/text.go index bcca0bc1..64376784 100644 --- a/platform/text/text.go +++ b/platform/text/text.go @@ -46,8 +46,6 @@ func HandleTextMessage(from string, to string, body string) { // We don't know if they're subscribed or not. if subscribed == nil { switch body_l { - case "stop": - setSubscribed(ctx, src, false) case "yes": setSubscribed(ctx, src, true) handleWaitingTextJobs(ctx, src) @@ -60,10 +58,19 @@ func HandleTextMessage(from string, to string, body string) { } return } - // If we get the super-special "reset conversation" then wipe the LLM's memory - if body_l == "reset conversation" { + switch body_l { + case "stop": + content := "You have successfully been unsubscribed. You will not receive any more messages from this number. Reply START to resubscribe." + err = sendText(ctx, dst, src, content, enums.CommsTextoriginCommandResponse, false, false) + if err != nil { + log.Error().Err(err).Msg("Failed to send unsubscribe acknowledgement.") + } + setSubscribed(ctx, src, false) + return + case "reset conversation": handleResetConversation(ctx, src, dst) return + default: } previous_messages, err := loadPreviousMessagesForLLM(ctx, dst, src) if err != nil { @@ -90,8 +97,13 @@ func ParsePhoneNumber(input string) (*E164, error) { func StoreSources() error { ctx := context.TODO() - src := phonenumbers.Format(&config.PhoneNumberReport, phonenumbers.E164) - return ensureInDB(ctx, src) + for _, n := range []string{config.PhoneNumberReportStr, config.PhoneNumberSupportStr, config.VoipMSNumber} { + err := ensureInDB(ctx, n) + if err != nil { + return fmt.Errorf("Failed to add number '%s' to DB: %w", n, err) + } + } + return nil } func UpdateMessageStatus(twilio_sid string, status string) {