Compare commits

...

2 Commits

Author SHA1 Message Date
Cameron
4806e50736 Added locking to Irc Callback lists 2024-03-31 19:09:12 -05:00
Cameron
1bf8afc68b Added ability to construct IRC messages from parts when sending them. 2024-03-25 05:06:51 -05:00
4 changed files with 103 additions and 32 deletions

View File

@@ -139,19 +139,68 @@ namespace TwitchIrcClient.IRC
var bytes = Encoding.UTF8.GetBytes(line + ENDL); var bytes = Encoding.UTF8.GetBytes(line + ENDL);
_Stream.Write(bytes, 0, bytes.Length); _Stream.Write(bytes, 0, bytes.Length);
} }
//TODO make this unit testable?
/// <summary>
/// Construct an IRC message from parts and sends it. Does little to no validation on inputs.
/// </summary>
/// <param name="command"></param>
/// <param name="parameters"></param>
/// <param name="tags"></param>
/// <param name="prefix"></param>
public void SendMessage(IrcMessageType command, IEnumerable<string>? parameters = null,
Dictionary<string, string?>? tags = null, string? prefix = null)
{
var message = "";
if (tags is not null && tags.Count != 0)
{
message = "@" + string.Join(';',
tags.OrderBy(p => p.Key).Select(p => $"{p.Key}={EscapeTagValue(p.Value)}"))
+ " ";
}
if (prefix is not null && !string.IsNullOrWhiteSpace(prefix))
message += ":" + prefix + " ";
message += command.ToCommand() + " ";
if (parameters is not null && parameters.Any())
{
//if ((command == IrcMessageType.NICK || command == IrcMessageType.PASS)
// && parameters.Count() == 1)
if (false)
{
message += " " + parameters.Single();
}
else
{
message += string.Join(' ', parameters.SkipLast(1));
message += " :" + parameters.Last();
}
}
SendLine(message);
}
private static string EscapeTagValue(string? s)
{
if (s is null)
return "";
return string.Join("", s.Select(c => c switch
{
';' => @"\:",
' ' => @"\s",
'\\' => @"\\",
'\r' => @"\r",
'\n' => @"\n",
char ch => ch.ToString(),
}));
}
public void Authenticate(string? user, string? pass) public void Authenticate(string? user, string? pass)
{ {
if (user == null) user ??= $"justinfan{Random.Shared.NextInt64(10000):D4}";
user = $"justinfan{Random.Shared.NextInt64(10000):D4}"; pass ??= "pass";
if (pass == null) SendMessage(IrcMessageType.PASS, parameters: [pass]);
pass = "pass"; SendMessage(IrcMessageType.NICK, parameters: [user]);
SendLine($"NICK {user}");
SendLine($"PASS {pass}");
} }
public void JoinChannel(string channel) public void JoinChannel(string channel)
{ {
channel = channel.TrimStart('#'); channel = channel.TrimStart('#');
SendLine($"JOIN #{channel}"); SendMessage(IrcMessageType.JOIN, ["#" + channel]);
} }
private async void ListenForInput() private async void ListenForInput()
{ {
@@ -268,30 +317,36 @@ namespace TwitchIrcClient.IRC
public void AddCallback(MessageCallbackItem callbackItem) public void AddCallback(MessageCallbackItem callbackItem)
{ {
ObjectDisposedException.ThrowIf(disposedValue, this); ObjectDisposedException.ThrowIf(disposedValue, this);
UserCallbacks.Add(callbackItem); lock (UserCallbacks)
UserCallbacks.Add(callbackItem);
} }
public bool RemoveCallback(MessageCallbackItem callbackItem) public bool RemoveCallback(MessageCallbackItem callbackItem)
{ {
ObjectDisposedException.ThrowIf(disposedValue, this); ObjectDisposedException.ThrowIf(disposedValue, this);
return UserCallbacks.Remove(callbackItem); lock (UserCallbacks)
return UserCallbacks.Remove(callbackItem);
} }
protected void AddSystemCallback(MessageCallbackItem callbackItem) protected void AddSystemCallback(MessageCallbackItem callbackItem)
{ {
ObjectDisposedException.ThrowIf(disposedValue, this); ObjectDisposedException.ThrowIf(disposedValue, this);
SystemCallbacks.Add(callbackItem); lock (SystemCallbacks)
SystemCallbacks.Add(callbackItem);
} }
protected bool RemoveSystemCallback(MessageCallbackItem callbackItem) protected bool RemoveSystemCallback(MessageCallbackItem callbackItem)
{ {
ObjectDisposedException.ThrowIf(disposedValue, this); ObjectDisposedException.ThrowIf(disposedValue, this);
return SystemCallbacks.Remove(callbackItem); lock (SystemCallbacks)
return SystemCallbacks.Remove(callbackItem);
} }
private void RunCallbacks(ReceivedMessage message) private void RunCallbacks(ReceivedMessage message)
{ {
ArgumentNullException.ThrowIfNull(message, nameof(message)); ArgumentNullException.ThrowIfNull(message, nameof(message));
if (disposedValue) if (disposedValue)
return; return;
SystemCallbacks.ForEach(c => c.TryCall(this, message)); lock (SystemCallbacks)
UserCallbacks.ForEach(c => c.TryCall(this, message)); SystemCallbacks.ForEach(c => c.TryCall(this, message));
lock (UserCallbacks)
UserCallbacks.ForEach(c => c.TryCall(this, message));
} }
#region Dispose #region Dispose
@@ -306,6 +361,7 @@ namespace TwitchIrcClient.IRC
TokenSource.Dispose(); TokenSource.Dispose();
Client?.Dispose(); Client?.Dispose();
_HeartbeatTimer?.Dispose(); _HeartbeatTimer?.Dispose();
_Stream?.Dispose();
} }
disposedValue = true; disposedValue = true;
} }

