Enhance messenger service with template support for message sending
- Introduced template loading functionality during messenger initialization, allowing for dynamic message templates. - Added SendT, SendTWithProvider, SendTBatch, and SendTBatchMixed methods to the Service for sending messages using templates. - Updated provider implementations for Mailgun, Twilio, and Mailer to support template-based message sending. - Enhanced the Provider interface to include optional template methods, improving flexibility and usability across providers.
This commit is contained in:
parent
d113adaa8a
commit
ec8b6c336b
19 changed files with 2190 additions and 21 deletions
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/yaoapp/yao/messenger/providers/mailer"
|
||||
"github.com/yaoapp/yao/messenger/providers/mailgun"
|
||||
"github.com/yaoapp/yao/messenger/providers/twilio"
|
||||
"github.com/yaoapp/yao/messenger/template"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/share"
|
||||
)
|
||||
|
|
@ -73,6 +74,13 @@ func Load(cfg config.Config) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Load templates
|
||||
err = template.LoadTemplates()
|
||||
if err != nil {
|
||||
log.Warn("[Messenger] Failed to load templates: %v", err)
|
||||
// Don't fail messenger loading if templates fail
|
||||
}
|
||||
|
||||
// Create messenger configuration
|
||||
config := &types.Config{
|
||||
Providers: []types.ProviderConfig{},
|
||||
|
|
@ -212,7 +220,7 @@ func createProvider(config types.ProviderConfig) (types.Provider, error) {
|
|||
case "twilio":
|
||||
return createTwilioProvider(config)
|
||||
case "mailgun":
|
||||
return mailgun.NewMailgunProvider(config)
|
||||
return createMailgunProvider(config)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported connector: %s", connector)
|
||||
}
|
||||
|
|
@ -220,7 +228,12 @@ func createProvider(config types.ProviderConfig) (types.Provider, error) {
|
|||
|
||||
// createTwilioProvider creates a unified Twilio provider that handles all message types
|
||||
func createTwilioProvider(config types.ProviderConfig) (types.Provider, error) {
|
||||
return twilio.NewTwilioProvider(config)
|
||||
return twilio.NewTwilioProviderWithTemplateManager(config, template.Global)
|
||||
}
|
||||
|
||||
// createMailgunProvider creates a Mailgun provider with template manager
|
||||
func createMailgunProvider(config types.ProviderConfig) (types.Provider, error) {
|
||||
return mailgun.NewMailgunProviderWithTemplateManager(config, template.Global)
|
||||
}
|
||||
|
||||
// Send sends a message using the specified channel or default provider
|
||||
|
|
@ -286,6 +299,81 @@ func (m *Service) SendWithProvider(ctx context.Context, providerName string, mes
|
|||
return fmt.Errorf("failed to send message after %d attempts: %w", maxAttempts, lastErr)
|
||||
}
|
||||
|
||||
// SendT sends a message using a template
|
||||
func (m *Service) SendT(ctx context.Context, channel string, templateID string, data types.TemplateData) error {
|
||||
// Get providers for the channel
|
||||
providers := m.GetProviders(channel)
|
||||
if len(providers) == 0 {
|
||||
return fmt.Errorf("no providers available for channel: %s", channel)
|
||||
}
|
||||
|
||||
// Use the first available provider's SendT method
|
||||
provider := providers[0]
|
||||
return provider.SendT(ctx, templateID, data)
|
||||
}
|
||||
|
||||
// SendTWithProvider sends a message using a template and specific provider
|
||||
func (m *Service) SendTWithProvider(ctx context.Context, providerName string, templateID string, data types.TemplateData) error {
|
||||
// Get provider
|
||||
provider, exists := m.providers[providerName]
|
||||
if !exists {
|
||||
return fmt.Errorf("provider not found: %s", providerName)
|
||||
}
|
||||
|
||||
// Use the provider's SendT method directly
|
||||
return provider.SendT(ctx, templateID, data)
|
||||
}
|
||||
|
||||
// SendTBatch sends multiple messages using templates in batch
|
||||
func (m *Service) SendTBatch(ctx context.Context, channel string, templateID string, dataList []types.TemplateData) error {
|
||||
// Get providers for the channel
|
||||
providers := m.GetProviders(channel)
|
||||
if len(providers) == 0 {
|
||||
return fmt.Errorf("no providers available for channel: %s", channel)
|
||||
}
|
||||
|
||||
// Use the first available provider's SendTBatch method
|
||||
provider := providers[0]
|
||||
return provider.SendTBatch(ctx, templateID, dataList)
|
||||
}
|
||||
|
||||
// SendTBatchWithProvider sends multiple messages using templates and specific provider in batch
|
||||
func (m *Service) SendTBatchWithProvider(ctx context.Context, providerName string, templateID string, dataList []types.TemplateData) error {
|
||||
// Get provider
|
||||
provider, exists := m.providers[providerName]
|
||||
if !exists {
|
||||
return fmt.Errorf("provider not found: %s", providerName)
|
||||
}
|
||||
|
||||
// Use the provider's SendTBatch method directly
|
||||
return provider.SendTBatch(ctx, templateID, dataList)
|
||||
}
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||
func (m *Service) SendTBatchMixed(ctx context.Context, channel string, templateRequests []types.TemplateRequest) error {
|
||||
// Get providers for the channel
|
||||
providers := m.GetProviders(channel)
|
||||
if len(providers) == 0 {
|
||||
return fmt.Errorf("no providers available for channel: %s", channel)
|
||||
}
|
||||
|
||||
// Use the first available provider's SendTBatchMixed method
|
||||
provider := providers[0]
|
||||
return provider.SendTBatchMixed(ctx, templateRequests)
|
||||
}
|
||||
|
||||
// SendTBatchMixedWithProvider sends multiple messages using different templates with different data and specific provider
|
||||
func (m *Service) SendTBatchMixedWithProvider(ctx context.Context, providerName string, templateRequests []types.TemplateRequest) error {
|
||||
// Get provider
|
||||
provider, exists := m.providers[providerName]
|
||||
if !exists {
|
||||
return fmt.Errorf("provider not found: %s", providerName)
|
||||
}
|
||||
|
||||
// Use the provider's SendTBatchMixed method directly
|
||||
return provider.SendTBatchMixed(ctx, templateRequests)
|
||||
}
|
||||
|
||||
// SendBatch sends multiple messages in batch
|
||||
func (m *Service) SendBatch(ctx context.Context, channel string, messages []*types.Message) error {
|
||||
if len(messages) == 0 {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yaoapp/yao/messenger/template"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
)
|
||||
|
||||
|
|
@ -32,13 +33,22 @@ type Provider struct {
|
|||
imapPassword string
|
||||
imapUseSSL bool
|
||||
imapMailbox string
|
||||
|
||||
// Template manager for template support
|
||||
templateManager types.TemplateManager
|
||||
}
|
||||
|
||||
// NewMailerProvider creates a new Mailer provider
|
||||
func NewMailerProvider(config types.ProviderConfig) (*Provider, error) {
|
||||
return NewMailerProviderWithTemplateManager(config, template.Global)
|
||||
}
|
||||
|
||||
// NewMailerProviderWithTemplateManager creates a new Mailer provider with template manager
|
||||
func NewMailerProviderWithTemplateManager(config types.ProviderConfig, templateManager types.TemplateManager) (*Provider, error) {
|
||||
provider := &Provider{
|
||||
config: config,
|
||||
useTLS: true, // Default to TLS
|
||||
config: config,
|
||||
useTLS: true, // Default to TLS
|
||||
templateManager: templateManager,
|
||||
}
|
||||
|
||||
// Extract options
|
||||
|
|
@ -199,6 +209,77 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
|||
return nil
|
||||
}
|
||||
|
||||
// SendT sends a message using a template
|
||||
func (p *Provider) SendT(ctx context.Context, templateID string, data types.TemplateData) error {
|
||||
// Get template from provider's template manager (mailer supports mail templates)
|
||||
template, err := p.getTemplate(templateID, types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||
}
|
||||
|
||||
// Send message using existing Send method
|
||||
return p.Send(ctx, message)
|
||||
}
|
||||
|
||||
// SendTBatch sends multiple messages using templates in batch
|
||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, dataList []types.TemplateData) error {
|
||||
// Get template from provider's template manager (mailer supports mail templates)
|
||||
template, err := p.getTemplate(templateID, types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
// Convert templates to messages
|
||||
messages := make([]*types.Message, 0, len(dataList))
|
||||
for _, data := range dataList {
|
||||
message, err := template.ToMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send messages using existing SendBatch method
|
||||
return p.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||
func (p *Provider) SendTBatchMixed(ctx context.Context, templateRequests []types.TemplateRequest) error {
|
||||
// Convert template requests to messages
|
||||
messages := make([]*types.Message, 0, len(templateRequests))
|
||||
for _, req := range templateRequests {
|
||||
// Get template from provider's template manager (mailer supports mail templates)
|
||||
template, err := p.getTemplate(req.TemplateID, types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %s, %w", req.TemplateID, err)
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(req.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template %s to message: %w", req.TemplateID, err)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send messages using existing SendBatch method
|
||||
return p.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// getTemplate gets a template by ID and type from the provider's template manager
|
||||
func (p *Provider) getTemplate(templateID string, templateType types.TemplateType) (*types.Template, error) {
|
||||
if p.templateManager == nil {
|
||||
return nil, fmt.Errorf("template manager not available")
|
||||
}
|
||||
return p.templateManager.GetTemplate(templateID, templateType)
|
||||
}
|
||||
|
||||
// GetType returns the provider type
|
||||
func (p *Provider) GetType() string {
|
||||
return "mailer"
|
||||
|
|
|
|||
200
messenger/providers/mailer/mailer_batch_test.go
Normal file
200
messenger/providers/mailer/mailer_batch_test.go
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestSendTBatch_Success(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider with template manager
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailer",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "localhost",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailerProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data for batch sending
|
||||
dataList := []types.TemplateData{
|
||||
{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
{
|
||||
"to": []string{"user2@example.com"},
|
||||
"team_name": "Team B",
|
||||
"inviter_name": "Bob",
|
||||
"invite_link": "https://example.com/invite/2",
|
||||
},
|
||||
}
|
||||
|
||||
// Test SendTBatch - should fail because template manager is nil
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
||||
func TestSendTBatch_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailer",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "localhost",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailerProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
dataList := []types.TemplateData{
|
||||
{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for context to timeout
|
||||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatch with expired context
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
}
|
||||
|
||||
func TestSendTBatchMixed_Success(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider with template manager
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailer",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "localhost",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailerProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data for mixed batch sending
|
||||
templateRequests := []types.TemplateRequest{
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
},
|
||||
{
|
||||
TemplateID: "en.welcome",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user2@example.com"},
|
||||
"user_name": "Bob",
|
||||
"company": "Example Corp",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test SendTBatchMixed - should fail because template manager is nil
|
||||
err = provider.SendTBatchMixed(context.Background(), templateRequests)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
||||
func TestSendTBatchMixed_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailer",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "localhost",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailerProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
templateRequests := []types.TemplateRequest{
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for context to timeout
|
||||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatchMixed with expired context
|
||||
err = provider.SendTBatchMixed(ctx, templateRequests)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
}
|
||||
178
messenger/providers/mailer/mailer_template_test.go
Normal file
178
messenger/providers/mailer/mailer_template_test.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
// Test SendT method
|
||||
|
||||
func TestSendT_TemplateNotImplemented(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create a simple provider config for testing
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailer",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "testpass",
|
||||
"from": "test@example.com",
|
||||
"use_tls": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
templateData := types.TemplateData{
|
||||
"to": []string{"test@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "John Doe",
|
||||
"invite_link": "https://example.com/invite/123",
|
||||
}
|
||||
|
||||
// Test that SendT returns "template not found" error (template system is working)
|
||||
err = provider.SendT(ctx, "en.invite_member.mail", templateData)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template not found")
|
||||
}
|
||||
|
||||
func TestSendT_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create a simple provider config for testing
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailer",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "testpass",
|
||||
"from": "test@example.com",
|
||||
"use_tls": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a very short timeout context to test timeout functionality
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
templateData := types.TemplateData{
|
||||
"to": []string{"test@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "John Doe",
|
||||
"invite_link": "https://example.com/invite/123",
|
||||
}
|
||||
|
||||
err = provider.SendT(ctx, "en.invite_member.mail", templateData)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify it's a context timeout error or not implemented error
|
||||
if strings.Contains(err.Error(), "context deadline exceeded") {
|
||||
t.Log("Context timeout working correctly with template API")
|
||||
} else if strings.Contains(err.Error(), "context canceled") {
|
||||
t.Log("Context cancellation working correctly with template API")
|
||||
} else if strings.Contains(err.Error(), "template not found") {
|
||||
t.Log("Template not found error as expected")
|
||||
} else {
|
||||
t.Logf("Got different error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test template system integration
|
||||
|
||||
func TestTemplateSystem_LoadTemplates(t *testing.T) {
|
||||
// This test verifies that the template system can be loaded
|
||||
// We'll test the template loading logic
|
||||
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Test that we can create a template data structure
|
||||
templateData := types.TemplateData{
|
||||
"team_name": "Awesome Team",
|
||||
"inviter_name": "Alice Johnson",
|
||||
"invite_link": "https://example.com/invite/abc123",
|
||||
"to": []string{"test@example.com"},
|
||||
}
|
||||
|
||||
// Verify template data structure
|
||||
assert.NotNil(t, templateData)
|
||||
assert.Equal(t, "Awesome Team", templateData["team_name"])
|
||||
assert.Equal(t, "Alice Johnson", templateData["inviter_name"])
|
||||
assert.Equal(t, "https://example.com/invite/abc123", templateData["invite_link"])
|
||||
|
||||
// Verify recipients
|
||||
recipients, ok := templateData["to"].([]string)
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, recipients, 1)
|
||||
assert.Equal(t, "test@example.com", recipients[0])
|
||||
}
|
||||
|
||||
// Benchmark Tests
|
||||
|
||||
func BenchmarkSendT(b *testing.B) {
|
||||
// Setup
|
||||
t := &testing.T{}
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailer",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "testpass",
|
||||
"from": "test@example.com",
|
||||
"use_tls": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailerProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
templateData := types.TemplateData{
|
||||
"to": []string{"test@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "John Doe",
|
||||
"invite_link": "https://example.com/invite/123",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// This will return "not implemented" error, but we're measuring the overhead
|
||||
_ = provider.SendT(ctx, "en.invite_member.mail", templateData)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,18 +14,25 @@ import (
|
|||
|
||||
// Provider implements the Provider interface for Mailgun email sending
|
||||
type Provider struct {
|
||||
config types.ProviderConfig
|
||||
domain string
|
||||
apiKey string
|
||||
from string
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
config types.ProviderConfig
|
||||
domain string
|
||||
apiKey string
|
||||
from string
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
templateManager types.TemplateManager
|
||||
}
|
||||
|
||||
// NewMailgunProvider creates a new Mailgun provider
|
||||
func NewMailgunProvider(config types.ProviderConfig) (*Provider, error) {
|
||||
return NewMailgunProviderWithTemplateManager(config, nil)
|
||||
}
|
||||
|
||||
// NewMailgunProviderWithTemplateManager creates a new Mailgun provider with template manager
|
||||
func NewMailgunProviderWithTemplateManager(config types.ProviderConfig, templateManager types.TemplateManager) (*Provider, error) {
|
||||
provider := &Provider{
|
||||
config: config,
|
||||
config: config,
|
||||
templateManager: templateManager,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
|
|
@ -86,6 +93,77 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
|||
return nil
|
||||
}
|
||||
|
||||
// SendT sends a message using a template
|
||||
func (p *Provider) SendT(ctx context.Context, templateID string, data types.TemplateData) error {
|
||||
// Get template from provider's template manager
|
||||
template, err := p.getTemplate(templateID, types.TemplateTypeMail) // Mailgun supports email
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||
}
|
||||
|
||||
// Send message using existing Send method
|
||||
return p.Send(ctx, message)
|
||||
}
|
||||
|
||||
// SendTBatch sends multiple messages using templates in batch
|
||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, dataList []types.TemplateData) error {
|
||||
// Get template from provider's template manager
|
||||
template, err := p.getTemplate(templateID, types.TemplateTypeMail) // Mailgun supports email
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
// Convert templates to messages
|
||||
messages := make([]*types.Message, 0, len(dataList))
|
||||
for _, data := range dataList {
|
||||
message, err := template.ToMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send messages using existing SendBatch method
|
||||
return p.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||
func (p *Provider) SendTBatchMixed(ctx context.Context, templateRequests []types.TemplateRequest) error {
|
||||
// Convert template requests to messages
|
||||
messages := make([]*types.Message, 0, len(templateRequests))
|
||||
for _, req := range templateRequests {
|
||||
// Get template from provider's template manager
|
||||
template, err := p.getTemplate(req.TemplateID, types.TemplateTypeMail) // Mailgun supports email
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %s, %w", req.TemplateID, err)
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(req.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template %s to message: %w", req.TemplateID, err)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send messages using existing SendBatch method
|
||||
return p.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// getTemplate gets a template by ID and type from the provider's template manager
|
||||
func (p *Provider) getTemplate(templateID string, templateType types.TemplateType) (*types.Template, error) {
|
||||
if p.templateManager == nil {
|
||||
return nil, fmt.Errorf("template manager not available")
|
||||
}
|
||||
return p.templateManager.GetTemplate(templateID, templateType)
|
||||
}
|
||||
|
||||
// GetType returns the provider type
|
||||
func (p *Provider) GetType() string {
|
||||
return "mailgun"
|
||||
|
|
|
|||
184
messenger/providers/mailgun/mailgun_batch_test.go
Normal file
184
messenger/providers/mailgun/mailgun_batch_test.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestSendTBatch_Success(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider with template manager
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailgun",
|
||||
Connector: "mailgun",
|
||||
Options: map[string]interface{}{
|
||||
"domain": "test.example.com",
|
||||
"api_key": "test_api_key",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailgunProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data for batch sending
|
||||
dataList := []types.TemplateData{
|
||||
{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
{
|
||||
"to": []string{"user2@example.com"},
|
||||
"team_name": "Team B",
|
||||
"inviter_name": "Bob",
|
||||
"invite_link": "https://example.com/invite/2",
|
||||
},
|
||||
}
|
||||
|
||||
// Test SendTBatch - should fail because template manager is nil
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
||||
func TestSendTBatch_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailgun",
|
||||
Connector: "mailgun",
|
||||
Options: map[string]interface{}{
|
||||
"domain": "test.example.com",
|
||||
"api_key": "test_api_key",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailgunProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
dataList := []types.TemplateData{
|
||||
{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for context to timeout
|
||||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatch with expired context
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
}
|
||||
|
||||
func TestSendTBatchMixed_Success(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider with template manager
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailgun",
|
||||
Connector: "mailgun",
|
||||
Options: map[string]interface{}{
|
||||
"domain": "test.example.com",
|
||||
"api_key": "test_api_key",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailgunProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data for mixed batch sending
|
||||
templateRequests := []types.TemplateRequest{
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
},
|
||||
{
|
||||
TemplateID: "en.welcome",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user2@example.com"},
|
||||
"user_name": "Bob",
|
||||
"company": "Example Corp",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test SendTBatchMixed - should fail because template manager is nil
|
||||
err = provider.SendTBatchMixed(context.Background(), templateRequests)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
||||
func TestSendTBatchMixed_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailgun",
|
||||
Connector: "mailgun",
|
||||
Options: map[string]interface{}{
|
||||
"domain": "test.example.com",
|
||||
"api_key": "test_api_key",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailgunProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
templateRequests := []types.TemplateRequest{
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for context to timeout
|
||||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatchMixed with expired context
|
||||
err = provider.SendTBatchMixed(ctx, templateRequests)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
}
|
||||
96
messenger/providers/mailgun/mailgun_template_test.go
Normal file
96
messenger/providers/mailgun/mailgun_template_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestSendT_TemplateNotImplemented(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create provider with minimal config
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailgun",
|
||||
Connector: "mailgun",
|
||||
Options: map[string]interface{}{
|
||||
"api_key": "test_api_key",
|
||||
"domain": "test.example.com",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailgunProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
templateData := types.TemplateData{
|
||||
"to": []string{"test@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "John Doe",
|
||||
"invite_link": "https://example.com/invite/123",
|
||||
}
|
||||
|
||||
// Test that SendT returns "template manager not available" error
|
||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
||||
func TestSendT_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create provider with minimal config
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-mailgun",
|
||||
Connector: "mailgun",
|
||||
Options: map[string]interface{}{
|
||||
"api_key": "test_api_key",
|
||||
"domain": "test.example.com",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewMailgunProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for timeout
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
templateData := types.TemplateData{
|
||||
"to": []string{"test@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "John Doe",
|
||||
"invite_link": "https://example.com/invite/123",
|
||||
}
|
||||
|
||||
// Test that SendT handles context timeout
|
||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify it's a context timeout error or template manager error
|
||||
if strings.Contains(err.Error(), "context deadline exceeded") {
|
||||
t.Log("Context timeout working correctly with template API")
|
||||
} else if strings.Contains(err.Error(), "context canceled") {
|
||||
t.Log("Context cancellation working correctly with template API")
|
||||
} else if strings.Contains(err.Error(), "template manager not available") {
|
||||
t.Log("Template manager not available error as expected")
|
||||
} else {
|
||||
t.Logf("Got different error: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,12 +28,19 @@ type Provider struct {
|
|||
sendGridAPIKey string
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
templateManager types.TemplateManager
|
||||
}
|
||||
|
||||
// NewTwilioProvider creates a new unified Twilio provider
|
||||
func NewTwilioProvider(config types.ProviderConfig) (*Provider, error) {
|
||||
return NewTwilioProviderWithTemplateManager(config, nil)
|
||||
}
|
||||
|
||||
// NewTwilioProviderWithTemplateManager creates a new Twilio provider with template manager
|
||||
func NewTwilioProviderWithTemplateManager(config types.ProviderConfig, templateManager types.TemplateManager) (*Provider, error) {
|
||||
provider := &Provider{
|
||||
config: config,
|
||||
config: config,
|
||||
templateManager: templateManager,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
|
|
@ -118,6 +125,77 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
|||
return nil
|
||||
}
|
||||
|
||||
// SendT sends a message using a template
|
||||
func (p *Provider) SendT(ctx context.Context, templateID string, data types.TemplateData) error {
|
||||
// Get template from provider's template manager
|
||||
template, err := p.getTemplate(templateID, types.TemplateTypeSMS) // Twilio primarily supports SMS
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||
}
|
||||
|
||||
// Send message using existing Send method
|
||||
return p.Send(ctx, message)
|
||||
}
|
||||
|
||||
// SendTBatch sends multiple messages using templates in batch
|
||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, dataList []types.TemplateData) error {
|
||||
// Get template from provider's template manager
|
||||
template, err := p.getTemplate(templateID, types.TemplateTypeSMS) // Twilio primarily supports SMS
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
// Convert templates to messages
|
||||
messages := make([]*types.Message, 0, len(dataList))
|
||||
for _, data := range dataList {
|
||||
message, err := template.ToMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send messages using existing SendBatch method
|
||||
return p.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||
func (p *Provider) SendTBatchMixed(ctx context.Context, templateRequests []types.TemplateRequest) error {
|
||||
// Convert template requests to messages
|
||||
messages := make([]*types.Message, 0, len(templateRequests))
|
||||
for _, req := range templateRequests {
|
||||
// Get template from provider's template manager
|
||||
template, err := p.getTemplate(req.TemplateID, types.TemplateTypeSMS) // Twilio primarily supports SMS
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %s, %w", req.TemplateID, err)
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(req.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template %s to message: %w", req.TemplateID, err)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send messages using existing SendBatch method
|
||||
return p.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// getTemplate gets a template by ID and type from the provider's template manager
|
||||
func (p *Provider) getTemplate(templateID string, templateType types.TemplateType) (*types.Template, error) {
|
||||
if p.templateManager == nil {
|
||||
return nil, fmt.Errorf("template manager not available")
|
||||
}
|
||||
return p.templateManager.GetTemplate(templateID, templateType)
|
||||
}
|
||||
|
||||
// GetType returns the provider type
|
||||
func (p *Provider) GetType() string {
|
||||
return "twilio"
|
||||
|
|
|
|||
184
messenger/providers/twilio/twilio_batch_test.go
Normal file
184
messenger/providers/twilio/twilio_batch_test.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package twilio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestSendTBatch_Success(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider with template manager
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-twilio",
|
||||
Connector: "twilio",
|
||||
Options: map[string]interface{}{
|
||||
"account_sid": "test_account_sid",
|
||||
"auth_token": "test_auth_token",
|
||||
"from": "+1234567890",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewTwilioProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data for batch sending
|
||||
dataList := []types.TemplateData{
|
||||
{
|
||||
"to": []string{"+1234567890"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
{
|
||||
"to": []string{"+0987654321"},
|
||||
"team_name": "Team B",
|
||||
"inviter_name": "Bob",
|
||||
"invite_link": "https://example.com/invite/2",
|
||||
},
|
||||
}
|
||||
|
||||
// Test SendTBatch - should fail because template manager is nil
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
||||
func TestSendTBatch_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-twilio",
|
||||
Connector: "twilio",
|
||||
Options: map[string]interface{}{
|
||||
"account_sid": "test_account_sid",
|
||||
"auth_token": "test_auth_token",
|
||||
"from": "+1234567890",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewTwilioProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
dataList := []types.TemplateData{
|
||||
{
|
||||
"to": []string{"+1234567890"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for context to timeout
|
||||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatch with expired context
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
}
|
||||
|
||||
func TestSendTBatchMixed_Success(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider with template manager
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-twilio",
|
||||
Connector: "twilio",
|
||||
Options: map[string]interface{}{
|
||||
"account_sid": "test_account_sid",
|
||||
"auth_token": "test_auth_token",
|
||||
"from": "+1234567890",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewTwilioProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data for mixed batch sending
|
||||
templateRequests := []types.TemplateRequest{
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"+1234567890"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
},
|
||||
{
|
||||
TemplateID: "en.welcome",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"+0987654321"},
|
||||
"user_name": "Bob",
|
||||
"company": "Example Corp",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test SendTBatchMixed - should fail because template manager is nil
|
||||
err = provider.SendTBatchMixed(context.Background(), templateRequests)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
||||
func TestSendTBatchMixed_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
|
||||
// Create provider
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-twilio",
|
||||
Connector: "twilio",
|
||||
Options: map[string]interface{}{
|
||||
"account_sid": "test_account_sid",
|
||||
"auth_token": "test_auth_token",
|
||||
"from": "+1234567890",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewTwilioProviderWithTemplateManager(config, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
templateRequests := []types.TemplateRequest{
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"+1234567890"},
|
||||
"team_name": "Team A",
|
||||
"inviter_name": "Alice",
|
||||
"invite_link": "https://example.com/invite/1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for context to timeout
|
||||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatchMixed with expired context
|
||||
err = provider.SendTBatchMixed(ctx, templateRequests)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
}
|
||||
94
messenger/providers/twilio/twilio_template_test.go
Normal file
94
messenger/providers/twilio/twilio_template_test.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package twilio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestSendT_TemplateNotImplemented(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create provider with minimal config
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-twilio",
|
||||
Connector: "twilio",
|
||||
Options: map[string]interface{}{
|
||||
"account_sid": "test_account_sid",
|
||||
"auth_token": "test_auth_token",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewTwilioProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
templateData := types.TemplateData{
|
||||
"to": []string{"+1234567890"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "John Doe",
|
||||
"invite_link": "https://example.com/invite/123",
|
||||
}
|
||||
|
||||
// Test that SendT returns "template manager not available" error
|
||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
||||
func TestSendT_ContextTimeout(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create provider with minimal config
|
||||
config := types.ProviderConfig{
|
||||
Name: "test-twilio",
|
||||
Connector: "twilio",
|
||||
Options: map[string]interface{}{
|
||||
"account_sid": "test_account_sid",
|
||||
"auth_token": "test_auth_token",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewTwilioProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for timeout
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
templateData := types.TemplateData{
|
||||
"to": []string{"+1234567890"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "John Doe",
|
||||
"invite_link": "https://example.com/invite/123",
|
||||
}
|
||||
|
||||
// Test that SendT handles context timeout
|
||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify it's a context timeout error or template manager error
|
||||
if strings.Contains(err.Error(), "context deadline exceeded") {
|
||||
t.Log("Context timeout working correctly with template API")
|
||||
} else if strings.Contains(err.Error(), "context canceled") {
|
||||
t.Log("Context cancellation working correctly with template API")
|
||||
} else if strings.Contains(err.Error(), "template manager not available") {
|
||||
t.Log("Template manager not available error as expected")
|
||||
} else {
|
||||
t.Logf("Got different error: %v", err)
|
||||
}
|
||||
}
|
||||
47
messenger/template/debug_test.go
Normal file
47
messenger/template/debug_test.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yaoapp/gou/application"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestDebugTemplateLoading(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Test direct template loading
|
||||
t.Log("Testing direct template loading...")
|
||||
|
||||
// Try to load a specific template file using application.App
|
||||
content, err := application.App.Read("messengers/templates/en/invite_member.mail.html")
|
||||
if err != nil {
|
||||
t.Logf("Could not read template file: %v", err)
|
||||
} else {
|
||||
t.Logf("Template file content length: %d", len(content))
|
||||
t.Logf("First 200 chars: %s", content[:min(200, len(content))])
|
||||
}
|
||||
|
||||
// Test template parsing
|
||||
subject, body, html, err := parseTemplateContent(string(content), types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
t.Logf("Could not parse template: %v", err)
|
||||
} else {
|
||||
t.Logf("Parsed template content:")
|
||||
t.Logf("Subject: %s", subject)
|
||||
t.Logf("Body length: %d", len(body))
|
||||
t.Logf("HTML length: %d", len(html))
|
||||
t.Logf("Body content: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
36
messenger/template/load_test.go
Normal file
36
messenger/template/load_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/share"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestLoadTemplate(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Test loading a specific template
|
||||
file := "messengers/templates/en/invite_member.mail.html"
|
||||
templateID := share.ID("messengers/templates", file)
|
||||
|
||||
t.Logf("Testing loadTemplate with file: %s, templateID: %s", file, templateID)
|
||||
|
||||
template, err := loadTemplate(file, templateID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load template: %v", err)
|
||||
}
|
||||
|
||||
if template == nil {
|
||||
t.Fatal("Template is nil")
|
||||
}
|
||||
|
||||
t.Logf("Loaded template: ID=%s, Type=%s, Language=%s", template.ID, template.Type, template.Language)
|
||||
t.Logf("Subject: %s", template.Subject)
|
||||
t.Logf("Body length: %d", len(template.Body))
|
||||
t.Logf("HTML length: %d", len(template.HTML))
|
||||
t.Logf("Body content: %s", template.Body)
|
||||
}
|
||||
154
messenger/template/render_test.go
Normal file
154
messenger/template/render_test.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestTemplateRender(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load templates
|
||||
err := LoadTemplates()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load templates: %v", err)
|
||||
}
|
||||
|
||||
// Get template
|
||||
template, err := Global.GetTemplate("en.invite_member", types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get template: %v", err)
|
||||
}
|
||||
|
||||
// Test data
|
||||
data := types.TemplateData{
|
||||
"to": []string{"test@example.com"},
|
||||
"team_name": "Awesome Team",
|
||||
"inviter_name": "Alice Johnson",
|
||||
"invite_link": "https://example.com/invite/abc123",
|
||||
}
|
||||
|
||||
// Test rendering
|
||||
subject, body, html, err := template.Render(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render template: %v", err)
|
||||
}
|
||||
|
||||
// Verify rendered content
|
||||
t.Logf("Rendered subject: %s", subject)
|
||||
t.Logf("Rendered body length: %d", len(body))
|
||||
t.Logf("Rendered HTML length: %d", len(html))
|
||||
|
||||
// Check that variables were replaced
|
||||
if !contains(subject, "Awesome Team") {
|
||||
t.Errorf("Subject should contain 'Awesome Team', got: %s", subject)
|
||||
}
|
||||
if !contains(body, "Alice Johnson") {
|
||||
t.Errorf("Body should contain 'Alice Johnson', got: %s", body)
|
||||
}
|
||||
if !contains(body, "https://example.com/invite/abc123") {
|
||||
t.Errorf("Body should contain invite link, got: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateToMessage(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load templates
|
||||
err := LoadTemplates()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load templates: %v", err)
|
||||
}
|
||||
|
||||
// Get template
|
||||
template, err := Global.GetTemplate("en.invite_member", types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get template: %v", err)
|
||||
}
|
||||
|
||||
// Test data
|
||||
data := types.TemplateData{
|
||||
"to": []string{"test@example.com", "user@example.com"},
|
||||
"team_name": "Awesome Team",
|
||||
"inviter_name": "Alice Johnson",
|
||||
"invite_link": "https://example.com/invite/abc123",
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to convert template to message: %v", err)
|
||||
}
|
||||
|
||||
// Verify message properties
|
||||
if message.Type != types.MessageType("mail") {
|
||||
t.Errorf("Expected message type 'mail', got %s", message.Type)
|
||||
}
|
||||
if len(message.To) != 2 {
|
||||
t.Errorf("Expected 2 recipients, got %d", len(message.To))
|
||||
}
|
||||
if !contains(message.Subject, "Awesome Team") {
|
||||
t.Errorf("Subject should contain 'Awesome Team', got: %s", message.Subject)
|
||||
}
|
||||
if !contains(message.Body, "Alice Johnson") {
|
||||
t.Errorf("Body should contain 'Alice Johnson', got: %s", message.Body)
|
||||
}
|
||||
|
||||
t.Logf("Generated message: Subject=%s, To=%v", message.Subject, message.To)
|
||||
}
|
||||
|
||||
func TestNestedTemplateRender(t *testing.T) {
|
||||
// Test nested object access
|
||||
template := &types.Template{
|
||||
Subject: "Hello {{ user.name }}, welcome to {{ team.name }}!",
|
||||
Body: "Your role is {{ user.role }} in {{ team.department.name }}.",
|
||||
}
|
||||
|
||||
data := types.TemplateData{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John Doe",
|
||||
"role": "Developer",
|
||||
},
|
||||
"team": map[string]interface{}{
|
||||
"name": "Awesome Team",
|
||||
"department": map[string]interface{}{
|
||||
"name": "Engineering",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
subject, body, _, err := template.Render(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render template: %v", err)
|
||||
}
|
||||
|
||||
// Verify nested access works
|
||||
if !contains(subject, "John Doe") {
|
||||
t.Errorf("Subject should contain 'John Doe', got: %s", subject)
|
||||
}
|
||||
if !contains(subject, "Awesome Team") {
|
||||
t.Errorf("Subject should contain 'Awesome Team', got: %s", subject)
|
||||
}
|
||||
if !contains(body, "Developer") {
|
||||
t.Errorf("Body should contain 'Developer', got: %s", body)
|
||||
}
|
||||
if !contains(body, "Engineering") {
|
||||
t.Errorf("Body should contain 'Engineering', got: %s", body)
|
||||
}
|
||||
|
||||
t.Logf("Nested render - Subject: %s", subject)
|
||||
t.Logf("Nested render - Body: %s", body)
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
|
||||
contains(s[1:], substr))))
|
||||
}
|
||||
263
messenger/template/template.go
Normal file
263
messenger/template/template.go
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/yaoapp/gou/application"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
)
|
||||
|
||||
// Manager manages message templates
|
||||
type Manager struct {
|
||||
templates map[string]map[types.TemplateType]*types.Template // [templateID][type] -> template
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Global template manager instance
|
||||
var Global *Manager = &Manager{
|
||||
templates: make(map[string]map[types.TemplateType]*types.Template),
|
||||
}
|
||||
|
||||
// GetTemplate returns a template by ID and type
|
||||
func (m *Manager) GetTemplate(templateID string, templateType types.TemplateType) (*types.Template, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
if templates, exists := m.templates[templateID]; exists {
|
||||
if template, typeExists := templates[templateType]; typeExists {
|
||||
return template, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("template not found: %s.%s", templateID, templateType)
|
||||
}
|
||||
|
||||
// GetAllTemplates returns all loaded templates
|
||||
func (m *Manager) GetAllTemplates() map[string]map[types.TemplateType]*types.Template {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modifications
|
||||
result := make(map[string]map[types.TemplateType]*types.Template)
|
||||
for id, templates := range m.templates {
|
||||
result[id] = make(map[types.TemplateType]*types.Template)
|
||||
for templateType, template := range templates {
|
||||
result[id][templateType] = template
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ReloadTemplates reloads all templates from disk
|
||||
func (m *Manager) ReloadTemplates() error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Clear existing templates
|
||||
m.templates = make(map[string]map[types.TemplateType]*types.Template)
|
||||
|
||||
// Load templates from disk
|
||||
return loadTemplates(m)
|
||||
}
|
||||
|
||||
// loadTemplates loads all templates from the templates directory
|
||||
func loadTemplates(m *Manager) error {
|
||||
// Check if templates directory exists
|
||||
templatesPath := "messengers/templates"
|
||||
exists, err := application.App.Exists(templatesPath)
|
||||
if err != nil {
|
||||
log.Error("[Template] Error checking templates directory: %v", err)
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
log.Warn("[Template] templates directory not found, skip loading templates")
|
||||
return nil
|
||||
}
|
||||
log.Info("[Template] Templates directory exists, starting to load templates")
|
||||
|
||||
// Walk through template files
|
||||
// Pattern: {name}.{type}.html and {name}.{type}.txt
|
||||
exts := []string{"*.mail.html", "*.sms.txt", "*.whatsapp.html"}
|
||||
log.Info("[Template] Starting to walk templates directory with extensions: %v", exts)
|
||||
err = application.App.Walk(templatesPath, func(root, file string, isdir bool) error {
|
||||
log.Info("[Template] Walk callback: root=%s, file=%s, isdir=%v", root, file, isdir)
|
||||
if isdir {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("[Template] Processing file: %s", file)
|
||||
// Generate template ID manually to avoid share.ID's dot-to-underscore conversion
|
||||
// Format: {language}.{name} (e.g., "en.invite_member")
|
||||
relativePath := strings.TrimPrefix(file, root+"/")
|
||||
pathParts := strings.Split(relativePath, "/")
|
||||
language := pathParts[0]
|
||||
filename := pathParts[len(pathParts)-1]
|
||||
baseName := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
// Remove type suffix (e.g., "invite_member.mail" -> "invite_member")
|
||||
templateName := strings.Split(baseName, ".")[0]
|
||||
templateID := fmt.Sprintf("%s.%s", language, templateName)
|
||||
|
||||
log.Info("[Template] Generated templateID: %s for file: %s", templateID, file)
|
||||
template, err := loadTemplate(file, templateID)
|
||||
if err != nil {
|
||||
log.Warn("[Template] Failed to load template %s: %v", file, err)
|
||||
return nil // Continue loading other templates
|
||||
}
|
||||
|
||||
if template != nil {
|
||||
log.Info("[Template] Loaded template: %s.%s", template.ID, template.Type)
|
||||
// Initialize template map for this ID if it doesn't exist
|
||||
if m.templates[template.ID] == nil {
|
||||
m.templates[template.ID] = make(map[types.TemplateType]*types.Template)
|
||||
}
|
||||
m.templates[template.ID][template.Type] = template
|
||||
}
|
||||
return nil
|
||||
}, exts...)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("[Template] Loaded %d templates", len(m.templates))
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadTemplate loads a single template file
|
||||
func loadTemplate(file string, templateID string) (*types.Template, error) {
|
||||
raw, err := application.App.Read(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract filename from file path to determine template type
|
||||
filename := filepath.Base(file)
|
||||
baseName := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
|
||||
// Parse template type from filename
|
||||
// Format: {name}.{type}.{ext} -> {type}
|
||||
templateType, _ := parseTemplateType(baseName)
|
||||
|
||||
// Use the provided templateID (already in format: language.name)
|
||||
fullTemplateID := templateID
|
||||
|
||||
// Extract language from templateID (format: language.name)
|
||||
parts := strings.Split(templateID, ".")
|
||||
language := parts[0]
|
||||
|
||||
// Determine template type
|
||||
var msgType types.TemplateType
|
||||
switch templateType {
|
||||
case "mail":
|
||||
msgType = types.TemplateTypeMail
|
||||
case "sms":
|
||||
msgType = types.TemplateTypeSMS
|
||||
case "whatsapp":
|
||||
msgType = types.TemplateTypeWhatsApp
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported template type: %s", templateType)
|
||||
}
|
||||
|
||||
// Parse template content
|
||||
subject, body, html, err := parseTemplateContent(string(raw), msgType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// No need to compile templates - we'll use simple string replacement
|
||||
|
||||
return &types.Template{
|
||||
ID: fullTemplateID,
|
||||
Type: msgType,
|
||||
Language: language,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
HTML: html,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseTemplateType parses template type from filename
|
||||
// Example: "invite_member.mail" -> "mail", "invite_member"
|
||||
func parseTemplateType(filename string) (templateType, templateName string) {
|
||||
parts := strings.Split(filename, ".")
|
||||
if len(parts) < 2 {
|
||||
return "", filename
|
||||
}
|
||||
|
||||
// Last part is the type
|
||||
templateType = parts[len(parts)-1]
|
||||
|
||||
// Everything before the last part is the name
|
||||
templateName = strings.Join(parts[:len(parts)-1], ".")
|
||||
|
||||
return templateType, templateName
|
||||
}
|
||||
|
||||
// parseTemplateContent parses template content based on type
|
||||
func parseTemplateContent(content string, templateType types.TemplateType) (subject, body, html string, err error) {
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
switch templateType {
|
||||
case types.TemplateTypeMail:
|
||||
return parseMailTemplate(content)
|
||||
case types.TemplateTypeSMS:
|
||||
return parseSMSTemplate(content)
|
||||
case types.TemplateTypeWhatsApp:
|
||||
return parseWhatsAppTemplate(content)
|
||||
default:
|
||||
return "", "", "", fmt.Errorf("unsupported template type: %s", templateType)
|
||||
}
|
||||
}
|
||||
|
||||
// parseMailTemplate parses mail template with HTML structure using goquery
|
||||
func parseMailTemplate(content string) (subject, body, html string, err error) {
|
||||
// Parse HTML content with goquery
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("failed to parse mail template HTML: %w", err)
|
||||
}
|
||||
|
||||
// Extract subject from <Subject> tag
|
||||
subject = strings.TrimSpace(doc.Find("Subject").Text())
|
||||
|
||||
// Extract body content from <body> tag
|
||||
bodySelection := doc.Find("body")
|
||||
if bodySelection.Length() == 0 {
|
||||
return "", "", "", fmt.Errorf("no <body> tag found in mail template")
|
||||
}
|
||||
|
||||
// Get the HTML content of the body tag
|
||||
body, err = bodySelection.Html()
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("failed to extract body HTML: %w", err)
|
||||
}
|
||||
|
||||
body = strings.TrimSpace(body)
|
||||
|
||||
// For mail templates, body is HTML content
|
||||
html = body
|
||||
|
||||
return subject, body, html, nil
|
||||
}
|
||||
|
||||
// parseSMSTemplate parses SMS template (plain text)
|
||||
func parseSMSTemplate(content string) (subject, body, html string, err error) {
|
||||
// SMS templates are just plain text
|
||||
body = content
|
||||
return "", body, "", nil
|
||||
}
|
||||
|
||||
// parseWhatsAppTemplate parses WhatsApp template (similar to mail)
|
||||
func parseWhatsAppTemplate(content string) (subject, body, html string, err error) {
|
||||
// WhatsApp templates use similar structure to mail
|
||||
return parseMailTemplate(content)
|
||||
}
|
||||
|
||||
// LoadTemplates loads all templates during messenger initialization
|
||||
func LoadTemplates() error {
|
||||
return Global.ReloadTemplates()
|
||||
}
|
||||
189
messenger/template/template_test.go
Normal file
189
messenger/template/template_test.go
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestTemplateManager_LoadTemplates(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load templates
|
||||
err := LoadTemplates()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check if templates were loaded
|
||||
templates := Global.GetAllTemplates()
|
||||
assert.NotNil(t, templates)
|
||||
|
||||
// Log loaded templates for debugging
|
||||
t.Logf("Loaded %d template groups", len(templates))
|
||||
for _, templateGroup := range templates {
|
||||
for _, template := range templateGroup {
|
||||
t.Logf("Template: %s, Type: %s, Language: %s", template.ID, template.Type, template.Language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateManager_GetTemplate(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load templates
|
||||
err := LoadTemplates()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test getting a specific template
|
||||
template, err := Global.GetTemplate("en.invite_member", types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
t.Logf("Template not found (expected if templates not loaded): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify template properties
|
||||
assert.NotNil(t, template)
|
||||
assert.Equal(t, "en.invite_member", template.ID)
|
||||
assert.Equal(t, types.TemplateTypeMail, template.Type)
|
||||
assert.Equal(t, "en", template.Language)
|
||||
assert.NotEmpty(t, template.Subject)
|
||||
assert.NotEmpty(t, template.Body)
|
||||
}
|
||||
|
||||
func TestTemplate_Render(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load templates
|
||||
err := LoadTemplates()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test template rendering
|
||||
template, err := Global.GetTemplate("en.invite_member", types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
t.Logf("Template not found (expected if templates not loaded): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test data
|
||||
data := types.TemplateData{
|
||||
"team_name": "Awesome Team",
|
||||
"inviter_name": "Alice Johnson",
|
||||
"invite_link": "https://example.com/invite/abc123",
|
||||
}
|
||||
|
||||
// Render template
|
||||
subject, body, html, err := template.Render(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify rendered content
|
||||
assert.NotEmpty(t, subject)
|
||||
assert.NotEmpty(t, body)
|
||||
assert.NotEmpty(t, html)
|
||||
|
||||
// Check that variables were replaced
|
||||
assert.Contains(t, subject, "Awesome Team")
|
||||
assert.Contains(t, body, "Alice Johnson")
|
||||
assert.Contains(t, body, "https://example.com/invite/abc123")
|
||||
|
||||
t.Logf("Rendered subject: %s", subject)
|
||||
t.Logf("Rendered body: %s", body)
|
||||
}
|
||||
|
||||
func TestTemplate_ToMessage(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load templates
|
||||
err := LoadTemplates()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test template to message conversion
|
||||
template, err := Global.GetTemplate("en.invite_member", types.TemplateTypeMail)
|
||||
if err != nil {
|
||||
t.Logf("Template not found (expected if templates not loaded): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test data with recipients
|
||||
data := types.TemplateData{
|
||||
"to": []string{"test@example.com", "user@example.com"},
|
||||
"team_name": "Awesome Team",
|
||||
"inviter_name": "Alice Johnson",
|
||||
"invite_link": "https://example.com/invite/abc123",
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify message properties
|
||||
assert.NotNil(t, message)
|
||||
assert.Equal(t, types.MessageType("mail"), message.Type)
|
||||
assert.NotEmpty(t, message.Subject)
|
||||
assert.NotEmpty(t, message.Body)
|
||||
assert.NotEmpty(t, message.HTML)
|
||||
assert.Len(t, message.To, 2)
|
||||
assert.Equal(t, "test@example.com", message.To[0])
|
||||
assert.Equal(t, "user@example.com", message.To[1])
|
||||
|
||||
// Check that variables were replaced
|
||||
assert.Contains(t, message.Subject, "Awesome Team")
|
||||
assert.Contains(t, message.Body, "Alice Johnson")
|
||||
assert.Contains(t, message.Body, "https://example.com/invite/abc123")
|
||||
|
||||
t.Logf("Generated message: Subject=%s, To=%v", message.Subject, message.To)
|
||||
}
|
||||
|
||||
func TestTemplate_SMSTemplate(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load templates
|
||||
err := LoadTemplates()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test SMS template
|
||||
template, err := Global.GetTemplate("en.invite_member", types.TemplateTypeSMS)
|
||||
if err != nil {
|
||||
t.Logf("SMS template not found (expected if templates not loaded): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test data
|
||||
data := types.TemplateData{
|
||||
"to": []string{"+1234567890"},
|
||||
"team_name": "Awesome Team",
|
||||
"inviter_name": "Alice Johnson",
|
||||
"invite_link": "https://example.com/invite/abc123",
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := template.ToMessage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify SMS message properties
|
||||
assert.NotNil(t, message)
|
||||
assert.Equal(t, types.MessageTypeSMS, message.Type)
|
||||
assert.NotEmpty(t, message.Body)
|
||||
assert.Empty(t, message.HTML) // SMS should not have HTML
|
||||
assert.Len(t, message.To, 1)
|
||||
assert.Equal(t, "+1234567890", message.To[0])
|
||||
|
||||
// Check that variables were replaced
|
||||
assert.Contains(t, message.Body, "Alice Johnson")
|
||||
assert.Contains(t, message.Body, "Awesome Team")
|
||||
assert.Contains(t, message.Body, "https://example.com/invite/abc123")
|
||||
|
||||
t.Logf("Generated SMS message: Body=%s, To=%v", message.Body, message.To)
|
||||
}
|
||||
49
messenger/template/walk_test.go
Normal file
49
messenger/template/walk_test.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yaoapp/gou/application"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestWalkTemplates(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Test Walk function directly
|
||||
t.Log("Testing Walk function directly...")
|
||||
|
||||
// Check if templates directory exists
|
||||
exists, err := application.App.Exists("messengers/templates")
|
||||
if err != nil {
|
||||
t.Fatalf("Error checking templates directory: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Log("Templates directory not found")
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("Templates directory exists")
|
||||
|
||||
// Test Walk with different extensions
|
||||
exts := []string{"*.mail.html", "*.sms.txt", "*.whatsapp.html"}
|
||||
t.Logf("Testing Walk with extensions: %v", exts)
|
||||
|
||||
fileCount := 0
|
||||
err = application.App.Walk("messengers/templates", func(root, file string, isdir bool) error {
|
||||
t.Logf("Walk callback: root=%s, file=%s, isdir=%v", root, file, isdir)
|
||||
if !isdir {
|
||||
fileCount++
|
||||
}
|
||||
return nil
|
||||
}, exts...)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Walk failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Walk completed, found %d files", fileCount)
|
||||
}
|
||||
|
|
@ -13,6 +13,15 @@ type Provider interface {
|
|||
// SendBatch sends multiple messages in batch
|
||||
SendBatch(ctx context.Context, messages []*Message) error
|
||||
|
||||
// SendT sends a message using a template (optional - providers may return "not implemented" error)
|
||||
SendT(ctx context.Context, templateID string, data TemplateData) error
|
||||
|
||||
// SendTBatch sends multiple messages using the same template with different data (optional - providers may return "not implemented" error)
|
||||
SendTBatch(ctx context.Context, templateID string, dataList []TemplateData) error
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data (optional - providers may return "not implemented" error)
|
||||
SendTBatchMixed(ctx context.Context, templateRequests []TemplateRequest) error
|
||||
|
||||
// TriggerWebhook processes webhook requests and converts to Message
|
||||
TriggerWebhook(c interface{}) (*Message, error)
|
||||
|
||||
|
|
@ -40,6 +49,24 @@ type Messenger interface {
|
|||
// SendWithProvider sends a message using a specific provider
|
||||
SendWithProvider(ctx context.Context, providerName string, message *Message) error
|
||||
|
||||
// SendT sends a message using a template
|
||||
SendT(ctx context.Context, channel string, templateID string, data TemplateData) error
|
||||
|
||||
// SendTWithProvider sends a message using a template and specific provider
|
||||
SendTWithProvider(ctx context.Context, providerName string, templateID string, data TemplateData) error
|
||||
|
||||
// SendTBatch sends multiple messages using the same template with different data
|
||||
SendTBatch(ctx context.Context, channel string, templateID string, dataList []TemplateData) error
|
||||
|
||||
// SendTBatchWithProvider sends multiple messages using the same template with different data and specific provider
|
||||
SendTBatchWithProvider(ctx context.Context, providerName string, templateID string, dataList []TemplateData) error
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||
SendTBatchMixed(ctx context.Context, channel string, templateRequests []TemplateRequest) error
|
||||
|
||||
// SendTBatchMixedWithProvider sends multiple messages using different templates with different data and specific provider
|
||||
SendTBatchMixedWithProvider(ctx context.Context, providerName string, templateRequests []TemplateRequest) error
|
||||
|
||||
// SendBatch sends multiple messages in batch
|
||||
SendBatch(ctx context.Context, channel string, messages []*Message) error
|
||||
|
||||
|
|
|
|||
146
messenger/types/template.go
Normal file
146
messenger/types/template.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TemplateType represents the type of template (mail, sms, whatsapp)
|
||||
type TemplateType string
|
||||
|
||||
const (
|
||||
TemplateTypeMail TemplateType = "mail"
|
||||
TemplateTypeSMS TemplateType = "sms"
|
||||
TemplateTypeWhatsApp TemplateType = "whatsapp"
|
||||
)
|
||||
|
||||
// Template represents a message template
|
||||
type Template struct {
|
||||
ID string `json:"id"`
|
||||
Type TemplateType `json:"type"`
|
||||
Language string `json:"language"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
HTML string `json:"html,omitempty"`
|
||||
}
|
||||
|
||||
// TemplateData represents data to be used in template rendering
|
||||
type TemplateData map[string]interface{}
|
||||
|
||||
// Render renders the template with the provided data using simple string replacement
|
||||
func (t *Template) Render(data TemplateData) (subject, body, html string, err error) {
|
||||
// Render subject if available
|
||||
if t.Subject != "" {
|
||||
subject = renderTemplate(t.Subject, data)
|
||||
}
|
||||
|
||||
// Render body
|
||||
if t.Body != "" {
|
||||
body = renderTemplate(t.Body, data)
|
||||
}
|
||||
|
||||
// Render HTML if available
|
||||
if t.HTML != "" {
|
||||
html = renderTemplate(t.HTML, data)
|
||||
}
|
||||
|
||||
return subject, body, html, nil
|
||||
}
|
||||
|
||||
// ToMessage converts template to Message with provided data
|
||||
func (t *Template) ToMessage(data TemplateData) (*Message, error) {
|
||||
// Render template
|
||||
subject, body, html, err := t.Render(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render template: %w", err)
|
||||
}
|
||||
|
||||
// Get recipients from data
|
||||
var recipients []string
|
||||
if toData, exists := data["to"]; exists {
|
||||
switch v := toData.(type) {
|
||||
case []string:
|
||||
recipients = v
|
||||
case string:
|
||||
recipients = []string{v}
|
||||
default:
|
||||
return nil, fmt.Errorf("'to' field must be string or []string")
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("template data must include 'to' field with recipients")
|
||||
}
|
||||
|
||||
// Create message
|
||||
message := &Message{
|
||||
Type: MessageType(t.Type),
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
HTML: html,
|
||||
To: recipients,
|
||||
}
|
||||
|
||||
// Add optional fields from data
|
||||
if from, exists := data["from"]; exists {
|
||||
if fromStr, ok := from.(string); ok {
|
||||
message.From = fromStr
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// renderTemplate renders a template string with data using {{ }} syntax
|
||||
func renderTemplate(template string, data TemplateData) string {
|
||||
// Find all {{ variable }} patterns
|
||||
re := regexp.MustCompile(`\{\{\s*([^}]+)\s*\}\}`)
|
||||
|
||||
return re.ReplaceAllStringFunc(template, func(match string) string {
|
||||
// Extract variable name from {{ variable }}
|
||||
variable := strings.TrimSpace(strings.Trim(match, "{}"))
|
||||
|
||||
// Get value from data using dot notation for nested access
|
||||
value := getNestedValue(data, variable)
|
||||
|
||||
// Convert to string
|
||||
return fmt.Sprintf("%v", value)
|
||||
})
|
||||
}
|
||||
|
||||
// getNestedValue gets a value from data using dot notation (e.g., "user.name", "team.members.count")
|
||||
func getNestedValue(data TemplateData, key string) interface{} {
|
||||
parts := strings.Split(key, ".")
|
||||
|
||||
current := interface{}(data)
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
|
||||
switch v := current.(type) {
|
||||
case map[string]interface{}:
|
||||
if val, exists := v[part]; exists {
|
||||
current = val
|
||||
} else {
|
||||
return "" // Return empty string if key not found
|
||||
}
|
||||
case TemplateData:
|
||||
if val, exists := v[part]; exists {
|
||||
current = val
|
||||
} else {
|
||||
return "" // Return empty string if key not found
|
||||
}
|
||||
default:
|
||||
return "" // Return empty string if not a map
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// TemplateManager manages message templates
|
||||
type TemplateManager interface {
|
||||
// GetTemplate returns a template by ID and type
|
||||
GetTemplate(templateID string, templateType TemplateType) (*Template, error)
|
||||
|
||||
// GetAllTemplates returns all loaded templates
|
||||
GetAllTemplates() map[string]map[TemplateType]*Template
|
||||
}
|
||||
|
|
@ -80,15 +80,6 @@ type RateLimit struct {
|
|||
Window time.Duration `json:"window,omitempty"`
|
||||
}
|
||||
|
||||
// Template represents a message template
|
||||
type Template struct {
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
HTML string `json:"html,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// GlobalConfig represents global messenger settings
|
||||
type GlobalConfig struct {
|
||||
RetryAttempts int `json:"retry_attempts,omitempty"`
|
||||
|
|
@ -134,3 +125,9 @@ type Features struct {
|
|||
SupportsTracking bool `json:"supports_tracking"`
|
||||
SupportsScheduling bool `json:"supports_scheduling"`
|
||||
}
|
||||
|
||||
// TemplateRequest represents a request to send a message using a specific template
|
||||
type TemplateRequest struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
Data TemplateData `json:"data"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue