Skip to content

Commit

Permalink
Merge pull request #127 from OCOtheOmega/Discord-integration-update
Browse files Browse the repository at this point in the history
Better discord notifications
  • Loading branch information
Cheackraze committed Aug 7, 2023
2 parents ce6bf1b + 6e85cc0 commit d1ba595
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 11 deletions.
2 changes: 2 additions & 0 deletions Content.Server/GameTicking/GameTicker.RoundFlow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ public void StartRound(bool force = false)
UpdateLateJoinStatus();
AnnounceRound();
UpdateInfoText();
RaiseLocalEvent(new RoundStartedEvent(RoundId));

#if EXCEPTION_TOLERANCE
}
Expand Down Expand Up @@ -359,6 +360,7 @@ public void ShowRoundEndScoreboard(string text = "")
RaiseNetworkEvent(new RoundEndMessageEvent(gamemodeTitle, roundEndText, roundDuration, RoundId,
listOfPlayerInfoFinal.Length, listOfPlayerInfoFinal, LobbySong,
new SoundCollectionSpecifier("RoundEnd").GetSound()));
RaiseLocalEvent(new RoundEndedEvent(RoundId, roundDuration));
}

public void RestartRound()
Expand Down
13 changes: 13 additions & 0 deletions Content.Server/Nyanotrasen/GameTicking/RoundEndedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Content.Shared.GameTicking;

public sealed class RoundEndedEvent : EntityEventArgs
{
public int RoundId { get; }
public TimeSpan RoundDuration { get; }

public RoundEndedEvent(int roundId, TimeSpan roundDuration)
{
RoundId = roundId;
RoundDuration = roundDuration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Shared.CCVar;
using Content.Server.Maps;
using Content.Shared.GameTicking;
using Robust.Shared;
using Robust.Shared.Configuration;

namespace Content.Server.Nyanotrasen.RoundNotifications;

/// <summary>
/// Listen game events and send notifications to Discord
/// </summary>
public sealed class RoundNotificationsSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IGameMapManager _gameMapManager = default!;

private ISawmill _sawmill = default!;
private readonly HttpClient _httpClient = new();

private string _webhookUrl = String.Empty;
private string _roleId = String.Empty;
private bool _roundStartOnly;
private string _serverName = string.Empty;

/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
SubscribeLocalEvent<RoundStartedEvent>(OnRoundStarted);
SubscribeLocalEvent<RoundEndedEvent>(OnRoundEnded);

_config.OnValueChanged(CCVars.DiscordRoundWebhook, value => _webhookUrl = value, true);
_config.OnValueChanged(CCVars.DiscordRoundRoleId, value => _roleId = value, true);
_config.OnValueChanged(CCVars.DiscordRoundStartOnly, value => _roundStartOnly = value, true);
_config.OnValueChanged(CVars.GameHostName, OnServerNameChanged, true);

_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("notifications");
}

private void OnServerNameChanged(string obj)
{
_serverName = obj;
}

private void OnRoundRestart(RoundRestartCleanupEvent e)
{
if (String.IsNullOrEmpty(_webhookUrl))
return;

var text = Loc.GetString("discord-round-new");

SendDiscordMessage(text, true, 0x91B2C7);
}

private void OnRoundStarted(RoundStartedEvent e)
{
if (String.IsNullOrEmpty(_webhookUrl))
return;

var map = _gameMapManager.GetSelectedMap();
var mapName = map?.MapName ?? Loc.GetString("discord-round-unknown-map");
var text = Loc.GetString("discord-round-start",
("id", e.RoundId),
("map", mapName));

SendDiscordMessage(text, false);
}

private void OnRoundEnded(RoundEndedEvent e)
{
if (String.IsNullOrEmpty(_webhookUrl) || _roundStartOnly)
return;

var text = Loc.GetString("discord-round-end",
("id", e.RoundId),
("hours", Math.Truncate(e.RoundDuration.TotalHours)),
("minutes", e.RoundDuration.Minutes),
("seconds", e.RoundDuration.Seconds));

SendDiscordMessage(text, false, 0xB22B27);
}

private async void SendDiscordMessage(string text, bool ping = false, int color = 0x41F097)
{

// Limit server name to 1500 characters, in case someone tries to be a little funny
var serverName = _serverName[..Math.Min(_serverName.Length, 1500)];
var message = "";
if (!String.IsNullOrEmpty(_roleId) && ping)
message = $"<@&{_roleId}>";

// Build the embed
var payload = new WebhookPayload
{
Message = message,
Embeds = new List<Embed>
{
new()
{
Title = Loc.GetString("discord-round-title"),
Description = text,
Color = color,
Footer = new EmbedFooter
{
Text = $"{serverName}"
},
},
},
};
if (!String.IsNullOrEmpty(_roleId) && ping)
payload.AllowedMentions = new Dictionary<string, string[]> {{ "roles", new []{ _roleId } }};

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: {request.StatusCode}\nResponse: {content}");
return;
}
}

