Добавлена поддержка прокси для Discord API (#3379)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: VigersRay <60344369+VigersRay@users.noreply.github.com>
Co-authored-by: drdth <drdtheuser@gmail.com>
This commit is contained in:
Copilot 2025-12-23 00:18:47 +03:00 committed by GitHub
parent 1881dcb4d0
commit acaf743701
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 225 additions and 12 deletions

View file

@ -25,6 +25,7 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Content.Server.Discord;
using Content.Shared._Sunrise.SunriseCCVars;
using JetBrains.Annotations;
using Robust.Shared;
@ -49,6 +50,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly UserDbDataManager _userDbData = default!;
// Sunrise added start - поддержка прокси
[Dependency] private readonly DiscordWebhook _discord = default!;
// Sunrise added end
private IServerServiceAuthManager? _serviceAuth;
private ISawmill _sawmill = default!;
@ -56,7 +61,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public const string SawmillId = "admin.bans";
public const string PrefixAntag = "Antag:";
public const string PrefixJob = "Job:";
private readonly HttpClient _httpClient = new();
private HttpClient _httpClient = default!; // Sunrise edit - поддержка прокси
private string _serverName = string.Empty;
private string _webhookUrl = string.Empty;
private WebhookData? _webhookData;
@ -73,6 +78,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public void Initialize()
{
// Sunrise added start - поддержка прокси
_httpClient = _discord.GetClient();
// Sunrise added end
_netManager.RegisterNetMessage<MsgRoleBans>();
_db.SubscribeToJsonNotification<BanNotificationData>(

View file

@ -48,6 +48,7 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;
private ISharedSponsorsManager? _sponsorsManager; // Sunrise-Sponsors
[Dependency] private readonly IBanManager _banManager = default!; // Sunrise-Ahelp-Antispam, based on Starlight Build: https://github.com/ss14Starlight/space-station-14/pull/85
[Dependency] private readonly DiscordWebhook _discord = default!;
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
private static partial Regex DiscordRegex();
@ -59,7 +60,7 @@ namespace Content.Server.Administration.Systems
private WebhookData? _onCallData;
private ISawmill _sawmill = default!;
private readonly HttpClient _httpClient = new();
private HttpClient _httpClient = default!;
private string _footerIconUrl = string.Empty;
private string _avatarUrl = string.Empty;
@ -100,6 +101,10 @@ namespace Content.Server.Administration.Systems
{
base.Initialize();
// Sunrise added start
_httpClient = _discord.GetClient();
// Sunrise added end
Subs.CVar(_config, CCVars.DiscordOnCallWebhook, OnCallChanged, true);
Subs.CVar(_config, CCVars.DiscordAHelpWebhook, OnWebhookChanged, true);
@ -1026,7 +1031,7 @@ namespace Content.Server.Administration.Systems
private bool IsOnCooldown(NetUserId channelId, TimeSpan currentTime, out TimeSpan remainingCooldown)
{
remainingCooldown = TimeSpan.Zero;
var lastMessage = _recentMessages
.Where(msg => msg.Channel == channelId)
.OrderByDescending(msg => msg.Timestamp)

View file

@ -116,10 +116,12 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
_averageFrameTime = (_averageFrameTime * _frameCount + _lastFrameTime) / (_frameCount + 1);
_frameCount++;
#if DEBUG
if (_frameCount % 100 == 0)
{
Logger.Debug($"AtmosphereSystem: Last frame time: {_lastFrameTime:F2}ms, Average frame time: {_averageFrameTime:F2}ms"); // Sunrise-edit
Log.Debug($"AtmosphereSystem: Last frame time: {_lastFrameTime:F2}ms, Average frame time: {_averageFrameTime:F2}ms"); // Sunrise-edit
}
#endif
_exposedTimer += frameTime;

View file

@ -1,4 +1,7 @@
using System.Threading.Tasks;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Content.Shared._Sunrise.SunriseCCVars;
using Content.Shared.CCVar;
using NetCord;
using NetCord.Gateway;
@ -36,6 +39,8 @@ public sealed class DiscordLink : IPostInjectInit
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly DiscordWebhook _discord = default!; // Sunrise added
/// <summary>
/// The Discord client. This is null if the bot is not connected.
/// </summary>
@ -106,6 +111,7 @@ public sealed class DiscordLink : IPostInjectInit
| GatewayIntents.MessageContent
| GatewayIntents.DirectMessages,
Logger = new DiscordSawmillLogger(_sawmillLog),
RestClientConfiguration = CreateClientConfiguration(), // Sunrise added - поддержка прокси
});
_client.MessageCreate += OnCommandReceivedInternal;
_client.MessageCreate += OnMessageReceivedInternal;
@ -236,4 +242,29 @@ public sealed class DiscordLink : IPostInjectInit
}
#endregion
// Sunrise added start - поддержка прокси
private RestClientConfiguration CreateClientConfiguration()
{
var httpMessageHandler = CreateHttpMessageHandler();
var restClientConfiguration = new RestClientConfiguration
{
RequestHandler = httpMessageHandler != null ? new RestRequestHandler(httpMessageHandler) : null,
};
return restClientConfiguration;
}
private SocketsHttpHandler? CreateHttpMessageHandler()
{
var proxyAddress = _configuration.GetCVar(SunriseCCVars.DiscordProxyAddress);
if (string.IsNullOrWhiteSpace(proxyAddress))
{
_sawmill.Debug("No proxy configured for Discord bot connection.");
return null;
}
return _discord.CreateHandler(proxyAddress);
}
// Sunrise added end
}

View file

@ -1,22 +1,32 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Content.Shared._Sunrise.SunriseCCVars;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
namespace Content.Server.Discord;
public sealed class DiscordWebhook : IPostInjectInit
public sealed class DiscordWebhook : IPostInjectInit, IDisposable
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
private const string BaseUrl = "https://discord.com/api/v10/webhooks";
private readonly HttpClient _http = new();
private HttpClient _http = default!;
private ISawmill _sawmill = default!;
// Sunrise added start
private const char CustomHeadersSplitter = '|';
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30f);
// Sunrise added end
private string GetUrl(WebhookIdentifier identifier)
{
return $"{BaseUrl}/{identifier.Id}/{identifier.Token}";
@ -116,6 +126,124 @@ public sealed class DiscordWebhook : IPostInjectInit
_sawmill = _log.GetSawmill("DISCORD");
}
// Sunrise edit start
// Тут добавлена поддержка прокси и кастомных заголовков. РКН сосать
#region HTTP client
public void SetupClient()
{
_http = CreateHttpClient();
}
public HttpClient GetClient()
{
return _http;
}
private HttpClient CreateHttpClient()
{
HttpClient client;
var proxyAddress = _configuration.GetCVar(SunriseCCVars.DiscordProxyAddress);
if (string.IsNullOrWhiteSpace(proxyAddress))
{
client = new HttpClient();
_sawmill.Debug("No proxy configured for Discord webhooks.");
}
else
{
client = CreateClientWithProxies(proxyAddress);
_sawmill.Info("Configured Discord webhooks to use proxies");
}
// Add custom headers if configured
TryAddCustomHeaders(client);
return client;
}
private HttpClient CreateClientWithProxies(string proxyAddress)
{
var handler = CreateHandler(proxyAddress);
var client = new HttpClient(handler);
client.Timeout = Timeout;
return client;
}
public SocketsHttpHandler CreateHandler(string proxyAddress)
{
if (!Uri.TryCreate(proxyAddress, UriKind.Absolute, out var proxyUri))
{
_sawmill.Error($"Invalid proxy URI: {proxyAddress}. Discord connections will fail.");
throw new UriFormatException($"Invalid proxy address: {proxyAddress}");
}
var proxyUsername = _configuration.GetCVar(SunriseCCVars.DiscordProxyUsername);
var proxyPassword = _configuration.GetCVar(SunriseCCVars.DiscordProxyPassword);
var handler = new SocketsHttpHandler();
NetworkCredential? credentials = null;
if (!string.IsNullOrWhiteSpace(proxyUsername) && !string.IsNullOrWhiteSpace(proxyPassword))
credentials = new NetworkCredential(proxyUsername, proxyPassword);
var proxy = new WebProxy(proxyUri, false, null, credentials)
{
UseDefaultCredentials = false,
};
handler.Proxy = proxy;
handler.UseProxy = true;
handler.ConnectTimeout = Timeout;
return handler;
}
private bool TryAddCustomHeaders(HttpClient client)
{
var customHeaders = _configuration.GetCVar(SunriseCCVars.DiscordCustomHeaders);
if (string.IsNullOrWhiteSpace(customHeaders))
return false;
var usedHeadersCount = 0;
var headers = customHeaders.Split(CustomHeadersSplitter);
foreach (var header in headers)
{
var parts = header.Trim().Split(':', 2);
if (parts.Length != 2)
{
_sawmill.Error($"Invalid header: {header}");
continue;
}
var headerName = parts[0].Trim();
var headerValue = parts[1].Trim();
if (string.IsNullOrWhiteSpace(headerName))
{
_sawmill.Error($"Empty header name in: {header}");
continue;
}
client.DefaultRequestHeaders.Add(headerName, headerValue);
usedHeadersCount++;
}
if (usedHeadersCount == 0)
return false;
_sawmill.Info($"Configured Discord webhooks with {usedHeadersCount} custom headers.");
return true;
}
#endregion
public void Dispose()
{
_http?.Dispose();
}
// Sunrise edit end
/// <summary>
/// Logs detailed information about the HTTP response received from a Discord webhook request.
/// If the response status code is non-2XX it logs the status code, relevant rate limit headers.

