package main
import (
"context"
"fmt"
"regexp"
"strings"
"time"
"github.com/oarkflow/json"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq/utils"
"github.com/oarkflow/jet"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/consts"
)
func main() {
flow := dag.NewDAG("SMS Sender", "sms-sender", func(taskID string, result mq.Result) {
fmt.Printf("SMS workflow completed for task %s: %s\n", taskID, string(utils.RemoveRecursiveFromJSON(result.Payload, "html_content")))
})
// Add SMS workflow nodes
// Note: Page nodes have no timeout by default, allowing users unlimited time for form input
flow.AddNode(dag.Page, "SMS Form", "SMSForm", &SMSFormNode{})
flow.AddNode(dag.Function, "Validate Input", "ValidateInput", &ValidateInputNode{})
flow.AddNode(dag.Function, "Send SMS", "SendSMS", &SendSMSNode{})
flow.AddNode(dag.Page, "SMS Result", "SMSResult", &SMSResultNode{})
flow.AddNode(dag.Page, "Error Page", "ErrorPage", &ErrorPageNode{})
// Define edges for SMS workflow
flow.AddEdge(dag.Simple, "Form to Validation", "SMSForm", "ValidateInput")
flow.AddCondition("ValidateInput", map[string]string{"valid": "SendSMS", "invalid": "ErrorPage"})
flow.AddCondition("SendSMS", map[string]string{"sent": "SMSResult", "failed": "ErrorPage"})
// Start the flow
if flow.Error != nil {
panic(flow.Error)
}
fmt.Println("Starting SMS DAG server on http://0.0.0.0:8083")
fmt.Println("Navigate to the URL to access the SMS form")
flow.Start(context.Background(), "0.0.0.0:8083")
}
// SMSFormNode - Initial form to collect SMS data
type SMSFormNode struct {
dag.Operation
}
func (s *SMSFormNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
// Check if this is a form submission
var inputData map[string]any
if task.Payload != nil && len(task.Payload) > 0 {
if err := json.Unmarshal(task.Payload, &inputData); err == nil {
// If we have valid input data, pass it through for validation
return mq.Result{Payload: task.Payload, Ctx: ctx}
}
}
// Otherwise, show the form
htmlTemplate := `
SMS Sender
`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(htmlTemplate, map[string]any{
"task_id": ctx.Value("task_id"),
})
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
data := map[string]any{
"html_content": rs,
"step": "form",
}
bt, _ := json.Marshal(data)
return mq.Result{Payload: bt, Ctx: ctx}
}
// ValidateInputNode - Validates phone number and message
type ValidateInputNode struct {
dag.Operation
}
func (v *ValidateInputNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{
Error: fmt.Errorf("invalid input data: %v", err),
Ctx: ctx,
}
}
// Extract form data
phone, _ := inputData["phone"].(string)
message, _ := inputData["message"].(string)
senderName, _ := inputData["sender_name"].(string)
// Validate phone number
if phone == "" {
inputData["validation_error"] = "Phone number is required"
inputData["error_field"] = "phone"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Clean and validate phone number format
cleanPhone := regexp.MustCompile(`\D`).ReplaceAllString(phone, "")
// Check for valid US phone number (10 or 11 digits)
if len(cleanPhone) == 10 {
cleanPhone = "1" + cleanPhone // Add country code
} else if len(cleanPhone) != 11 || !strings.HasPrefix(cleanPhone, "1") {
inputData["validation_error"] = "Invalid phone number format. Please use US format: +1234567890 or 1234567890"
inputData["error_field"] = "phone"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Validate message
if message == "" {
inputData["validation_error"] = "Message is required"
inputData["error_field"] = "message"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
if len(message) > 160 {
inputData["validation_error"] = "Message too long. Maximum 160 characters allowed"
inputData["error_field"] = "message"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Check for potentially harmful content
forbiddenWords := []string{"spam", "scam", "fraud", "hack"}
messageLower := strings.ToLower(message)
for _, word := range forbiddenWords {
if strings.Contains(messageLower, word) {
inputData["validation_error"] = "Message contains prohibited content"
inputData["error_field"] = "message"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
}
// All validations passed
validatedData := map[string]any{
"phone": cleanPhone,
"message": message,
"sender_name": senderName,
"validated_at": time.Now().Format("2006-01-02 15:04:05"),
"validation_status": "success",
"formatted_phone": formatPhoneForDisplay(cleanPhone),
"char_count": len(message),
}
bt, _ := json.Marshal(validatedData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "valid"}
}
// SendSMSNode - Simulates sending SMS
type SendSMSNode struct {
dag.Operation
}
func (s *SendSMSNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
phone, _ := inputData["phone"].(string)
message, _ := inputData["message"].(string)
senderName, _ := inputData["sender_name"].(string)
// Simulate SMS sending delay
time.Sleep(500 * time.Millisecond)
// Simulate occasional failures for demo purposes
timestamp := time.Now()
success := timestamp.Second()%10 != 0 // 90% success rate
if !success {
errorData := map[string]any{
"phone": phone,
"message": message,
"sender_name": senderName,
"sms_status": "failed",
"error_message": "SMS gateway temporarily unavailable. Please try again.",
"sent_at": timestamp.Format("2006-01-02 15:04:05"),
"retry_suggested": true,
}
bt, _ := json.Marshal(errorData)
return mq.Result{
Payload: bt,
Ctx: ctx,
ConditionStatus: "failed",
}
}
// Generate mock SMS ID and response
smsID := fmt.Sprintf("SMS_%d_%s", timestamp.Unix(), phone[len(phone)-4:])
resultData := map[string]any{
"phone": phone,
"formatted_phone": formatPhoneForDisplay(phone),
"message": message,
"sender_name": senderName,
"sms_status": "sent",
"sms_id": smsID,
"sent_at": timestamp.Format("2006-01-02 15:04:05"),
"delivery_estimate": "1-2 minutes",
"cost_estimate": "$0.02",
"gateway": "MockSMS Gateway",
"char_count": len(message),
}
fmt.Printf("📱 SMS sent successfully! ID: %s, Phone: %s\n", smsID, formatPhoneForDisplay(phone))
bt, _ := json.Marshal(resultData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "sent"}
}
// SMSResultNode - Shows successful SMS result
type SMSResultNode struct {
dag.Operation
}
func (r *SMSResultNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
htmlTemplate := `
SMS Sent Successfully
✅
SMS Sent Successfully!
{{sms_status}}
📱 Phone Number
{{formatted_phone}}
🚚 Delivery
{{delivery_estimate}}
{{if sender_name}}
{{end}}
💬 Message Sent ({{char_count}} chars):
"{{message}}"
Gateway: {{gateway}} | Task completed in DAG workflow
`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(htmlTemplate, inputData)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
finalData := map[string]any{
"html_content": rs,
"result": inputData,
"step": "success",
}
bt, _ := json.Marshal(finalData)
return mq.Result{Payload: bt, Ctx: ctx}
}
// ErrorPageNode - Shows validation or sending errors
type ErrorPageNode struct {
dag.Operation
}
func (e *ErrorPageNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
// Determine error type and message
errorMessage, _ := inputData["validation_error"].(string)
errorField, _ := inputData["error_field"].(string)
smsError, _ := inputData["error_message"].(string)
if errorMessage == "" && smsError != "" {
errorMessage = smsError
errorField = "sms_sending"
}
if errorMessage == "" {
errorMessage = "An unknown error occurred"
}
htmlTemplate := `
SMS Error
❌
SMS Error
{{error_message}}
{{if error_field}}
Error Field: {{error_field}}
Action Required: Please correct the highlighted field and try again.
{{end}}
{{if retry_suggested}}
⚠️ Temporary Issue: This appears to be a temporary gateway issue.
Please try sending your SMS again in a few moments.
{{end}}
DAG Error Handler | SMS Workflow Failed
`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
templateData := map[string]any{
"error_message": errorMessage,
"error_field": errorField,
"retry_suggested": inputData["retry_suggested"],
}
rs, err := parser.ParseTemplate(htmlTemplate, templateData)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
finalData := map[string]any{
"html_content": rs,
"error_data": inputData,
"step": "error",
}
bt, _ := json.Marshal(finalData)
return mq.Result{Payload: bt, Ctx: ctx}
}
// Helper function to format phone number for display
func formatPhoneForDisplay(phone string) string {
if len(phone) == 11 && strings.HasPrefix(phone, "1") {
// Format as +1 (XXX) XXX-XXXX
return fmt.Sprintf("+1 (%s) %s-%s",
phone[1:4],
phone[4:7],
phone[7:11])
}
return phone
}