Fix build
This commit is contained in:
@@ -127,11 +127,59 @@ export TASK_ID="$TASK_ID"
|
|||||||
export PORT_OFFSET="$PORT_OFFSET"
|
export PORT_OFFSET="$PORT_OFFSET"
|
||||||
export TASK_SLOT="$TASK_SLOT"
|
export TASK_SLOT="$TASK_SLOT"
|
||||||
|
|
||||||
# Change to AppHost directory
|
# Ensure HTTPS dev certificate is available (Aspire may need it even for HTTP mode)
|
||||||
|
echo "🔐 Ensuring HTTPS developer certificate is available..."
|
||||||
|
if ! dotnet dev-certs https --check > /dev/null 2>&1; then
|
||||||
|
echo " Generating HTTPS developer certificate..."
|
||||||
|
dotnet dev-certs https --trust > /dev/null 2>&1 || {
|
||||||
|
echo "⚠️ Could not generate/trust certificate"
|
||||||
|
echo " Will try to use HTTP-only profile"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure Aspire to use HTTP only (avoid certificate issues)
|
||||||
|
# Use the "http" launch profile which is configured for HTTP-only
|
||||||
|
export ASPNETCORE_URLS="http://localhost:15242"
|
||||||
|
export DOTNET_DASHBOARD_OTLP_ENDPOINT_URL="http://localhost:19204"
|
||||||
|
export DOTNET_RESOURCE_SERVICE_ENDPOINT_URL="http://localhost:20284"
|
||||||
|
export DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL="http://localhost:19204"
|
||||||
|
export ASPNETCORE_ENVIRONMENT="Development"
|
||||||
|
export DOTNET_ENVIRONMENT="Development"
|
||||||
|
|
||||||
|
# Restore packages in the worktree first to ensure all dependencies are available
|
||||||
|
# This is important because Aspire will build projects that may reference worktree paths
|
||||||
|
echo ""
|
||||||
|
echo "📦 Restoring NuGet packages..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# Restore at solution level in worktree if it exists
|
||||||
|
if [ -f "$WORKTREE_PROJECT_ROOT/src/Managing.sln" ]; then
|
||||||
|
echo " Restoring in worktree solution..."
|
||||||
|
cd "$WORKTREE_PROJECT_ROOT/src"
|
||||||
|
# Suppress all warnings and only show errors
|
||||||
|
dotnet restore Managing.sln --verbosity quiet --nologo 2>&1 | \
|
||||||
|
grep -vE "(warning|Warning|WARNING|NU[0-9]|\.csproj :)" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore at solution level in main repo (where we'll actually run from)
|
||||||
|
echo " Restoring in main repo solution..."
|
||||||
|
cd "$MAIN_REPO/src"
|
||||||
|
# Suppress all warnings and only show errors
|
||||||
|
RESTORE_OUTPUT=$(dotnet restore Managing.sln --verbosity quiet --nologo 2>&1 | \
|
||||||
|
grep -vE "(warning|Warning|WARNING|NU[0-9]|\.csproj :)" || true)
|
||||||
|
if echo "$RESTORE_OUTPUT" | grep -qE "(error|Error|ERROR|failed|Failed)"; then
|
||||||
|
echo "❌ Package restore failed:"
|
||||||
|
echo "$RESTORE_OUTPUT"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ Packages restored successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure we're in the AppHost directory for running Aspire
|
||||||
cd "$MAIN_REPO/src/Managing.AppHost"
|
cd "$MAIN_REPO/src/Managing.AppHost"
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Run Aspire (this will start the API and Workers)
|
# Run Aspire (this will start the API and Workers)
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "🚀 Starting Aspire..."
|
echo "🚀 Starting Aspire..."
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@@ -141,8 +189,9 @@ echo ""
|
|||||||
ASPIRE_LOG="$WORKTREE_PROJECT_ROOT/.task-pids/aspire-${TASK_ID}.log"
|
ASPIRE_LOG="$WORKTREE_PROJECT_ROOT/.task-pids/aspire-${TASK_ID}.log"
|
||||||
mkdir -p "$(dirname "$ASPIRE_LOG")"
|
mkdir -p "$(dirname "$ASPIRE_LOG")"
|
||||||
|
|
||||||
# Start Aspire
|
# Start Aspire using the "http" launch profile (HTTP only, no HTTPS)
|
||||||
dotnet run > "$ASPIRE_LOG" 2>&1 &
|
# All output goes to log file (warnings will be filtered when displaying)
|
||||||
|
dotnet run --launch-profile http > "$ASPIRE_LOG" 2>&1 &
|
||||||
ASPIRE_PID=$!
|
ASPIRE_PID=$!
|
||||||
|
|
||||||
# Save PID
|
# Save PID
|
||||||
@@ -201,9 +250,9 @@ for i in {1..120}; do
|
|||||||
# Show progress every 10 seconds
|
# Show progress every 10 seconds
|
||||||
if [ $((i % 10)) -eq 0 ]; then
|
if [ $((i % 10)) -eq 0 ]; then
|
||||||
echo " Still starting... (${i}/120 seconds)"
|
echo " Still starting... (${i}/120 seconds)"
|
||||||
# Show last few lines of log for progress
|
# Show last few lines of log for progress (filter warnings)
|
||||||
if [ -f "$ASPIRE_LOG" ]; then
|
if [ -f "$ASPIRE_LOG" ]; then
|
||||||
LAST_LINE=$(tail -1 "$ASPIRE_LOG" 2>/dev/null | cut -c1-80)
|
LAST_LINE=$(tail -20 "$ASPIRE_LOG" 2>/dev/null | grep -vE "(warning|Warning|WARNING|NU[0-9]|\.csproj :)" | tail -1 | cut -c1-80)
|
||||||
if [ -n "$LAST_LINE" ]; then
|
if [ -n "$LAST_LINE" ]; then
|
||||||
echo " Latest: $LAST_LINE"
|
echo " Latest: $LAST_LINE"
|
||||||
fi
|
fi
|
||||||
@@ -213,8 +262,8 @@ for i in {1..120}; do
|
|||||||
if [ $i -eq 120 ]; then
|
if [ $i -eq 120 ]; then
|
||||||
echo "⚠️ Aspire dashboard did not become ready after 120 seconds"
|
echo "⚠️ Aspire dashboard did not become ready after 120 seconds"
|
||||||
echo "💡 Check the log: $ASPIRE_LOG"
|
echo "💡 Check the log: $ASPIRE_LOG"
|
||||||
echo "💡 Last 10 lines of log:"
|
echo "💡 Last 10 lines of log (warnings filtered):"
|
||||||
tail -10 "$ASPIRE_LOG" 2>/dev/null || echo " (log file not found)"
|
tail -30 "$ASPIRE_LOG" 2>/dev/null | grep -vE "(warning|Warning|WARNING|NU[0-9]|\.csproj :)" | tail -10 || echo " (log file not found)"
|
||||||
# Try to use default port anyway
|
# Try to use default port anyway
|
||||||
if [ -z "$ASPIRE_DASHBOARD_URL" ]; then
|
if [ -z "$ASPIRE_DASHBOARD_URL" ]; then
|
||||||
ASPIRE_DASHBOARD_PORT=15242
|
ASPIRE_DASHBOARD_PORT=15242
|
||||||
@@ -269,10 +318,11 @@ echo " Health check: http://localhost:${API_PORT}/alive"
|
|||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Tail the Aspire log
|
# Tail the Aspire log (filter out warnings for cleaner output)
|
||||||
echo "📋 Showing Aspire logs (Press Ctrl+C to stop)"
|
echo "📋 Showing Aspire logs (Press Ctrl+C to stop)"
|
||||||
|
echo " (Warnings are hidden for cleaner output - full logs in: $ASPIRE_LOG)"
|
||||||
echo ""
|
echo ""
|
||||||
tail -f "$ASPIRE_LOG" 2>/dev/null || {
|
tail -f "$ASPIRE_LOG" 2>/dev/null | grep -vE "(warning|Warning|WARNING|NU[0-9]|\.csproj :)" || {
|
||||||
echo "❌ Cannot read Aspire log: $ASPIRE_LOG"
|
echo "❌ Cannot read Aspire log: $ASPIRE_LOG"
|
||||||
echo "💡 Aspire may still be starting. Check the log manually."
|
echo "💡 Aspire may still be starting. Check the log manually."
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />
|
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />
|
||||||
|
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,31 +1,152 @@
|
|||||||
using Aspire.Hosting;
|
using DotNetEnv;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
var builder = DistributedApplication.CreateBuilder(args);
|
// Detect running mode: IDE mode (appsettings + .env) or Vibe-kanban mode (env vars)
|
||||||
|
var isVibeKanbanMode = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TASK_ID"))
|
||||||
|
&& Environment.GetEnvironmentVariable("TASK_ID") != "DEFAULT";
|
||||||
|
|
||||||
// Get task-specific configuration from environment variables
|
string taskId;
|
||||||
var taskId = Environment.GetEnvironmentVariable("TASK_ID") ?? "DEFAULT";
|
int portOffset;
|
||||||
var portOffset = int.Parse(Environment.GetEnvironmentVariable("PORT_OFFSET") ?? "0");
|
string taskSlot;
|
||||||
var taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? "1";
|
int apiPort;
|
||||||
|
int postgresPort;
|
||||||
|
int redisPort;
|
||||||
|
int orleansSiloPort;
|
||||||
|
int orleansGatewayPort;
|
||||||
|
int orleansDashboardPort;
|
||||||
|
string dbName;
|
||||||
|
string orleansDbName;
|
||||||
|
string postgresConnectionString;
|
||||||
|
string postgresOrleansConnectionString;
|
||||||
|
string redisConnectionString;
|
||||||
|
string influxDbUrl;
|
||||||
|
string influxDbToken;
|
||||||
|
|
||||||
|
if (isVibeKanbanMode)
|
||||||
|
{
|
||||||
|
// Vibe-kanban mode: Use environment variables directly
|
||||||
|
Console.WriteLine("🔧 Running in Vibe-kanban mode (using environment variables)");
|
||||||
|
|
||||||
|
taskId = Environment.GetEnvironmentVariable("TASK_ID") ?? "DEFAULT";
|
||||||
|
portOffset = int.Parse(Environment.GetEnvironmentVariable("PORT_OFFSET") ?? "0");
|
||||||
|
taskSlot = Environment.GetEnvironmentVariable("TASK_SLOT") ?? "1";
|
||||||
|
|
||||||
// Calculate ports based on task configuration
|
// Calculate ports based on task configuration
|
||||||
var apiPort = 5000 + portOffset;
|
apiPort = 5000 + portOffset;
|
||||||
var postgresPort = 5432 + portOffset;
|
postgresPort = 5432 + portOffset;
|
||||||
var redisPort = 6379 + portOffset;
|
redisPort = 6379 + portOffset;
|
||||||
|
|
||||||
// Calculate Orleans ports from TASK_SLOT
|
// Calculate Orleans ports from TASK_SLOT
|
||||||
var taskSlotInt = int.Parse(taskSlot);
|
var taskSlotInt = int.Parse(taskSlot);
|
||||||
var orleansSiloPort = 11111 + (taskSlotInt - 1) * 10;
|
orleansSiloPort = 11111 + (taskSlotInt - 1) * 10;
|
||||||
var orleansGatewayPort = 30000 + (taskSlotInt - 1) * 10;
|
orleansGatewayPort = 30000 + (taskSlotInt - 1) * 10;
|
||||||
var orleansDashboardPort = 9999 + (taskSlotInt - 1);
|
orleansDashboardPort = 9999 + (taskSlotInt - 1);
|
||||||
|
|
||||||
// Database names
|
// Database names
|
||||||
var dbName = $"managing_{taskId.ToLower()}";
|
dbName = $"managing_{taskId.ToLower()}";
|
||||||
var orleansDbName = $"orleans_{taskId.ToLower()}";
|
orleansDbName = $"orleans_{taskId.ToLower()}";
|
||||||
|
|
||||||
// Connection strings (using existing Docker containers managed by Docker Compose)
|
// Connection strings (using existing Docker containers managed by Docker Compose)
|
||||||
var postgresConnectionString = $"Host=localhost;Port={postgresPort};Database={dbName};Username=postgres;Password=postgres";
|
postgresConnectionString = $"Host=localhost;Port={postgresPort};Database={dbName};Username=postgres;Password=postgres";
|
||||||
var postgresOrleansConnectionString = $"Host=localhost;Port={postgresPort};Database={orleansDbName};Username=postgres;Password=postgres";
|
postgresOrleansConnectionString = $"Host=localhost;Port={postgresPort};Database={orleansDbName};Username=postgres;Password=postgres";
|
||||||
var redisConnectionString = $"localhost:{redisPort}";
|
redisConnectionString = $"localhost:{redisPort}";
|
||||||
|
|
||||||
|
// InfluxDB from environment or defaults
|
||||||
|
influxDbUrl = Environment.GetEnvironmentVariable("InfluxDb__Url") ?? "http://localhost:8086/";
|
||||||
|
influxDbToken = Environment.GetEnvironmentVariable("InfluxDb__Token") ?? "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA==";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// IDE mode: Load .env file and optional appsettings.json
|
||||||
|
Console.WriteLine("🔧 Running in IDE mode (using environment variables, .env, and optional appsettings.json)");
|
||||||
|
|
||||||
|
// Load .env file if it exists (optional)
|
||||||
|
var enableEnvFile = Environment.GetEnvironmentVariable("ENABLE_ENV_FILE") != "false";
|
||||||
|
if (enableEnvFile)
|
||||||
|
{
|
||||||
|
var envFilePaths = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(Directory.GetCurrentDirectory(), ".env"),
|
||||||
|
Path.Combine(AppContext.BaseDirectory, ".env"),
|
||||||
|
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".env")),
|
||||||
|
Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", ".env")), // From AppHost to project root
|
||||||
|
};
|
||||||
|
|
||||||
|
string? loadedEnvPath = null;
|
||||||
|
foreach (var envPath in envFilePaths)
|
||||||
|
{
|
||||||
|
if (File.Exists(envPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Env.Load(envPath);
|
||||||
|
loadedEnvPath = envPath;
|
||||||
|
Console.WriteLine($"✅ Loaded .env file from: {envPath}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"⚠️ Failed to load .env file from {envPath}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build configuration from appsettings.json (optional) and environment variables
|
||||||
|
// appsettings.json is optional because environment variables and .env files take precedence
|
||||||
|
var configBuilder = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
|
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||||
|
.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
var configuration = configBuilder.Build();
|
||||||
|
|
||||||
|
// Read configuration values
|
||||||
|
taskId = configuration["TASK_ID"] ?? Environment.GetEnvironmentVariable("TASK_ID") ?? "DEFAULT";
|
||||||
|
portOffset = int.Parse(configuration["PORT_OFFSET"] ?? Environment.GetEnvironmentVariable("PORT_OFFSET") ?? "0");
|
||||||
|
taskSlot = configuration["TASK_SLOT"] ?? Environment.GetEnvironmentVariable("TASK_SLOT") ?? "1";
|
||||||
|
|
||||||
|
// Calculate ports based on task configuration
|
||||||
|
apiPort = 5000 + portOffset;
|
||||||
|
postgresPort = 5432 + portOffset;
|
||||||
|
redisPort = 6379 + portOffset;
|
||||||
|
|
||||||
|
// Calculate Orleans ports from TASK_SLOT
|
||||||
|
var taskSlotInt = int.Parse(taskSlot);
|
||||||
|
orleansSiloPort = 11111 + (taskSlotInt - 1) * 10;
|
||||||
|
orleansGatewayPort = 30000 + (taskSlotInt - 1) * 10;
|
||||||
|
orleansDashboardPort = 9999 + (taskSlotInt - 1);
|
||||||
|
|
||||||
|
// Database names
|
||||||
|
dbName = $"managing_{taskId.ToLower()}";
|
||||||
|
orleansDbName = $"orleans_{taskId.ToLower()}";
|
||||||
|
|
||||||
|
// Connection strings from configuration or defaults
|
||||||
|
postgresConnectionString = configuration["PostgreSql:ConnectionString"]
|
||||||
|
?? configuration["PostgreSql__ConnectionString"]
|
||||||
|
?? $"Host=localhost;Port={postgresPort};Database={dbName};Username=postgres;Password=postgres";
|
||||||
|
|
||||||
|
postgresOrleansConnectionString = configuration["PostgreSql:Orleans"]
|
||||||
|
?? configuration["PostgreSql__Orleans"]
|
||||||
|
?? $"Host=localhost;Port={postgresPort};Database={orleansDbName};Username=postgres;Password=postgres";
|
||||||
|
|
||||||
|
redisConnectionString = configuration["Redis:ConnectionString"]
|
||||||
|
?? configuration["Redis__ConnectionString"]
|
||||||
|
?? $"localhost:{redisPort}";
|
||||||
|
|
||||||
|
// InfluxDB from configuration
|
||||||
|
influxDbUrl = configuration["InfluxDb:Url"]
|
||||||
|
?? configuration["InfluxDb__Url"]
|
||||||
|
?? "http://localhost:8086/";
|
||||||
|
|
||||||
|
influxDbToken = configuration["InfluxDb:Token"]
|
||||||
|
?? configuration["InfluxDb__Token"]
|
||||||
|
?? "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA==";
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = DistributedApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
|
||||||
// Add API project
|
// Add API project
|
||||||
var api = builder.AddProject("api", "../Managing.Api/Managing.Api.csproj")
|
var api = builder.AddProject("api", "../Managing.Api/Managing.Api.csproj")
|
||||||
@@ -39,8 +160,8 @@ var api = builder.AddProject("api", "../Managing.Api/Managing.Api.csproj")
|
|||||||
.WithEnvironment("SILO_ROLE", "Trading")
|
.WithEnvironment("SILO_ROLE", "Trading")
|
||||||
.WithEnvironment("PostgreSql__ConnectionString", postgresConnectionString)
|
.WithEnvironment("PostgreSql__ConnectionString", postgresConnectionString)
|
||||||
.WithEnvironment("PostgreSql__Orleans", postgresOrleansConnectionString)
|
.WithEnvironment("PostgreSql__Orleans", postgresOrleansConnectionString)
|
||||||
.WithEnvironment("InfluxDb__Url", "http://localhost:8086/")
|
.WithEnvironment("InfluxDb__Url", influxDbUrl)
|
||||||
.WithEnvironment("InfluxDb__Token", "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA==")
|
.WithEnvironment("InfluxDb__Token", influxDbToken)
|
||||||
.WithEnvironment("ORLEANS_SILO_PORT", orleansSiloPort.ToString())
|
.WithEnvironment("ORLEANS_SILO_PORT", orleansSiloPort.ToString())
|
||||||
.WithEnvironment("ORLEANS_GATEWAY_PORT", orleansGatewayPort.ToString())
|
.WithEnvironment("ORLEANS_GATEWAY_PORT", orleansGatewayPort.ToString())
|
||||||
.WithEnvironment("ORLEANS_DASHBOARD_PORT", orleansDashboardPort.ToString());
|
.WithEnvironment("ORLEANS_DASHBOARD_PORT", orleansDashboardPort.ToString());
|
||||||
@@ -51,8 +172,8 @@ var workers = builder.AddProject("workers", "../Managing.Workers/Managing.Worker
|
|||||||
.WithEnvironment("TASK_SLOT", taskSlot)
|
.WithEnvironment("TASK_SLOT", taskSlot)
|
||||||
.WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development")
|
.WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development")
|
||||||
.WithEnvironment("PostgreSql__ConnectionString", postgresConnectionString)
|
.WithEnvironment("PostgreSql__ConnectionString", postgresConnectionString)
|
||||||
.WithEnvironment("InfluxDb__Url", "http://localhost:8086/")
|
.WithEnvironment("InfluxDb__Url", influxDbUrl)
|
||||||
.WithEnvironment("InfluxDb__Token", "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA==");
|
.WithEnvironment("InfluxDb__Token", influxDbToken);
|
||||||
|
|
||||||
// Build and run
|
// Build and run
|
||||||
builder.Build().Run();
|
builder.Build().Run();
|
||||||
|
|||||||
Reference in New Issue
Block a user