diff --git a/Content.Server/_EE/Scribe/ScribeComponent.cs b/Content.Server/_EE/Scribe/ScribeComponent.cs new file mode 100644 index 00000000000..45c5df3a318 --- /dev/null +++ b/Content.Server/_EE/Scribe/ScribeComponent.cs @@ -0,0 +1,40 @@ +namespace Content.Server._EE.Scribe; + +[RegisterComponent] +public sealed partial class ScribeBookComponent : Component +{ + /// + /// The key to find the Webhook data by. + /// See the formatting of the DiscordScribeWebhooks cvar for more information. + /// + [DataField] + public string WebhookKey = ""; + + /// + /// The fluent format to be used in the Webhook's username. + /// Null for default name. + /// + [DataField] + public string? NameFormat = null; + + /// + /// The fluent format to be used in the Webhook's content. + /// Null for no formatting. + /// + [DataField] + public string? ContentFormat = null; + + /// + /// The footer to be provided. Can be a fluent format string but can also be raw. + /// Null for no footer. + /// + [DataField] + public string? Footer = null; + + /// + /// The colour to be used with the Webhook, in decimal. + /// See this guide for more info. + /// + [DataField] + public int Color = 0; +} diff --git a/Content.Server/_EE/Scribe/ScribeSystem.cs b/Content.Server/_EE/Scribe/ScribeSystem.cs new file mode 100644 index 00000000000..9214081390a --- /dev/null +++ b/Content.Server/_EE/Scribe/ScribeSystem.cs @@ -0,0 +1,112 @@ +using System.Linq; +using Content.Server.GameTicking; +using Content.Server.Paper; +using Content.Server.Discord; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; + +namespace Content.Server._EE.Scribe; + +public sealed partial class ScribeSystem : EntitySystem +{ + [Dependency] private readonly DiscordWebhook _discord = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + + private Dictionary _webHooks = new (); + + public override void Initialize() + { + base.Initialize(); + + Subs.CVar(_config, CCVars.DiscordScribeWebhooks, cvar => _webHooks = ParseCVarTable(cvar), true); + + SubscribeLocalEvent(OnRoundEnded); + } + + private void OnRoundEnded(RoundEndedEvent ev) + { + // Collect all the scribebooks into groups based on the Webhook they target. + foreach (var result in EntityQuery().GroupBy(q => q.Item1.WebhookKey)) + { + if (!_webHooks.TryGetValue(result.Key, out var identifier)) + continue; + + // Collect them into messages. + var embeds = new List(); + + foreach (var (scribeComp, paperComp) in result) + { + var embed = new WebhookEmbed(); + + if (scribeComp.NameFormat is { } nameLoc) + embed.Title = Loc.GetString(nameLoc, ("name", "Urist McHands")); //TODO: Need to work out how to hold on to people's names. + + embed.Description = scribeComp.ContentFormat is { } contentLoc + ? Loc.GetString(contentLoc, ("content", paperComp.Content)) + : paperComp.Content; + + if (scribeComp.Footer is { } footer) + { + embed.Footer = new () + { + Text = Loc.GetString(footer, ("round-id", ev.RoundId)), + }; + } + + embed.Color = scribeComp.Color; + + embeds.Add(embed); + } + + _discord.CreateMessage(identifier, new () { Embeds = embeds, }); + } + } + + private Dictionary ParseCVarTable(string src) + { + var dict = new Dictionary(); + foreach (var line in src.Split('\n')) + { + // Expected format is: + // + // : / + // : / + // : / + + // Maps as CCVars when? ;~; + + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (Split(src, ':') is not { } keyValue) + continue; + + if (Split(keyValue.Item2, '/') is not { } idToken) + continue; + + dict.Add(keyValue.Item1.Trim(), new (idToken.Item1.Trim(), idToken.Item2.Trim())); + + // Helper function to split the lines. + (string, string)? Split(string input, char c) + { + var parts = input.Split(c, 2); + if (parts.Length != 2) + { + Log.Warning($"Webhook table line missing key: {line}"); + return null; + } + + var (part1, part2) = (parts[0], parts[1]); + if (string.IsNullOrWhiteSpace(part1) || string.IsNullOrWhiteSpace(part2)) + { + Log.Warning($"Webhook table line malformed: {line}"); + return null; + } + + return (part1, part2); + } + } + + return dict; + } +} diff --git a/Content.Shared/CCVar/CCVars.Discord.cs b/Content.Shared/CCVar/CCVars.Discord.cs index 10fe6e1f44c..320b076f9a3 100644 --- a/Content.Shared/CCVar/CCVars.Discord.cs +++ b/Content.Shared/CCVar/CCVars.Discord.cs @@ -69,4 +69,22 @@ public sealed partial class CCVars /// public static readonly CVarDef DiscordAuthApiKey = CVarDef.Create("discord.auth_api_key", "", CVar.SERVERONLY | CVar.CONFIDENTIAL); + + /// + /// Table of the Discord webhook URL IDs/tokens which will relay all scribe messages. + /// + /// + /// Expected to be formatted as one <key>: <id>/<token> per line, + /// where the key is the same as a key in the ScribeComponent's WebhookKey field and + /// the id and token are the same as they appear in a Webhook URL. + /// For example + /// + /// scribe_webhooks: """ + /// first_scribe_key: 0000000000000000000/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + /// second_scribe_key: 1111111111112222222/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccc + /// """ + /// + /// + public static readonly CVarDef DiscordScribeWebhooks = + CVarDef.Create("discord.scribe_webhooks", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL); } diff --git a/Resources/Locale/en-US/_EE/scribe.ftl b/Resources/Locale/en-US/_EE/scribe.ftl new file mode 100644 index 00000000000..4e09305fead --- /dev/null +++ b/Resources/Locale/en-US/_EE/scribe.ftl @@ -0,0 +1,3 @@ +scribe-name-format = Scribe {$name} +scribe-content-format = {$content} +scribe-footer-format = Nanotrasen shift #{$round-id} diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml b/Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml index 1556db286f0..013b17c9b7a 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml @@ -54,7 +54,7 @@ outerClothing: ClothingOuterCoatRnd id: LibrarianPDA ears: ClothingHeadsetScience - pocket1: BookPsionicsGuidebook + pocket1: PaperScribeBook pocket2: HandLabeler innerClothingSkirt: ClothingUniformJumpskirtLibrarian satchel: ClothingBackpackSatchelLibrarianFilled diff --git a/Resources/Prototypes/_EE/Entities/Objects/Misc/scribe.yml b/Resources/Prototypes/_EE/Entities/Objects/Misc/scribe.yml new file mode 100644 index 00000000000..ff81693b7c3 --- /dev/null +++ b/Resources/Prototypes/_EE/Entities/Objects/Misc/scribe.yml @@ -0,0 +1,31 @@ +- type: entity + name: scribe + parent: Paper + id: PaperScribeBook + description: "A page of the captain's journal. In luxurious lavender." + components: + - type: Sprite + sprite: Objects/Misc/bureaucracy.rsi + layers: + - state: paper + color: "#e6e6fa" + - state: paper_words + map: ["enum.PaperVisualLayers.Writing"] + color: "#e6e6fa" + visible: false + - state: paper_stamp-generic + map: ["enum.PaperVisualLayers.Stamp"] + visible: false + - type: PaperLabelType + paperType: CaptainsPaper + - type: PaperVisuals + headerImagePath: "/Textures/Interface/Paper/paper_heading_captains_thoughts.svg.96dpi.png" + backgroundImagePath: "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png" + backgroundModulate: "#e6e6fa" + backgroundPatchMargin: 16.0, 16.0, 16.0, 16.0 + contentMargin: 32.0, 16.0, 32.0, 0.0 + - type: ScribeBook + webhookKey: librarian + nameFormat: scribe-name-format + contentFormat: scribe-content-format + footer: scribe-footer-format