2022-05-27 02:41:18 -05:00
using System.Linq ;
2021-12-22 13:34:09 +01:00
using System.Net.Http ;
using System.Text ;
using System.Text.Json ;
2021-12-29 21:12:07 +01:00
using System.Text.Json.Nodes ;
2022-05-27 02:41:18 -05:00
using System.Text.Json.Serialization ;
2021-10-06 16:25:27 +01:00
using Content.Server.Administration.Managers ;
2022-05-27 02:41:18 -05:00
using Content.Server.GameTicking ;
using Content.Server.GameTicking.Events ;
2022-09-11 17:43:38 +02:00
using Content.Server.Players ;
2021-10-06 16:25:27 +01:00
using Content.Shared.Administration ;
2021-12-22 13:34:09 +01:00
using Content.Shared.CCVar ;
2021-10-06 16:25:27 +01:00
using JetBrains.Annotations ;
using Robust.Server.Player ;
2021-12-22 13:34:09 +01:00
using Robust.Shared ;
using Robust.Shared.Configuration ;
2021-12-29 21:12:07 +01:00
using Robust.Shared.Network ;
2021-11-14 20:21:18 +01:00
using Robust.Shared.Utility ;
2021-10-06 16:25:27 +01:00
2022-05-27 02:41:18 -05:00
namespace Content.Server.Administration.Systems
2021-10-06 16:25:27 +01:00
{
[UsedImplicitly]
2022-02-16 00:23:23 -07:00
public sealed class BwoinkSystem : SharedBwoinkSystem
2021-10-06 16:25:27 +01:00
{
[Dependency] private readonly IPlayerManager _playerManager = default ! ;
[Dependency] private readonly IAdminManager _adminManager = default ! ;
2021-12-22 13:34:09 +01:00
[Dependency] private readonly IConfigurationManager _config = default ! ;
[Dependency] private readonly IPlayerLocator _playerLocator = default ! ;
2022-04-13 15:32:28 -07:00
[Dependency] private readonly GameTicker _gameTicker = default ! ;
2021-12-22 13:34:09 +01:00
2021-12-29 21:12:07 +01:00
private ISawmill _sawmill = default ! ;
2021-12-22 13:34:09 +01:00
private readonly HttpClient _httpClient = new ( ) ;
private string _webhookUrl = string . Empty ;
2022-09-11 17:43:38 +02:00
private string _footerIconUrl = string . Empty ;
private string _avatarUrl = string . Empty ;
2021-12-22 13:34:09 +01:00
private string _serverName = string . Empty ;
2022-09-11 17:43:38 +02:00
private readonly Dictionary < NetUserId , ( string id , string username , string messages , string? characterName ) > _relayMessages = new ( ) ;
2021-12-29 21:12:07 +01:00
private readonly Dictionary < NetUserId , Queue < string > > _messageQueues = new ( ) ;
private readonly HashSet < NetUserId > _processingChannels = new ( ) ;
2022-09-11 17:43:38 +02:00
// Max embed description length is 4096, according to https://discord.com/developers/docs/resources/channel#embed-object-embed-limits
// Keep small margin, just to be safe
private const ushort DescriptionMax = 4000 ;
2021-12-29 21:12:07 +01:00
private int _maxAdditionalChars ;
2021-12-22 13:34:09 +01:00
public override void Initialize ( )
{
base . Initialize ( ) ;
_config . OnValueChanged ( CCVars . DiscordAHelpWebhook , OnWebhookChanged , true ) ;
2022-09-11 17:43:38 +02:00
_config . OnValueChanged ( CCVars . DiscordAHelpFooterIcon , OnFooterIconChanged , true ) ;
_config . OnValueChanged ( CCVars . DiscordAHelpAvatar , OnAvatarChanged , true ) ;
2021-12-22 13:34:09 +01:00
_config . OnValueChanged ( CVars . GameHostName , OnServerNameChanged , true ) ;
2021-12-29 21:12:07 +01:00
_sawmill = IoCManager . Resolve < ILogManager > ( ) . GetSawmill ( "AHELP" ) ;
2022-09-11 17:43:38 +02:00
_maxAdditionalChars = GenerateAHelpMessage ( "" , "" , true ) . Length ;
2022-04-13 15:32:28 -07:00
SubscribeLocalEvent < RoundStartingEvent > ( RoundStarting ) ;
}
private void RoundStarting ( RoundStartingEvent ev )
{
_relayMessages . Clear ( ) ;
2021-12-22 13:34:09 +01:00
}
private void OnServerNameChanged ( string obj )
{
_serverName = obj ;
}
public override void Shutdown ( )
{
base . Shutdown ( ) ;
_config . UnsubValueChanged ( CCVars . DiscordAHelpWebhook , OnWebhookChanged ) ;
2022-09-11 17:43:38 +02:00
_config . UnsubValueChanged ( CCVars . DiscordAHelpFooterIcon , OnFooterIconChanged ) ;
2021-12-22 13:34:09 +01:00
_config . UnsubValueChanged ( CVars . GameHostName , OnServerNameChanged ) ;
}
private void OnWebhookChanged ( string obj )
{
_webhookUrl = obj ;
}
2021-10-06 16:25:27 +01:00
2022-09-11 17:43:38 +02:00
private void OnFooterIconChanged ( string url )
{
_footerIconUrl = url ;
}
private void OnAvatarChanged ( string url )
{
_avatarUrl = url ;
}
2021-12-29 21:12:07 +01:00
private async void ProcessQueue ( NetUserId channelId , Queue < string > messages )
{
2022-09-11 17:43:38 +02:00
if ( ! _relayMessages . TryGetValue ( channelId , out var oldMessage ) | | messages . Sum ( x = > x . Length + 2 ) + oldMessage . messages . Length > DescriptionMax )
2021-12-29 21:12:07 +01:00
{
var lookup = await _playerLocator . LookupIdAsync ( channelId ) ;
if ( lookup = = null )
{
_sawmill . Log ( LogLevel . Error , $"Unable to find player for netuserid {channelId} when sending discord webhook." ) ;
_relayMessages . Remove ( channelId ) ;
return ;
}
2022-09-11 17:43:38 +02:00
var characterName = _playerManager . GetPlayerData ( channelId ) . ContentData ( ) ? . Mind ? . CharacterName ;
oldMessage = ( string . Empty , lookup . Username , string . Empty , characterName ) ;
2021-12-29 21:12:07 +01:00
}
while ( messages . TryDequeue ( out var message ) )
{
2022-09-11 17:43:38 +02:00
oldMessage . messages + = $"\n{message}" ;
2021-12-29 21:12:07 +01:00
}
2022-09-12 05:52:27 +02:00
var payload = GeneratePayload ( oldMessage . messages , oldMessage . username , oldMessage . characterName ) ;
2021-12-29 21:12:07 +01:00
if ( oldMessage . id = = string . Empty )
{
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 )
{
2022-09-11 17:43:38 +02:00
_sawmill . Log ( LogLevel . Error , $"Discord returned bad status code when posting message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}" ) ;
2021-12-29 21:12:07 +01:00
_relayMessages . Remove ( channelId ) ;
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}" ) ;
_relayMessages . Remove ( channelId ) ;
return ;
}
oldMessage . id = id . ToString ( ) ;
}
else
{
var request = await _httpClient . PatchAsync ( $"{_webhookUrl}/messages/{oldMessage.id}" ,
new StringContent ( JsonSerializer . Serialize ( payload ) , Encoding . UTF8 , "application/json" ) ) ;
if ( ! request . IsSuccessStatusCode )
{
var content = await request . Content . ReadAsStringAsync ( ) ;
2022-09-11 17:43:38 +02:00
_sawmill . Log ( LogLevel . Error , $"Discord returned bad status code when patching message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}" ) ;
2021-12-29 21:12:07 +01:00
_relayMessages . Remove ( channelId ) ;
return ;
}
}
_relayMessages [ channelId ] = oldMessage ;
_processingChannels . Remove ( channelId ) ;
}
2022-09-12 05:52:27 +02:00
private WebhookPayload GeneratePayload ( string messages , string username , string? characterName = null )
{
// Add character name
if ( characterName ! = null )
username + = $" ({characterName})" ;
// If no admins are online, set embed color to red. Otherwise green
var color = GetTargetAdmins ( ) . Count > 0 ? 0x41F097 : 0xFF0000 ;
// Limit server name to 1500 characters, in case someone tries to be a little funny
var serverName = _serverName [ . . Math . Min ( _serverName . Length , 1500 ) ] ;
// If the round ID is 0, it most likely means we are in the lobby
var round = _gameTicker . RoundId = = 0 ? "lobby" : $"round {_gameTicker.RoundId}" ;
return new WebhookPayload
{
Username = username ,
AvatarUrl = _avatarUrl ,
Embeds = new List < Embed >
{
new Embed
{
Description = messages ,
Color = color ,
Footer = new EmbedFooter
{
Text = $"{serverName} ({round})" ,
IconUrl = _footerIconUrl ,
} ,
} ,
} ,
} ;
}
2021-12-29 21:12:07 +01:00
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
foreach ( var channelId in _messageQueues . Keys . ToArray ( ) )
{
2022-09-11 17:43:38 +02:00
if ( _processingChannels . Contains ( channelId ) )
continue ;
2021-12-29 21:12:07 +01:00
var queue = _messageQueues [ channelId ] ;
_messageQueues . Remove ( channelId ) ;
2022-09-11 17:43:38 +02:00
if ( queue . Count = = 0 )
continue ;
2021-12-29 21:12:07 +01:00
_processingChannels . Add ( channelId ) ;
ProcessQueue ( channelId , queue ) ;
}
}
2021-10-06 16:25:27 +01:00
protected override void OnBwoinkTextMessage ( BwoinkTextMessage message , EntitySessionEventArgs eventArgs )
{
base . OnBwoinkTextMessage ( message , eventArgs ) ;
var senderSession = ( IPlayerSession ) eventArgs . SenderSession ;
// TODO: Sanitize text?
// Confirm that this person is actually allowed to send a message here.
2021-12-22 13:34:09 +01:00
var personalChannel = senderSession . UserId = = message . ChannelId ;
2021-12-14 15:17:08 -06:00
var senderAdmin = _adminManager . GetAdminData ( senderSession ) ;
2022-09-05 16:50:52 +01:00
var senderAHelpAdmin = senderAdmin ? . HasFlag ( AdminFlags . Adminhelp ) ? ? false ;
var authorized = personalChannel | | senderAHelpAdmin ;
2021-11-11 09:47:45 +00:00
if ( ! authorized )
2021-10-06 16:25:27 +01:00
{
// Unauthorized bwoink (log?)
return ;
}
2021-12-20 12:42:42 +01:00
var escapedText = FormattedMessage . EscapeText ( message . Text ) ;
2021-11-14 20:21:18 +01:00
2021-12-14 15:17:08 -06:00
var bwoinkText = senderAdmin switch
{
var x when x is not null & & x . Flags = = AdminFlags . Adminhelp = >
$"[color=purple]{senderSession.Name}[/color]: {escapedText}" ,
var x when x is not null & & x . HasFlag ( AdminFlags . Adminhelp ) = >
$"[color=red]{senderSession.Name}[/color]: {escapedText}" ,
_ = > $"{senderSession.Name}: {escapedText}" ,
} ;
2021-11-14 20:21:18 +01:00
var msg = new BwoinkTextMessage ( message . ChannelId , senderSession . UserId , bwoinkText ) ;
2021-10-06 16:25:27 +01:00
LogBwoink ( msg ) ;
2022-09-11 17:43:38 +02:00
var admins = GetTargetAdmins ( ) ;
2021-10-06 16:25:27 +01:00
2022-09-11 17:43:38 +02:00
// Notify all admins
foreach ( var channel in admins )
{
2021-11-11 09:47:45 +00:00
RaiseNetworkEvent ( msg , channel ) ;
2022-09-11 17:43:38 +02:00
}
2021-11-11 09:47:45 +00:00
2022-09-11 17:43:38 +02:00
// Notify player
if ( _playerManager . TryGetSessionById ( message . ChannelId , out var session ) )
{
if ( ! admins . Contains ( session . ConnectedClient ) )
RaiseNetworkEvent ( msg , session . ConnectedClient ) ;
}
2021-12-29 21:12:07 +01:00
2021-12-22 13:34:09 +01:00
var sendsWebhook = _webhookUrl ! = string . Empty ;
if ( sendsWebhook )
{
2021-12-29 21:12:07 +01:00
if ( ! _messageQueues . ContainsKey ( msg . ChannelId ) )
_messageQueues [ msg . ChannelId ] = new Queue < string > ( ) ;
var str = message . Text ;
var unameLength = senderSession . Name . Length ;
2022-09-11 17:43:38 +02:00
if ( unameLength + str . Length + _maxAdditionalChars > DescriptionMax )
2021-12-22 13:34:09 +01:00
{
2022-09-11 17:43:38 +02:00
str = str [ . . ( DescriptionMax - _maxAdditionalChars - unameLength ) ] ;
2021-12-22 13:34:09 +01:00
}
2022-09-12 05:52:27 +02:00
_messageQueues [ msg . ChannelId ] . Enqueue ( GenerateAHelpMessage ( senderSession . Name , str , ! personalChannel , admins . Count = = 0 ) ) ;
2021-12-22 13:34:09 +01:00
}
2022-09-11 17:43:38 +02:00
if ( admins . Count ! = 0 )
return ;
// No admin online, let the player know
var systemText = sendsWebhook ?
Loc . GetString ( "bwoink-system-starmute-message-no-other-users-webhook" ) :
Loc . GetString ( "bwoink-system-starmute-message-no-other-users" ) ;
var starMuteMsg = new BwoinkTextMessage ( message . ChannelId , SystemUserId , systemText ) ;
RaiseNetworkEvent ( starMuteMsg , senderSession . ConnectedClient ) ;
2021-10-06 16:25:27 +01:00
}
2021-12-22 13:34:09 +01:00
2022-09-11 17:43:38 +02:00
// Returns all online admins with AHelp access
private IList < INetChannel > GetTargetAdmins ( )
{
return _adminManager . ActiveAdmins
. Where ( p = > _adminManager . GetAdminData ( p ) ? . HasFlag ( AdminFlags . Adminhelp ) ? ? false )
. Select ( p = > p . ConnectedClient )
. ToList ( ) ;
}
2022-09-12 05:52:27 +02:00
private static string GenerateAHelpMessage ( string username , string message , bool admin , bool noReceivers = false )
2021-12-29 21:12:07 +01:00
{
var stringbuilder = new StringBuilder ( ) ;
2022-09-12 05:52:27 +02:00
if ( admin )
stringbuilder . Append ( ":outbox_tray:" ) ;
else if ( noReceivers )
stringbuilder . Append ( ":sos:" ) ;
else
stringbuilder . Append ( ":inbox_tray:" ) ;
2022-09-11 17:43:38 +02:00
stringbuilder . Append ( $" **{username}:** " ) ;
2021-12-29 21:12:07 +01:00
stringbuilder . Append ( message ) ;
return stringbuilder . ToString ( ) ;
}
2022-09-11 17:43:38 +02:00
// https://discord.com/developers/docs/resources/channel#message-object-message-structure
2021-12-22 13:34:09 +01:00
private struct WebhookPayload
{
2022-01-09 20:10:36 -08:00
[JsonPropertyName("username")]
public string Username { get ; set ; } = "" ;
2021-12-22 13:34:09 +01:00
2022-09-11 17:43:38 +02:00
[JsonPropertyName("avatar_url")]
public string AvatarUrl { get ; set ; } = "" ;
[JsonPropertyName("embeds")]
public List < Embed > ? Embeds { get ; set ; } = null ;
2021-12-22 13:34:09 +01:00
2022-01-09 20:10:36 -08:00
[JsonPropertyName("allowed_mentions")]
public Dictionary < string , string [ ] > AllowedMentions { get ; set ; } =
2021-12-22 13:34:09 +01:00
new ( )
{
2022-09-11 17:43:38 +02:00
{ "parse" , Array . Empty < string > ( ) } ,
2021-12-22 13:34:09 +01:00
} ;
2022-02-16 16:03:26 -07:00
2022-09-11 17:43:38 +02:00
public WebhookPayload ( ) { }
}
// https://discord.com/developers/docs/resources/channel#embed-object-embed-structure
private struct Embed
{
[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 ( )
{
}
2021-12-22 13:34:09 +01:00
}
2021-10-06 16:25:27 +01:00
}
}