diff --git a/messenger/messenger.go b/messenger/messenger.go index ac940f11..59ad3350 100644 --- a/messenger/messenger.go +++ b/messenger/messenger.go @@ -368,6 +368,18 @@ func (m *Service) GetProvidersByMessageType() map[types.MessageType][]types.Prov return result } +// GetAllProviders returns all providers +func (m *Service) GetAllProviders() []types.Provider { + m.mutex.RLock() + defer m.mutex.RUnlock() + + providers := make([]types.Provider, 0, len(m.providers)) + for _, provider := range m.providers { + providers = append(providers, provider) + } + return providers +} + // GetChannels returns all available channels func (m *Service) GetChannels() []string { m.mutex.RLock() diff --git a/messenger/messenger_test.go b/messenger/messenger_test.go index a632c22b..69e96756 100644 --- a/messenger/messenger_test.go +++ b/messenger/messenger_test.go @@ -303,6 +303,55 @@ func TestGetProvidersByMessageType(t *testing.T) { } } +func TestGetAllProviders(t *testing.T) { + // Setup test environment variables + setupTestEnvironment() + + test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION") + defer test.Clean() + + err := Load(config.Conf) + require.NoError(t, err, "Load should succeed") + + service, ok := Instance.(*Service) + require.True(t, ok, "Instance should be of type *Service") + + // Test getting all providers + allProviders := service.GetAllProviders() + + t.Logf("Total providers: %d", len(allProviders)) + + // Should have at least some providers + assert.GreaterOrEqual(t, len(allProviders), 1, "Should have at least one provider") + + // Verify each provider has required methods + for _, provider := range allProviders { + assert.NotEmpty(t, provider.GetName(), "Provider should have a name") + assert.NotEmpty(t, provider.GetType(), "Provider should have a type") + + // Test GetPublicInfo returns valid data + publicInfo := provider.GetPublicInfo() + assert.NotEmpty(t, publicInfo.Name, "Public info should have name") + assert.NotEmpty(t, publicInfo.Type, "Public info should have type") + assert.NotEmpty(t, publicInfo.Description, "Public info should have description") + assert.NotNil(t, publicInfo.Capabilities, "Public info should have capabilities") + } + + // Verify that all providers are accessible + emailProviders := service.GetProviders("email") + + // Note: Some providers may support multiple message types, so this is just a sanity check + assert.GreaterOrEqual(t, len(allProviders), len(emailProviders), "Total should be >= email providers") + + // Each provider should be unique by name + providerNames := make(map[string]bool) + for _, provider := range allProviders { + providerName := provider.GetName() + assert.False(t, providerNames[providerName], "Provider names should be unique: %s", providerName) + providerNames[providerName] = true + } +} + func TestValidateMessage(t *testing.T) { // Setup test environment variables setupTestEnvironment() diff --git a/messenger/providers/mailer/mailer.go b/messenger/providers/mailer/mailer.go index 44d62f87..85de7ed5 100644 --- a/messenger/providers/mailer/mailer.go +++ b/messenger/providers/mailer/mailer.go @@ -209,6 +209,27 @@ func (p *Provider) GetName() string { return p.config.Name } +// GetPublicInfo returns public information about the provider +func (p *Provider) GetPublicInfo() types.ProviderPublicInfo { + description := "SMTP email provider" + if p.config.Description != "" { + description = p.config.Description + } + + return types.ProviderPublicInfo{ + Name: p.config.Name, + Type: "mailer", + Description: description, + Capabilities: []string{"email"}, + Features: types.Features{ + SupportsWebhooks: false, + SupportsReceiving: p.SupportsReceiving(), + SupportsTracking: false, + SupportsScheduling: false, + }, + } +} + // Validate validates the provider configuration func (p *Provider) Validate() error { if p.host == "" { diff --git a/messenger/providers/mailer/mailer_test.go b/messenger/providers/mailer/mailer_test.go index d2e69deb..5e2f611c 100644 --- a/messenger/providers/mailer/mailer_test.go +++ b/messenger/providers/mailer/mailer_test.go @@ -685,3 +685,86 @@ func BenchmarkBuildMessage(b *testing.B) { } } } + +func TestProvider_GetPublicInfo(t *testing.T) { + config := types.ProviderConfig{ + Name: "test-mailer", + Connector: "mailer", + Description: "Test SMTP Provider", + Options: map[string]interface{}{ + "smtp": map[string]interface{}{ + "host": "smtp.example.com", + "port": 587, + "username": "test@example.com", + "password": "testpass", + "from": "test@example.com", + }, + }, + } + + provider, err := NewMailerProvider(config) + require.NoError(t, err) + + info := provider.GetPublicInfo() + + // Verify public information + assert.Equal(t, "test-mailer", info.Name) + assert.Equal(t, "mailer", info.Type) + assert.Equal(t, "Test SMTP Provider", info.Description) + assert.Equal(t, false, info.Features.SupportsWebhooks) + assert.Equal(t, false, info.Features.SupportsReceiving) // No IMAP config + assert.Equal(t, false, info.Features.SupportsTracking) + assert.Equal(t, false, info.Features.SupportsScheduling) + + // Verify capabilities + assert.Contains(t, info.Capabilities, "email") +} + +func TestProvider_GetPublicInfo_DefaultDescription(t *testing.T) { + config := types.ProviderConfig{ + Name: "test-mailer-no-desc", + 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", + }, + }, + } + + provider, err := NewMailerProvider(config) + require.NoError(t, err) + + info := provider.GetPublicInfo() + + // Should use default description when none provided + assert.Equal(t, "SMTP email provider", info.Description) +} + +func TestProvider_TriggerWebhook(t *testing.T) { + 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", + }, + }, + } + + provider, err := NewMailerProvider(config) + require.NoError(t, err) + + // TriggerWebhook should return an error for SMTP providers + msg, err := provider.TriggerWebhook(nil) + assert.Error(t, err) + assert.Nil(t, msg) + assert.Contains(t, err.Error(), "TriggerWebhook not supported for SMTP/mailer provider") +} diff --git a/messenger/providers/mailgun/mailgun.go b/messenger/providers/mailgun/mailgun.go index 2f8bad0c..28a68d95 100644 --- a/messenger/providers/mailgun/mailgun.go +++ b/messenger/providers/mailgun/mailgun.go @@ -96,6 +96,27 @@ func (p *Provider) GetName() string { return p.config.Name } +// GetPublicInfo returns public information about the provider +func (p *Provider) GetPublicInfo() types.ProviderPublicInfo { + description := "Mailgun email service provider" + if p.config.Description != "" { + description = p.config.Description + } + + return types.ProviderPublicInfo{ + Name: p.config.Name, + Type: "mailgun", + Description: description, + Capabilities: []string{"email", "webhooks", "tracking"}, + Features: types.Features{ + SupportsWebhooks: true, + SupportsReceiving: false, + SupportsTracking: true, + SupportsScheduling: true, + }, + } +} + // Validate validates the provider configuration func (p *Provider) Validate() error { if p.domain == "" { diff --git a/messenger/providers/mailgun/mailgun_receive_test.go b/messenger/providers/mailgun/mailgun_receive_test.go index 380608f2..6393500a 100644 --- a/messenger/providers/mailgun/mailgun_receive_test.go +++ b/messenger/providers/mailgun/mailgun_receive_test.go @@ -169,3 +169,55 @@ func TestProvider_TriggerWebhook_InvalidInput(t *testing.T) { assert.Nil(t, msg) assert.Contains(t, err.Error(), "expected *gin.Context") } + +func TestProvider_GetPublicInfo(t *testing.T) { + config := types.ProviderConfig{ + Name: "test-mailgun", + Connector: "mailgun", + Description: "Test Mailgun Provider", + Options: map[string]interface{}{ + "domain": "test.mailgun.org", + "api_key": "test-api-key", + "from": "test@test.mailgun.org", + }, + } + + provider, err := NewMailgunProvider(config) + require.NoError(t, err) + + info := provider.GetPublicInfo() + + // Verify public information + assert.Equal(t, "test-mailgun", info.Name) + assert.Equal(t, "mailgun", info.Type) + assert.Equal(t, "Test Mailgun Provider", info.Description) + assert.Equal(t, true, info.Features.SupportsWebhooks) + assert.Equal(t, true, info.Features.SupportsTracking) + assert.Equal(t, true, info.Features.SupportsScheduling) + assert.Equal(t, false, info.Features.SupportsReceiving) + + // Verify capabilities + assert.Contains(t, info.Capabilities, "email") + assert.Contains(t, info.Capabilities, "webhooks") + assert.Contains(t, info.Capabilities, "tracking") +} + +func TestProvider_GetPublicInfo_DefaultDescription(t *testing.T) { + config := types.ProviderConfig{ + Name: "test-mailgun-no-desc", + Connector: "mailgun", + Options: map[string]interface{}{ + "domain": "test.mailgun.org", + "api_key": "test-api-key", + "from": "test@test.mailgun.org", + }, + } + + provider, err := NewMailgunProvider(config) + require.NoError(t, err) + + info := provider.GetPublicInfo() + + // Should use default description when none provided + assert.Equal(t, "Mailgun email service provider", info.Description) +} diff --git a/messenger/providers/twilio/twilio.go b/messenger/providers/twilio/twilio.go index 4e95768d..c6a3c843 100644 --- a/messenger/providers/twilio/twilio.go +++ b/messenger/providers/twilio/twilio.go @@ -128,6 +128,41 @@ func (p *Provider) GetName() string { return p.config.Name } +// GetPublicInfo returns public information about the provider +func (p *Provider) GetPublicInfo() types.ProviderPublicInfo { + description := "Twilio multi-channel communication provider" + if p.config.Description != "" { + description = p.config.Description + } + + capabilities := []string{} + if p.fromPhone != "" || p.messagingServiceSID != "" { + capabilities = append(capabilities, "sms") + } + if p.fromPhone != "" { + capabilities = append(capabilities, "whatsapp") + } + if p.sendGridAPIKey != "" { + capabilities = append(capabilities, "email") + } + if len(capabilities) == 0 { + capabilities = []string{"sms", "whatsapp", "email"} // Default capabilities + } + + return types.ProviderPublicInfo{ + Name: p.config.Name, + Type: "twilio", + Description: description, + Capabilities: capabilities, + Features: types.Features{ + SupportsWebhooks: true, + SupportsReceiving: false, + SupportsTracking: true, + SupportsScheduling: true, + }, + } +} + // Validate validates the provider configuration func (p *Provider) Validate() error { if p.accountSID == "" { diff --git a/messenger/types/interfaces.go b/messenger/types/interfaces.go index 70ee2b06..3a1912d2 100644 --- a/messenger/types/interfaces.go +++ b/messenger/types/interfaces.go @@ -22,6 +22,9 @@ type Provider interface { // GetName returns the provider name/identifier GetName() string + // GetPublicInfo returns public information about the provider (name, description, type) + GetPublicInfo() ProviderPublicInfo + // Validate validates the provider configuration Validate() error @@ -46,6 +49,9 @@ type Messenger interface { // GetProviders returns all providers for a channel type GetProviders(channelType string) []Provider + // GetAllProviders returns all providers + GetAllProviders() []Provider + // GetChannels returns all available channels GetChannels() []string diff --git a/messenger/types/types.go b/messenger/types/types.go index ea99697e..f1c0177d 100644 --- a/messenger/types/types.go +++ b/messenger/types/types.go @@ -117,3 +117,20 @@ type SendResult struct { SentAt time.Time `json:"sent_at"` Metadata map[string]interface{} `json:"metadata,omitempty"` } + +// ProviderPublicInfo defines the public information structure for providers +type ProviderPublicInfo struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Capabilities []string `json:"capabilities"` + Features Features `json:"features"` +} + +// Features defines the features supported by a provider +type Features struct { + SupportsWebhooks bool `json:"supports_webhooks"` + SupportsReceiving bool `json:"supports_receiving"` + SupportsTracking bool `json:"supports_tracking"` + SupportsScheduling bool `json:"supports_scheduling"` +} diff --git a/openapi/messenger/messenger.go b/openapi/messenger/messenger.go index 00ad8278..d61a5535 100644 --- a/openapi/messenger/messenger.go +++ b/openapi/messenger/messenger.go @@ -1,20 +1,25 @@ package messenger import ( - "net/http" - "github.com/gin-gonic/gin" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/messenger" - "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/messenger/types" + oauthTypes "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/openapi/response" ) // Attach attaches the messenger webhook handlers to the router -func Attach(group *gin.RouterGroup, oauth types.OAuth) { +func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) { // Webhook endpoint with provider parameter - public interface group.GET("/webhook/:provider", webhookHandler) group.POST("/webhook/:provider", webhookHandler) + + // Private API endpoints for provider and channel information + group.GET("/providers", oauth.Guard, getProvidersHandler) + group.GET("/providers/:name", oauth.Guard, getProviderHandler) + group.GET("/channels", oauth.Guard, getChannelsHandler) } // webhookHandler is the handler for webhook endpoint @@ -22,18 +27,22 @@ func webhookHandler(c *gin.Context) { // Get provider name from URL parameter providerName := c.Param("provider") if providerName == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "provider parameter is required", - }) + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Provider parameter is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) return } // Check if messenger service is available if messenger.Instance == nil { log.Warn("[OpenAPI Messenger] Messenger service not initialized") - c.JSON(http.StatusServiceUnavailable, gin.H{ - "error": "messenger service not available", - }) + errorResp := &response.ErrorResponse{ + Code: response.ErrTemporarilyUnavailable.Code, + ErrorDescription: "Messenger service not available", + } + response.RespondWithError(c, response.StatusServiceUnavailable, errorResp) return } @@ -41,17 +50,120 @@ func webhookHandler(c *gin.Context) { err := messenger.Instance.TriggerWebhook(providerName, c) if err != nil { log.Error("[OpenAPI Messenger] Failed to process webhook for provider %s: %v", providerName, err) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to process webhook", - "details": err.Error(), - }) + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to process webhook: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) return } // Return success response - c.JSON(http.StatusOK, gin.H{ + successResp := gin.H{ "status": "received", "message": "webhook processed successfully", "provider": providerName, - }) + } + response.RespondWithSuccess(c, response.StatusOK, successResp) +} + +// getProviderHandler returns public information about a specific provider +func getProviderHandler(c *gin.Context) { + providerName := c.Param("name") + if providerName == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Provider name is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Check if messenger service is available + if messenger.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrTemporarilyUnavailable.Code, + ErrorDescription: "Messenger service not available", + } + response.RespondWithError(c, response.StatusServiceUnavailable, errorResp) + return + } + + // Get provider + provider, err := messenger.Instance.GetProvider(providerName) + if err != nil { + errorResp := &response.ErrorResponse{ + Code: "provider_not_found", + ErrorDescription: "Provider not found: " + providerName, + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + + // Return public information directly + response.RespondWithSuccess(c, response.StatusOK, provider.GetPublicInfo()) +} + +// getProvidersHandler returns public information about all providers, with optional channel type filter +func getProvidersHandler(c *gin.Context) { + // Check if messenger service is available + if messenger.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrTemporarilyUnavailable.Code, + ErrorDescription: "Messenger service not available", + } + response.RespondWithError(c, response.StatusServiceUnavailable, errorResp) + return + } + + // Get optional channel type filter from query parameter + channelType := c.Query("channel_type") + + var providers []types.Provider + if channelType != "" { + // Filter by channel type + providers = messenger.Instance.GetProviders(channelType) + } else { + // Get all providers + providers = messenger.Instance.GetAllProviders() + } + + // Convert to public information + publicProviders := make([]interface{}, 0, len(providers)) + for _, provider := range providers { + publicProviders = append(publicProviders, provider.GetPublicInfo()) + } + + successResp := gin.H{ + "providers": publicProviders, + "count": len(publicProviders), + } + + if channelType != "" { + successResp["channel_type"] = channelType + } + + response.RespondWithSuccess(c, response.StatusOK, successResp) +} + +// getChannelsHandler returns all available channels +func getChannelsHandler(c *gin.Context) { + // Check if messenger service is available + if messenger.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrTemporarilyUnavailable.Code, + ErrorDescription: "Messenger service not available", + } + response.RespondWithError(c, response.StatusServiceUnavailable, errorResp) + return + } + + // Get all channels + channels := messenger.Instance.GetChannels() + + successResp := gin.H{ + "channels": channels, + "count": len(channels), + } + response.RespondWithSuccess(c, response.StatusOK, successResp) }