Compare commits

...

3 Commits

Author SHA1 Message Date
Cameron
7e089e1705 Added "origin" to MessageCallbacks, and added LastRoomstate to IrcConnection 2024-03-20 05:08:37 -05:00
Cameron
a1e5d9f533 Added roomstate message type 2024-03-20 04:26:54 -05:00
Cameron
8ee231d8e7 Add SSL support 2024-03-20 00:26:45 -05:00
3 changed files with 144 additions and 23 deletions

View File

@@ -11,7 +11,7 @@ namespace TwitchLogger.IRC
//{ //{
// public ReceivedMessage Message = message; // public ReceivedMessage Message = message;
//} //}
public delegate void MessageCallback(ReceivedMessage message); public delegate void MessageCallback(IrcConnection origin, ReceivedMessage message);
/// <summary> /// <summary>
/// Callback to be run for received messages of specific types. /// Callback to be run for received messages of specific types.
/// </summary> /// </summary>
@@ -21,11 +21,11 @@ namespace TwitchLogger.IRC
MessageCallback Callback, MessageCallback Callback,
IReadOnlyList<IrcMessageType>? CallbackTypes) IReadOnlyList<IrcMessageType>? CallbackTypes)
{ {
public bool TryCall(ReceivedMessage message) public bool TryCall(IrcConnection origin, ReceivedMessage message)
{ {
if (CallbackTypes?.Contains(message.MessageType) ?? true) if (CallbackTypes?.Contains(message.MessageType) ?? true)
{ {
Callback(message); Callback(origin, message);
return true; return true;
} }
return false; return false;

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Net.Security;
using System.Net.Sockets; using System.Net.Sockets;
using System.Reflection.Metadata; using System.Reflection.Metadata;
using System.Security.Cryptography; using System.Security.Cryptography;
@@ -11,16 +12,14 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
using TwitchIrcClient.IRC.Messages;
using TwitchLogger.IRC.Messages; using TwitchLogger.IRC.Messages;
namespace TwitchLogger.IRC namespace TwitchLogger.IRC
{ {
/// <summary> /// <summary>
/// Connects to a single Twitch chat channel via limited IRC implementation. /// Connects to a single Twitch chat channel via limited IRC implementation.
///
/// </summary> /// </summary>
/// <param name="url"></param>
/// <param name="port"></param>
public class IrcConnection : IDisposable public class IrcConnection : IDisposable
{ {
public static readonly string ENDL = "\r\n"; public static readonly string ENDL = "\r\n";
@@ -28,6 +27,16 @@ namespace TwitchLogger.IRC
public string Url { get; } public string Url { get; }
public bool Connected { get; } = false; public bool Connected { get; } = false;
public bool TrackUsers { get; } public bool TrackUsers { get; }
public bool UsesSsl { get; }
private Roomstate? _LastRoomstate;
public Roomstate? LastRoomstate
{ get
{
if (_LastRoomstate == null)
return null;
return new Roomstate(_LastRoomstate);
}
}
//this seems to be the only concurrentcollection that allows //this seems to be the only concurrentcollection that allows
//removing specific items //removing specific items
protected ConcurrentDictionary<string, byte> UserCollection = new(); protected ConcurrentDictionary<string, byte> UserCollection = new();
@@ -42,7 +51,8 @@ namespace TwitchLogger.IRC
public event EventHandler<UserChangeEventArgs>? onUserChange; public event EventHandler<UserChangeEventArgs>? onUserChange;
private TcpClient Client = new(); private TcpClient Client = new();
private NetworkStream Stream => Client.GetStream(); //private NetworkStream Stream => Client.GetStream();
private Stream _Stream;
private CancellationTokenSource TokenSource = new(); private CancellationTokenSource TokenSource = new();
//it looks like you can't get the Token after the Source is disposed //it looks like you can't get the Token after the Source is disposed
protected CancellationToken Token; protected CancellationToken Token;
@@ -51,44 +61,48 @@ namespace TwitchLogger.IRC
private Task? UserUpdateTask; private Task? UserUpdateTask;
public IrcConnection(string url, int port, public IrcConnection(string url, int port,
RateLimiter? limiter = null, bool trackUsers = false) RateLimiter? limiter = null, bool trackUsers = false, bool useSsl = false)
{ {
Url = url; Url = url;
Port = port; Port = port;
Limiter = limiter; Limiter = limiter;
TrackUsers = trackUsers; TrackUsers = trackUsers;
UsesSsl = useSsl;
Token = TokenSource.Token; Token = TokenSource.Token;
if (TrackUsers) if (TrackUsers)
{ {
AddSystemCallback(new MessageCallbackItem(m => AddSystemCallback(new MessageCallbackItem((o, m) =>
{ {
if (m is NamReply nr) if (m is NamReply nr)
foreach (var u in nr.Users) foreach (var u in nr.Users)
UserCollection.TryAdd(u, 0); o.UserCollection.TryAdd(u, 0);
else else
throw new ArgumentException(null, nameof(m)); throw new ArgumentException(null, nameof(m));
}, [IrcMessageType.RPL_NAMREPLY])); }, [IrcMessageType.RPL_NAMREPLY]));
AddSystemCallback(new MessageCallbackItem(m => AddSystemCallback(new MessageCallbackItem((o, m) =>
{ {
if (m is Join j) if (m is Join j)
{ {
UserCollection.TryAdd(j.Username, 0); o.UserCollection.TryAdd(j.Username, 0);
UserJoin(j); o.UserJoin(j);
} }
else else
throw new ArgumentException(null, nameof(m)); throw new ArgumentException(null, nameof(m));
}, [IrcMessageType.JOIN])); }, [IrcMessageType.JOIN]));
AddSystemCallback(new MessageCallbackItem(m => AddSystemCallback(new MessageCallbackItem((o, m) =>
{ {
if (m is Part j) if (m is Part j)
{ {
UserCollection.TryRemove(j.Username, out _); o.UserCollection.TryRemove(j.Username, out _);
UserLeave(j); o.UserLeave(j);
} }
else else
throw new ArgumentException(null, nameof(m)); throw new ArgumentException(null, nameof(m));
}, [IrcMessageType.PART])); }, [IrcMessageType.PART]));
} }
AddSystemCallback(new MessageCallbackItem(
(o, m) => { o._LastRoomstate = new Roomstate(m); },
[IrcMessageType.ROOMSTATE]));
} }
public async Task<bool> ConnectAsync() public async Task<bool> ConnectAsync()
@@ -99,6 +113,16 @@ namespace TwitchLogger.IRC
await Client.ConnectAsync(Url, Port); await Client.ConnectAsync(Url, Port);
if (!Client.Connected) if (!Client.Connected)
return false; return false;
if (UsesSsl)
{
var stream = new SslStream(Client.GetStream());
await stream.AuthenticateAsClientAsync(Url);
_Stream = stream;
}
else
{
_Stream = Client.GetStream();
}
ListenerTask = Task.Run(ListenForInput, Token); ListenerTask = Task.Run(ListenForInput, Token);
UserUpdateTask = Task.Run(UpdateUsers, Token); UserUpdateTask = Task.Run(UpdateUsers, Token);
return true; return true;
@@ -112,7 +136,8 @@ namespace TwitchLogger.IRC
Limiter?.WaitForAvailable(Token); Limiter?.WaitForAvailable(Token);
if (Token.IsCancellationRequested) if (Token.IsCancellationRequested)
return; return;
Stream.Write(new Span<byte>(Encoding.UTF8.GetBytes(line + ENDL))); var bytes = Encoding.UTF8.GetBytes(line + ENDL);
_Stream.Write(bytes, 0, bytes.Length);
} }
public void Authenticate(string? user, string? pass) public void Authenticate(string? user, string? pass)
{ {
@@ -134,17 +159,17 @@ namespace TwitchLogger.IRC
byte[] buffer = new byte[5 * 1024]; byte[] buffer = new byte[5 * 1024];
while (!Token.IsCancellationRequested) while (!Token.IsCancellationRequested)
{ {
var bytesRead = await Stream.ReadAsync(buffer, 0, buffer.Length, Token); var bytesRead = await _Stream.ReadAsync(buffer, Token);
if (bytesRead > 0) if (bytesRead > 0)
onDataReceived(buffer, bytesRead); onDataReceived(buffer, bytesRead);
if (!Stream.CanRead) if (!_Stream.CanRead)
return; return;
} }
Token.ThrowIfCancellationRequested(); Token.ThrowIfCancellationRequested();
} }
ConcurrentBag<string> _JoinedUsers = []; private readonly ConcurrentBag<string> _JoinedUsers = [];
ConcurrentBag<string> _LeftUsers = []; private readonly ConcurrentBag<string> _LeftUsers = [];
private void UserJoin(Join message) private void UserJoin(Join message)
{ {
_JoinedUsers.Add(message.Username); _JoinedUsers.Add(message.Username);
@@ -265,8 +290,8 @@ namespace TwitchLogger.IRC
ArgumentNullException.ThrowIfNull(message, nameof(message)); ArgumentNullException.ThrowIfNull(message, nameof(message));
if (disposedValue) if (disposedValue)
return; return;
SystemCallbacks.ForEach(c => c.TryCall(message)); SystemCallbacks.ForEach(c => c.TryCall(this, message));
UserCallbacks.ForEach(c => c.TryCall(message)); UserCallbacks.ForEach(c => c.TryCall(this, message));
} }
#region Dispose #region Dispose

View File

@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using TwitchLogger.IRC;
using TwitchLogger.IRC.Messages;
namespace TwitchIrcClient.IRC.Messages
{
public class Roomstate : ReceivedMessage
{
/// <summary>
/// A Boolean value that determines whether the chat room allows only messages with emotes.
/// </summary>
public bool EmoteOnly
{ get
{
var value = TryGetTag("emote-only");
if (value == "1")
return true;
if (value == "0")
return false;
throw new InvalidDataException($"tag \"emote-only\" does not have a proper value: {value}");
}
}
/// <summary>
/// An integer value that determines whether only followers can post messages in the chat room.
/// The value indicates how long, in minutes, the user must have followed the broadcaster before
/// posting chat messages. If the value is -1, the chat room is not restricted to followers only.
/// </summary>
public int FollowersOnly
{ get
{
var value = TryGetTag("followers-only");
if (!int.TryParse(value, out int result))
throw new InvalidDataException();
return result;
}
}
/// <summary>
/// A Boolean value that determines whether a users messages must be unique.
/// Applies only to messages with more than 9 characters.
/// </summary>
public bool UniqueMode
{ get
{
var value = TryGetTag("r9k");
if (value == "1")
return true;
if (value == "0")
return false;
throw new InvalidDataException($"tag \"r9k\" does not have a proper value: {value}");
}
}
/// <summary>
/// An ID that identifies the chat room (channel).
/// </summary>
public string RoomId => TryGetTag("room-id");
/// <summary>
/// An integer value that determines how long, in seconds, users must wait between sending messages.
/// </summary>
public int Slow
{ get
{
string value = TryGetTag("slow");
if (!int.TryParse(value, out int result))
throw new InvalidDataException($"tag \"slow\" does not have a proper value: {value}");
return result;
}
}
/// <summary>
/// A Boolean value that determines whether only subscribers and moderators can chat in the chat room.
/// </summary>
public bool SubsOnly
{ get
{
var value = TryGetTag("subs-only");
if (value == "1")
return true;
if (value == "0")
return false;
throw new InvalidDataException($"tag \"subs-only\" does not have a proper value: {value}");
}
}
public Roomstate(ReceivedMessage other) : base(other)
{
Debug.Assert(MessageType == IrcMessageType.ROOMSTATE,
$"{nameof(Roomstate)} must have type {IrcMessageType.ROOMSTATE}" +
$" but has {MessageType}");
}
}
}