// https://discord.com/developers/docs/resources/channel#message-object-message-structure
private struct WebhookPayload
{
[JsonPropertyName("username")] public string? Username { get; set; } = null;

[JsonPropertyName("avatar_url")] public string? AvatarUrl { get; set; } = null;

[JsonPropertyName("content")] public string Message { get; set; } = "";

[JsonPropertyName("embeds")] public List<Embed>? Embeds { get; set; } = null;

[JsonPropertyName("allowed_mentions")]
public Dictionary<string, string[]> AllowedMentions { get; set; } =
new()
{
{ "parse", Array.Empty<string>() },
};

public WebhookPayload()
{
}
}

// https://discord.com/developers/docs/resources/channel#embed-object-embed-structure
private struct Embed
{
[JsonPropertyName("title")] public string Title { get; set; } = "";

[JsonPropertyName("description")] public string Description { get; set; } = "";

[JsonPropertyName("color")] public int Color { get; set; } = 0;

[JsonPropertyName("footer")] public EmbedFooter? Footer { get; set; } = null;

public Embed()
{
}
}

// https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure
private struct EmbedFooter
{
[JsonPropertyName("text")] public string Text { get; set; } = "";

[JsonPropertyName("icon_url")] public string? IconUrl { get; set; }

public EmbedFooter()
{
}
}
}
67 changes: 62 additions & 5 deletions Content.Server/_NF/GameRule/NfAdventureRuleSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,14 +222,26 @@ private void OnStartup(RoundStartingEvent ev)
}
}

private async Task ReportRound(String message)
private async Task ReportRound(String message, int color = 0x77DDE7)
{
Logger.InfoS("discord", message);
String _webhookUrl = _configurationManager.GetCVar(CCVars.DiscordEndRoundWebhook);
String _webhookUrl = _configurationManager.GetCVar(CCVars.DiscordLeaderboardWebhook);
if (_webhookUrl == string.Empty)
return;

var payload = new WebhookPayload{ Content = message };
var payload = new WebhookPayload
{
Embeds = new List<Embed>
{
new()
{
Title = Loc.GetString("adventure-list-start"),
Description = message,
Color = color,
},
},
};

var ser_payload = JsonSerializer.Serialize(payload);
var content = new StringContent(ser_payload, Encoding.UTF8, "application/json");
var request = await _httpClient.PostAsync($"{_webhookUrl}?wait=true", content);
Expand All @@ -240,9 +252,54 @@ private async Task ReportRound(String message)
}
}

