Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Discord webhook support #106

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ArmaForces.Arma.Server.Tests/Helpers/TestSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
public string ServerExecutableName { get; set; } = "arma3.exe";
public string? SteamUser { get; set; } = "TEST_USER";
public string? SteamPassword { get; set; } = "TEST_PASSWORD";
public string? WebhookUrl { get; set; }

public Result LoadSettings() => throw new System.NotImplementedException();

public Result ReloadSettings() => throw new System.NotImplementedException();

public async Task<Result> ReloadSettings(ISettings settings) => throw new System.NotImplementedException();

Check warning on line 28 in ArmaForces.Arma.Server.Tests/Helpers/TestSettings.cs

View workflow job for this annotation

GitHub Actions / tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
}
}
5 changes: 5 additions & 0 deletions ArmaForces.Arma.Server/Config/ISettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public interface ISettings {
/// TODO: Handle no steam password
string? SteamPassword { get; }

/// <summary>
/// URL for sending webhook notifications.
/// </summary>
string? WebhookUrl { get; }

/// <summary>
/// Loads settings from configuration.
/// </summary>
Expand Down
7 changes: 6 additions & 1 deletion ArmaForces.Arma.Server/Config/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
public string ServerExecutableName { get; set; } = "arma3server_x64.exe";
public string? SteamUser { get; set; }
public string? SteamPassword { get; set; }
public string? WebhookUrl { get; set; }

private readonly IFileSystem _fileSystem;
private readonly IRegistryReader _registryReader;
Expand All @@ -51,7 +52,7 @@
public static Settings LoadSettings(IServiceProvider serviceProvider)
{
var json = File.ReadAllTextAsync(SettingsJsonPath).Result;
return JsonSerializer.Deserialize<Settings>(json);

Check warning on line 55 in ArmaForces.Arma.Server/Config/Settings.cs

View workflow job for this annotation

GitHub Actions / tests

Possible null reference return.
}

public Result LoadSettings()
Expand All @@ -64,7 +65,8 @@
.Tap(ObtainApiMissionsBaseUrl)
.Tap(ObtainApiModsetsBaseUrl)
.Tap(ObtainSteamUserName)
.Tap(ObtainSteamPassword);
.Tap(ObtainSteamPassword)
.Tap(ObtainWebhookUrl);
}

public Result ReloadSettings()
Expand All @@ -86,6 +88,7 @@
ServerExecutableName = settings.ServerExecutableName;
SteamUser = settings.SteamUser;
SteamPassword = settings.SteamPassword;
WebhookUrl = settings.WebhookUrl;

var json = JsonSerializer.Serialize(this, JsonOptions.Default);
await _fileSystem.File.WriteAllTextAsync(SettingsJsonPath, json);
Expand Down Expand Up @@ -113,6 +116,8 @@
private void ObtainSteamUserName() => SteamUser ??= _config["steamUserName"];

private void ObtainSteamPassword() => SteamPassword ??= _config["steamPassword"];

private void ObtainWebhookUrl() => WebhookUrl ??= _config["webhookUrl"];

private Result GetServerPath()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Serilog.Sinks.Discord.Lite" Version="0.2.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>

