Enhance messenger service with optional message type support for template sending
- Updated SendT, SendTWithProvider, SendTBatch, and SendTBatchMixed methods to accept an optional message type parameter, allowing for more flexible template usage. - Refactored provider implementations for Mailgun, Twilio, and Mailer to accommodate the new message type parameter in their SendT and SendTBatch methods. - Improved error handling for template retrieval and provider configuration, ensuring clearer feedback in case of issues. - Added tests to validate the new functionality and ensure compatibility across different message types.
This commit is contained in:
parent
cf42d255d3
commit
03ecf96b51
19 changed files with 839 additions and 124 deletions
|
|
@ -300,69 +300,305 @@ func (m *Service) SendWithProvider(ctx context.Context, providerName string, mes
|
|||
}
|
||||
|
||||
// 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)
|
||||
// messageType is optional - if not specified, the first available template type will be used
|
||||
func (m *Service) SendT(ctx context.Context, channel string, templateID string, data types.TemplateData, messageType ...types.MessageType) error {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
// Determine which message type to use
|
||||
var msgType types.MessageType
|
||||
if len(messageType) > 0 {
|
||||
// Use specified message type
|
||||
msgType = messageType[0]
|
||||
} else {
|
||||
// Get available template types and use the first one
|
||||
availableTypes := template.Global.GetAvailableTypes(templateID)
|
||||
if len(availableTypes) == 0 {
|
||||
return fmt.Errorf("template not found: %s", templateID)
|
||||
}
|
||||
// Convert TemplateType to MessageType
|
||||
msgType = templateTypeToMessageType(availableTypes[0])
|
||||
}
|
||||
|
||||
// Use the first available provider's SendT method
|
||||
provider := providers[0]
|
||||
return provider.SendT(ctx, templateID, data)
|
||||
// Get provider for this channel and message type
|
||||
providerName := m.getProviderForChannel(channel, string(msgType))
|
||||
if providerName == "" {
|
||||
return fmt.Errorf("no provider configured for channel %s with message type %s", channel, msgType)
|
||||
}
|
||||
|
||||
// Get the provider
|
||||
provider, exists := m.providers[providerName]
|
||||
if !exists {
|
||||
return fmt.Errorf("provider not found: %s", providerName)
|
||||
}
|
||||
|
||||
// Convert MessageType back to TemplateType
|
||||
templateType := messageTypeToTemplateType(msgType)
|
||||
|
||||
// Call provider's SendT method
|
||||
return provider.SendT(ctx, templateID, templateType, data)
|
||||
}
|
||||
|
||||
// templateTypeToMessageType converts TemplateType to MessageType
|
||||
func templateTypeToMessageType(templateType types.TemplateType) types.MessageType {
|
||||
switch templateType {
|
||||
case types.TemplateTypeMail:
|
||||
return types.MessageTypeEmail
|
||||
case types.TemplateTypeSMS:
|
||||
return types.MessageTypeSMS
|
||||
case types.TemplateTypeWhatsApp:
|
||||
return types.MessageTypeWhatsApp
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// messageTypeToTemplateType converts MessageType to TemplateType
|
||||
func messageTypeToTemplateType(messageType types.MessageType) types.TemplateType {
|
||||
switch messageType {
|
||||
case types.MessageTypeEmail:
|
||||
return types.TemplateTypeMail
|
||||
case types.MessageTypeSMS:
|
||||
return types.TemplateTypeSMS
|
||||
case types.MessageTypeWhatsApp:
|
||||
return types.TemplateTypeWhatsApp
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// messageType is optional - if not specified, the first available template type will be used
|
||||
func (m *Service) SendTWithProvider(ctx context.Context, providerName string, templateID string, data types.TemplateData, messageType ...types.MessageType) 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)
|
||||
// Determine which message type to use
|
||||
var msgType types.MessageType
|
||||
if len(messageType) > 0 {
|
||||
// Use specified message type
|
||||
msgType = messageType[0]
|
||||
} else {
|
||||
// Get available template types and use the first one
|
||||
availableTypes := template.Global.GetAvailableTypes(templateID)
|
||||
if len(availableTypes) == 0 {
|
||||
return fmt.Errorf("template not found: %s", templateID)
|
||||
}
|
||||
// Convert TemplateType to MessageType
|
||||
msgType = templateTypeToMessageType(availableTypes[0])
|
||||
}
|
||||
|
||||
// Convert MessageType to TemplateType
|
||||
templateType := messageTypeToTemplateType(msgType)
|
||||
|
||||
// Call provider's SendT method
|
||||
return provider.SendT(ctx, templateID, templateType, 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)
|
||||
// messageType is optional - if not specified, the first available template type will be used
|
||||
func (m *Service) SendTBatch(ctx context.Context, channel string, templateID string, dataList []types.TemplateData, messageType ...types.MessageType) error {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
if len(dataList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use the first available provider's SendTBatch method
|
||||
provider := providers[0]
|
||||
return provider.SendTBatch(ctx, templateID, dataList)
|
||||
// Determine which message type to use
|
||||
var msgType types.MessageType
|
||||
if len(messageType) > 0 {
|
||||
// Use specified message type
|
||||
msgType = messageType[0]
|
||||
} else {
|
||||
// Get available template types and use the first one
|
||||
availableTypes := template.Global.GetAvailableTypes(templateID)
|
||||
if len(availableTypes) == 0 {
|
||||
return fmt.Errorf("template not found: %s", templateID)
|
||||
}
|
||||
// Convert TemplateType to MessageType
|
||||
msgType = templateTypeToMessageType(availableTypes[0])
|
||||
}
|
||||
|
||||
// Get provider for this channel and message type
|
||||
providerName := m.getProviderForChannel(channel, string(msgType))
|
||||
if providerName == "" {
|
||||
return fmt.Errorf("no provider configured for channel %s with message type %s", channel, msgType)
|
||||
}
|
||||
|
||||
// Get the provider
|
||||
provider, exists := m.providers[providerName]
|
||||
if !exists {
|
||||
return fmt.Errorf("provider not found: %s", providerName)
|
||||
}
|
||||
|
||||
// Convert MessageType to TemplateType
|
||||
templateType := messageTypeToTemplateType(msgType)
|
||||
|
||||
// Get the template
|
||||
tmpl, err := template.Global.GetTemplate(templateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
// Convert all template data to messages
|
||||
messages := make([]*types.Message, 0, len(dataList))
|
||||
for _, data := range dataList {
|
||||
message, err := tmpl.ToMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send batch using the provider
|
||||
return provider.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// messageType is optional - if not specified, the first available template type will be used
|
||||
func (m *Service) SendTBatchWithProvider(ctx context.Context, providerName string, templateID string, dataList []types.TemplateData, messageType ...types.MessageType) 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)
|
||||
if len(dataList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Determine which message type to use
|
||||
var msgType types.MessageType
|
||||
if len(messageType) > 0 {
|
||||
// Use specified message type
|
||||
msgType = messageType[0]
|
||||
} else {
|
||||
// Get available template types and use the first one
|
||||
availableTypes := template.Global.GetAvailableTypes(templateID)
|
||||
if len(availableTypes) == 0 {
|
||||
return fmt.Errorf("template not found: %s", templateID)
|
||||
}
|
||||
// Convert TemplateType to MessageType
|
||||
msgType = templateTypeToMessageType(availableTypes[0])
|
||||
}
|
||||
|
||||
// Convert MessageType to TemplateType
|
||||
templateType := messageTypeToTemplateType(msgType)
|
||||
|
||||
// Get the template
|
||||
tmpl, err := template.Global.GetTemplate(templateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
// Convert all template data to messages
|
||||
messages := make([]*types.Message, 0, len(dataList))
|
||||
for _, data := range dataList {
|
||||
message, err := tmpl.ToMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||
}
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send batch using the provider
|
||||
return provider.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||
// Each TemplateRequest can optionally specify its MessageType
|
||||
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)
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
if len(templateRequests) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use the first available provider's SendTBatchMixed method
|
||||
provider := providers[0]
|
||||
return provider.SendTBatchMixed(ctx, templateRequests)
|
||||
// Group messages by provider
|
||||
providerMessages := make(map[string][]*types.Message)
|
||||
|
||||
// Process each template request
|
||||
for _, request := range templateRequests {
|
||||
// Determine message type
|
||||
var msgType types.MessageType
|
||||
if request.MessageType != nil {
|
||||
// Use specified message type
|
||||
msgType = *request.MessageType
|
||||
} else {
|
||||
// Get available template types and use the first one
|
||||
availableTypes := template.Global.GetAvailableTypes(request.TemplateID)
|
||||
if len(availableTypes) == 0 {
|
||||
return fmt.Errorf("template not found: %s", request.TemplateID)
|
||||
}
|
||||
// Convert TemplateType to MessageType
|
||||
msgType = templateTypeToMessageType(availableTypes[0])
|
||||
}
|
||||
|
||||
// Get provider for this channel and message type
|
||||
providerName := m.getProviderForChannel(channel, string(msgType))
|
||||
if providerName == "" {
|
||||
return fmt.Errorf("no provider configured for channel %s with message type %s", channel, msgType)
|
||||
}
|
||||
|
||||
// Verify provider exists
|
||||
if _, exists := m.providers[providerName]; !exists {
|
||||
return fmt.Errorf("provider not found: %s", providerName)
|
||||
}
|
||||
|
||||
// Convert MessageType to TemplateType
|
||||
templateType := messageTypeToTemplateType(msgType)
|
||||
|
||||
// Get the template
|
||||
tmpl, err := template.Global.GetTemplate(request.TemplateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template %s not found: %w", request.TemplateID, err)
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := tmpl.ToMessage(request.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template %s to message: %w", request.TemplateID, err)
|
||||
}
|
||||
|
||||
// Add to provider's message list
|
||||
providerMessages[providerName] = append(providerMessages[providerName], message)
|
||||
}
|
||||
|
||||
// Send batches to each provider
|
||||
var errors []string
|
||||
for providerName, messages := range providerMessages {
|
||||
// Check if context is cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("batch send cancelled: %w", ctx.Err())
|
||||
default:
|
||||
}
|
||||
|
||||
provider, exists := m.providers[providerName]
|
||||
if !exists {
|
||||
errors = append(errors, fmt.Sprintf("provider not found: %s", providerName))
|
||||
continue
|
||||
}
|
||||
|
||||
err := provider.SendBatch(ctx, messages)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Sprintf("provider %s: %v", providerName, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("batch send errors: %s", strings.Join(errors, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendTBatchMixedWithProvider sends multiple messages using different templates with different data and specific provider
|
||||
// Each TemplateRequest can optionally specify its MessageType
|
||||
func (m *Service) SendTBatchMixedWithProvider(ctx context.Context, providerName string, templateRequests []types.TemplateRequest) error {
|
||||
// Get provider
|
||||
provider, exists := m.providers[providerName]
|
||||
|
|
@ -370,8 +606,49 @@ func (m *Service) SendTBatchMixedWithProvider(ctx context.Context, providerName
|
|||
return fmt.Errorf("provider not found: %s", providerName)
|
||||
}
|
||||
|
||||
// Use the provider's SendTBatchMixed method directly
|
||||
return provider.SendTBatchMixed(ctx, templateRequests)
|
||||
if len(templateRequests) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert all template requests to messages
|
||||
messages := make([]*types.Message, 0, len(templateRequests))
|
||||
|
||||
for _, request := range templateRequests {
|
||||
// Determine message type
|
||||
var msgType types.MessageType
|
||||
if request.MessageType != nil {
|
||||
// Use specified message type
|
||||
msgType = *request.MessageType
|
||||
} else {
|
||||
// Get available template types and use the first one
|
||||
availableTypes := template.Global.GetAvailableTypes(request.TemplateID)
|
||||
if len(availableTypes) == 0 {
|
||||
return fmt.Errorf("template not found: %s", request.TemplateID)
|
||||
}
|
||||
// Convert TemplateType to MessageType
|
||||
msgType = templateTypeToMessageType(availableTypes[0])
|
||||
}
|
||||
|
||||
// Convert MessageType to TemplateType
|
||||
templateType := messageTypeToTemplateType(msgType)
|
||||
|
||||
// Get the template
|
||||
tmpl, err := template.Global.GetTemplate(request.TemplateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template %s not found: %w", request.TemplateID, err)
|
||||
}
|
||||
|
||||
// Convert template to message
|
||||
message, err := tmpl.ToMessage(request.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert template %s to message: %w", request.TemplateID, err)
|
||||
}
|
||||
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Send batch using the provider
|
||||
return provider.SendBatch(ctx, messages)
|
||||
}
|
||||
|
||||
// SendBatch sends multiple messages in batch
|
||||
|
|
|
|||
252
messenger/messenger_sendt_test.go
Normal file
252
messenger/messenger_sendt_test.go
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
package messenger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/template"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
// TestSendT_TemplateTypeSelection tests that SendT correctly selects template type and provider based on channel configuration
|
||||
func TestSendT_TemplateTypeSelection(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load messenger
|
||||
err := Load(config.Conf)
|
||||
require.NoError(t, err, "Load messenger should succeed")
|
||||
|
||||
// Load templates
|
||||
err = template.LoadTemplates()
|
||||
require.NoError(t, err, "Load templates should succeed")
|
||||
|
||||
service, ok := Instance.(*Service)
|
||||
require.True(t, ok, "Instance should be of type *Service")
|
||||
|
||||
// Test 1: Get available types for a template
|
||||
availableTypes := template.Global.GetAvailableTypes("en.invite_member")
|
||||
t.Logf("Available types for en.invite_member: %v", availableTypes)
|
||||
|
||||
// Test 2: SendT without specifying messageType (should use first available)
|
||||
ctx := context.Background()
|
||||
data := types.TemplateData{
|
||||
"to": []string{"test@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "Test User",
|
||||
"invite_link": "https://example.com/invite/test",
|
||||
}
|
||||
|
||||
// Note: This will fail with actual sending due to test credentials, but we're testing the logic
|
||||
err = service.SendT(ctx, "default", "en.invite_member", data)
|
||||
if err != nil {
|
||||
t.Logf("SendT failed (expected with test credentials): %v", err)
|
||||
// Should not be template-not-found or provider-not-found error
|
||||
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||
}
|
||||
|
||||
// Test 3: SendT with explicit messageType
|
||||
err = service.SendT(ctx, "default", "en.invite_member", data, types.MessageTypeEmail)
|
||||
if err != nil {
|
||||
t.Logf("SendT with explicit type failed (expected with test credentials): %v", err)
|
||||
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetProviderForChannel tests the provider selection logic
|
||||
func TestGetProviderForChannel(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load messenger
|
||||
err := Load(config.Conf)
|
||||
require.NoError(t, err, "Load messenger should succeed")
|
||||
|
||||
service, ok := Instance.(*Service)
|
||||
require.True(t, ok, "Instance should be of type *Service")
|
||||
|
||||
// Test provider selection for different channels and message types
|
||||
tests := []struct {
|
||||
channel string
|
||||
messageType string
|
||||
expected string // Expected provider name
|
||||
}{
|
||||
{"default", "email", "primary"},
|
||||
{"default", "sms", "unified"},
|
||||
{"default", "whatsapp", "unified"},
|
||||
{"promotions", "email", "marketing"},
|
||||
{"alerts", "email", "reliable"},
|
||||
{"notifications", "email", "primary"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.channel+"_"+tt.messageType, func(t *testing.T) {
|
||||
providerName := service.getProviderForChannel(tt.channel, tt.messageType)
|
||||
assert.Equal(t, tt.expected, providerName,
|
||||
"Channel %s with message type %s should use provider %s",
|
||||
tt.channel, tt.messageType, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateTypeConversion tests the conversion between MessageType and TemplateType
|
||||
func TestTemplateTypeConversion(t *testing.T) {
|
||||
// Test templateTypeToMessageType
|
||||
tests := []struct {
|
||||
templateType types.TemplateType
|
||||
expected types.MessageType
|
||||
}{
|
||||
{types.TemplateTypeMail, types.MessageTypeEmail},
|
||||
{types.TemplateTypeSMS, types.MessageTypeSMS},
|
||||
{types.TemplateTypeWhatsApp, types.MessageTypeWhatsApp},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := templateTypeToMessageType(tt.templateType)
|
||||
assert.Equal(t, tt.expected, result,
|
||||
"TemplateType %s should convert to MessageType %s",
|
||||
tt.templateType, tt.expected)
|
||||
}
|
||||
|
||||
// Test messageTypeToTemplateType
|
||||
reverseTests := []struct {
|
||||
messageType types.MessageType
|
||||
expected types.TemplateType
|
||||
}{
|
||||
{types.MessageTypeEmail, types.TemplateTypeMail},
|
||||
{types.MessageTypeSMS, types.TemplateTypeSMS},
|
||||
{types.MessageTypeWhatsApp, types.TemplateTypeWhatsApp},
|
||||
}
|
||||
|
||||
for _, tt := range reverseTests {
|
||||
result := messageTypeToTemplateType(tt.messageType)
|
||||
assert.Equal(t, tt.expected, result,
|
||||
"MessageType %s should convert to TemplateType %s",
|
||||
tt.messageType, tt.expected)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendTBatch tests the batch sending with template type selection
|
||||
func TestSendTBatch(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load messenger
|
||||
err := Load(config.Conf)
|
||||
require.NoError(t, err, "Load messenger should succeed")
|
||||
|
||||
// Load templates
|
||||
err = template.LoadTemplates()
|
||||
require.NoError(t, err, "Load templates should succeed")
|
||||
|
||||
service, ok := Instance.(*Service)
|
||||
require.True(t, ok, "Instance should be of type *Service")
|
||||
|
||||
// Test batch sending
|
||||
ctx := context.Background()
|
||||
dataList := []types.TemplateData{
|
||||
{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "Test User",
|
||||
"invite_link": "https://example.com/invite/test1",
|
||||
},
|
||||
{
|
||||
"to": []string{"user2@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "Test User",
|
||||
"invite_link": "https://example.com/invite/test2",
|
||||
},
|
||||
}
|
||||
|
||||
// Test without explicit messageType
|
||||
err = service.SendTBatch(ctx, "default", "en.invite_member", dataList)
|
||||
if err != nil {
|
||||
t.Logf("SendTBatch failed (expected with test credentials): %v", err)
|
||||
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||
}
|
||||
|
||||
// Test with explicit messageType
|
||||
err = service.SendTBatch(ctx, "default", "en.invite_member", dataList, types.MessageTypeEmail)
|
||||
if err != nil {
|
||||
t.Logf("SendTBatch with explicit type failed (expected with test credentials): %v", err)
|
||||
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendTBatchMixed tests mixed template batch sending
|
||||
func TestSendTBatchMixed(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load messenger
|
||||
err := Load(config.Conf)
|
||||
require.NoError(t, err, "Load messenger should succeed")
|
||||
|
||||
// Load templates
|
||||
err = template.LoadTemplates()
|
||||
require.NoError(t, err, "Load templates should succeed")
|
||||
|
||||
service, ok := Instance.(*Service)
|
||||
require.True(t, ok, "Instance should be of type *Service")
|
||||
|
||||
// Test mixed batch sending
|
||||
ctx := context.Background()
|
||||
|
||||
// Test without specifying MessageType in requests
|
||||
requests := []types.TemplateRequest{
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "Test User",
|
||||
"invite_link": "https://example.com/invite/test1",
|
||||
},
|
||||
},
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user2@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "Test User",
|
||||
"invite_link": "https://example.com/invite/test2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = service.SendTBatchMixed(ctx, "default", requests)
|
||||
if err != nil {
|
||||
t.Logf("SendTBatchMixed failed (expected with test credentials): %v", err)
|
||||
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||
}
|
||||
|
||||
// Test with explicit MessageType in requests
|
||||
emailType := types.MessageTypeEmail
|
||||
requestsWithType := []types.TemplateRequest{
|
||||
{
|
||||
TemplateID: "en.invite_member",
|
||||
MessageType: &emailType,
|
||||
Data: types.TemplateData{
|
||||
"to": []string{"user1@example.com"},
|
||||
"team_name": "Test Team",
|
||||
"inviter_name": "Test User",
|
||||
"invite_link": "https://example.com/invite/test1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = service.SendTBatchMixed(ctx, "default", requestsWithType)
|
||||
if err != nil {
|
||||
t.Logf("SendTBatchMixed with explicit type failed (expected with test credentials): %v", err)
|
||||
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||
}
|
||||
}
|
||||
|
|
@ -210,9 +210,9 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
|||
}
|
||||
|
||||
// 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)
|
||||
func (p *Provider) SendT(ctx context.Context, templateID string, templateType types.TemplateType, data types.TemplateData) error {
|
||||
// Get template from provider's template manager with specified type
|
||||
template, err := p.getTemplate(templateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
|
@ -228,9 +228,9 @@ func (p *Provider) SendT(ctx context.Context, templateID string, data types.Temp
|
|||
}
|
||||
|
||||
// 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)
|
||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, templateType types.TemplateType, dataList []types.TemplateData) error {
|
||||
// Get template from provider's template manager with specified type
|
||||
template, err := p.getTemplate(templateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func TestSendTBatch_Success(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test SendTBatch - should fail because template manager is nil
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", types.TemplateTypeMail, dataList)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ func TestSendTBatch_ContextTimeout(t *testing.T) {
|
|||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatch with expired context
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", types.TemplateTypeMail, dataList)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func TestSendT_TemplateNotImplemented(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test that SendT returns "template not found" error (template system is working)
|
||||
err = provider.SendT(ctx, "en.invite_member.mail", templateData)
|
||||
err = provider.SendT(ctx, "en.invite_member.mail", types.TemplateTypeMail, templateData)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template not found")
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ func TestSendT_ContextTimeout(t *testing.T) {
|
|||
"invite_link": "https://example.com/invite/123",
|
||||
}
|
||||
|
||||
err = provider.SendT(ctx, "en.invite_member.mail", templateData)
|
||||
err = provider.SendT(ctx, "en.invite_member.mail", types.TemplateTypeMail, templateData)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify it's a context timeout error or not implemented error
|
||||
|
|
@ -173,6 +173,6 @@ func BenchmarkSendT(b *testing.B) {
|
|||
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)
|
||||
_ = provider.SendT(ctx, "en.invite_member.mail", types.TemplateTypeMail, templateData)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,9 +94,9 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
|||
}
|
||||
|
||||
// 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
|
||||
func (p *Provider) SendT(ctx context.Context, templateID string, templateType types.TemplateType, data types.TemplateData) error {
|
||||
// Get template from provider's template manager with specified type
|
||||
template, err := p.getTemplate(templateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
|
@ -112,9 +112,9 @@ func (p *Provider) SendT(ctx context.Context, templateID string, data types.Temp
|
|||
}
|
||||
|
||||
// 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
|
||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, templateType types.TemplateType, dataList []types.TemplateData) error {
|
||||
// Get template from provider's template manager with specified type
|
||||
template, err := p.getTemplate(templateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func TestSendTBatch_Success(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test SendTBatch - should fail because template manager is nil
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", types.TemplateTypeMail, dataList)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ func TestSendTBatch_ContextTimeout(t *testing.T) {
|
|||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatch with expired context
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", types.TemplateTypeMail, dataList)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func TestSendT_TemplateNotImplemented(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test that SendT returns "template manager not available" error
|
||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
||||
err = provider.SendT(ctx, "en.invite_member", types.TemplateTypeMail, templateData)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ func TestSendT_ContextTimeout(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test that SendT handles context timeout
|
||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
||||
err = provider.SendT(ctx, "en.invite_member", types.TemplateTypeMail, templateData)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify it's a context timeout error or template manager error
|
||||
|
|
|
|||
|
|
@ -126,9 +126,9 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
|||
}
|
||||
|
||||
// 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
|
||||
func (p *Provider) SendT(ctx context.Context, templateID string, templateType types.TemplateType, data types.TemplateData) error {
|
||||
// Get template from provider's template manager with specified type
|
||||
template, err := p.getTemplate(templateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
|
@ -144,9 +144,9 @@ func (p *Provider) SendT(ctx context.Context, templateID string, data types.Temp
|
|||
}
|
||||
|
||||
// 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
|
||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, templateType types.TemplateType, dataList []types.TemplateData) error {
|
||||
// Get template from provider's template manager with specified type
|
||||
template, err := p.getTemplate(templateID, templateType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func TestSendTBatch_Success(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test SendTBatch - should fail because template manager is nil
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
||||
err = provider.SendTBatch(context.Background(), "en.invite_member", types.TemplateTypeSMS, dataList)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ func TestSendTBatch_ContextTimeout(t *testing.T) {
|
|||
time.Sleep(2 * time.Nanosecond)
|
||||
|
||||
// Test SendTBatch with expired context
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
||||
err = provider.SendTBatch(ctx, "en.invite_member", types.TemplateTypeSMS, dataList)
|
||||
assert.Error(t, err)
|
||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||
t.Logf("Error: %v", err)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func TestSendT_TemplateNotImplemented(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test that SendT returns "template manager not available" error
|
||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
||||
err = provider.SendT(ctx, "en.invite_member", types.TemplateTypeSMS, templateData)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template manager not available")
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ func TestSendT_ContextTimeout(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test that SendT handles context timeout
|
||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
||||
err = provider.SendT(ctx, "en.invite_member", types.TemplateTypeSMS, templateData)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify it's a context timeout error or template manager error
|
||||
|
|
|
|||
|
|
@ -52,6 +52,31 @@ func (m *Manager) GetAllTemplates() map[string]map[types.TemplateType]*types.Tem
|
|||
return result
|
||||
}
|
||||
|
||||
// GetAvailableTypes returns all available template types for a given templateID
|
||||
// Returns types in a consistent order: mail, sms, whatsapp
|
||||
func (m *Manager) GetAvailableTypes(templateID string) []types.TemplateType {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
if templates, exists := m.templates[templateID]; exists {
|
||||
// Return in preferred order
|
||||
var result []types.TemplateType
|
||||
preferredOrder := []types.TemplateType{
|
||||
types.TemplateTypeMail,
|
||||
types.TemplateTypeSMS,
|
||||
types.TemplateTypeWhatsApp,
|
||||
}
|
||||
|
||||
for _, templateType := range preferredOrder {
|
||||
if _, exists := templates[templateType]; exists {
|
||||
result = append(result, templateType)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return []types.TemplateType{}
|
||||
}
|
||||
|
||||
// ReloadTemplates reloads all templates from disk
|
||||
func (m *Manager) ReloadTemplates() error {
|
||||
m.mutex.Lock()
|
||||
|
|
@ -224,16 +249,16 @@ func parseMailTemplate(content string) (subject, body, html string, err error) {
|
|||
// Extract subject from <Subject> tag
|
||||
subject = strings.TrimSpace(doc.Find("Subject").Text())
|
||||
|
||||
// Extract body content from <body> tag
|
||||
bodySelection := doc.Find("body")
|
||||
// Extract body content from <Content> tag (using custom tag to avoid HTML parser auto-conversion)
|
||||
bodySelection := doc.Find("Content")
|
||||
if bodySelection.Length() == 0 {
|
||||
return "", "", "", fmt.Errorf("no <body> tag found in mail template")
|
||||
return "", "", "", fmt.Errorf("no <Content> tag found in mail template")
|
||||
}
|
||||
|
||||
// Get the HTML content of the body tag
|
||||
// Get the HTML content of the Content tag
|
||||
body, err = bodySelection.Html()
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("failed to extract body HTML: %w", err)
|
||||
return "", "", "", fmt.Errorf("failed to extract content HTML: %w", err)
|
||||
}
|
||||
|
||||
body = strings.TrimSpace(body)
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@ 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
|
||||
// SendT sends a message using a template with specified type
|
||||
// templateType specifies which template variant to use (mail, sms, whatsapp)
|
||||
SendT(ctx context.Context, templateID string, templateType TemplateType, 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
|
||||
// SendTBatch sends multiple messages using the same template with different data
|
||||
// templateType specifies which template variant to use (mail, sms, whatsapp)
|
||||
SendTBatch(ctx context.Context, templateID string, templateType TemplateType, dataList []TemplateData) error
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data (optional - providers may return "not implemented" error)
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||
// Each TemplateRequest can optionally specify its own MessageType
|
||||
SendTBatchMixed(ctx context.Context, templateRequests []TemplateRequest) error
|
||||
|
||||
// TriggerWebhook processes webhook requests and converts to Message
|
||||
|
|
@ -50,16 +53,20 @@ type Messenger interface {
|
|||
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
|
||||
// messageType is optional - if not specified, the first available template type will be used
|
||||
SendT(ctx context.Context, channel string, templateID string, data TemplateData, messageType ...MessageType) error
|
||||
|
||||
// SendTWithProvider sends a message using a template and specific provider
|
||||
SendTWithProvider(ctx context.Context, providerName string, templateID string, data TemplateData) error
|
||||
// messageType is optional - if not specified, the first available template type will be used
|
||||
SendTWithProvider(ctx context.Context, providerName string, templateID string, data TemplateData, messageType ...MessageType) error
|
||||
|
||||
// SendTBatch sends multiple messages using the same template with different data
|
||||
SendTBatch(ctx context.Context, channel string, templateID string, dataList []TemplateData) error
|
||||
// messageType is optional - if not specified, the first available template type will be used
|
||||
SendTBatch(ctx context.Context, channel string, templateID string, dataList []TemplateData, messageType ...MessageType) 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
|
||||
// messageType is optional - if not specified, the first available template type will be used
|
||||
SendTBatchWithProvider(ctx context.Context, providerName string, templateID string, dataList []TemplateData, messageType ...MessageType) error
|
||||
|
||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||
SendTBatchMixed(ctx context.Context, channel string, templateRequests []TemplateRequest) error
|
||||
|
|
|
|||
|
|
@ -15,6 +15,20 @@ const (
|
|||
TemplateTypeWhatsApp TemplateType = "whatsapp"
|
||||
)
|
||||
|
||||
// templateTypeToMessageType converts TemplateType to MessageType
|
||||
func templateTypeToMessageType(templateType TemplateType) MessageType {
|
||||
switch templateType {
|
||||
case TemplateTypeMail:
|
||||
return MessageTypeEmail
|
||||
case TemplateTypeSMS:
|
||||
return MessageTypeSMS
|
||||
case TemplateTypeWhatsApp:
|
||||
return MessageTypeWhatsApp
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Template represents a message template
|
||||
type Template struct {
|
||||
ID string `json:"id"`
|
||||
|
|
@ -71,9 +85,12 @@ func (t *Template) ToMessage(data TemplateData) (*Message, error) {
|
|||
return nil, fmt.Errorf("template data must include 'to' field with recipients")
|
||||
}
|
||||
|
||||
// Convert TemplateType to MessageType
|
||||
messageType := templateTypeToMessageType(t.Type)
|
||||
|
||||
// Create message
|
||||
message := &Message{
|
||||
Type: MessageType(t.Type),
|
||||
Type: messageType,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
HTML: html,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ type Features struct {
|
|||
|
||||
// TemplateRequest represents a request to send a message using a specific template
|
||||
type TemplateRequest struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
Data TemplateData `json:"data"`
|
||||
TemplateID string `json:"template_id"`
|
||||
Data TemplateData `json:"data"`
|
||||
MessageType *MessageType `json:"message_type,omitempty"` // Optional: if not specified, will use first available template type
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ func GetTeamConfig(locale string) *TeamConfig {
|
|||
|
||||
// Normalize language code to lowercase
|
||||
if locale != "" {
|
||||
locale = strings.ToLower(locale)
|
||||
locale = strings.TrimSpace(strings.ToLower(locale))
|
||||
}
|
||||
|
||||
// Try to get the specific locale configuration
|
||||
|
|
@ -427,7 +427,12 @@ func GetTeamConfig(locale string) *TeamConfig {
|
|||
return config
|
||||
}
|
||||
|
||||
// If no specific locale, try to get any available configuration
|
||||
// If no specific locale, try to get "en" as default
|
||||
if config, exists := teamConfigs["en"]; exists {
|
||||
return config
|
||||
}
|
||||
|
||||
// If "en" is not available, try to get any available configuration
|
||||
for _, config := range teamConfigs {
|
||||
return config
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
messengertypes "github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/share"
|
||||
)
|
||||
|
||||
// Team Invitation Management Handlers
|
||||
|
|
@ -118,6 +119,9 @@ func GinInvitationGet(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Extract base URL from request
|
||||
requestBaseURL := getRequestBaseURL(c)
|
||||
|
||||
// Call business logic
|
||||
invitationData, err := invitationGet(c.Request.Context(), authInfo.UserID, teamID, invitationID)
|
||||
if err != nil {
|
||||
|
|
@ -145,8 +149,8 @@ func GinInvitationGet(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
invitation := mapToInvitationDetailResponse(invitationData)
|
||||
// Convert to response format (with requestBaseURL for building full invitation link)
|
||||
invitation := mapToInvitationDetailResponse(invitationData, requestBaseURL)
|
||||
c.JSON(http.StatusOK, invitation)
|
||||
}
|
||||
|
||||
|
|
@ -184,14 +188,18 @@ func GinInvitationCreate(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Extract base URL from request
|
||||
requestBaseURL := getRequestBaseURL(c)
|
||||
|
||||
// Prepare invitation data
|
||||
invitationData := maps.MapStrAny{
|
||||
"user_id": req.UserID,
|
||||
"email": req.Email,
|
||||
"member_type": req.MemberType,
|
||||
"role_id": req.RoleID,
|
||||
"message": req.Message,
|
||||
"expiry": req.Expiry,
|
||||
"user_id": req.UserID,
|
||||
"email": req.Email,
|
||||
"member_type": req.MemberType,
|
||||
"role_id": req.RoleID,
|
||||
"message": req.Message,
|
||||
"expiry": req.Expiry,
|
||||
"request_base_url": requestBaseURL,
|
||||
}
|
||||
|
||||
// Prepare settings
|
||||
|
|
@ -205,6 +213,11 @@ func GinInvitationCreate(c *gin.Context) {
|
|||
settings.SendEmail = *req.SendEmail
|
||||
}
|
||||
|
||||
// Add locale from top-level field (for backward compatibility)
|
||||
if req.Locale != "" {
|
||||
settings.Locale = req.Locale
|
||||
}
|
||||
|
||||
// Add settings to invitation data
|
||||
if settings.SendEmail || settings.Locale != "" {
|
||||
invitationData["settings"] = settings
|
||||
|
|
@ -258,8 +271,8 @@ func GinInvitationCreate(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Convert to InvitationResponse
|
||||
invitationResp := convertToInvitationResponse(invitation)
|
||||
// Convert to InvitationResponse (with requestBaseURL for building full invitation link)
|
||||
invitationResp := convertToInvitationResponse(invitation, requestBaseURL)
|
||||
|
||||
// Return created invitation with full details (including token)
|
||||
c.JSON(http.StatusCreated, invitationResp)
|
||||
|
|
@ -290,7 +303,7 @@ func GinInvitationResend(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Call business logic
|
||||
err := invitationResend(c.Request.Context(), authInfo.UserID, teamID, invitationID)
|
||||
err := invitationResend(c.Request.Context(), authInfo.UserID, teamID, invitationID, getRequestBaseURL(c))
|
||||
if err != nil {
|
||||
log.Error("Failed to resend invitation: %v", err)
|
||||
// Check error type for appropriate response
|
||||
|
|
@ -526,8 +539,8 @@ func ProcessInvitationResend(process *process.Process) interface{} {
|
|||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
err := invitationResend(ctx, userIDStr, teamID, invitationID)
|
||||
// Call business logic (no requestBaseURL available in process context)
|
||||
err := invitationResend(ctx, userIDStr, teamID, invitationID, "")
|
||||
if err != nil {
|
||||
exception.New("failed to resend invitation: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
|
@ -573,6 +586,75 @@ func ProcessInvitationDelete(process *process.Process) interface{} {
|
|||
|
||||
// Private Business Logic Functions (internal use only)
|
||||
|
||||
// getAdminRoot returns the admin root path from share.App configuration
|
||||
// Similar to service.setupAdminRoot but without caching to avoid circular dependencies
|
||||
func getAdminRoot() string {
|
||||
adminRoot := "/yao/"
|
||||
if share.App.AdminRoot != "" {
|
||||
root := strings.TrimPrefix(share.App.AdminRoot, "/")
|
||||
root = strings.TrimSuffix(root, "/")
|
||||
adminRoot = fmt.Sprintf("/%s/", root)
|
||||
}
|
||||
return adminRoot
|
||||
}
|
||||
|
||||
// getRequestBaseURL extracts the base URL from the gin context request
|
||||
// Returns: scheme://host (e.g., "https://example.com" or "http://localhost:8000")
|
||||
func getRequestBaseURL(c *gin.Context) string {
|
||||
if c == nil || c.Request == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
// Check X-Forwarded-Proto header
|
||||
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
|
||||
scheme = proto
|
||||
}
|
||||
|
||||
host := c.Request.Host
|
||||
if host == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s", scheme, host)
|
||||
}
|
||||
|
||||
// buildInvitationLink constructs a full invitation link from invitation_id, token and team configuration
|
||||
// This is a centralized function to ensure consistency across email sending and link generation
|
||||
// Format:
|
||||
// - With team config baseURL: {base_url}/{invitation_id}/{token}
|
||||
// - With requestBaseURL (from HTTP request): {scheme}://{host}{AdminRoot}team/invite/{invitation_id}/{token}
|
||||
// - Without any baseURL (fallback): {AdminRoot}team/invite/{invitation_id}/{token}
|
||||
func buildInvitationLink(invitationID, token string, teamConfig *TeamConfig, requestBaseURL string) string {
|
||||
// Priority 1: Use team config baseURL if specified
|
||||
if teamConfig != nil && teamConfig.Invite != nil && teamConfig.Invite.BaseURL != "" {
|
||||
baseURL := teamConfig.Invite.BaseURL
|
||||
// Ensure baseURL ends with /
|
||||
if !strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL + "/"
|
||||
}
|
||||
return fmt.Sprintf("%s%s/%s", baseURL, invitationID, token)
|
||||
}
|
||||
|
||||
// Get admin root from configuration
|
||||
adminRoot := getAdminRoot()
|
||||
// Ensure adminRoot doesn't end with / for URL construction
|
||||
adminRoot = strings.TrimSuffix(adminRoot, "/")
|
||||
|
||||
// Priority 2: Use request baseURL with AdminRoot
|
||||
if requestBaseURL != "" {
|
||||
// Ensure requestBaseURL doesn't end with /
|
||||
requestBaseURL = strings.TrimSuffix(requestBaseURL, "/")
|
||||
return fmt.Sprintf("%s%s/team/invite/%s/%s", requestBaseURL, adminRoot, invitationID, token)
|
||||
}
|
||||
|
||||
// Priority 3: Fallback to relative path with AdminRoot
|
||||
return fmt.Sprintf("%s/team/invite/%s/%s", adminRoot, invitationID, token)
|
||||
}
|
||||
|
||||
// invitationList handles the business logic for listing team invitations
|
||||
func invitationList(ctx context.Context, userID, teamID string, page, pagesize int, status string) (maps.MapStr, error) {
|
||||
// Check if user has access to the team (read permission: owner or member)
|
||||
|
|
@ -762,6 +844,10 @@ func invitationCreate(ctx context.Context, userID, teamID string, invitationData
|
|||
return "", fmt.Errorf("failed to parse expiry duration: %w", err)
|
||||
}
|
||||
|
||||
// Save request_base_url and settings before database operation (they will be lost in DB)
|
||||
requestBaseURL := toString(invitationData["request_base_url"])
|
||||
savedSettings := invitationData["settings"] // Save settings reference
|
||||
|
||||
// Set invitation-specific fields
|
||||
invitationData["team_id"] = teamID
|
||||
if invitationData["member_type"] == nil || invitationData["member_type"] == "" {
|
||||
|
|
@ -792,21 +878,34 @@ func invitationCreate(ctx context.Context, userID, teamID string, invitationData
|
|||
|
||||
// Send email if requested (shouldSendEmail was already determined earlier)
|
||||
if shouldSendEmail {
|
||||
err = sendInvitationEmail(ctx, inviteeEmail, inviterName, teamName, token, invitationID, invitationData)
|
||||
if err != nil {
|
||||
log.Error("Failed to send invitation email: %v", err)
|
||||
// Don't fail the invitation creation if email fails
|
||||
// The invitation link can still be shared manually
|
||||
} else {
|
||||
log.Info("Invitation email sent to %s for team %s (invitation_id: %s)", inviteeEmail, teamName, invitationID)
|
||||
}
|
||||
// Use the saved requestBaseURL and settings (not from invitationData, as they were lost in DB operation)
|
||||
// Send email asynchronously to improve user experience
|
||||
go func() {
|
||||
// Use background context for async operation
|
||||
bgCtx := context.Background()
|
||||
|
||||
// Ensure request_base_url and settings are in invitationData for email sending
|
||||
emailData := maps.MapStrAny{}
|
||||
for k, v := range invitationData {
|
||||
emailData[k] = v
|
||||
}
|
||||
emailData["request_base_url"] = requestBaseURL
|
||||
emailData["settings"] = savedSettings // Restore settings
|
||||
|
||||
err := sendInvitationEmail(bgCtx, inviteeEmail, inviterName, teamName, token, invitationID, emailData)
|
||||
if err != nil {
|
||||
log.Error("Failed to send invitation email: %v", err)
|
||||
} else {
|
||||
log.Info("Invitation email sent to %s for team %s (invitation_id: %s)", inviteeEmail, teamName, invitationID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return invitationID, nil
|
||||
}
|
||||
|
||||
// invitationResend handles the business logic for resending a team invitation
|
||||
func invitationResend(ctx context.Context, userID, teamID, invitationID string) error {
|
||||
func invitationResend(ctx context.Context, userID, teamID, invitationID, requestBaseURL string) error {
|
||||
// Check if user has access to the team (write permission: owner only)
|
||||
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
||||
if err != nil {
|
||||
|
|
@ -889,6 +988,7 @@ func invitationResend(ctx context.Context, userID, teamID, invitationID string)
|
|||
// Update the invitation data for email sending
|
||||
invitationData["invitation_token"] = newToken
|
||||
invitationData["invitation_expires_at"] = newExpiryTime
|
||||
invitationData["request_base_url"] = requestBaseURL
|
||||
|
||||
// Get email from invitation data
|
||||
var inviteeEmail string
|
||||
|
|
@ -902,13 +1002,18 @@ func invitationResend(ctx context.Context, userID, teamID, invitationID string)
|
|||
}
|
||||
}
|
||||
|
||||
// Send new invitation email if email is available
|
||||
// Send new invitation email if email is available (asynchronously)
|
||||
if inviteeEmail != "" {
|
||||
err = sendInvitationEmail(ctx, inviteeEmail, inviterName, teamName, newToken, invitationID, invitationData)
|
||||
if err != nil {
|
||||
log.Error("Failed to resend invitation email: %v", err)
|
||||
// Don't fail the resend if email fails
|
||||
}
|
||||
go func() {
|
||||
// Use background context for async operation
|
||||
bgCtx := context.Background()
|
||||
err := sendInvitationEmail(bgCtx, inviteeEmail, inviterName, teamName, newToken, invitationID, invitationData)
|
||||
if err != nil {
|
||||
log.Error("Failed to resend invitation email: %v", err)
|
||||
} else {
|
||||
log.Info("Invitation email resent to %s for team %s (invitation_id: %s)", inviteeEmail, teamName, invitationID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -1044,6 +1149,7 @@ func sendInvitationEmail(ctx context.Context, email, inviterName, teamName, toke
|
|||
}
|
||||
|
||||
// Get team config for email template and channel
|
||||
// Note: GetTeamConfig will normalize locale internally (trim, lowercase, etc.)
|
||||
teamConfig := GetTeamConfig(locale)
|
||||
if teamConfig == nil || teamConfig.Invite == nil {
|
||||
return fmt.Errorf("team configuration not found for locale: %s", locale)
|
||||
|
|
@ -1069,35 +1175,41 @@ func sendInvitationEmail(ctx context.Context, email, inviterName, teamName, toke
|
|||
// Get custom message from invitation data
|
||||
customMessage := toString(invitationData["message"])
|
||||
|
||||
// Get request base URL from invitation data (if provided)
|
||||
requestBaseURL := toString(invitationData["request_base_url"])
|
||||
|
||||
// Build invitation link using centralized helper function
|
||||
invitationLink := buildInvitationLink(invitationID, token, teamConfig, requestBaseURL)
|
||||
|
||||
// Prepare template data for messenger
|
||||
templateData := messengertypes.TemplateData{
|
||||
"to": email,
|
||||
"inviter_name": inviterName,
|
||||
"team_name": teamName,
|
||||
"invitation_id": invitationID,
|
||||
"token": token,
|
||||
"message": customMessage,
|
||||
"role_id": toString(invitationData["role_id"]),
|
||||
"expires_at": toString(invitationData["invitation_expires_at"]),
|
||||
"to": email,
|
||||
"inviter_name": inviterName,
|
||||
"team_name": teamName,
|
||||
"invitation_id": invitationID,
|
||||
"invitation_link": invitationLink, // Full invitation link
|
||||
"token": token, // Keep token for backward compatibility
|
||||
"message": customMessage,
|
||||
"role_id": toString(invitationData["role_id"]),
|
||||
"expires_at": toString(invitationData["invitation_expires_at"]),
|
||||
}
|
||||
|
||||
// Send email using messenger template
|
||||
err := messenger.Instance.SendT(ctx, channel, emailTemplate, templateData)
|
||||
err := messenger.Instance.SendT(ctx, channel, emailTemplate, templateData, messengertypes.MessageTypeEmail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send invitation email: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Invitation email sent to %s for team %s (invitation_id: %s)", email, teamName, invitationID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToInvitationResponse converts a map to InvitationResponse (alias for mapToInvitationResponse)
|
||||
func convertToInvitationResponse(data maps.MapStrAny) InvitationResponse {
|
||||
return mapToInvitationResponse(maps.MapStr(data))
|
||||
func convertToInvitationResponse(data maps.MapStrAny, requestBaseURL string) InvitationResponse {
|
||||
return mapToInvitationResponse(maps.MapStr(data), requestBaseURL)
|
||||
}
|
||||
|
||||
// mapToInvitationResponse converts a map to InvitationResponse
|
||||
func mapToInvitationResponse(data maps.MapStr) InvitationResponse {
|
||||
func mapToInvitationResponse(data maps.MapStr, requestBaseURL string) InvitationResponse {
|
||||
invitation := InvitationResponse{
|
||||
ID: toInt64(data["id"]),
|
||||
InvitationID: toString(data["invitation_id"]),
|
||||
|
|
@ -1116,9 +1228,13 @@ func mapToInvitationResponse(data maps.MapStr) InvitationResponse {
|
|||
}
|
||||
|
||||
// Add settings if available
|
||||
locale := "en" // Default locale
|
||||
if settings, ok := data["settings"]; ok {
|
||||
if invSettings, ok := settings.(*InvitationSettings); ok {
|
||||
invitation.Settings = invSettings
|
||||
if invSettings.Locale != "" {
|
||||
locale = invSettings.Locale
|
||||
}
|
||||
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||
// Convert map to InvitationSettings
|
||||
invSettings := &InvitationSettings{
|
||||
|
|
@ -1126,16 +1242,25 @@ func mapToInvitationResponse(data maps.MapStr) InvitationResponse {
|
|||
Locale: toString(settingsMap["locale"]),
|
||||
}
|
||||
invitation.Settings = invSettings
|
||||
if invSettings.Locale != "" {
|
||||
locale = invSettings.Locale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build invitation link if token is available
|
||||
if invitation.InvitationToken != "" && invitation.InvitationID != "" {
|
||||
teamConfig := GetTeamConfig(locale)
|
||||
invitation.InvitationLink = buildInvitationLink(invitation.InvitationID, invitation.InvitationToken, teamConfig, requestBaseURL)
|
||||
}
|
||||
|
||||
return invitation
|
||||
}
|
||||
|
||||
// mapToInvitationDetailResponse converts a map to InvitationDetailResponse
|
||||
func mapToInvitationDetailResponse(data maps.MapStr) InvitationDetailResponse {
|
||||
func mapToInvitationDetailResponse(data maps.MapStr, requestBaseURL string) InvitationDetailResponse {
|
||||
invitation := InvitationDetailResponse{
|
||||
InvitationResponse: mapToInvitationResponse(data),
|
||||
InvitationResponse: mapToInvitationResponse(data, requestBaseURL),
|
||||
}
|
||||
|
||||
// Add user info if available (could be joined from user table)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ func GinTeamConfig(c *gin.Context) {
|
|||
locale = "en" // default locale
|
||||
}
|
||||
|
||||
// Clean locale: remove whitespace and special characters
|
||||
locale = strings.TrimSpace(locale)
|
||||
locale = strings.Trim(locale, "?&=")
|
||||
|
||||
config := GetTeamConfig(locale)
|
||||
if config == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Team configuration not found"})
|
||||
|
|
|
|||
|
|
@ -315,6 +315,7 @@ type InvitationResponse struct {
|
|||
InvitedBy string `json:"invited_by"`
|
||||
InvitedAt string `json:"invited_at"`
|
||||
InvitationToken string `json:"invitation_token,omitempty"`
|
||||
InvitationLink string `json:"invitation_link,omitempty"` // Full invitation link
|
||||
InvitationExpiresAt string `json:"invitation_expires_at,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Settings *InvitationSettings `json:"settings,omitempty"`
|
||||
|
|
@ -339,6 +340,7 @@ type CreateInvitationRequest struct {
|
|||
Message string `json:"message,omitempty"`
|
||||
Expiry string `json:"expiry,omitempty"` // Custom expiry duration (e.g., "1d", "8h"), defaults to team config
|
||||
SendEmail *bool `json:"send_email,omitempty"` // Whether to send email (defaults to false)
|
||||
Locale string `json:"locale,omitempty"` // Language code for email template (e.g., "zh-CN", "en")
|
||||
Settings *InvitationSettings `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue