This project is currently hosted in Azure. Run the Deploy action to update the deployed version.
To setup locally:
- Install Docker Desktop.
- Download the code.
- Create the database container:
- Download the SQL Server instance using
docker pull mcr.microsoft.com/mssql/server:2019-latest - Create and run a new SQL Server container using
docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=<password>" -e "MSSQL_PID=Express" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-latest, substituting<password>with a strong, unique password. - Set the new
OrderBotpassword indeploy/db.sqllogin around line 7. - Run
deploy/db.sqlas sa, such as via SQL Management Studio, to create the database. - Run
deploy/tables.sqlas sa to create the table structure.
- Download the SQL Server instance using
- Create the application container:
- (Optional - Visual Studio does this automatically) Download a base image for the application using
docker pull mcr.microsoft.com/dotnet/runtime:8.0. - Create a
.envfile insrc/OrderBot. Create four entries inside it:ConnectionStrings__OrderBot, containing the SQL server connection string.Discord__ApiKey, containing the Discord Bot's API key.LogAnalytics__WorkspaceId, containing an Azure LogAnalytics workspace ID.LogAnalytics__WorkspaceKey, containing an Azure LogAnalytics primary key.
- (Optional - Visual Studio does this automatically) Download a base image for the application using
- Build and run the code.
An overview of the BGS Bot's deployed infrastructure is:
flowchart TB
edmc["Elite Dangerous<br/>Market Connector (EDMC)"] -->|Journal Entries| eddn["Elite Dangerous<br/>Data Network (EDDN)"]
edd["ED Discovery"] -->|Journal Entries| eddn
gameglass["Game Glass"] -->|Journal Entries| eddn
eddn -->|Journal Entries| Azure
eddn -->|Journal Entries| Inara
eddn -->|Journal Entries| EDSM
eddn -->|Journal Entries| EDDB
subgraph Azure
direction TB
ci[Azure Container Instance] <-->|Data| db[(Azure SQL Database)]
ci -->|Logs| logs[(Azure Log Analytics)]
end
Azure <-->|Commands and Messages| discord["Discord Server Infrastructure"]
discord <--> discordClient["Discord Clients"]
Key points:
- The BGS Order Bot receives data from Elite Dangerous Data Network (EDDN) via an AMQP queue. EDDN receives data from common ED companion applications and is used by many popular ED resources.
- The BGS Order Bot consists of a container, hosted in Azure Container Instance, an Azure SQL database for data and an Azure Log Analytics store for the logs. Deployment is automated via the Github repository
Deployaction. The container is configured by environment variables and is deployed from an Azure Container Registry (not shown for clarity). - The BGS Order Bot's primary user interface is via Discord. Individual Discord guilds (tenants) can invite the bot to their servers, configure it using commands then receive suggestions and carrier movement notifications.
Overview:
sequenceDiagram
DiscordClient->>BotHostedService : Client_InteractionCreated()
BotHostedService->>InteractionService: ExecuteCommandAsync()
InteractionService->>+CommandsModule: Call method with [SlashCommand()]
CommandsModule--)-InteractionService: void
InteractionService--)BotHostedService: void
BotHostedService--)DiscordClient: void
Key points:
CommandsModulerefers to a class derived fromInteractionModuleBase<SocketInteractionContext>. There are currently three:AdminCommandsModule, which handles administrative commands like audit and role management.CarrierMovementCommandsModule, which handles commands to ignore or track carrier movements.ToDoListCommandsModule, which handles viewing the To-Do list, supporting minor factions and adding goals.
- The
InteractionServiceprovided by Discord.Net provides a nice wrapper over manually parsing and handling commands.
Client_InteractionCreated in BotHostedService provides the following:
- Creates an
IServiceScopeso scoped DI services can be returned and cleaned up. - Adds a logging scope with common details such as the user, guild and command details. This is done here and not in
BaseCommandsModule<T>so errors captured here are logged with the same scope. - Shows an access denied-style error messages for unmet preconditions.
- Logs details of other errors and exceptions.
Best practice for writing slash (application) commands:
- Derive command modules classes from
BaseCommandsModule<T>. This class handles common tasks like creating database connections, audit logs and aResultobject. - Wrap the code for each command in a
try ... catchblock with anExceptionhandler containingResult.Exception. This handles any unexpected exceptions. While Discord.Net will catch and log unthrown exceptions, it will not notify the user. - Use
Resultmethods to communicate with the user and wraps logging and auditing for most situations. Specifically:Informationfor responses to queries or acknowledgements. These are logged as Information by default but not audited.Successfor successful changes or actions. These are audited by default and logged as Information.Errorfor unsuccessful changes or actions, such as invalid command parameter values. These are logged as Warnings. The error message has three parts: what, why and a fix. This encourages better error messages and separates the loggable portion (why).Exceptionfor unhandled or unknown exceptions. These are logged as Errors.
- The
Resultobject also does some housekeeping like (1) callingDeferAsyncearly to ensure long-running commands do not time out and (2) capping the message length to the max ephemeral response length. - Auditing is usually handled through the
Resultobject but you can still audit directly usingAuditLogger. Keep it to one audit message per command execution. - Logging is usually handled also through the
Resultobject but you can still log directly usingLogger. Keep it to one non-verbose/diagnostic log message per command execution. - Use
TransactionScope.Complete()as the last statement to save any database work. Otherwise, results will not be saved. - Remember that the class housing the command handler is instantiated for each interaction.
- Do not duplicate work in
BaseCommandsModuleorBotHostedService.Client_InteractionCreated. The general goal is to move as much work to there as possible. This standardizes behaviour and prevents code repetition.
To provide data for the Discord bot, this system listens for Elite Dangerous Data Network (EDDN) messages via the EddnMessageHostedService, which are handled by EddnMessageMessageProcessor subclasses. There are currently two: TodoListMessageProcessor, which captures system BGS data, and CarrierMovementMessageProcessor, which looks for carrier movements and notifies Discord guilds which have registered a carrier movement channel. These classes are instantiated for each message.
This structure provides separation of responsibilities. Classes for each message processor are in separate namespaces to further emphasize this.
An overview:
sequenceDiagram
participant EddnMessageHostedService
participant ToDoListMessageProcessor
participant CarrierMovementMessageProcessor
participant Caches
participant TextChannelWriter
activate EddnMessageHostedService
activate Caches
par
EddnMessageHostedService-)+ToDoListMessageProcessor: ProcessAsync()
ToDoListMessageProcessor->>Caches: Get Value
Caches-->>ToDoListMessageProcessor: Result
ToDoListMessageProcessor--)-EddnMessageHostedService: void
and
EddnMessageHostedService-)+CarrierMovementMessageProcessor: ProcessAsync()
CarrierMovementMessageProcessor->>Caches: Get Value
Caches-->>CarrierMovementMessageProcessor: Result
CarrierMovementMessageProcessor->>+TextChannelWriter: WriteLine()
TextChannelWriter-->>-CarrierMovementMessageProcessor: void
CarrierMovementMessageProcessor--)-EddnMessageHostedService: void
end
deactivate EddnMessageHostedService
deactivate Caches
Key points:
EddnMessageHostedServiceis started from Program.cs and runs for the container's lifetime.Cachesincludes various classes inherited fromMessageProcessorCache. Singleton objects instantiated from these cache classes minimize database access when processing and eliminating messages.TodoListMessageProcessorusesSupportedMinorFactionsCacheandGoalStarSystemsCache.CarrierMovementMessageProcessorusesStarSystemToDiscordGuildCache,IgnoredCarriersCacheandCarrierMovementChannelCache.
- Technically, the
TextChannelWriteris aTextWritercreated via aTextChannelWritterFactory. This is used to write to carrier movement channel(s). - Database or ORM classes like
OrderbotDbContextare omitted for clarity.
Regarding Caches, there is currently no cache invalidation mechanism, but the cache durations are short: five minutes. An unfinished invalidation pattern is in MessageProcessorCacheInvalidator.
- Using Docker with .Net Core: https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/visual-studio-tools-for-docker?view=aspnetcore-6.0
- Github action to build SQL server database: https://github.com/ankane/setup-sqlserver
- Discord.Net documentation: https://discordnet.dev/
- Using Log Analytics with Container Instances: https://learn.microsoft.com/en-us/azure/container-instances/container-instances-log-analytics
- CsvHelper quickstart: https://joshclose.github.io/CsvHelper/getting-started/
- Avoid record types with Entity Framework: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record
- Mermaid Sequence diagrams: https://mermaid-js.github.io/mermaid/#/sequenceDiagram