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"))) }, mq.WithSyncMode(true)) // Add SMS workflow nodes // Note: Page nodes have no timeout by default, allowing users unlimited time for form input flow.AddDAGNode(dag.Page, "Login", "login", loginSubDAG().Clone(), true) 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, "Login to Form", "login", "SMSForm") flow.AddEdge(dag.Simple, "Form to Validation", "SMSForm", "ValidateInput") flow.AddCondition("ValidateInput", map[string]string{"valid": "SendSMS"}) // Removed invalid -> ErrorPage since we use ResetTo 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") } // loginSubDAG creates a login sub-DAG with page for authentication func loginSubDAG() *dag.DAG { login := dag.NewDAG("Login Sub DAG", "login-sub-dag", func(taskID string, result mq.Result) { fmt.Printf("Login Sub DAG Final result for task %s: %s\n", taskID, string(result.Payload)) }, mq.WithSyncMode(true)) login. AddNode(dag.Page, "Login Page", "login-page", &LoginPage{}). AddNode(dag.Function, "Verify Credentials", "verify-credentials", &VerifyCredentials{}). AddNode(dag.Function, "Generate Token", "generate-token", &GenerateToken{}). AddEdge(dag.Simple, "Login to Verify", "login-page", "verify-credentials"). AddEdge(dag.Simple, "Verify to Token", "verify-credentials", "generate-token") return login } type LoginPage struct { dag.Operation } func (p *LoginPage) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { // Check if this is a form submission var inputData map[string]any if len(task.Payload) > 0 { if err := json.Unmarshal(task.Payload, &inputData); err == nil { // Check if we have form data (username/password) if formData, ok := inputData["form"].(map[string]any); ok { // This is a form submission, pass it through for verification credentials := map[string]any{ "username": formData["username"], "password": formData["password"], } inputData["credentials"] = credentials updatedPayload, _ := json.Marshal(inputData) return mq.Result{Payload: updatedPayload, Ctx: ctx} } } } // Otherwise, show the form var data map[string]any if err := json.Unmarshal(task.Payload, &data); err != nil { data = make(map[string]any) } // HTML content for login page htmlContent := ` Phone Processing System - Login

📱 Phone Processing System

Please login to continue

{{if error}}
❌ Login Failed: {{error}}
{{end}}
` parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) rs, err := parser.ParseTemplate(htmlContent, map[string]any{ "task_id": ctx.Value("task_id"), "error": data["error"], "username": data["username"], }) if err != nil { return mq.Result{Error: err, Ctx: ctx} } ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml) resultData := map[string]any{ "html_content": rs, "step": "login", "data": data, } resultPayload, _ := json.Marshal(resultData) return mq.Result{ Payload: resultPayload, Ctx: ctx, } } type VerifyCredentials struct { dag.Operation } func (p *VerifyCredentials) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { var data map[string]any if err := json.Unmarshal(task.Payload, &data); err != nil { return mq.Result{Error: fmt.Errorf("VerifyCredentials Error: %s", err.Error()), Ctx: ctx} } username, _ := data["username"].(string) password, _ := data["password"].(string) // Simple verification logic if username == "admin" && password == "password123" { data["authenticated"] = true data["user_role"] = "administrator" } else { data["authenticated"] = false data["error"] = "Invalid credentials" data["validation_error"] = "Phone number is required" data["error_field"] = "phone" bt, _ := json.Marshal(data) return mq.Result{ Payload: bt, Ctx: ctx, ResetTo: "back", // Reset to form instead of going to error page } } delete(data, "html_content") updatedPayload, _ := json.Marshal(data) return mq.Result{Payload: updatedPayload, Ctx: ctx} } type GenerateToken struct { dag.Operation } func (p *GenerateToken) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { var data map[string]any if err := json.Unmarshal(task.Payload, &data); err != nil { return mq.Result{Error: fmt.Errorf("GenerateToken Error: %s", err.Error()), Ctx: ctx} } if authenticated, ok := data["authenticated"].(bool); ok && authenticated { data["auth_token"] = "jwt_token_123456789" data["token_expires"] = "2025-09-19T13:00:00Z" data["login_success"] = true } delete(data, "html_content") updatedPayload, _ := json.Marshal(data) return mq.Result{ Payload: updatedPayload, Ctx: ctx, Status: mq.Completed, } } // SMSFormNode - Initial form to collect SMS data type SMSFormNode struct { dag.Operation } func (s *SMSFormNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { var inputData map[string]any if len(task.Payload) > 0 { json.Unmarshal(task.Payload, &inputData) } if inputData == nil { inputData = make(map[string]any) } // Show the form (either initial load or with validation errors) htmlTemplate := ` SMS Sender

📱 SMS Sender

Send SMS messages through our secure DAG workflow

{{if validation_error}}
⚠️ Validation Error: {{validation_error}}
{{end}}
Supports US format: +1234567890 or 1234567890
{{message_length}}/160 characters
` messageStr, _ := inputData["message"].(string) messageLength := len(messageStr) parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) rs, err := parser.ParseTemplate(htmlTemplate, map[string]any{ "task_id": ctx.Value("task_id"), "validation_error": inputData["validation_error"], "error_field": inputData["error_field"], "error_field_phone": inputData["error_field"] == "phone", "error_field_message": inputData["error_field"] == "message", "phone": inputData["phone"], "message": inputData["message"], "message_length": messageLength, "sender_name": inputData["sender_name"], }) 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, ResetTo: "back", // Reset to form instead of going to error page } } // 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, ResetTo: "SMSForm", // Reset to form instead of going to error page } } // 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, ResetTo: "SMSForm", // Reset to form instead of going to error page } } 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, ResetTo: "SMSForm", // Reset to form instead of going to error page } } // 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, ResetTo: "SMSForm", // Reset to form instead of going to error page } } } // 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 len(task.Payload) > 0 { if err := json.Unmarshal(task.Payload, &inputData); err != nil { return mq.Result{Error: err, Ctx: ctx} } } else { inputData = make(map[string]any) } htmlTemplate := ` SMS Sent Successfully

SMS Sent Successfully!

{{sms_status}}
📱 Phone Number
{{formatted_phone}}
🆔 SMS ID
{{sms_id}}
⏰ Sent At
{{sent_at}}
🚚 Delivery
{{delivery_estimate}}
{{if sender_name}}
👤 Sender
{{sender_name}}
{{end}}
💰 Cost
{{cost_estimate}}
💬 Message Sent ({{char_count}} chars):
"{{message}}"
📱 Send Another SMS 📊 View Metrics
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}}
🔄 Try Again 📊 Check Status
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 }