// https://discord.com/developers/docs/resources/channel#message-object-message-structure
private struct WebhookPayload
{
[JsonPropertyName("content")]
public String Content { get; set; }
[JsonPropertyName("username")] public string? Username { get; set; } = null;

[JsonPropertyName("avatar_url")] public string? AvatarUrl { get; set; } = null;

[JsonPropertyName("content")] public string Message { get; set; } = "";

[JsonPropertyName("embeds")] public List<Embed>? Embeds { get; set; } = null;

[JsonPropertyName("allowed_mentions")]
public Dictionary<string, string[]> AllowedMentions { get; set; } =
new()
{
{ "parse", Array.Empty<string>() },
};

public WebhookPayload()
{
}
}

// https://discord.com/developers/docs/resources/channel#embed-object-embed-structure
private struct Embed
{
[JsonPropertyName("title")] public string Title { get; set; } = "";

[JsonPropertyName("description")] public string Description { get; set; } = "";

[JsonPropertyName("color")] public int Color { get; set; } = 0;

[JsonPropertyName("footer")] public EmbedFooter? Footer { get; set; } = null;

public Embed()
{
}
}

// https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure
private struct EmbedFooter
{
[JsonPropertyName("text")] public string Text { get; set; } = "";

[JsonPropertyName("icon_url")] public string? IconUrl { get; set; }

public EmbedFooter()
{
}
}
}
30 changes: 24 additions & 6 deletions Content.Shared/CCVar/CCVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,6 @@ public static readonly CVarDef<bool>
public static readonly CVarDef<string> DiscordAHelpWebhook =
CVarDef.Create("discord.ahelp_webhook", string.Empty, CVar.SERVERONLY);

/// <summary>
/// URL of the Discord webhook which will relay all round end messages.
/// </summary>
public static readonly CVarDef<string> DiscordEndRoundWebhook =
CVarDef.Create("discord.end_round_webhook", string.Empty, CVar.SERVERONLY);

/// <summary>
/// The server icon to use in the Discord ahelp embed footer.
/// Valid values are specified at https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure.
Expand All @@ -341,6 +335,30 @@ public static readonly CVarDef<bool>
public static readonly CVarDef<string> DiscordAHelpAvatar =
CVarDef.Create("discord.ahelp_avatar", string.Empty, CVar.SERVERONLY);

/// <summary>
/// URL of the Discord webhook which will send round status notifications.
/// </summary>
public static readonly CVarDef<string> DiscordRoundWebhook =
CVarDef.Create("discord.round_webhook", string.Empty, CVar.SERVERONLY);

/// <summary>
/// Discord ID of role which will be pinged on new round start message.
/// </summary>
public static readonly CVarDef<string> DiscordRoundRoleId =
CVarDef.Create("discord.round_roleid", string.Empty, CVar.SERVERONLY);

/// <summary>
/// Send notifications only about a new round begins.
/// </summary>
public static readonly CVarDef<bool> DiscordRoundStartOnly =
CVarDef.Create("discord.round_start_only", false, CVar.SERVERONLY);

/// <summary>
/// URL of the Discord webhook which will relay all round end messages.
/// </summary>
public static readonly CVarDef<string> DiscordLeaderboardWebhook =
CVarDef.Create("discord.leaderboard_webhook", string.Empty, CVar.SERVERONLY);

/*
* Suspicion
*/
Expand Down
15 changes: 15 additions & 0 deletions Content.Shared/GameTicking/RoundRestartedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Robust.Shared.Serialization;

namespace Content.Shared.GameTicking
{
[Serializable, NetSerializable]
public sealed class RoundStartedEvent : EntityEventArgs
{
public int RoundId { get; }

public RoundStartedEvent(int roundId)
{
RoundId = roundId;
}
}
}
4 changes: 4 additions & 0 deletions Resources/Locale/en-US/round-notifications/notifications.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
discord-round-new = A new round is starting!
discord-round-start = Round #{ $id } on map "{ $map }" has started.
discord-round-end = Round #{ $id } has ended. It lasted for {$hours} hours, {$minutes} minutes, and {$seconds} seconds.
discord-round-title = Round Notification

0 comments on commit d1ba595

Please sign in to comment.