View File

@@ -6,6 +6,9 @@ using System.Threading.Tasks;
namespace TwitchIrcClient.IRC namespace TwitchIrcClient.IRC
{ {
/// <summary>
/// Represents the "command" of an IRC message.
/// </summary>
public enum IrcMessageType public enum IrcMessageType
{ {
//twitch standard messages //twitch standard messages
@@ -174,12 +177,26 @@ namespace TwitchIrcClient.IRC
} }
public static class IrcMessageTypeHelper public static class IrcMessageTypeHelper
{ {
//parses a string that is either a numeric code or the command name /// <summary>
/// Parses a string that is either a numeric code or the command name.
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
/// <remarks>
/// The value range 000-999 is reserved for numeric commands, and will
/// be converted to a numeric string when forming a message.
/// </remarks>
public static IrcMessageType Parse(string s) public static IrcMessageType Parse(string s)
{ {
if (int.TryParse(s, out int result)) if (int.TryParse(s, out int result))
return (IrcMessageType)result; return (IrcMessageType)result;
return Enum.Parse<IrcMessageType>(s); return Enum.Parse<IrcMessageType>(s);
} }
public static string ToCommand(this IrcMessageType type)
{
if ((int)type >= 0 && (int)type < 1000)
return $"{(int)type,3}";
return type.ToString();
}
} }
} }

View File

@@ -96,19 +96,19 @@ namespace TwitchIrcClient.IRC.Messages
} }
return message.MessageType switch return message.MessageType switch
{ {
IrcMessageType.CLEARCHAT => new ClearChat(message), IrcMessageType.CLEARCHAT => new ClearChat(message),
IrcMessageType.CLEARMSG => new ClearMsg(message), IrcMessageType.CLEARMSG => new ClearMsg(message),
IrcMessageType.JOIN => new Join(message), IrcMessageType.JOIN => new Join(message),
IrcMessageType.GLOBALUSERSTATE => new GlobalUserState(message), IrcMessageType.GLOBALUSERSTATE => new GlobalUserState(message),
IrcMessageType.HOSTTARGET => new HostTarget(message), IrcMessageType.HOSTTARGET => new HostTarget(message),
IrcMessageType.NOTICE => new Notice(message), IrcMessageType.NOTICE => new Notice(message),
IrcMessageType.PART => new Part(message), IrcMessageType.PART => new Part(message),
IrcMessageType.PRIVMSG => new Privmsg(message), IrcMessageType.PRIVMSG => new Privmsg(message),
IrcMessageType.ROOMSTATE => new Roomstate(message), IrcMessageType.ROOMSTATE => new Roomstate(message),
IrcMessageType.RPL_NAMREPLY => new NamReply(message), IrcMessageType.RPL_NAMREPLY => new NamReply(message),
IrcMessageType.USERNOTICE => new UserNotice(message), IrcMessageType.USERNOTICE => new UserNotice(message),
IrcMessageType.USERSTATE => new UserState(message), IrcMessageType.USERSTATE => new UserState(message),
IrcMessageType.WHISPER => new Whisper(message), IrcMessageType.WHISPER => new Whisper(message),
_ => message, _ => message,
}; };
} }

View File

@@ -9,11 +9,9 @@ RateLimiter limiter = new(20, 30);
bool ssl = true; bool ssl = true;
async Task<IrcConnection> CreateConnection(string channel) async Task<IrcConnection> CreateConnection(string channel)
{ {
IrcConnection connection; IrcConnection connection = ssl
if (ssl) ? connection = new IrcConnection("irc.chat.twitch.tv", 6697, limiter, true, true)
connection = new IrcConnection("irc.chat.twitch.tv", 6697, limiter, true, true); : connection = new IrcConnection("irc.chat.twitch.tv", 6667, limiter, true, false);
else
connection = new IrcConnection("irc.chat.twitch.tv", 6667, limiter, true, false);
connection.AddCallback(new MessageCallbackItem( connection.AddCallback(new MessageCallbackItem(
(o, m) => (o, m) =>
{ {
@@ -51,7 +49,7 @@ async Task<IrcConnection> CreateConnection(string channel)
} }
Console.Write("Channel: "); Console.Write("Channel: ");
var channelName = Console.ReadLine(); var channelName = Console.ReadLine();
ArgumentNullException.ThrowIfNull(channelName, nameof(Channel)); ArgumentNullException.ThrowIfNullOrWhiteSpace(channelName, nameof(channelName));
var connection = await CreateConnection(channelName); var connection = await CreateConnection(channelName);
while (true) while (true)
{ {