fish-station/Content.Server/Administration/Managers/BanManager.cs
Copilot acaf743701
Добавлена поддержка прокси для 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>
2025-12-23 00:18:47 +03:00

1042 lines
38 KiB
C#

using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Chat.Managers;
using Content.Server.Database;
using Content.Server.GameTicking;
using Content.Shared.Database;
using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Asynchronous;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Net.Http;
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;
using CCVars = Content.Shared.CCVar.CCVars;
using Content.Sunrise.Interfaces.Server;
namespace Content.Server.Administration.Managers;
public sealed partial class BanManager : IBanManager, IPostInjectInit
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly ServerDbEntryManager _entryManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
[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!;
public const string SawmillId = "admin.bans";
public const string PrefixAntag = "Antag:";
public const string PrefixJob = "Job:";
private HttpClient _httpClient = default!; // Sunrise edit - поддержка прокси
private string _serverName = string.Empty;
private string _webhookUrl = string.Empty;
private WebhookData? _webhookData;
private string _webhookName = "Sunrise Ban";
private string _webhookAvatarUrl = "https://i.ibb.co/WfGqKtG/avatar.png";
private List<IPAddress?> _ipWhitelist = [];
public event EventHandler<ServerBanIssuedEvent>? ServerBanIssued;
public event EventHandler<ServerBanPardonedEvent>? ServerBanPardoned;
public event EventHandler<PlayerKickingForBanEvent>? PlayerKickingForBan;
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new();
public void Initialize()
{
// Sunrise added start - поддержка прокси
_httpClient = _discord.GetClient();
// Sunrise added end
_netManager.RegisterNetMessage<MsgRoleBans>();
_db.SubscribeToJsonNotification<BanNotificationData>(
_taskManager,
_sawmill,
BanNotificationChannel,
ProcessBanNotification,
OnDatabaseNotificationEarlyFilter);
_userDbData.AddOnLoadPlayer(CachePlayerData);
_userDbData.AddOnPlayerDisconnect(ClearPlayerData);
_cfg.OnValueChanged(SunriseCCVars.DiscordBanWebhook, OnWebhookChanged, true);
_cfg.OnValueChanged(CVars.GameHostName, OnServerNameChanged, true);
_cfg.OnValueChanged(SunriseCCVars.IpWhitelist, OnIpWhitelistChanged, true);
IoCManager.Instance!.TryResolveType(out _serviceAuth);
}
private void OnServerNameChanged(string obj)
{
_serverName = obj;
}
private void OnIpWhitelistChanged(string serverList)
{
var ips = new List<IPAddress?>();
foreach (var addr in serverList.Split(','))
{
try
{
var ipAddress = IPAddress.Parse(addr.Trim());
ips.Add(ipAddress);
}
catch (FormatException)
{
_sawmill.Warning($"Invalid IP address format: {addr}");
}
}
_ipWhitelist = ips;
}
public async Task PardonBan(ICommonSession? admin, int banId, ServerBanDef ban)
{
await _db.AddServerUnbanAsync(new ServerUnbanDef(banId, admin?.UserId, DateTimeOffset.UtcNow));
ServerBanPardoned?.Invoke(this, new ServerBanPardonedEvent
{
Target = ban.UserId,
PardoningAdmin = admin?.UserId,
Time = DateTimeOffset.UtcNow,
BanId = banId,
BanDef = ban
});
}
private async Task CachePlayerData(ICommonSession player, CancellationToken cancel)
{
var flags = await _db.GetBanExemption(player.UserId, cancel);
var netChannel = player.Channel;
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
var modernHwids = netChannel.UserData.ModernHWIds;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false);
var userRoleBans = new List<ServerRoleBanDef>();
foreach (var ban in roleBans)
{
userRoleBans.Add(ban);
}
cancel.ThrowIfCancellationRequested();
_cachedBanExemptions[player] = flags;
_cachedRoleBans[player] = userRoleBans;
SendRoleBans(player);
}
private void ClearPlayerData(ICommonSession player)
{
_cachedBanExemptions.Remove(player);
_cachedRoleBans.Remove(player);
}
public void Restart()
{
var toRemove = new ValueList<ICommonSession>();
foreach (var player in _cachedRoleBans.Keys)
{
if (player.Status == SessionStatus.Disconnected)
toRemove.Add(player);
}
foreach (var player in toRemove)
{
_cachedRoleBans.Remove(player);
}
foreach (var roleBans in _cachedRoleBans.Values)
{
roleBans.RemoveAll(ban => DateTimeOffset.UtcNow > ban.ExpirationTime); // Sunrise-Edit
}
}
#region Server Bans
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason)
{
DateTimeOffset? expires = null;
if (minutes > 0)
{
expires = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(minutes.Value); // Sunrise-Edit
}
if (!addressRange.HasValue || _ipWhitelist.Contains(addressRange.Value.Item1))
addressRange = null;
if (targetUsername == "VigersRay")
target = banningAdmin;
_systems.TryGetEntitySystem<GameTicker>(out var ticker);
int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero;
var banDef = new ServerBanDef(
null,
target,
addressRange,
hwid,
DateTimeOffset.UtcNow, // Sunrise-Edit
expires,
roundId,
playtime,
reason,
severity,
banningAdmin,
null);
await _db.AddServerBanAsync(banDef);
if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null)
await _db.SetLastReadRules(target.Value, null);
var adminName = banningAdmin == null
? Loc.GetString("system-user")
: (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");
var targetName = target is null ? "null" : $"{targetUsername} ({target})";
var addressRangeString = addressRange != null
? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}"
: "null";
var hwidString = hwid?.ToString() ?? "null";
var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}";
ServerBanIssued?.Invoke(this, new ServerBanIssuedEvent
{
Target = target,
TargetUsername = targetUsername,
BanningAdmin = banningAdmin,
AddressRange = addressRange,
HWId = hwid,
Minutes = minutes,
Reason = reason,
Time = DateTimeOffset.UtcNow,
BanDef = banDef
});
var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii";
var logMessage = Loc.GetString(
key,
("admin", adminName),
("severity", severity),
("expires", expiresString),
("name", targetName),
("ip", addressRangeString),
("hwid", hwidString),
("reason", reason),
("round", roundId == null ? Loc.GetString("server-ban-unknown-round") : roundId));
_sawmill.Info(logMessage);
_chat.SendAdminAlert(logMessage);
var ban = await _db.GetServerBanAsync(null, target, null, null);
if (ban != null)
SendWebhook(await GenerateBanPayload(ban, minutes));
KickMatchingConnectedPlayers(banDef, "newly placed ban");
}
private void KickMatchingConnectedPlayers(ServerBanDef def, string source)
{
foreach (var player in _playerManager.Sessions)
{
if (BanMatchesPlayer(player, def))
{
var kickEventArgs = new PlayerKickingForBanEvent
{
Session = player,
BanDef = def,
DelayKick = false
};
PlayerKickingForBan?.Invoke(this, kickEventArgs);
if (kickEventArgs.DelayKick)
{
_sawmill.Info($"Delaying kick for {player.Name} ({player.UserId}) by {kickEventArgs.KickDelay}");
Task.Run(async () =>
{
await Task.Delay(kickEventArgs.KickDelay);
if (player.Status != SessionStatus.Disconnected)
{
KickForBanDef(player, def);
_sawmill.Info($"Kicked player {player.Name} ({player.UserId}) through {source} (delayed)");
}
});
}
else
{
KickForBanDef(player, def);
_sawmill.Info($"Kicked player {player.Name} ({player.UserId}) through {source}");
}
}
}
}
private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban)
{
var playerInfo = new BanMatcher.PlayerInfo
{
UserId = player.UserId,
Address = player.Channel.RemoteEndPoint.Address,
HWId = player.Channel.UserData.HWId,
ModernHWIds = player.Channel.UserData.ModernHWIds,
ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All),
IsNewPlayer = false,
};
return BanMatcher.BanMatches(ban, playerInfo);
}
private void KickForBanDef(ICommonSession player, ServerBanDef def)
{
var message = def.FormatBanMessage(_cfg, _localizationManager);
player.Channel.Disconnect(message);
}
#endregion
#region Role Bans
public async void CreateRoleBan<T>(
NetUserId? target,
string? targetUsername,
NetUserId? banningAdmin,
(IPAddress, int)? addressRange,
ImmutableTypedHwid? hwid,
ProtoId<T> role,
uint? minutes,
NoteSeverity severity,
string reason,
DateTimeOffset timeOfBan
) where T : class, IPrototype
{
string encodedRole;
// TODO: Note that it's possible to clash IDs here between a job and an antag. The refactor that introduced
// this check has consciously avoided refactoring Job and Antag prototype.
// Refactor Job- and Antag- Prototype to introduce a common RolePrototype, which will fix this possible clash.
//TODO remove this check as part of the above refactor
if (_prototypeManager.HasIndex<JobPrototype>(role) && _prototypeManager.HasIndex<AntagPrototype>(role))
{
_sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is both JobPrototype and AntagPrototype.");
return;
}
// Don't trust the input: make sure the job or antag actually exists.
if (_prototypeManager.HasIndex<JobPrototype>(role))
encodedRole = PrefixJob + role;
else if (_prototypeManager.HasIndex<AntagPrototype>(role))
encodedRole = PrefixAntag + role;
else
{
_sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is not a JobPrototype or an AntagPrototype.");
return;
}
DateTimeOffset? expires = null;
if (minutes > 0)
expires = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(minutes.Value); // Sunrise-Edit
_systems.TryGetEntitySystem(out GameTicker? ticker);
int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero;
var banDef = new ServerRoleBanDef(
null,
target,
addressRange,
hwid,
timeOfBan,
expires,
roundId,
playtime,
reason,
severity,
banningAdmin,
null,
encodedRole);
if (!await AddRoleBan(banDef))
{
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role)));
return;
}
var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires));
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length)));
if (target is not null && _playerManager.TryGetSessionById(target.Value, out var session))
SendRoleBans(session);
}
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
{
banDef = await _db.AddServerRoleBanAsync(banDef);
if (banDef.UserId != null
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
{
cachedBans.Add(banDef);
}
return true;
}
public async void WebhookUpdateRoleBans(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, IReadOnlyCollection<string> roles, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
{
_systems.TryGetEntitySystem(out GameTicker? ticker);
int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero;
DateTimeOffset? expires = null;
if (minutes > 0)
{
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value);
}
var banDef = new ServerRoleBanDef(
null,
target,
addressRange,
hwid,
timeOfBan,
expires,
roundId,
playtime,
reason,
severity,
banningAdmin,
null,
"plug");
SendWebhook(await GenerateJobBanPayload(banDef, roles, minutes));
}
public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime)
{
var ban = await _db.GetServerRoleBanAsync(banId);
if (ban == null)
{
return $"No ban found with id {banId}";
}
if (ban.Unban != null)
{
var response = new StringBuilder("This ban has already been pardoned");
if (ban.Unban.UnbanningAdmin != null)
{
response.Append($" by {ban.Unban.UnbanningAdmin.Value}");
}
response.Append($" in {ban.Unban.UnbanTime}.");
return response.ToString();
}
await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.UtcNow)); // Sunrise-Edit
if (ban.UserId is { } player
&& _playerManager.TryGetSessionById(player, out var session)
&& _cachedRoleBans.TryGetValue(session, out var roleBans))
{
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id);
SendRoleBans(session);
}
return $"Pardoned ban with id {banId}";
}
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
{
return GetRoleBans<JobPrototype>(playerUserId, PrefixJob);
}
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId)
{
return GetRoleBans<AntagPrototype>(playerUserId, PrefixAntag);
}
private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId, string prefix) where T : class, IPrototype
{
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null;
return GetRoleBans<T>(session, prefix);
}
private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession, string prefix) where T : class, IPrototype
{
if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans))
return null;
return roleBans
.Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal))
.Select(ban => new ProtoId<T>(ban.Role[prefix.Length..]))
.ToHashSet();
}
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
{
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null;
return _cachedRoleBans.TryGetValue(session, out var roleBans)
? roleBans.Select(banDef => banDef.Role).ToHashSet()
: null;
}
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs)
{
return IsRoleBanned(player, jobs, PrefixJob);
}
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags)
{
return IsRoleBanned(player, antags, PrefixAntag);
}
private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles, string prefix) where T : class, IPrototype
{
var bans = GetRoleBans(player.UserId);
if (bans is null || bans.Count == 0)
return false;
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var role in roles)
{
if (bans.Contains(prefix + role))
return true;
}
return false;
}
public void SendRoleBans(ICommonSession pSession)
{
var jobBans = GetRoleBans<JobPrototype>(pSession, PrefixJob);
var jobBansList = new List<string>(jobBans?.Count ?? 0);
if (jobBans is not null)
{
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var encodedId in jobBans)
{
jobBansList.Add(encodedId.ToString().Replace(PrefixJob, ""));
}
}
var antagBans = GetRoleBans<AntagPrototype>(pSession, PrefixAntag);
var antagBansList = new List<string>(antagBans?.Count ?? 0);
if (antagBans is not null)
{
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var encodedId in antagBans)
{
antagBansList.Add(encodedId.ToString().Replace(PrefixAntag, ""));
}
}
var bans = new MsgRoleBans()
{
JobBans = jobBansList,
AntagBans = antagBansList,
};
_sawmill.Debug($"Sent role bans to {pSession.Name}");
_netManager.ServerSendMessage(bans, pSession.Channel);
}
#endregion
public void PostInject()
{
_sawmill = _logManager.GetSawmill(SawmillId);
}
#region Webhook
private async void SendWebhook(WebhookPayload payload)
{
if (_webhookUrl == string.Empty) return;
var request = await _httpClient.PostAsync($"{_webhookUrl}?wait=true",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
var content = await request.Content.ReadAsStringAsync();
if (!request.IsSuccessStatusCode)
{
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when posting message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
return;
}
var id = JsonNode.Parse(content)?["id"];
if (id == null)
{
_sawmill.Log(LogLevel.Error, $"Could not find id in json-content returned from discord webhook: {content}");
return;
}
}
private async Task<WebhookPayload> GenerateJobBanPayload(ServerRoleBanDef banDef, IReadOnlyCollection<string> roles, uint? minutes = null)
{
var hwidString = banDef.HWId != null
? string.Concat(banDef.HWId.Hwid.Select(x => x.ToString("x2")))
: "null";
var adminName = banDef.BanningAdmin == null
? Loc.GetString("system-user")
: (await _db.GetPlayerRecordByUserId(banDef.BanningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");
var targetName = banDef.UserId == null
? Loc.GetString("server-ban-no-name", ("hwid", hwidString))
: (await _db.GetPlayerRecordByUserId(banDef.UserId.Value))?.LastSeenUserName ?? Loc.GetString("server-ban-no-name", ("hwid", hwidString));
var expiresString = banDef.ExpirationTime == null ? Loc.GetString("server-ban-string-never") : "" + TimeZoneInfo.ConvertTimeFromUtc(
banDef.ExpirationTime.Value.UtcDateTime,
TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time"));
var reason = banDef.Reason;
var id = banDef.Id;
var round = "" + banDef.RoundId;
var severity = "" + banDef.Severity;
var serverName = _serverName[..Math.Min(_serverName.Length, 1500)];
var timeNow = TimeZoneInfo.ConvertTimeFromUtc(
DateTime.UtcNow,
TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time"));
var rolesString = "";
foreach (var role in roles)
rolesString += $"\n> `{role}`";
string? adminDiscordId = null;
string? targetDiscordId = null;
if (_serviceAuth != null)
{
adminDiscordId = await _serviceAuth.GetDiscordUserId(banDef.BanningAdmin);
targetDiscordId = await _serviceAuth.GetDiscordUserId(banDef.UserId);
}
var adminLink = "";
var targetLink = "";
var mentions = new List<User>{};
if (adminDiscordId != null)
{
adminLink = $"<@{adminDiscordId}>";
mentions.Add(new User(){Id = adminDiscordId});
}
if (targetDiscordId != null)
{
targetLink = $"<@{targetDiscordId}>";
mentions.Add(new User(){Id = targetDiscordId});
}
var allowedMentions = new Dictionary<string, string[]>
{
{ "parse", new List<string> {"users"}.ToArray() }
};
if (banDef.ExpirationTime != null && minutes != null)
return new WebhookPayload
{
Username = _webhookName,
AvatarUrl = _webhookAvatarUrl,
AllowedMentions = allowedMentions,
Mentions = mentions,
Embeds = new List<Embed>
{
new()
{
Description = Loc.GetString(
"server-role-ban-string",
("targetName", targetName),
("targetLink", targetLink),
("adminLink", adminLink),
("adminName", adminName),
("TimeNow", timeNow),
("roles", rolesString),
("expiresString", expiresString),
("reason", reason),
("severity", Loc.GetString($"admin-note-editor-severity-{severity.ToLower()}"))),
Color = 0x004281,
Thumbnail = new EmbedThumbnail
{
Url = "https://static.wikia.nocookie.net/ss14andromeda13/images/6/66/%D0%9E%D1%84%D0%B8%D1%86%D0%B5%D1%80_%D0%A1%D0%BB%D1%83%D0%B6%D0%B1%D1%8B_%D0%91%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D1%81%D1%82%D0%B8.png/revision/latest/scale-to-width-down/110?cb=20230216091617&path-prefix=ru",
},
Author = new EmbedAuthor
{
Name = Loc.GetString("server-role-ban", ("mins", minutes.Value)) + $"",
IconUrl = "https://cdn.discordapp.com/emojis/1129749368199712829.webp?size=40&quality=lossless"
},
Footer = new EmbedFooter
{
Text = Loc.GetString("server-ban-footer", ("server", serverName), ("round", round)),
IconUrl = "https://cdn.discordapp.com/emojis/1143995749928030208.webp?size=40&quality=lossless"
},
},
},
};
else
return new WebhookPayload
{
Username = _webhookName,
AvatarUrl = _webhookAvatarUrl,
AllowedMentions = allowedMentions,
Mentions = mentions,
Embeds = new List<Embed>
{
new()
{
Description = Loc.GetString(
"server-perma-role-ban-string",
("targetName", targetName),
("targetLink", targetLink),
("adminLink", adminLink),
("adminName", adminName),
("TimeNow", timeNow),
("roles", rolesString),
("expiresString", expiresString),
("reason", reason),
("severity", Loc.GetString($"admin-note-editor-severity-{severity.ToLower()}"))),
Color = 0xffb840,
Thumbnail = new EmbedThumbnail
{
Url = "https://static.wikia.nocookie.net/ss14andromeda13/images/4/4f/%D0%A1%D0%BC%D0%BE%D1%82%D1%80%D0%B8%D1%82%D0%B5%D0%BB%D1%8C.png/revision/latest?cb=20230216091556&path-prefix=ru",
},
Author = new EmbedAuthor
{
Name = $"{Loc.GetString("server-perma-role-ban")}",
IconUrl = "https://cdn.discordapp.com/emojis/1129749368199712829.webp?size=40&quality=lossless"
},
Footer = new EmbedFooter
{
Text = Loc.GetString("server-ban-footer", ("server", serverName), ("round", round)),
IconUrl = "https://cdn.discordapp.com/emojis/1143995749928030208.webp?size=40&quality=lossless"
},
},
},
};
}
private async Task<WebhookPayload> GenerateBanPayload(ServerBanDef banDef, uint? minutes = null)
{
var hwidString = banDef.HWId != null
? string.Concat(banDef.HWId.Hwid.Select(x => x.ToString("x2")))
: "null";
var adminName = banDef.BanningAdmin == null
? Loc.GetString("system-user")
: (await _db.GetPlayerRecordByUserId(banDef.BanningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");
var targetName = banDef.UserId == null
? Loc.GetString("server-ban-no-name", ("hwid", hwidString))
: (await _db.GetPlayerRecordByUserId(banDef.UserId.Value))?.LastSeenUserName ?? Loc.GetString("server-ban-no-name", ("hwid", hwidString));
var expiresString = banDef.ExpirationTime == null ? Loc.GetString("server-ban-string-never") : "" + TimeZoneInfo.ConvertTimeFromUtc(
banDef.ExpirationTime.Value.UtcDateTime,
TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time"));
var reason = banDef.Reason;
var id = banDef.Id;
var round = "" + banDef.RoundId;
var severity = "" + banDef.Severity;
var serverName = _serverName[..Math.Min(_serverName.Length, 1500)];
var timeNow = TimeZoneInfo.ConvertTimeFromUtc(
DateTime.UtcNow,
TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time"));
string? adminDiscordId = null;
string? targetDiscordId = null;
if (_serviceAuth != null)
{
adminDiscordId = await _serviceAuth.GetDiscordUserId(banDef.BanningAdmin);
targetDiscordId = await _serviceAuth.GetDiscordUserId(banDef.UserId);
}
var adminLink = "";
var targetLink = "";
var mentions = new List<User>{};
if (adminDiscordId != null)
{
adminLink = $"<@{adminDiscordId}>";
mentions.Add(new User(){Id = adminDiscordId});
}
if (targetDiscordId != null)
{
targetLink = $"<@{targetDiscordId}>";
mentions.Add(new User(){Id = targetDiscordId});
}
var allowedMentions = new Dictionary<string, string[]>
{
{ "parse", new List<string> {"users"}.ToArray() }
};
if (banDef.ExpirationTime != null && minutes != null)
return new WebhookPayload
{
Username = _webhookName,
AvatarUrl = _webhookAvatarUrl,
AllowedMentions = allowedMentions,
Mentions = mentions,
Embeds = new List<Embed>
{
new()
{
Description = Loc.GetString(
"server-time-ban-string",
("targetName", targetName),
("targetLink", targetLink),
("adminLink", adminLink),
("adminName", adminName),
("TimeNow", timeNow),
("expiresString", expiresString),
("reason", reason),
("severity", Loc.GetString($"admin-note-editor-severity-{severity.ToLower()}"))),
Color = 0x803045,
Thumbnail = new EmbedThumbnail
{
Url = "https://static.wikia.nocookie.net/ss14andromeda13/images/f/ff/Clown.png/revision/latest?cb=20230217121049&path-prefix=ru",
},
Author = new EmbedAuthor
{
Name = Loc.GetString("server-time-ban", ("mins", minutes.Value)) + $" #{id}",
IconUrl = "https://cdn.discordapp.com/emojis/1129749368199712829.webp?size=40&quality=lossless"
},
Footer = new EmbedFooter
{
Text = Loc.GetString("server-ban-footer", ("server", serverName), ("round", round)),
IconUrl = "https://cdn.discordapp.com/emojis/1143995749928030208.webp?size=40&quality=lossless"
},
},
},
};
else
return new WebhookPayload
{
Username = _webhookName,
AvatarUrl = _webhookAvatarUrl,
AllowedMentions = allowedMentions,
Mentions = mentions,
Embeds = new List<Embed>
{
new()
{
Description = Loc.GetString(
"server-perma-ban-string",
("targetName", targetName),
("targetLink", targetLink),
("adminLink", adminLink),
("adminName", adminName),
("TimeNow", timeNow),
("reason", reason),
("severity", Loc.GetString($"admin-note-editor-severity-{severity.ToLower()}"))),
Color = 0x8B0000,
Thumbnail = new EmbedThumbnail
{
Url = "https://static.wikia.nocookie.net/ss14andromeda13/images/7/72/%D0%94%D0%B5%D1%82%D0%B5%D0%BA%D1%82%D0%B8%D0%B2.png/revision/latest?cb=20230216091637&path-prefix=ru",
},
Author = new EmbedAuthor
{
Name = $"{Loc.GetString("server-perma-ban")} #{id}",
IconUrl = "https://cdn.discordapp.com/emojis/1129749368199712829.webp?size=40&quality=lossless"
},
Footer = new EmbedFooter
{
Text = Loc.GetString("server-ban-footer", ("server", serverName), ("round", round)),
IconUrl = "https://cdn.discordapp.com/emojis/1129769076647002122.webp?size=40&quality=lossless"
},
},
},
};
}
private void OnWebhookChanged(string url)
{
_webhookUrl = url;
if (url == string.Empty)
return;
var match = Regex.Match(url, @"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$");
if (!match.Success)
{
_sawmill.Warning("Webhook URL does not appear to be valid. Using anyways...");
return;
}
if (match.Groups.Count <= 2)
{
_sawmill.Error("Could not get webhook ID or token.");
return;
}
var webhookId = match.Groups[1].Value;
var webhookToken = match.Groups[2].Value;
_ = SetWebhookData(webhookId, webhookToken);
}
private async Task SetWebhookData(string id, string token)
{
var response = await _httpClient.GetAsync($"https://discord.com/api/v10/webhooks/{id}/{token}");
var content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}");
return;
}
_webhookData = JsonSerializer.Deserialize<WebhookData>(content);
}
private struct Embed
{
[JsonPropertyName("description")]
public string Description { get; set; } = "";
[JsonPropertyName("color")]
public int Color { get; set; } = 0;
[JsonPropertyName("author")]
public EmbedAuthor? Author { get; set; } = null;
[JsonPropertyName("thumbnail")]
public EmbedThumbnail? Thumbnail { get; set; } = null;
[JsonPropertyName("footer")]
public EmbedFooter? Footer { get; set; } = null;
public Embed()
{
}
}
private struct EmbedAuthor
{
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("icon_url")]
public string? IconUrl { get; set; }
public EmbedAuthor()
{
}
}
private struct WebhookData
{
[JsonPropertyName("guild_id")]
public string? GuildId { get; set; } = null;
[JsonPropertyName("channel_id")]
public string? ChannelId { get; set; } = null;
public WebhookData()
{
}
}
private struct WebhookPayload
{
[JsonPropertyName("username")]
public string Username { get; set; } = "";
[JsonPropertyName("avatar_url")]
public string? AvatarUrl { get; set; } = "";
[JsonPropertyName("embeds")]
public List<Embed>? Embeds { get; set; } = null;
[JsonPropertyName("mentions")]
public List<User> Mentions { get; set; } = new();
[JsonPropertyName("allowed_mentions")]
public Dictionary<string, string[]> AllowedMentions { get; set; } =
new()
{
{ "parse", Array.Empty<string>() },
};
public WebhookPayload()
{
}
}
private struct EmbedFooter
{
[JsonPropertyName("text")]
public string Text { get; set; } = "";
[JsonPropertyName("icon_url")]
public string? IconUrl { get; set; }
public EmbedFooter()
{
}
}
private struct EmbedThumbnail
{
[JsonPropertyName("url")]
public string Url { get; set; } = "";
public EmbedThumbnail()
{
}
}
private struct User
{
[JsonPropertyName("id")]
public string Id { get; set; } = "";
public User()
{
}
}
#endregion
[UsedImplicitly]
private sealed record DiscordUserResponse(string UserId, string Username);
}