View file

@ -12,6 +12,7 @@ using Content.Server.Ani;
using Content.Server.Chat.Managers;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Server.Discord;
using Content.Server.Discord.DiscordLink;
using Content.Server.EUI;
using Content.Server.GameTicking;
@ -88,6 +89,7 @@ namespace Content.Server.Entry
[Dependency] private readonly ContributorsManager _contributorsManager = default!; // Sunrise-Edit
[Dependency] private readonly PlayerCacheManager _playerCacheManager = default!; // Sunrise-Edit
[Dependency] private readonly TTSManager _ttsManager = default!; // Sunrise-Edit
[Dependency] private readonly DiscordWebhook _discord = default!; // Sunrise-Edit
private ISharedSponsorsManager? _sponsorsManager; // Sunrise-Sponsors
public override void PreInit()
@ -141,9 +143,10 @@ namespace Content.Server.Entry
_serverApi.Initialize();
// Sunrise-Sponsors-Start
_ttsManager.Initialize(); // Sunrise-Edit
_ttsManager.Initialize();
SunriseServerEntry.Init();
IoCManager.Instance!.TryResolveType(out _sponsorsManager);
_discord.SetupClient();
// Sunrise-Sponsors-End
_voteManager.Initialize();
@ -236,6 +239,10 @@ namespace Content.Server.Entry
// TODO Should this be awaited?
_discordLink.Shutdown();
_discordChatLink.Shutdown();
// Sunrise added start
_discord.Dispose();
// Sunrise added end
}
private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill)

