Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor IrcProvider just a little #1935

Merged
merged 7 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ Create an `appsettings.Production.yml` file next to `appsettings.yml`. This will

- `FileLogging:LogLevel`: Can be one of `Trace`, `Debug`, `Information`, `Warning`, `Error`, or `Critical`. Restricts what is put into the log files. Currently `Debug` is reccommended for help with error reporting.

- `FileLogging:ProviderNetworkDebug`: Boolean controlling whether or not Chat bot providers should log their raw network traffic. Currently only applies to IrcProvider.

- `Kestrel:Endpoints:Http:Url`: The URL (i.e. interface and ports) your application should listen on. General use case should be `http://localhost:<port>` for restricted local connections. See the Remote Access section for configuring public access to the World Wide Web. This doesn't need to be changed using the docker setup and should be mapped with the `-p` option instead

- `Database:DatabaseType`: Can be one of `SqlServer`, `MariaDB`, `MySql`, `PostgresSql`, or `Sqlite`.
Expand Down
2 changes: 1 addition & 1 deletion build/Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.10.0</TgsCoreVersion>
<TgsConfigVersion>5.2.0</TgsConfigVersion>
<TgsConfigVersion>5.3.0</TgsConfigVersion>
<TgsApiVersion>10.10.0</TgsApiVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>16.0.0</TgsApiLibraryVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public IrcConnectionStringBuilder(string connectionString)
case IrcPasswordType.NickServ:
case IrcPasswordType.Sasl:
case IrcPasswordType.Server:
case IrcPasswordType.Oper:
Cyberboss marked this conversation as resolved.
Show resolved Hide resolved
PasswordType = passwordType;
break;
default:
Expand Down
5 changes: 5 additions & 0 deletions src/Tgstation.Server.Api/Models/IrcPasswordType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@ public enum IrcPasswordType
/// Use NickServ authentication.
/// </summary>
NickServ,

/// <summary>
/// Use OPER authentication.
/// </summary>
Oper,
}
}
223 changes: 142 additions & 81 deletions src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
Expand Down Expand Up @@ -42,11 +43,6 @@ sealed class IrcProvider : Provider
/// <inheritdoc />
public override string BotMention => client.Nickname;

/// <summary>
/// The <see cref="IrcFeatures"/> client.
/// </summary>
readonly IrcFeatures client;

/// <summary>
/// Address of the server to connect to.
/// </summary>
Expand All @@ -57,6 +53,11 @@ sealed class IrcProvider : Provider
/// </summary>
readonly ushort port;

/// <summary>
/// Wether or not this IRC client is to use ssl.
/// </summary>
readonly bool ssl;

/// <summary>
/// IRC nickname.
/// </summary>
Expand All @@ -82,6 +83,21 @@ sealed class IrcProvider : Provider
/// </summary>
readonly Dictionary<ulong, string> queryChannelIdMap;

/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> obtained from constructor, used for the CTCP version string.
/// </summary>
readonly IAssemblyInformationProvider assemblyInfo;

/// <summary>
/// The <see cref="FileLoggingConfiguration"/> for the <see cref="IrcProvider"/>.
/// </summary>
readonly FileLoggingConfiguration loggingConfiguration;

/// <summary>
/// The <see cref="IrcFeatures"/> client.
/// </summary>
IrcFeatures client;

/// <summary>
/// The <see cref="ValueTask"/> used for <see cref="IrcConnection.Listen(bool)"/>.
/// </summary>
Expand All @@ -92,11 +108,6 @@ sealed class IrcProvider : Provider
/// </summary>
ulong channelIdCounter;

/// <summary>
/// If we are disconnecting.
/// </summary>
bool disconnecting;

