using Azure.Messaging.ServiceBus; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Dirt.Models.Data.Teams; using Bit.Core.Dirt.Repositories; using Bit.Core.Dirt.Services; using Bit.Core.Dirt.Services.Implementations; using Bit.Core.Dirt.Services.NoopImplementations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion; using TableStorageRepos = Bit.Core.Repositories.TableStorage; namespace Microsoft.Extensions.DependencyInjection; public static class EventIntegrationsServiceCollectionExtensions { /// /// Adds all event integrations commands, queries, and required cache infrastructure. /// This method is idempotent and can be called multiple times safely. /// public static IServiceCollection AddEventIntegrationsCommandsQueries( this IServiceCollection services, GlobalSettings globalSettings) { // Ensure cache is registered first - commands depend on this keyed cache. // This is idempotent for the same named cache, so it's safe to call. services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings); // Add Validator services.TryAddSingleton(); // Add all commands/queries services.AddOrganizationIntegrationCommandsQueries(); services.AddOrganizationIntegrationConfigurationCommandsQueries(); return services; } /// /// Registers event write services based on available configuration. /// /// The service collection to add services to. /// The global settings containing event logging configuration. /// The service collection for chaining. /// /// /// This method registers the appropriate IEventWriteService implementation based on the available /// configuration, checking in the following priority order: /// /// /// 1. Azure Service Bus - If all Azure Service Bus settings are present, registers /// EventIntegrationEventWriteService with AzureServiceBusService as the publisher /// /// /// 2. RabbitMQ - If all RabbitMQ settings are present, registers EventIntegrationEventWriteService with /// RabbitMqService as the publisher /// /// /// 3. Azure Queue Storage - If Events.ConnectionString is present, registers AzureQueueEventWriteService /// /// /// 4. Repository (Self-Hosted) - If SelfHosted is true, registers RepositoryEventWriteService /// /// /// 5. Noop - If none of the above are configured, registers NoopEventWriteService (no-op implementation) /// /// public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings) { if (IsAzureServiceBusEnabled(globalSettings)) { services.TryAddSingleton(); services.TryAddSingleton(); return services; } if (IsRabbitMqEnabled(globalSettings)) { services.TryAddSingleton(); services.TryAddSingleton(); return services; } if (CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.Events.QueueName)) { services.TryAddSingleton(); return services; } if (globalSettings.SelfHosted) { services.TryAddSingleton(); return services; } services.TryAddSingleton(); return services; } /// /// Registers Azure Service Bus-based event integration listeners and supporting infrastructure. /// /// The service collection to add services to. /// The global settings containing Azure Service Bus configuration. /// The service collection for chaining. /// /// /// If Azure Service Bus is not enabled (missing required settings), this method returns immediately /// without registering any services. /// /// /// When Azure Service Bus is enabled, this method registers: /// - IAzureServiceBusService and IEventIntegrationPublisher implementations /// - Table Storage event repository /// - Azure Table Storage event handler /// - All event integration services via AddEventIntegrationServices /// /// /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before this method, /// as it is required to create the event integrations extended cache. /// /// public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings) { if (!IsAzureServiceBusEnabled(globalSettings)) { return services; } services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddKeyedSingleton("persistent"); services.TryAddSingleton(); services.AddEventIntegrationServices(globalSettings); return services; } /// /// Registers RabbitMQ-based event integration listeners and supporting infrastructure. /// /// The service collection to add services to. /// The global settings containing RabbitMQ configuration. /// The service collection for chaining. /// /// /// If RabbitMQ is not enabled (missing required settings), this method returns immediately /// without registering any services. /// /// /// When RabbitMQ is enabled, this method registers: /// - IRabbitMqService and IEventIntegrationPublisher implementations /// - Event repository handler /// - All event integration services via AddEventIntegrationServices /// /// /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before this method, /// as it is required to create the event integrations extended cache. /// /// public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings) { if (!IsRabbitMqEnabled(globalSettings)) { return services; } services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.AddEventIntegrationServices(globalSettings); return services; } /// /// Registers Slack integration services based on configuration settings. /// /// The service collection to add services to. /// The global settings containing Slack configuration. /// The service collection for chaining. /// /// If all required Slack settings are configured (ClientId, ClientSecret, Scopes), registers the full SlackService, /// including an HttpClient for Slack API calls. Otherwise, registers a NoopSlackService that performs no operations. /// public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings) { if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) { services.AddHttpClient(SlackService.HttpClientName); services.TryAddSingleton(); } else { services.TryAddSingleton(); } return services; } /// /// Registers Microsoft Teams integration services based on configuration settings. /// /// The service collection to add services to. /// The global settings containing Teams configuration. /// The service collection for chaining. /// /// If all required Teams settings are configured (ClientId, ClientSecret, Scopes), registers: /// - TeamsService and its interfaces (IBot, ITeamsService) /// - IBotFrameworkHttpAdapter with Teams credentials /// - HttpClient for Teams API calls /// Otherwise, registers a NoopTeamsService that performs no operations. /// public static IServiceCollection AddTeamsService(this IServiceCollection services, GlobalSettings globalSettings) { if (CoreHelpers.SettingHasValue(globalSettings.Teams.ClientId) && CoreHelpers.SettingHasValue(globalSettings.Teams.ClientSecret) && CoreHelpers.SettingHasValue(globalSettings.Teams.Scopes)) { services.AddHttpClient(TeamsService.HttpClientName); services.TryAddSingleton(); services.TryAddSingleton(sp => sp.GetRequiredService()); services.TryAddSingleton(sp => sp.GetRequiredService()); services.TryAddSingleton(_ => new BotFrameworkHttpAdapter( new TeamsBotCredentialProvider( clientId: globalSettings.Teams.ClientId, clientSecret: globalSettings.Teams.ClientSecret ) ) ); } else { services.TryAddSingleton(); } return services; } /// /// Registers event integration services including handlers, listeners, and supporting infrastructure. /// /// The service collection to add services to. /// The global settings containing integration configuration. /// The service collection for chaining. /// /// /// This method orchestrates the registration of all event integration components based on the enabled /// message broker (Azure Service Bus or RabbitMQ). It is an internal method called by the public /// entry points AddAzureServiceBusListeners and AddRabbitMqListeners. /// /// /// NOTE: If both Azure Service Bus and RabbitMQ are configured, Azure Service Bus takes precedence. This means that /// Azure Service Bus listeners will be registered (and RabbitMQ listeners will NOT) even if this event is called /// from AddRabbitMqListeners when Azure Service Bus settings are configured. /// /// /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before invoking this method. /// This method depends on distributed cache infrastructure being available for the keyed extended /// cache registration. /// /// /// Registered Services: /// - Keyed ExtendedCache for event integrations /// - Integration filter service /// - Integration handlers for Slack, Webhook, Hec, Datadog, and Teams /// - Hosted services for event and integration listeners (based on enabled message broker) /// /// internal static IServiceCollection AddEventIntegrationServices(this IServiceCollection services, GlobalSettings globalSettings) { // Add common services // NOTE: AddDistributedCache must be called by the caller before this method services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings); services.TryAddSingleton(); services.TryAddKeyedSingleton("persistent"); // Add services in support of handlers services.AddSlackService(globalSettings); services.AddTeamsService(globalSettings); services.TryAddSingleton(TimeProvider.System); services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); services.AddHttpClient(DatadogIntegrationHandler.HttpClientName); // Add integration handlers services.TryAddSingleton, SlackIntegrationHandler>(); services.TryAddSingleton, WebhookIntegrationHandler>(); services.TryAddSingleton, DatadogIntegrationHandler>(); services.TryAddSingleton, TeamsIntegrationHandler>(); var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings); var slackConfiguration = new SlackListenerConfiguration(globalSettings); var webhookConfiguration = new WebhookListenerConfiguration(globalSettings); var hecConfiguration = new HecListenerConfiguration(globalSettings); var datadogConfiguration = new DatadogListenerConfiguration(globalSettings); var teamsConfiguration = new TeamsListenerConfiguration(globalSettings); if (IsAzureServiceBusEnabled(globalSettings)) { services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => new AzureServiceBusEventListenerService( configuration: repositoryConfiguration, handler: provider.GetRequiredService(), serviceBusService: provider.GetRequiredService(), serviceBusOptions: new ServiceBusProcessorOptions() { PrefetchCount = repositoryConfiguration.EventPrefetchCount, MaxConcurrentCalls = repositoryConfiguration.EventMaxConcurrentCalls }, loggerFactory: provider.GetRequiredService() ) ) ); services.AddAzureServiceBusIntegration(slackConfiguration); services.AddAzureServiceBusIntegration(webhookConfiguration); services.AddAzureServiceBusIntegration(hecConfiguration); services.AddAzureServiceBusIntegration(datadogConfiguration); services.AddAzureServiceBusIntegration(teamsConfiguration); return services; } if (IsRabbitMqEnabled(globalSettings)) { services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => new RabbitMqEventListenerService( handler: provider.GetRequiredService(), configuration: repositoryConfiguration, rabbitMqService: provider.GetRequiredService(), loggerFactory: provider.GetRequiredService() ) ) ); services.AddRabbitMqIntegration(slackConfiguration); services.AddRabbitMqIntegration(webhookConfiguration); services.AddRabbitMqIntegration(hecConfiguration); services.AddRabbitMqIntegration(datadogConfiguration); services.AddRabbitMqIntegration(teamsConfiguration); } return services; } /// /// Registers Azure Service Bus-based event integration listeners for a specific integration type. /// /// The integration configuration details type (e.g., SlackIntegrationConfigurationDetails). /// The listener configuration type implementing IIntegrationListenerConfiguration. /// The service collection to add services to. /// The listener configuration containing routing keys and message processing settings. /// The service collection for chaining. /// /// /// This method registers three key components: /// 1. EventIntegrationHandler - Keyed singleton for processing integration events /// 2. AzureServiceBusEventListenerService - Hosted service for listening to event messages from Azure Service Bus /// for this integration type /// 3. AzureServiceBusIntegrationListenerService - Hosted service for listening to integration messages from /// Azure Service Bus for this integration type /// /// /// The handler uses the listener configuration's routing key as its service key, allowing multiple /// handlers to be registered for different integration types. /// /// /// Service Bus processor options (PrefetchCount and MaxConcurrentCalls) are configured from the listener /// configuration to optimize message throughput and concurrency. /// /// internal static IServiceCollection AddAzureServiceBusIntegration(this IServiceCollection services, TListenerConfig listenerConfiguration) where TConfig : class where TListenerConfig : IIntegrationListenerConfiguration { services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) => new EventIntegrationHandler( integrationType: listenerConfiguration.IntegrationType, eventIntegrationPublisher: provider.GetRequiredService(), integrationFilterService: provider.GetRequiredService(), cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName), configurationRepository: provider.GetRequiredService(), groupRepository: provider.GetRequiredService(), organizationRepository: provider.GetRequiredService(), organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>()) ); services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => new AzureServiceBusEventListenerService( configuration: listenerConfiguration, handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey), serviceBusService: provider.GetRequiredService(), serviceBusOptions: new ServiceBusProcessorOptions() { PrefetchCount = listenerConfiguration.EventPrefetchCount, MaxConcurrentCalls = listenerConfiguration.EventMaxConcurrentCalls }, loggerFactory: provider.GetRequiredService() ) ) ); services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => new AzureServiceBusIntegrationListenerService( configuration: listenerConfiguration, handler: provider.GetRequiredService>(), serviceBusService: provider.GetRequiredService(), serviceBusOptions: new ServiceBusProcessorOptions() { PrefetchCount = listenerConfiguration.IntegrationPrefetchCount, MaxConcurrentCalls = listenerConfiguration.IntegrationMaxConcurrentCalls }, loggerFactory: provider.GetRequiredService() ) ) ); return services; } /// /// Registers RabbitMQ-based event integration listeners for a specific integration type. /// /// The integration configuration details type (e.g., SlackIntegrationConfigurationDetails). /// The listener configuration type implementing IIntegrationListenerConfiguration. /// The service collection to add services to. /// The listener configuration containing routing keys and message processing settings. /// The service collection for chaining. /// /// /// This method registers three key components: /// 1. EventIntegrationHandler - Keyed singleton for processing integration events /// 2. RabbitMqEventListenerService - Hosted service for listening to event messages from RabbitMQ for /// this integration type /// 3. RabbitMqIntegrationListenerService - Hosted service for listening to integration messages from RabbitMQ for /// this integration type /// /// /// /// The handler uses the listener configuration's routing key as its service key, allowing multiple /// handlers to be registered for different integration types. /// /// internal static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services, TListenerConfig listenerConfiguration) where TConfig : class where TListenerConfig : IIntegrationListenerConfiguration { services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) => new EventIntegrationHandler( integrationType: listenerConfiguration.IntegrationType, eventIntegrationPublisher: provider.GetRequiredService(), integrationFilterService: provider.GetRequiredService(), cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName), configurationRepository: provider.GetRequiredService(), groupRepository: provider.GetRequiredService(), organizationRepository: provider.GetRequiredService(), organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>()) ); services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => new RabbitMqEventListenerService( handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey), configuration: listenerConfiguration, rabbitMqService: provider.GetRequiredService(), loggerFactory: provider.GetRequiredService() ) ) ); services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => new RabbitMqIntegrationListenerService( handler: provider.GetRequiredService>(), configuration: listenerConfiguration, rabbitMqService: provider.GetRequiredService(), loggerFactory: provider.GetRequiredService(), timeProvider: provider.GetRequiredService() ) ) ); return services; } internal static IServiceCollection AddOrganizationIntegrationCommandsQueries(this IServiceCollection services) { services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); return services; } internal static IServiceCollection AddOrganizationIntegrationConfigurationCommandsQueries(this IServiceCollection services) { services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); return services; } /// /// Determines if RabbitMQ is enabled for event integrations based on configuration settings. /// /// The global settings containing RabbitMQ configuration. /// True if all required RabbitMQ settings are present; otherwise, false. /// /// Requires all the following settings to be configured: /// /// EventLogging.RabbitMq.HostName /// EventLogging.RabbitMq.Username /// EventLogging.RabbitMq.Password /// EventLogging.RabbitMq.EventExchangeName /// EventLogging.RabbitMq.IntegrationExchangeName /// /// internal static bool IsRabbitMqEnabled(GlobalSettings settings) { return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) && CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) && CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) && CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName) && CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.IntegrationExchangeName); } /// /// Determines if Azure Service Bus is enabled for event integrations based on configuration settings. /// /// The global settings containing Azure Service Bus configuration. /// True if all required Azure Service Bus settings are present; otherwise, false. /// /// Requires all of the following settings to be configured: /// /// EventLogging.AzureServiceBus.ConnectionString /// EventLogging.AzureServiceBus.EventTopicName /// EventLogging.AzureServiceBus.IntegrationTopicName /// /// internal static bool IsAzureServiceBusEnabled(GlobalSettings settings) { return CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName) && CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.IntegrationTopicName); } }