Station news Discord webhook (#36807)

* Add news article Discord webhook

* Send all station articles on round end

* Changed event subscrice to RoundEndMessageEvent

* Review remarks fix

* Added new cvar discord.news_webhook_embed_color

Default color taken from news manager console sprite.

* Using EntityQueryEnumerator instead of GetStationInMap with TryComp

* Extra review remarks fixing

* Sorted imports

* Added article publication time in embed

* Removed markup from article content

* Added sorting for articles iteration

* Discord hook embed color cvar is string now

* Added comment about limits

* Added new cvar for posting articles during round

* Shitty discord rate limit handling

* Fixing copypaste accident

Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>

* Null initialization of webhook id

* SendArticleToDiscordWebhook is non-void now

---------

Co-authored-by: Morb0 <14136326+Morb0@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
This commit is contained in:
ssdaniel24
2025-05-10 21:21:02 +03:00
committed by GitHub
parent 5c3b613507
commit 9881528692
3 changed files with 122 additions and 4 deletions

View File

@@ -1,24 +1,33 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.CartridgeLoader;
using Content.Server.CartridgeLoader.Cartridges; using Content.Server.CartridgeLoader.Cartridges;
using Content.Server.CartridgeLoader;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Discord;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.MassMedia.Components; using Content.Server.MassMedia.Components;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Shared.Access.Components; using Content.Shared.Access.Components;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Content.Shared.CartridgeLoader; using Content.Shared.CCVar;
using Content.Shared.CartridgeLoader.Cartridges; using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.CartridgeLoader;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.IdentityManagement;
using Content.Shared.MassMedia.Components; using Content.Shared.MassMedia.Components;
using Content.Shared.MassMedia.Systems; using Content.Shared.MassMedia.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Content.Shared.IdentityManagement; using Robust.Shared.Configuration;
using Robust.Shared.Maths;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
namespace Content.Server.MassMedia.Systems; namespace Content.Server.MassMedia.Systems;
@@ -34,11 +43,36 @@ public sealed class NewsSystem : SharedNewsSystem
[Dependency] private readonly StationSystem _station = default!; [Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly GameTicker _ticker = default!; [Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
private WebhookIdentifier? _webhookId = null;
private Color _webhookEmbedColor;
private bool _webhookSendDuringRound;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
// Discord hook
_cfg.OnValueChanged(CCVars.DiscordNewsWebhook,
value =>
{
if (!string.IsNullOrWhiteSpace(value))
_discord.GetWebhook(value, data => _webhookId = data.ToIdentifier());
}, true);
_cfg.OnValueChanged(CCVars.DiscordNewsWebhookEmbedColor, value =>
{
_webhookEmbedColor = Color.LawnGreen;
if (Color.TryParse(value, out var color))
_webhookEmbedColor = color;
}, true);
_cfg.OnValueChanged(CCVars.DiscordNewsWebhookSendDuringRound, value => _webhookSendDuringRound = value, true);
SubscribeLocalEvent<RoundEndMessageEvent>(OnRoundEndMessageEvent);
// News writer // News writer
SubscribeLocalEvent<NewsWriterComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<NewsWriterComponent, MapInitEvent>(OnMapInit);
@@ -177,6 +211,9 @@ public sealed class NewsSystem : SharedNewsSystem
RaiseLocalEvent(readerUid, ref args); RaiseLocalEvent(readerUid, ref args);
} }
if (_webhookSendDuringRound)
Task.Run(async () => await SendArticleToDiscordWebhook(article));
UpdateWriterDevices(); UpdateWriterDevices();
} }
#endregion #endregion
@@ -324,4 +361,62 @@ public sealed class NewsSystem : SharedNewsSystem
{ {
UpdateWriterUi(ent); UpdateWriterUi(ent);
} }
#region Discord Hook
private void OnRoundEndMessageEvent(RoundEndMessageEvent ev)
{
if (_webhookSendDuringRound)
return;
var query = EntityManager.EntityQueryEnumerator<StationNewsComponent>();
while (query.MoveNext(out _, out var comp))
{
SendArticlesListToDiscordWebhook(comp.Articles.OrderBy(article => article.ShareTime));
}
}
private async void SendArticlesListToDiscordWebhook(IOrderedEnumerable<NewsArticle> articles)
{
foreach (var article in articles)
{
await Task.Delay(TimeSpan.FromSeconds(1)); // TODO: proper discord rate limit handling
await SendArticleToDiscordWebhook(article);
}
}
private async Task SendArticleToDiscordWebhook(NewsArticle article)
{
if (_webhookId is null)
return;
try
{
var embed = new WebhookEmbed
{
Title = article.Title,
// There is no need to cut article content. It's MaxContentLength smaller then discord's limit (4096):
Description = FormattedMessage.RemoveMarkupPermissive(article.Content),
Color = _webhookEmbedColor.ToArgb() & 0xFFFFFF, // HACK: way to get hex without A (transparency)
Footer = new WebhookEmbedFooter
{
Text = Loc.GetString("news-discord-footer",
("server", _baseServer.ServerName),
("round", _ticker.RoundId),
("author", article.Author ?? Loc.GetString("news-discord-unknown-author")),
("time", article.ShareTime.ToString(@"hh\:mm\:ss")))
}
};
var payload = new WebhookPayload { Embeds = [embed] };
await _discord.CreateMessage(_webhookId.Value, payload);
Log.Info("Sent news article to Discord webhook");
}
catch (Exception e)
{
Log.Error($"Error while sending discord news article:\n{e}");
}
}
#endregion
} }

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Maths;
namespace Content.Shared.CCVar; namespace Content.Shared.CCVar;
@@ -72,4 +73,24 @@ public sealed partial class CCVars
/// </summary> /// </summary>
public static readonly CVarDef<float> DiscordWatchlistConnectionBufferTime = public static readonly CVarDef<float> DiscordWatchlistConnectionBufferTime =
CVarDef.Create("discord.watchlist_connection_buffer_time", 5f, CVar.SERVERONLY); CVarDef.Create("discord.watchlist_connection_buffer_time", 5f, CVar.SERVERONLY);
/// <summary>
/// URL of the Discord webhook which will receive station news acticles at the round end.
/// If left empty, disables the webhook.
/// </summary>
public static readonly CVarDef<string> DiscordNewsWebhook =
CVarDef.Create("discord.news_webhook", string.Empty, CVar.SERVERONLY);
/// <summary>
/// HEX color of station news discord webhook's embed.
/// </summary>
public static readonly CVarDef<string> DiscordNewsWebhookEmbedColor =
CVarDef.Create("discord.news_webhook_embed_color", Color.LawnGreen.ToHex(), CVar.SERVERONLY);
/// <summary>
/// Whether or not articles should be sent mid-round instead of all at once at the round's end
/// </summary>
public static readonly CVarDef<bool> DiscordNewsWebhookSendDuringRound =
CVarDef.Create("discord.news_webhook_send_during_round", false, CVar.SERVERONLY);
} }

View File

@@ -0,0 +1,2 @@
news-discord-footer = Server: {$server} | Round: #{$round} | Author: {$author} | Time: {$time}
news-discord-unknown-author = Unknown