mirror of
https://github.com/Ikatono/TwitchIrcClient.git
synced 2025-10-29 04:56:12 -05:00
Compare commits
3 Commits
331cf6cdc0
...
7e089e1705
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e089e1705 | ||
|
|
a1e5d9f533 | ||
|
|
8ee231d8e7 |
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
96
TwitchIrcClient/IRC/Messages/Roomstate.cs
Normal file
96
TwitchIrcClient/IRC/Messages/Roomstate.cs
Normal 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 user’s 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user