Expand Down
34 changes: 26 additions & 8 deletions ArmaForces.ArmaServerManager/Common/HttpClientBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using ArmaForces.Arma.Server.Constants;
Expand All @@ -19,21 +20,38 @@ protected async Task<Result<T>> HttpGetAsync<T>(string requestUrl)
{
var httpResponseMessage = await _httpClient.GetAsync(requestUrl);

var responseBody = await httpResponseMessage.Content.ReadAsStringAsync();

if (httpResponseMessage.IsSuccessStatusCode)
{
var responseBody = await httpResponseMessage.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(responseBody, JsonOptions.Default) ??
Result.Failure<T>($"Failed to deserialize response: {responseBody}");
}
else
{
var responseBody = await httpResponseMessage.Content.ReadAsStringAsync();
var error = string.IsNullOrWhiteSpace(responseBody)
? httpResponseMessage.ReasonPhrase
: responseBody;

var error = string.IsNullOrWhiteSpace(responseBody)
? httpResponseMessage.ReasonPhrase
: responseBody;

return Result.Failure<T>(error);
return Result.Failure<T>(error);
}

protected async Task<Result> HttpPostAsync<T>(string? requestUrl, T content)
{
var stringContent = new StringContent(JsonSerializer.Serialize(content, JsonSerializerOptions.Web), Encoding.UTF8, "application/json");
var httpResponseMessage = await _httpClient.PostAsync(requestUrl, stringContent);

var responseBody = await httpResponseMessage.Content.ReadAsStringAsync();

if (httpResponseMessage.IsSuccessStatusCode)
{
return Result.Success();
}

var error = string.IsNullOrWhiteSpace(responseBody)
? httpResponseMessage.ReasonPhrase
: responseBody;

return Result.Failure(error);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;

namespace ArmaForces.ArmaServerManager.Features.Webhooks.DTOs;

internal record DiscordWebhook
{
[JsonPropertyName("username")]
public string UserName { get; init; }

public string Content { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading.Tasks;

namespace ArmaForces.ArmaServerManager.Features.Webhooks;

public interface IWebhookClient
{
Task Send(string content);
}
22 changes: 22 additions & 0 deletions ArmaForces.ArmaServerManager/Features/Webhooks/WebhookClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Net.Http;
using System.Threading.Tasks;
using ArmaForces.ArmaServerManager.Common;
using ArmaForces.ArmaServerManager.Features.Webhooks.DTOs;

namespace ArmaForces.ArmaServerManager.Features.Webhooks;

internal class WebhookClient : HttpClientBase, IWebhookClient
{
public WebhookClient(HttpClient httpClient) : base(httpClient)
{
}

public async Task Send(string content)
{
await HttpPostAsync(requestUrl: null, content: new DiscordWebhook
{
UserName = "Arma Server",
Content = content
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Net.Http;
using ArmaForces.Arma.Server.Config;
using Microsoft.Extensions.DependencyInjection;

namespace ArmaForces.ArmaServerManager.Features.Webhooks;

internal static class WebhookServiceCollectionExtensions
{
public static IServiceCollection AddWebhookClient(this IServiceCollection services)
=> services.AddHttpClientForWebhookClient();

private static IServiceCollection AddHttpClientForWebhookClient(this IServiceCollection services)
{
services
.AddHttpClient<IWebhookClient, WebhookClient>()
.ConfigureHttpClient(SetBaseAddress());

return services;
}

private static Action<IServiceProvider, HttpClient> SetBaseAddress()
=> (services, client) => client.BaseAddress = new Uri(
services.GetRequiredService<ISettings>().WebhookUrl ??
throw new NotSupportedException("Missions API is required, provide valid url address."));
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.Discord;

namespace ArmaForces.ArmaServerManager.Infrastructure.Logging
{
Expand All @@ -21,6 +23,7 @@ private static Action<HostBuilderContext, IServiceProvider, LoggerConfiguration>
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: OutputTemplate)
.AddDiscordSinkIfConfigured(services)
.WriteTo.File(GetFileLogsPath(services),
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true,
Expand All @@ -32,5 +35,19 @@ private static string GetFileLogsPath(IServiceProvider services)

return Path.Join(settings.ManagerDirectory, LogsDirectoryName, LogFileName);
}

private static LoggerConfiguration AddDiscordSinkIfConfigured(this LoggerConfiguration loggerConfiguration, IServiceProvider services)
{
var settings = services.GetRequiredService<ISettings>();
if (settings.WebhookUrl == null) return loggerConfiguration;

var webhookUrlSplit = settings.WebhookUrl.Split('/');
var webhookToken = webhookUrlSplit[-1];
var webhookIdParsed = ulong.TryParse(webhookUrlSplit[-2], out var webhookId);
if (!webhookIdParsed) return loggerConfiguration;

return loggerConfiguration
.WriteTo.Discord(webhookId, webhookToken, restrictedToMinimumLevel: LogEventLevel.Error);
}
}
}
8 changes: 8 additions & 0 deletions ArmaForces.ArmaServerManager/Services/IWebhookService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading.Tasks;

namespace ArmaForces.ArmaServerManager.Services;

public interface IWebhookService
{
Task AnnounceStart();
}
8 changes: 5 additions & 3 deletions ArmaForces.ArmaServerManager/Services/StartupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,25 @@ namespace ArmaForces.ArmaServerManager.Services
public class StartupService : IHostedService
{
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IWebhookService _webhookService;

/// <inheritdoc cref="StartupService"/>
public StartupService(IWebHostEnvironment webHostEnvironment)
public StartupService(IWebHostEnvironment webHostEnvironment, IWebhookService webhookService)
{
_webHostEnvironment = webHostEnvironment;
_webhookService = webhookService;
}

/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
public async Task StartAsync(CancellationToken cancellationToken)
{
if (_webHostEnvironment.IsProduction())
{
RecurringJob.AddOrUpdate<MaintenanceService>(x => x.PerformMaintenance(CancellationToken.None),
Cron.Daily(4));
}

return Task.CompletedTask;
await _webhookService.AnnounceStart();
}

/// <inheritdoc />
Expand Down
30 changes: 30 additions & 0 deletions ArmaForces.ArmaServerManager/Services/WebhookService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Threading.Tasks;
using ArmaForces.Arma.Server.Config;
using ArmaForces.ArmaServerManager.Features.Webhooks;
using Microsoft.Extensions.Logging;

namespace ArmaForces.ArmaServerManager.Services;

public class WebhookService : IWebhookService
{
private readonly IWebhookClient _webhookClient;
private readonly ILogger<WebhookService> _logger;
private readonly string? _webhookUrl;

public WebhookService(IWebhookClient webhookClient, ISettings settings, ILogger<WebhookService> logger)
{
_webhookClient = webhookClient;
_logger = logger;
_webhookUrl = settings.WebhookUrl;
}

public async Task AnnounceStart()
{
_logger.LogInformation("Announce start");
if (_webhookUrl == null) return;
_logger.LogInformation("Webhook url: {WebhookUrl}", _webhookUrl);

await Task.Delay(10000);
await _webhookClient.Send("Manager started");
}
}
5 changes: 5 additions & 0 deletions ArmaForces.ArmaServerManager/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using ArmaForces.ArmaServerManager.Features.Servers;
using ArmaForces.ArmaServerManager.Features.Servers.Providers;
using ArmaForces.ArmaServerManager.Features.Status;
using ArmaForces.ArmaServerManager.Features.Webhooks;
using ArmaForces.ArmaServerManager.Infrastructure.Authentication;
using ArmaForces.ArmaServerManager.Infrastructure.Converters;
using ArmaForces.ArmaServerManager.Infrastructure.Documentation;
Expand Down Expand Up @@ -127,6 +128,10 @@ public void ConfigureServices(IServiceCollection services)
// Status
.AddSingleton<IAppStatusStore, AppStatusStore>()
.AddSingleton<IStatusProvider, StatusProvider>()

// Webhooks
.AddSingleton<IWebhookService, WebhookService>()
.AddWebhookClient()

// Hangfire
.AddSingleton<IJobsScheduler, JobsScheduler>()
Expand Down
Loading