Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.Discord.Webhooks;
namespace RadicadeServer.Radicade.Discord;
public static class AdminActionDiscord
{
public static void LogAction(string action, string? admin, string? details)
{
if (string.IsNullOrWhiteSpace(WebhookConfig.AdminActivityUrl)) return;
if (string.IsNullOrWhiteSpace(action)) return;
ReportDiscord.SendAsync(WebhookConfig.AdminActivityUrl, $"🛠️ Admin Action: {action}",
new DiscordPayload
{
Title = $"Admin Action: {action}",
ThumbnailUrl = WebhookImages.AdminActivity,
Color = 0x5865F2,
Fields = new List<DiscordField>
{
new() { Name = "Action", Value = Truncate(action, 200), Inline = true },
new() { Name = "Admin", Value = Truncate(string.IsNullOrWhiteSpace(admin) ? "Unknown" : admin, 100), Inline = true },
new() { Name = "Details", Value = Truncate(string.IsNullOrWhiteSpace(details) ? "—" : details, 900), Inline = false }
},
TimestampUtc = DateTime.UtcNow.ToString("O")
});
}
public static async Task LogBanAsync(RecDBContext db, long bannedPlayerId, string? bannedBy,
bool causedByReports, string? reason, ReportCategory category, DateTime? expiry, bool isModerator,
string? banMethod = null)
{
if (string.IsNullOrWhiteSpace(WebhookConfig.InGameBansUrl)) return;
var playerName = bannedPlayerId.ToString();
try
{
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == bannedPlayerId);
if (profile != null)
playerName = string.IsNullOrWhiteSpace(profile.displayName)
? (profile.username ?? bannedPlayerId.ToString())
: profile.displayName;
}
catch
{
}
var durationText = FormatDuration(expiry);
var fields = new List<DiscordField>
{
new() { Name = "Banned Player", Value = $"{playerName} ({bannedPlayerId})", Inline = true },
new() { Name = "Banned By", Value = Truncate(string.IsNullOrWhiteSpace(bannedBy) ? "Unknown" : bannedBy, 100), Inline = true },
new() { Name = "Caused by Reports", Value = causedByReports ? "Yes" : "No", Inline = true },
new() { Name = "Category", Value = category.ToString(), Inline = true },
new() { Name = "Duration", Value = durationText, Inline = true },
new() { Name = "Is Moderator", Value = isModerator ? "Yes" : "No", Inline = true },
new() { Name = "Reason", Value = Truncate(string.IsNullOrWhiteSpace(reason) ? "—" : reason, 900), Inline = false }
};
if (!string.IsNullOrWhiteSpace(banMethod))
{
fields.Insert(5, new DiscordField { Name = "Ban Method", Value = Truncate(banMethod, 100), Inline = true });
}
ReportDiscord.SendAsync(WebhookConfig.InGameBansUrl, $"🚫 Player Banned: {playerName}",
new DiscordPayload
{
Title = $"Player Banned: {playerName}",
ThumbnailUrl = WebhookImages.InGameBan,
Color = 0xE74C3C,
Fields = fields,
TimestampUtc = DateTime.UtcNow.ToString("O")
});
}
private static string FormatDuration(DateTime? expiry)
{
if (expiry == null || expiry.Value == DateTime.MaxValue)
return "Permanent";
var remaining = expiry.Value - DateTime.UtcNow;
if (remaining <= TimeSpan.Zero)
return $"Expires {expiry.Value:yyyy-MM-dd HH:mm} UTC";
var parts = new List<string>();
if (remaining.Days > 0) parts.Add($"{remaining.Days}d");
if (remaining.Hours > 0) parts.Add($"{remaining.Hours}h");
if (remaining.Minutes > 0) parts.Add($"{remaining.Minutes}m");
if (remaining.Seconds > 0 || parts.Count == 0) parts.Add($"{remaining.Seconds}s");
return $"{string.Join(" ", parts)} (until {expiry.Value:yyyy-MM-dd HH:mm} UTC)";
}
private static string Truncate(string value, int max)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Length <= max) return value;
var cut = max;
if (char.IsHighSurrogate(value[cut - 1]) && cut < value.Length && char.IsLowSurrogate(value[cut]))
cut--;
return value[..cut];
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace RadicadeServer.Radicade.Discord;
public sealed class DiscordPayload
{
public string Title { get; set; } = "";
public string Description { get; set; } = "";
public int Color { get; set; }
public List<DiscordField> Fields { get; set; } = new();
public string TimestampUtc { get; set; } = "";
public string ImageUrl { get; set; } = "";
public string ThumbnailUrl { get; set; } = "";
}
public sealed class DiscordField
{
public string Name { get; set; } = "";
public string Value { get; set; } = "";
public bool Inline { get; set; }
}
+70
View File
@@ -0,0 +1,70 @@
using System.Globalization;
using System.Text.Json.Nodes;
namespace RadicadeServer.Radicade.Discord;
public static class DiscordPayloadJson
{
private const int MaxContentLength = 2000;
private const int MaxTitleLength = 256;
private const int MaxDescriptionLength = 4096;
private const int MaxFieldNameLength = 256;
private const int MaxFieldValueLength = 1024;
private const int MaxFieldCount = 25;
public static string Build(DiscordPayload payload, string? contentOverride = null, string? embedImageUrl = null)
{
var embed = new JsonObject();
if (!string.IsNullOrWhiteSpace(payload.Title))
embed["title"] = Truncate(payload.Title, MaxTitleLength);
if (!string.IsNullOrWhiteSpace(payload.Description))
embed["description"] = Truncate(payload.Description, MaxDescriptionLength);
embed["color"] = payload.Color;
var imageUrl = string.IsNullOrWhiteSpace(embedImageUrl) ? payload.ImageUrl : embedImageUrl;
if (!string.IsNullOrWhiteSpace(imageUrl))
embed["image"] = new JsonObject { ["url"] = Truncate(imageUrl, MaxTitleLength) };
if (!string.IsNullOrWhiteSpace(payload.ThumbnailUrl))
embed["thumbnail"] = new JsonObject { ["url"] = Truncate(payload.ThumbnailUrl, MaxTitleLength) };
if (!string.IsNullOrWhiteSpace(payload.TimestampUtc)
&& DateTime.TryParse(payload.TimestampUtc, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed))
{
embed["timestamp"] = parsed.ToString("O");
}
var fields = new JsonArray();
foreach (var field in (payload.Fields ?? new List<DiscordField>()).Take(MaxFieldCount))
{
if (field == null) continue;
fields.Add(new JsonObject
{
["name"] = Truncate(field.Name ?? "", MaxFieldNameLength),
["value"] = Truncate(field.Value ?? "", MaxFieldValueLength),
["inline"] = field.Inline
});
}
if (fields.Count > 0)
embed["fields"] = fields;
var message = new JsonObject
{
["content"] = Truncate(contentOverride ?? payload.Title ?? "", MaxContentLength)
};
message["embeds"] = new JsonArray { embed };
return message.ToJsonString();
}
private static string Truncate(string value, int max)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Length <= max) return value;
var cut = max;
if (char.IsHighSurrogate(value[cut - 1]) && cut < value.Length && char.IsLowSurrogate(value[cut]))
cut--;
return value[..cut];
}
}
+10
View File
@@ -0,0 +1,10 @@
using CSharpDiscordWebhook.Objects;
namespace RadicadeServer.Radicade.Discord
{
public interface IWebhookTemplate
{
public static MessageBuilder Construct(params object[] args) { return null!; }
public static async Task Send(params object[] args) { await RadicadeWebhook.SendMessage(Construct(args)); }
}
}
+83
View File
@@ -0,0 +1,83 @@
using CSharpDiscordWebhook;
using CSharpDiscordWebhook.Objects;
using System.Runtime.InteropServices;
using RadicadeServer.RecNet.Responses;
using RadicadeServer.Discord.Webhooks;
namespace RadicadeServer.Radicade.Discord;
public static class RadicadeWebhook
{
private static DiscordWebhook? Webhook;
private static string WebhookUrl => WebhookConfig.GeneralActivityUrl;
private static readonly Success WebhookInvalid = Success.Error("Webhook is disabled/invalid.");
private static readonly Success CouldntSendMessage = Success.Error("Couldn't send message.");
public static bool IsWebhookValid()
{
return Uri.TryCreate(WebhookUrl, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}
private static bool TryInit()
{
if (!IsWebhookValid()) return false;
if (Webhook != null) return true;
try
{
Webhook = new DiscordWebhook(new Uri(WebhookUrl));
}
catch (Exception)
{
Webhook = null;
return false;
}
return true;
}
public static async Task<Success> SendMessage(
[Optional] string content,
[Optional] List<EmbedBuilder> embed,
[Optional] PollBuilder poll,
[Optional] List<AttachmentBuilder> attachments
)
{
if (!TryInit()) return WebhookInvalid;
try
{
var result = await Webhook!.SendMessageAsync(new MessageBuilder
{
Content = content,
Embeds = embed,
Poll = poll,
Attachments = attachments
});
return (result.Success) ? Success.Ok : CouldntSendMessage;
}
catch (Exception)
{
return CouldntSendMessage;
}
}
public static async Task<Success> SendMessage(MessageBuilder message)
{
if (!TryInit()) return WebhookInvalid;
try
{
var result = await Webhook!.SendMessageAsync(message);
return (result.Success) ? Success.Ok : CouldntSendMessage;
}
catch (Exception)
{
return CouldntSendMessage;
}
}
}
+91
View File
@@ -0,0 +1,91 @@
using System.Net.Http.Headers;
using System.Text;
namespace RadicadeServer.Radicade.Discord;
public static class ReportDiscord
{
private static readonly HttpClient Http = CreateHttpClient();
private static HttpClient CreateHttpClient()
{
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
client.DefaultRequestHeaders.UserAgent.ParseAdd("DiscordBot (https://radicade.app, 1.0)");
return client;
}
public sealed record DiscordAttachment(string Filename, byte[] Data, string MimeType);
public static void SendAsync(string webhookUrl, string content, DiscordPayload payload)
=> SendAsync(webhookUrl, content, payload, null, null);
public static void SendAsync(string webhookUrl, string content, DiscordPayload payload,
IReadOnlyList<DiscordAttachment>? attachments, string? embedImageUrl = null)
{
if (string.IsNullOrWhiteSpace(webhookUrl)) return;
_ = Task.Run(async () =>
{
try
{
using var response = attachments is { Count: > 0 }
? await PostWithFilesAsync(webhookUrl, content, payload, attachments, embedImageUrl)
: await PostJsonAsync(webhookUrl, content, payload, embedImageUrl);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
}
}
catch (Exception)
{
}
});
}
private static async Task<HttpResponseMessage> PostJsonAsync(string url, string content, DiscordPayload payload, string? embedImageUrl)
{
var body = DiscordPayloadJson.Build(payload, content, embedImageUrl);
using var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
return await Http.SendAsync(request);
}
private static async Task<HttpResponseMessage> PostWithFilesAsync(string url, string content, DiscordPayload payload,
IReadOnlyList<DiscordAttachment> attachments, string? embedImageUrl)
{
var json = DiscordPayloadJson.Build(payload, content, embedImageUrl);
using var form = new MultipartFormDataContent();
form.Add(new StringContent(json, Encoding.UTF8, "application/json"), "payload_json");
for (int i = 0; i < attachments.Count; i++)
{
var file = new ByteArrayContent(attachments[i].Data);
file.Headers.ContentType = new MediaTypeHeaderValue(attachments[i].MimeType);
form.Add(file, $"files[{i}]", attachments[i].Filename);
}
return await Http.PostAsync(url, form);
}
private static string Truncate(string value, int max)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Length <= max) return value;
var cut = max;
if (char.IsHighSurrogate(value[cut - 1]) && cut < value.Length && char.IsLowSurrogate(value[cut]))
cut--;
return value[..cut];
}
}
+78
View File
@@ -0,0 +1,78 @@
using System.Text.Json;
using Microsoft.Extensions.Configuration;
namespace RadicadeServer.Discord.Webhooks;
public static class WebhookImages
{
public const string BanAppeal = "https://cdn.radicade.app/BanAppeal.png";
public const string AdminActivity = "https://cdn.radicade.app/AuditLogs.png";
public const string InGameBan = "https://cdn.radicade.app/BanRecords.png";
public const string BugReport = "https://cdn.radicade.app/BugReport.png";
public const string ClubReport = "https://cdn.radicade.app/ClubReport.png";
public const string PlayerReport = "https://cdn.radicade.app/PlayerReport.png";
public const string RoomReport = "https://cdn.radicade.app/RoomReport.png";
public const string EventReport = "https://cdn.radicade.app/EventReport.png";
public const string InventionReport = "https://cdn.radicade.app/InventionReport.png";
public static string? ForReport(string? reportType) => reportType?.Trim().ToLowerInvariant() switch
{
"club" => ClubReport,
"player" => PlayerReport,
"room" => RoomReport,
"event" => EventReport,
"invention" => InventionReport,
_ => null
};
}
public static class WebhookConfig
{
private const string ConfigurationPrefix = "Radicade:Discord:Webhooks:";
public static string BanAppealsUrl { get; private set; } = "";
public static string AdminActivityUrl { get; private set; } = "";
public static string InGameBansUrl { get; private set; } = "";
public static string ReportsUrl { get; private set; } = "";
public static string GeneralActivityUrl { get; private set; } = "";
public static void Load(IConfiguration configuration, string contentRootPath)
{
var fileValues = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var filePath = Path.Combine(contentRootPath, "Discord", "Webhooks", "webhooks.json");
if (File.Exists(filePath))
{
try
{
fileValues = JsonSerializer.Deserialize<Dictionary<string, string?>>(File.ReadAllText(filePath))
?? fileValues;
}
catch (JsonException)
{
}
}
BanAppealsUrl = ReadUrl("BanAppeals");
AdminActivityUrl = ReadUrl("AdminActivity");
InGameBansUrl = ReadUrl("InGameBans");
ReportsUrl = ReadUrl("Reports");
GeneralActivityUrl = ReadUrl("GeneralActivity");
string ReadUrl(string name)
{
var configuredValue = configuration[ConfigurationPrefix + name];
var value = configuredValue ?? (fileValues.TryGetValue(name, out var fileValue) ? fileValue : "");
if (string.IsNullOrWhiteSpace(value)) return "";
value = value.Trim();
if (Uri.TryCreate(value, UriKind.Absolute, out var uri)
&& uri.Scheme == Uri.UriSchemeHttps
&& string.Equals(uri.Host, "discord.com", StringComparison.OrdinalIgnoreCase)
&& uri.AbsolutePath.StartsWith("/api/webhooks/", StringComparison.Ordinal))
{
return value;
}
return "";
}
}
}
+43
View File
@@ -0,0 +1,43 @@
using CSharpDiscordWebhook.Objects;
using System.Drawing;
using System.Runtime.InteropServices;
namespace RadicadeServer.Radicade.Discord.Templates
{
public class WebhookImageUpload : IWebhookTemplate
{
public static async Task Send(string image, string username, string displayName, string profileImage, string? roomName = null) { await RadicadeWebhook.SendMessage(Construct(image,username,displayName,profileImage,roomName!)); }
public static MessageBuilder Construct(params object[] args)
{
if (args.Length < 4) {
throw new ArgumentException("you FUCKER i expected: image, username, display name, profile image (and probably room name)");
}
string image = (string)args[0];
string username = (string)args[1];
string displayName = (string)args[2];
string profileImage = (string)args[3];
string? roomName = (args.Length >= 5) ? (string)args[4] : null;
return new MessageBuilder
{
Embeds = [
new EmbedBuilder {
Author = new EmbedAuthorBuilder {
Name = $"@{username}",
IconUrl = profileImage + "?width=256&cropSquare=1"
},
Color = Color.LimeGreen,
Description = (roomName == null) ? $"{displayName} just uploaded an image!" : $"{displayName} just uploaded an image in **{roomName}**!",
Title = $"New image uploaded by {displayName}!",
Image = new EmbedMediaBuilder {
Url = image,
Width = 768
}
}
]
};
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using CSharpDiscordWebhook.Objects;
using System.Drawing;
using System.Runtime.InteropServices;
using RadicadeServer.RecNet.Rooms;
namespace RadicadeServer.Radicade.Discord.Templates
{
public class WebhookRoomUpload : IWebhookTemplate
{
public static async Task Send(string image, string username, string displayName, string profileImage, string roomName, bool republish = false, string description = "None.") { await RadicadeWebhook.SendMessage(Construct(image, username, displayName, profileImage, roomName, republish, description)); }
public static MessageBuilder Construct(string image, string username, string displayName, string profileImage, string roomName, bool republish = false, string description = "None.")
{
return new MessageBuilder
{
Embeds = [
new EmbedBuilder {
Author = new EmbedAuthorBuilder {
Name = $"@{username}",
IconUrl = profileImage + "?width=256&cropSquare=1"
},
Color = Color.Aqua,
Description = !republish ? $"{displayName} just published **{roomName}**!" : $"{displayName} just republished **{roomName}**!",
Title = !republish ? $"New room published by {displayName}!" : $"Room republished by {displayName}!",
Image = new EmbedMediaBuilder {
Url = image + "?width=768",
Width = 768
},
Fields = [
new EmbedFieldBuilder {
Inline = false,
Name = "Description",
Value = description
}
]
}
]
};
}
}
}
@@ -0,0 +1,8 @@
{
"BanAppeals": "",
"AntiCheat": "",
"AdminActivity": "",
"InGameBans": "",
"Reports": "",
"GeneralActivity": ""
}