Добавлена поддержка прокси для 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:
parent
1881dcb4d0
commit
acaf743701
11 changed files with 225 additions and 12 deletions
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ namespace Content.Shared._Sunrise.SunriseCCVars;
|
|||
|
||||
// TODO: Разбиние на partial файлы
|
||||
// TODO: Документация по каждому из сиваров
|
||||
[CVarDefs]
|
||||
public sealed partial class SunriseCCVars : CVars
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue