Get Voip.ms working again in the text system

Because we need it for the conference.
This commit is contained in:
Eli Ribble 2026-01-29 21:53:49 +00:00
parent a900c23090
commit d2d5f003d8
No known key found for this signature in database
8 changed files with 221 additions and 57 deletions

View file

@ -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
}

View file

@ -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")
}

View file

@ -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)
}

View file

@ -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)
}

52
comms/text/twilio.go Normal file
View file

@ -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
}

103
comms/text/voipms.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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) {