View file

@ -310,11 +310,12 @@ namespace Content.Server.Power.EntitySystems
_lastFrameTime = (float)_frameStopwatch.Elapsed.TotalMilliseconds;
_averageFrameTime = (_averageFrameTime * _frameCount + _lastFrameTime) / (_frameCount + 1);
_frameCount++;
#if DEBUG
if (_frameCount % 100 == 0)
{
Logger.Debug($"PowerNetSystem: Last frame time: {_lastFrameTime:F2}ms, Average frame time: {_averageFrameTime:F2}ms"); // Sunrise-edit
Log.Debug($"PowerNetSystem: Last frame time: {_lastFrameTime:F2}ms, Average frame time: {_averageFrameTime:F2}ms"); // Sunrise-edit
}
#endif
}
private void ReconnectNetworks()

View file

@ -113,5 +113,4 @@ public sealed partial class CCVars
/// </summary>
public static readonly CVarDef<bool> DiscordNewsWebhookSendDuringRound =
CVarDef.Create("discord.news_webhook_send_during_round", false, CVar.SERVERONLY);
}

View file

@ -2,7 +2,6 @@ using Robust.Shared.Configuration;
namespace Content.Shared._Sunrise.SunriseCCVars;
[CVarDefs]
public sealed partial class SunriseCCVars
{
public static readonly CVarDef<bool> ContributorsEnable =

View file

@ -0,0 +1,31 @@
using Robust.Shared.Configuration;
namespace Content.Shared._Sunrise.SunriseCCVars;
public sealed partial class SunriseCCVars
{
/// <summary>
/// Proxy server address for Discord API connections (e.g., "http://proxy.example.com:8080" or "socks5://proxy.example.com:1080").
/// If left empty, no proxy will be used.
/// </summary>
public static readonly CVarDef<string> DiscordProxyAddress =
CVarDef.Create("discord.proxy_address", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
/// <summary>
/// Username for proxy authentication. If left empty, no authentication will be used.
/// </summary>
public static readonly CVarDef<string> DiscordProxyUsername =
CVarDef.Create("discord.proxy_username", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
/// <summary>
/// Password for proxy authentication. If left empty, no authentication will be used.
/// </summary>
public static readonly CVarDef<string> DiscordProxyPassword =
CVarDef.Create("discord.proxy_password", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
/// <summary>
/// Custom headers to add to Discord webhook requests. Format: "Header1:Value1|Header2:Value2"
/// </summary>
public static readonly CVarDef<string> DiscordCustomHeaders =
CVarDef.Create("discord.custom_headers", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
}

View file

@ -5,6 +5,7 @@ namespace Content.Shared._Sunrise.SunriseCCVars;
// TODO: Разбиние на partial файлы
// TODO: Документация по каждому из сиваров
[CVarDefs]
public sealed partial class SunriseCCVars : CVars
{
/**