/// <summary>
/// Initializes a new instance of the <see cref="IrcProvider"/> class.
/// </summary>
Expand All @@ -105,12 +116,14 @@ sealed class IrcProvider : Provider
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="Provider"/>.</param>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to get the <see cref="IAssemblyInformationProvider.VersionString"/> from.</param>
/// <param name="chatBot">The <see cref="Models.ChatBot"/> for the <see cref="Provider"/>.</param>
/// <param name="loggingConfiguration">The <see cref="FileLoggingConfiguration"/> for the <see cref="Provider"/>.</param>
public IrcProvider(
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
ILogger<IrcProvider> logger,
IAssemblyInformationProvider assemblyInformationProvider,
Models.ChatBot chatBot)
Models.ChatBot chatBot,
FileLoggingConfiguration loggingConfiguration)
: base(jobManager, asyncDelayer, logger, chatBot)
{
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
Expand All @@ -121,33 +134,16 @@ public IrcProvider(

address = ircBuilder.Address!;
port = ircBuilder.Port!.Value;
ssl = ircBuilder.UseSsl!.Value;
nickname = ircBuilder.Nickname!;

password = ircBuilder.Password!;
passwordType = ircBuilder.PasswordType;

client = new IrcFeatures
{
SupportNonRfc = true,
CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
AutoRejoin = true,
AutoRejoinOnKick = true,
AutoRelogin = true,
AutoRetry = false,
AutoReconnect = false,
ActiveChannelSyncing = true,
AutoNickHandling = true,
CtcpVersion = assemblyInformationProvider.VersionString,
UseSsl = ircBuilder.UseSsl!.Value,
};
if (ircBuilder.UseSsl.Value)
client.ValidateServerCertificate = true; // dunno if it defaults to that or what

client.OnChannelMessage += Client_OnChannelMessage;
client.OnQueryMessage += Client_OnQueryMessage;
assemblyInfo = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.loggingConfiguration = loggingConfiguration ?? throw new ArgumentNullException(nameof(loggingConfiguration));

/*client.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line);
client.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line);*/
client = InstantiateClient();

channelIdMap = new Dictionary<ulong, string?>();
queryChannelIdMap = new Dictionary<ulong, string>();
Expand Down Expand Up @@ -369,83 +365,69 @@ await SendMessage(
/// <inheritdoc />
protected override async ValueTask Connect(CancellationToken cancellationToken)
{
disconnecting = false;
cancellationToken.ThrowIfCancellationRequested();
try
{
await Task.Factory.StartNew(
() => client.Connect(address, port),
() =>
{
client = InstantiateClient();
client.Connect(address, port);
},
cancellationToken,
DefaultIOManager.BlockingTaskCreationOptions,
TaskScheduler.Current)
.WaitAsync(cancellationToken);

cancellationToken.ThrowIfCancellationRequested();

listenTask = Task.Factory.StartNew(
() =>
{
Logger.LogTrace("Starting blocking listen...");
try
{
client.Listen();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "IRC Main Listen Exception!");
}

Logger.LogTrace("Exiting listening task...");
},
cancellationToken,
DefaultIOManager.BlockingTaskCreationOptions,
TaskScheduler.Current);

Logger.LogTrace("Authenticating ({passwordType})...", passwordType);
switch (passwordType)
{
case IrcPasswordType.Server:
client.Login(nickname, nickname, 0, nickname, password);
client.RfcPass(password);
await Login(client, nickname, cancellationToken);
break;
case IrcPasswordType.NickServ:
client.Login(nickname, nickname, 0, nickname);
await Login(client, nickname, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
break;
case IrcPasswordType.Sasl:
await SaslAuthenticate(cancellationToken);
break;
case IrcPasswordType.Oper:
await Login(client, nickname, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
client.RfcOper(nickname, password, Priority.Critical);
break;
case null:
client.Login(nickname, nickname, 0, nickname);
await Login(client, nickname, cancellationToken);
break;
default:
throw new InvalidOperationException($"Invalid IrcPasswordType: {passwordType.Value}");
}

cancellationToken.ThrowIfCancellationRequested();
Logger.LogTrace("Processing initial messages...");
await NonBlockingListen(cancellationToken);

var nickCheckCompleteTcs = new TaskCompletionSource();
using (cancellationToken.Register(() => nickCheckCompleteTcs.TrySetCanceled(cancellationToken)))
{
listenTask = Task.Factory.StartNew(
async () =>
{
Logger.LogTrace("Entering nick check loop");
while (!disconnecting && client.IsConnected && client.Nickname != nickname)
{
client.ListenOnce(true);
if (disconnecting || !client.IsConnected)
break;
await NonBlockingListen(cancellationToken);

// ensure we have the correct nick
if (client.GetIrcUser(nickname) == null)
client.RfcNick(nickname);
}

nickCheckCompleteTcs.TrySetResult();

Logger.LogTrace("Starting blocking listen...");
try
{
client.Listen();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "IRC Main Listen Exception!");
}

Logger.LogTrace("Exiting listening task...");
},
cancellationToken,
DefaultIOManager.BlockingTaskCreationOptions,
TaskScheduler.Current);

await nickCheckCompleteTcs.Task;
}

Logger.LogTrace("Connection established!");
}
Expand Down Expand Up @@ -487,6 +469,44 @@ await Task.Factory.StartNew(
}
}

/// <summary>
/// Register the client on the network.
/// </summary>
/// <param name="client">IRC client.</param>
/// <param name="nickname">Nickname.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns><see cref="Task"/> that resolves when registration has been completed. </returns>
/// <exception cref="TimeoutException">If the IRC server fails to respond.</exception>
async ValueTask Login(IrcFeatures client, string nickname, CancellationToken cancellationToken)
{
var promise = new TaskCompletionSource<object>();

void Callback(object? sender, EventArgs e)
{
Logger.LogTrace("IRC Registered.");
promise.TrySetResult(e);
}

client.OnRegistered += Callback;

client.Login(nickname, nickname, 0, nickname);

using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(30));

try
{
await promise.Task.WaitAsync(cts.Token);
client.OnRegistered -= Callback;
}
catch (OperationCanceledException)
{
if (client.IsConnected)
client.Disconnect();
throw new JobException("Timed out waiting for IRC Registration");
}
}

/// <summary>
/// Handle an IRC message.
/// </summary>
Expand Down Expand Up @@ -667,8 +687,6 @@ async ValueTask HardDisconnect(CancellationToken cancellationToken)

Logger.LogTrace("Hard disconnect");

disconnecting = true;

// This call blocks permanently randomly sometimes
// Frankly I don't give a shit
var disconnectTask = Task.Factory.StartNew(
Expand All @@ -693,5 +711,48 @@ await Task.WhenAny(
listenTask ?? Task.CompletedTask),
AsyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken));
}

/// <summary>
/// Creates a new instance of the IRC client.
/// Reusing the same client after a disconnection seems to cause issues.
/// </summary>
/// <returns>The <see cref="IrcFeatures"/> client to use.</returns>
IrcFeatures InstantiateClient()
{
var newClient = new IrcFeatures
{
SupportNonRfc = true,
CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
AutoRejoin = true,
AutoRejoinOnKick = true,
AutoRelogin = false,
AutoRetry = false,
AutoReconnect = false,
ActiveChannelSyncing = true,
AutoNickHandling = true,
CtcpVersion = assemblyInfo.VersionString,
UseSsl = ssl,
EnableUTF8Recode = true,
};
if (ssl)
newClient.ValidateServerCertificate = true; // dunno if it defaults to that or what

newClient.OnChannelMessage += Client_OnChannelMessage;
newClient.OnQueryMessage += Client_OnQueryMessage;

if (loggingConfiguration.ProviderNetworkDebug)
{
newClient.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line);
newClient.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line);
}

newClient.OnError += (sender, e) =>
{
Logger.LogError("IRC ERROR: {error}", e.ErrorMessage);
newClient.Disconnect();
};

return newClient;
}
}
}
Loading
Loading