mirror of
https://github.com/Ikatono/TwitchIrcClient.git
synced 2025-10-29 04:56:12 -05:00
Compare commits
2 Commits
9a69404c66
...
9dc86478a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dc86478a8 | ||
|
|
8302b2639b |
@@ -3,7 +3,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.9.34607.119
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchIrcClient", "TwitchIrcClient\TwitchIrcClient.csproj", "{465639B4-4511-473A-ADC8-23B994E3C21C}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchIrcClient", "TwitchIrcClient\TwitchIrcClient.csproj", "{465639B4-4511-473A-ADC8-23B994E3C21C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchIrcClientTests", "TwitchIrcClientTests\TwitchIrcClientTests.csproj", "{D1047D1F-2B92-40B3-90FE-D16E4D631333}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{465639B4-4511-473A-ADC8-23B994E3C21C} = {465639B4-4511-473A-ADC8-23B994E3C21C}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -15,6 +20,10 @@ Global
|
||||
{465639B4-4511-473A-ADC8-23B994E3C21C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{465639B4-4511-473A-ADC8-23B994E3C21C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{465639B4-4511-473A-ADC8-23B994E3C21C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D1047D1F-2B92-40B3-90FE-D16E4D631333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D1047D1F-2B92-40B3-90FE-D16E4D631333}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D1047D1F-2B92-40B3-90FE-D16E4D631333}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D1047D1F-2B92-40B3-90FE-D16E4D631333}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC
|
||||
namespace TwitchIrcClient.IRC
|
||||
{
|
||||
public record struct Badge(string Name, string Version)
|
||||
{
|
||||
|
||||
@@ -3,9 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TwitchLogger.IRC.Messages;
|
||||
using TwitchIrcClient.IRC.Messages;
|
||||
|
||||
namespace TwitchLogger.IRC
|
||||
namespace TwitchIrcClient.IRC
|
||||
{
|
||||
//public class IrcMessageEventArgs(ReceivedMessage message) : EventArgs
|
||||
//{
|
||||
|
||||
@@ -13,9 +13,9 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using TwitchIrcClient.IRC.Messages;
|
||||
using TwitchLogger.IRC.Messages;
|
||||
using TwitchIrcClient.IRC.Messages;
|
||||
|
||||
namespace TwitchLogger.IRC
|
||||
namespace TwitchIrcClient.IRC
|
||||
{
|
||||
/// <summary>
|
||||
/// Connects to a single Twitch chat channel via limited IRC implementation.
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC
|
||||
namespace TwitchIrcClient.IRC
|
||||
{
|
||||
public enum IrcMessageType
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC
|
||||
namespace TwitchIrcClient.IRC
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds key-value pairs of tags. Tag names are case-sensitive and DO NOT parse
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
public class ClearChat : ReceivedMessage
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that a message was deleted.
|
||||
@@ -34,7 +34,7 @@ namespace TwitchLogger.IRC.Messages
|
||||
string s = TryGetTag("tmi-sent-ts");
|
||||
if (!double.TryParse(s, out double d))
|
||||
return null;
|
||||
return DateTime.UnixEpoch.AddSeconds(d);
|
||||
return DateTime.UnixEpoch.AddSeconds(d / 1000);
|
||||
}
|
||||
}
|
||||
public ClearMsg(ReceivedMessage message) : base(message)
|
||||
|
||||
@@ -5,11 +5,12 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
public class Join : ReceivedMessage
|
||||
{
|
||||
public string Username => Prefix?.Split('!', 2).First() ?? "";
|
||||
public string ChannelName => Parameters.Single().TrimStart('#');
|
||||
public Join(ReceivedMessage message) : base(message)
|
||||
{
|
||||
Debug.Assert(MessageType == IrcMessageType.JOIN,
|
||||
|
||||
@@ -5,14 +5,14 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
public class NamReply : ReceivedMessage
|
||||
{
|
||||
public IEnumerable<string> Users =>
|
||||
Parameters.Last().Split(' ', StringSplitOptions.TrimEntries
|
||||
| StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
public string ChannelName => Parameters.TakeLast(2).First().TrimStart('#');
|
||||
public NamReply(ReceivedMessage message) : base(message)
|
||||
{
|
||||
Debug.Assert(MessageType == IrcMessageType.RPL_NAMREPLY,
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
public class Notice : ReceivedMessage
|
||||
{
|
||||
|
||||
@@ -5,11 +5,12 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
public class Part : ReceivedMessage
|
||||
{
|
||||
public string Username => Prefix?.Split('!', 2).First() ?? "";
|
||||
public string ChannelName => Parameters.Single().TrimStart('#');
|
||||
public Part(ReceivedMessage message) : base(message)
|
||||
{
|
||||
Debug.Assert(MessageType == IrcMessageType.PART,
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
@@ -109,6 +109,15 @@ namespace TwitchLogger.IRC.Messages
|
||||
return value == "1";
|
||||
}
|
||||
}
|
||||
public DateTime Timestamp
|
||||
{ get
|
||||
{
|
||||
var s = TryGetTag("tmi-sent-ts");
|
||||
if (!double.TryParse(s, out double result))
|
||||
throw new InvalidDataException();
|
||||
return DateTime.UnixEpoch.AddSeconds(result / 1000);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// A Boolean value that indicates whether the user has site-wide commercial
|
||||
/// free mode enabled
|
||||
@@ -157,6 +166,7 @@ namespace TwitchLogger.IRC.Messages
|
||||
/// A Boolean value that determines whether the user that sent the chat is a VIP.
|
||||
/// </summary>
|
||||
public bool Vip => MessageTags.ContainsKey("vip");
|
||||
public bool FirstMessage => TryGetTag("first-msg") == "1";
|
||||
public string ChatMessage => Parameters.Last();
|
||||
public Privmsg(ReceivedMessage message) : base(message)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Reflection.Metadata.Ecma335;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
@@ -109,6 +109,8 @@ namespace TwitchLogger.IRC.Messages
|
||||
return new Part(message);
|
||||
case IrcMessageType.RPL_NAMREPLY:
|
||||
return new NamReply(message);
|
||||
case IrcMessageType.ROOMSTATE:
|
||||
return new Roomstate(message);
|
||||
default:
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TwitchLogger.IRC;
|
||||
using TwitchLogger.IRC.Messages;
|
||||
using TwitchIrcClient.IRC;
|
||||
using TwitchIrcClient.IRC.Messages;
|
||||
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
@@ -86,6 +86,10 @@ namespace TwitchIrcClient.IRC.Messages
|
||||
throw new InvalidDataException($"tag \"subs-only\" does not have a proper value: {value}");
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string ChannelName => Parameters.Last().TrimStart('#');
|
||||
public Roomstate(ReceivedMessage other) : base(other)
|
||||
{
|
||||
Debug.Assert(MessageType == IrcMessageType.ROOMSTATE,
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC.Messages
|
||||
namespace TwitchIrcClient.IRC.Messages
|
||||
{
|
||||
public class UserNotice : ReceivedMessage
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC
|
||||
namespace TwitchIrcClient.IRC
|
||||
{
|
||||
/// <summary>
|
||||
/// Prevents sending too many messages in a time period. A single rate limiter can
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TwitchLogger.IRC
|
||||
namespace TwitchIrcClient.IRC
|
||||
{
|
||||
public enum UserType
|
||||
{
|
||||
|
||||
146
TwitchIrcClientTests/ParserTest.cs
Normal file
146
TwitchIrcClientTests/ParserTest.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System.Drawing;
|
||||
using System;
|
||||
using TwitchIrcClient.IRC;
|
||||
using TwitchIrcClient.IRC.Messages;
|
||||
|
||||
namespace TwitchIrcClientTests
|
||||
{
|
||||
[TestClass]
|
||||
public class ParserTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestSimpleMessages()
|
||||
{
|
||||
var ROOMSTATE = "@emote-only=0;followers-only=-1;r9k=0;room-id=321654987;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #channelname";
|
||||
var NAMREPLY = ":justinfan7550.tmi.twitch.tv 353 justinfan7550 = #channelname :user1 user2 user3 user4 user5";
|
||||
var JOIN = ":newuser!newuser@newuser.tmi.twitch.tv JOIN #channelname";
|
||||
var PART = ":leavinguser!leavinguser@leavinguser.tmi.twitch.tv PART #channelname";
|
||||
var PRIVMSG = "@badge-info=subscriber/1;badges=subscriber/0;client-nonce=202e32113a3768963eded865e051fc5b;color=#AAAAFF;" +
|
||||
"display-name=ChattingUser;emotes=;first-msg=0;flags=;id=24fe75a1-06a5-4078-a31f-cf615107b2a2;mod=0;returning-chatter=0;" +
|
||||
"room-id=321654987;subscriber=1;tmi-sent-ts=1710920497332;turbo=0;user-id=01234567;user-type= " +
|
||||
":chattinguser!chattinguser@chattinguser.tmi.twitch.tv PRIVMSG #channelname :This is a test chat message";
|
||||
var CHEER = "@badge-info=subscriber/9;badges=subscriber/9,twitch-recap-2023/1;bits=100;color=#FF0000;display-name=CheeringUser;" +
|
||||
//I haven't fixed this emote tag after rewriting the message
|
||||
"emotes=emotesv2_44a39d65e08f43adac871a80e9b96d85:17-24;first-msg=1;flags=;id=5eab1319-5d46-4c55-be29-33c2f834e42e;mod=0;" +
|
||||
"returning-chatter=0;room-id=321654987;subscriber=0;tmi-sent-ts=1710920826069;turbo=1;user-id=012345678;user-type=;vip " +
|
||||
":cheeringuser!cheeringuser@cheeringuser.tmi.twitch.tv PRIVMSG #channelname :This includes a cheer Cheer100";
|
||||
//var CLEARMSG = "";
|
||||
//var CLEARROOM = "";
|
||||
|
||||
var _roomstate = ReceivedMessage.Parse(ROOMSTATE);
|
||||
Assert.AreEqual(IrcMessageType.ROOMSTATE, _roomstate.MessageType);
|
||||
if (_roomstate is Roomstate roomstate)
|
||||
{
|
||||
Assert.AreEqual("channelname", roomstate.ChannelName);
|
||||
Assert.IsTrue(roomstate.MessageTags.TryGetValue("emote-only", out string emoteOnly));
|
||||
Assert.AreEqual("0", emoteOnly);
|
||||
Assert.IsFalse(roomstate.EmoteOnly);
|
||||
Assert.IsTrue(roomstate.MessageTags.TryGetValue("followers-only", out string followersOnly));
|
||||
Assert.AreEqual("-1", followersOnly);
|
||||
Assert.AreEqual(-1, roomstate.FollowersOnly);
|
||||
Assert.IsTrue(roomstate.MessageTags.TryGetValue("r9k", out string r9k));
|
||||
Assert.AreEqual("0", r9k);
|
||||
Assert.IsFalse(roomstate.UniqueMode);
|
||||
Assert.IsTrue(roomstate.MessageTags.TryGetValue("room-id", out string roomId));
|
||||
Assert.AreEqual("321654987", roomId);
|
||||
Assert.AreEqual("321654987", roomstate.RoomId);
|
||||
Assert.IsTrue(roomstate.MessageTags.TryGetValue("slow", out string slow));
|
||||
Assert.AreEqual("0", slow);
|
||||
Assert.AreEqual(0, roomstate.Slow);
|
||||
Assert.IsTrue(roomstate.MessageTags.TryGetValue("subs-only", out string subsOnly));
|
||||
Assert.AreEqual("0", subsOnly);
|
||||
Assert.AreEqual(false, roomstate.SubsOnly);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
var _namReply = ReceivedMessage.Parse(NAMREPLY);
|
||||
Assert.AreEqual(IrcMessageType.RPL_NAMREPLY, _namReply.MessageType);
|
||||
if (_namReply is NamReply namReply)
|
||||
{
|
||||
Assert.AreEqual("channelname", namReply.ChannelName);
|
||||
Assert.IsTrue("user1 user2 user3 user4 user5".Split().Order()
|
||||
.SequenceEqual(namReply.Users.Order()));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
var _join = ReceivedMessage.Parse(JOIN);
|
||||
Assert.AreEqual(IrcMessageType.JOIN, _join.MessageType);
|
||||
if (_join is Join join)
|
||||
{
|
||||
Assert.AreEqual("channelname", join.ChannelName);
|
||||
Assert.AreEqual("newuser", join.Username);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
var _part = ReceivedMessage.Parse(PART);
|
||||
Assert.AreEqual(IrcMessageType.PART, _part.MessageType);
|
||||
if (_part is Part part)
|
||||
{
|
||||
Assert.AreEqual("channelname", part.ChannelName);
|
||||
Assert.AreEqual("leavinguser", part.Username);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
var _priv = ReceivedMessage.Parse(PRIVMSG);
|
||||
Assert.AreEqual(IrcMessageType.PRIVMSG, _priv.MessageType);
|
||||
if (_priv is Privmsg priv)
|
||||
{
|
||||
Assert.AreEqual("This is a test chat message", priv.ChatMessage);
|
||||
Assert.AreEqual(0, priv.Bits);
|
||||
Assert.AreEqual("ChattingUser", priv.DisplayName);
|
||||
Assert.AreEqual(Color.FromArgb(170, 170, 255), priv.Color);
|
||||
Assert.AreEqual("24fe75a1-06a5-4078-a31f-cf615107b2a2", priv.Id);
|
||||
Assert.IsFalse(priv.FirstMessage);
|
||||
Assert.IsFalse(priv.Moderator);
|
||||
Assert.AreEqual("321654987", priv.RoomId);
|
||||
Assert.IsTrue(priv.Subscriber);
|
||||
Assert.IsFalse(priv.Turbo);
|
||||
Assert.AreEqual("01234567", priv.UserId);
|
||||
Assert.AreEqual(UserType.Normal, priv.UserType);
|
||||
Assert.IsFalse(priv.Vip);
|
||||
Assert.AreEqual(new DateTime(2024, 3, 20, 7, 41, 37, 332, DateTimeKind.Utc), priv.Timestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
var _cheer = ReceivedMessage.Parse(CHEER);
|
||||
Assert.AreEqual(IrcMessageType.PRIVMSG, _cheer.MessageType);
|
||||
if (_cheer is Privmsg cheer)
|
||||
{
|
||||
Assert.AreEqual("This includes a cheer Cheer100", cheer.ChatMessage);
|
||||
Assert.AreEqual(100, cheer.Bits);
|
||||
Assert.AreEqual("CheeringUser", cheer.DisplayName);
|
||||
Assert.AreEqual(Color.FromArgb(255, 0, 0), cheer.Color);
|
||||
Assert.AreEqual("5eab1319-5d46-4c55-be29-33c2f834e42e", cheer.Id);
|
||||
Assert.IsTrue(cheer.FirstMessage);
|
||||
Assert.IsFalse(cheer.Moderator);
|
||||
Assert.AreEqual("321654987", cheer.RoomId);
|
||||
Assert.IsFalse(cheer.Subscriber);
|
||||
Assert.IsTrue(cheer.Turbo);
|
||||
Assert.AreEqual("012345678", cheer.UserId);
|
||||
Assert.AreEqual(UserType.Normal, cheer.UserType);
|
||||
Assert.IsTrue(cheer.Vip);
|
||||
//test that timestamp is within 1 second
|
||||
Assert.AreEqual(new DateTime(2024, 3, 20, 7, 47, 6, 069, DateTimeKind.Utc), cheer.Timestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
TwitchIrcClientTests/TwitchIrcClientTests.csproj
Normal file
27
TwitchIrcClientTests/TwitchIrcClientTests.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TwitchIrcClient\TwitchIrcClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user