Compare commits

..

4 Commits

Author SHA1 Message Date
Cameron
cceae30d5e attempted fix to RateLimiter, not fully tested yet 2024-03-21 09:15:30 -05:00
Cameron
917e90558d tiny cleanup 2024-03-21 09:14:50 -05:00
Cameron
29b5b111b2 Added a demo that connects to a channel to print user changes and bit cheers. 2024-03-21 08:49:43 -05:00
Ikatono
c2ad6b9a8e Create README.md 2024-03-21 08:33:04 -05:00
5 changed files with 96 additions and 36 deletions

1
README.md Normal file
View File

@@ -0,0 +1 @@
Provides a light-weight client for Twitch chatrooms over IRC. Primarily focused on receiving messages rather than sending them, TwitchIrcClient automatically requests all messages and tags from Twitch and parses these into easy to use classes. Additionally, it provides an event-like interface to receive messages of a specific type, and quality of life features like user tracking and an event for batches of user updates. Future plans include better handling of outgoing messages, providing interfaces to more tags for features like Hype Chats, and a better way to read chat messages with emotes substituted.

View File

@@ -15,16 +15,6 @@ namespace TwitchIrcClient.IRC.Messages
/// </summary> /// </summary>
public NoticeId? MessageId => Enum.TryParse(TryGetTag("msg-id"), out NoticeId value) public NoticeId? MessageId => Enum.TryParse(TryGetTag("msg-id"), out NoticeId value)
? value : null; ? value : null;
//{ get
// {
// string spaced = TryGetTag("msg-id").Replace('_', ' ');
// string title = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(spaced);
// string pascal = title.Replace(" ", "");
// if (!Enum.TryParse(pascal, out NoticeId value))
// return null;
// return value;
// }
//}
public string TargetUserId => TryGetTag("target-user-id"); public string TargetUserId => TryGetTag("target-user-id");
public Notice(ReceivedMessage message) : base(message) public Notice(ReceivedMessage message) : base(message)

View File

@@ -82,12 +82,13 @@ namespace TwitchIrcClient.IRC.Messages
//is stripped //is stripped
if (s.StartsWith(':')) if (s.StartsWith(':'))
{ {
message.Parameters.Add(s.Substring(1)); message.Parameters.Add(s[1..]);
} }
else else
{ {
var spl_final = s.Split(" :", 2); var spl_final = s.Split(" :", 2);
var spl_initial = spl_final[0].Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var spl_initial = spl_final[0].Split(' ', StringSplitOptions.RemoveEmptyEntries
| StringSplitOptions.TrimEntries);
message.Parameters.AddRange(spl_initial); message.Parameters.AddRange(spl_initial);
if (spl_final.Length >= 2) if (spl_final.Length >= 2)
message.Parameters.Add(spl_final[1]); message.Parameters.Add(spl_final[1]);

View File

@@ -29,29 +29,36 @@ namespace TwitchIrcClient.IRC
Timer.Start(); Timer.Start();
} }
public void WaitForAvailable(CancellationToken? token = null) public bool WaitForAvailable(CancellationToken? token = null)
{ {
try try
{
lock (Semaphore)
{ {
if (token is CancellationToken actualToken) if (token is CancellationToken actualToken)
Semaphore.Wait(actualToken); Semaphore.Wait(actualToken);
else else
Semaphore.Wait(); Semaphore.Wait();
return true;
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
//caller is responsible for checking whether connection is cancelled before trying to send return false;
} }
} }
public bool WaitForAvailable(TimeSpan timeout, CancellationToken? token = null) public bool WaitForAvailable(TimeSpan timeout, CancellationToken? token = null)
{ {
try try
{
lock (Semaphore)
{ {
if (token is CancellationToken actualToken) if (token is CancellationToken actualToken)
return Semaphore.Wait(timeout, actualToken); return Semaphore.Wait(timeout, actualToken);
else else
return Semaphore.Wait(timeout); return Semaphore.Wait(timeout);
} }
}
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
return false; return false;
@@ -60,12 +67,15 @@ namespace TwitchIrcClient.IRC
public bool WaitForAvailable(int millis, CancellationToken? token = null) public bool WaitForAvailable(int millis, CancellationToken? token = null)
{ {
try try
{
lock (Semaphore)
{ {
if (token is CancellationToken actualToken) if (token is CancellationToken actualToken)
return Semaphore.Wait(millis, actualToken); return Semaphore.Wait(millis, actualToken);
else else
return Semaphore.Wait(millis); return Semaphore.Wait(millis);
} }
}
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
return false; return false;
@@ -76,16 +86,13 @@ namespace TwitchIrcClient.IRC
{ {
try try
{ {
Semaphore.Release(MessageLimit); lock (Semaphore)
}
catch (SemaphoreFullException)
{ {
Semaphore.Release(MessageLimit - Semaphore.CurrentCount);
} }
catch (ObjectDisposedException)
{
} }
catch (SemaphoreFullException) { }
catch (ObjectDisposedException) { }
} }
#region RateLimiter Dispose #region RateLimiter Dispose

View File

@@ -1 +1,62 @@
 using System.Collections.Concurrent;
using System.Reflection.Metadata;
using System.Security.AccessControl;
using System.Threading.Channels;
using TwitchIrcClient.IRC;
using TwitchIrcClient.IRC.Messages;
RateLimiter limiter = new(20, 30);
bool ssl = true;
async Task<IrcConnection> CreateConnection(string channel)
{
IrcConnection connection;
if (ssl)
connection = new IrcConnection("irc.chat.twitch.tv", 6697, limiter, true, true);
else
connection = new IrcConnection("irc.chat.twitch.tv", 6667, limiter, true, false);
connection.AddCallback(new MessageCallbackItem(
(o, m) =>
{
if (m is Privmsg priv)
{
if (priv.Bits > 0)
lock (Console.Out)
Console.WriteLine($"{priv.DisplayName}: {priv.Bits}{Environment.NewLine}");
}
else
throw new ArgumentException("Received an unrequested message type", nameof(m));
}, [IrcMessageType.PRIVMSG]));
connection.onUserChange += (object? o, UserChangeEventArgs args) =>
{
lock (Console.Out)
{
var resetColor = Console.BackgroundColor;
Console.BackgroundColor = ConsoleColor.DarkGreen;
Console.WriteLine(string.Join(", ", args.Joined.Order()));
Console.BackgroundColor = ConsoleColor.DarkRed;
Console.WriteLine(string.Join(", ", args.Left.Order()));
Console.BackgroundColor = resetColor;
Console.WriteLine();
}
};
if (!await connection.ConnectAsync())
{
Console.WriteLine("failed to connect");
Environment.Exit(-1);
}
connection.Authenticate(null, null);
connection.SendLine("CAP REQ :twitch.tv/commands twitch.tv/membership twitch.tv/tags");
connection.JoinChannel(channel);
return connection;
}
Console.Write("Channel: ");
var channelName = Console.ReadLine();
ArgumentNullException.ThrowIfNull(channelName, nameof(Channel));
var connection = CreateConnection(channelName);
while (true)
{
//all the work happens in other threads
//specifically the threadpool used by Task.Run for
//the tasks owned by the IrcConnection
await Task.Delay(1000);
}