diff --git a/TwitchIrcClient/IRC/Messages/ClearChat.cs b/TwitchIrcClient/IRC/Messages/ClearChat.cs
index 6e9ef91..1aa22b0 100644
--- a/TwitchIrcClient/IRC/Messages/ClearChat.cs
+++ b/TwitchIrcClient/IRC/Messages/ClearChat.cs
@@ -30,13 +30,13 @@ namespace TwitchIrcClient.IRC.Messages
/// The ID of the user that was banned or put in a timeout.
///
public string TargetUserId => TryGetTag("target-user-id");
- public DateTime? TmiSentTime
+ public DateTime? Timestamp
{ get
{
string s = TryGetTag("tmi-sent-ts");
if (!double.TryParse(s, out double d))
return null;
- return DateTime.UnixEpoch.AddSeconds(d);
+ return DateTime.UnixEpoch.AddMilliseconds(d);
}
}
///
@@ -52,12 +52,12 @@ namespace TwitchIrcClient.IRC.Messages
///
/// The name of the channel that either was cleared or banned the user
///
- public string Channel => Parameters.First();
+ public string Channel => Parameters.First().TrimStart('#');
///
/// The username of the banned user, or "" if message is a
/// channel clear.
///
- public string User => Parameters.ElementAtOrDefault(2) ?? "";
+ public string User => Parameters.ElementAtOrDefault(1) ?? "";
public ClearChat(ReceivedMessage message) : base(message)
{
Debug.Assert(MessageType == IrcMessageType.CLEARCHAT,
diff --git a/TwitchIrcClient/IRC/Messages/ClearMsg.cs b/TwitchIrcClient/IRC/Messages/ClearMsg.cs
index 517c7c8..71a1124 100644
--- a/TwitchIrcClient/IRC/Messages/ClearMsg.cs
+++ b/TwitchIrcClient/IRC/Messages/ClearMsg.cs
@@ -28,7 +28,7 @@ namespace TwitchIrcClient.IRC.Messages
///
///
///
- public DateTime? TmiSentTime
+ public DateTime? Timestamp
{ get
{
string s = TryGetTag("tmi-sent-ts");
@@ -37,6 +37,8 @@ namespace TwitchIrcClient.IRC.Messages
return DateTime.UnixEpoch.AddSeconds(d / 1000);
}
}
+ public string Channel => Parameters.FirstOrDefault("").TrimStart('#');
+ public string Message => Parameters.LastOrDefault("");
public ClearMsg(ReceivedMessage message) : base(message)
{
Debug.Assert(MessageType == IrcMessageType.CLEARMSG,
diff --git a/TwitchIrcClient/IRC/Messages/Emote.cs b/TwitchIrcClient/IRC/Messages/Emote.cs
index f4aeae8..2441c3c 100644
--- a/TwitchIrcClient/IRC/Messages/Emote.cs
+++ b/TwitchIrcClient/IRC/Messages/Emote.cs
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace TwitchIrcClient.IRC.Messages
{
- public record struct Emote(string Name, int Length)
+ public record struct Emote(string Name, int Position, int Length)
{
}
diff --git a/TwitchIrcClient/IRC/Messages/GlobalUserState.cs b/TwitchIrcClient/IRC/Messages/GlobalUserState.cs
new file mode 100644
index 0000000..3cd3e0f
--- /dev/null
+++ b/TwitchIrcClient/IRC/Messages/GlobalUserState.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace TwitchIrcClient.IRC.Messages
+{
+ public class GlobalUserState : ReceivedMessage
+ {
+ ///
+ /// Contains metadata related to the chat badges in the badges tag.
+ /// Currently, this tag contains metadata only for subscriber badges,
+ /// to indicate the number of months the user has been a subscriber.
+ ///
+ public IEnumerable BadgeInfo => TryGetTag("badge-info").Split(',');
+ ///
+ /// List of chat badges. Most badges have only 1 version, but some badges like
+ /// subscriber badges offer different versions of the badge depending on how
+ /// long the user has subscribed. To get the badge, use the Get Global Chat
+ /// Badges and Get Channel Chat Badges APIs. Match the badge to the set-id field’s
+ /// value in the response. Then, match the version to the id field in the list of versions.
+ ///
+ public List Badges
+ { get
+ {
+ if (!MessageTags.TryGetValue("badges", out string? value))
+ return [];
+ if (value == null)
+ return [];
+ List badges = [];
+ foreach (var item in value.Split(',', StringSplitOptions.RemoveEmptyEntries))
+ {
+ var spl = item.Split('/', 2);
+ badges.Add(new Badge(spl[0], spl[1]));
+ }
+ return badges;
+ }
+ }
+ ///
+ /// The color of the user’s name in the chat room. This is a hexadecimal
+ /// RGB color code in the form, #. This tag may be empty if it is never set.
+ ///
+ public Color? Color
+ { get
+ {
+ //should have format "#RRGGBB"
+ if (!MessageTags.TryGetValue("color", out string? value))
+ return null;
+ if (value.Length < 7)
+ return null;
+ int r = Convert.ToInt32(value.Substring(1, 2), 16);
+ int g = Convert.ToInt32(value.Substring(3, 2), 16);
+ int b = Convert.ToInt32(value.Substring(5, 2), 16);
+ return System.Drawing.Color.FromArgb(r, g, b);
+ }
+ }
+ ///
+ /// The user’s display name. This tag may be empty if it is never set.
+ ///
+ public string DisplayName
+ { get
+ {
+ if (!MessageTags.TryGetValue("display-name", out string? value))
+ return "";
+ return value ?? "";
+ }
+ }
+ ///
+ /// A comma-delimited list of IDs that identify the emote sets that the user has
+ /// access to. Is always set to at least zero (0). To access the emotes in the set,
+ /// use the Get Emote Sets API.
+ ///
+ ///
+ public IEnumerable EmoteSets
+ { get
+ {
+ var value = TryGetTag("emote-sets");
+ foreach (var s in value.Split(','))
+ {
+ if (int.TryParse(s, out int num))
+ yield return num;
+ else
+ throw new InvalidDataException();
+ }
+ }
+ }
+ ///
+ /// A Boolean value that indicates whether the user has site-wide commercial
+ /// free mode enabled
+ ///
+ public bool Turbo
+ { get
+ {
+ if (!MessageTags.TryGetValue("turbo", out string? value))
+ return false;
+ return value == "1";
+ }
+ }
+ public string UserId => TryGetTag("user-id");
+ ///
+ /// The type of the user. Assumes a normal user if this is not provided or is invalid.
+ ///
+ public UserType UserType
+ { get
+ {
+ if (!MessageTags.TryGetValue("user-type", out string? value))
+ return UserType.Normal;
+ switch (value)
+ {
+ case "admin":
+ return UserType.Admin;
+ case "global_mod":
+ return UserType.GlobalMod;
+ case "staff":
+ return UserType.Staff;
+ default:
+ return UserType.Normal;
+ }
+ }
+ }
+ public GlobalUserState(ReceivedMessage other) : base(other)
+ {
+ Debug.Assert(MessageType == IrcMessageType.GLOBALUSERSTATE,
+ $"{nameof(GlobalUserState)} must have type {IrcMessageType.GLOBALUSERSTATE}" +
+ $" but has {MessageType}");
+ }
+ }
+}
diff --git a/TwitchIrcClient/IRC/Messages/HostTarget.cs b/TwitchIrcClient/IRC/Messages/HostTarget.cs
new file mode 100644
index 0000000..ab1be1b
--- /dev/null
+++ b/TwitchIrcClient/IRC/Messages/HostTarget.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TwitchIrcClient.IRC.Messages
+{
+ public class HostTarget : ReceivedMessage
+ {
+ ///
+ /// The channel that’s hosting the viewers.
+ ///
+ public string HostingChannel => Parameters.FirstOrDefault("").TrimStart('#');
+ public string ChannelBeingHosted =>
+ Parameters.Last().Split(' ').First().TrimStart('-');
+ ///
+ /// true if the channel is now hosting another channel, false if it stopped hosting
+ ///
+ public bool NowHosting => !Parameters.Last().StartsWith('-');
+ public int NumberOfViewers
+ { get
+ {
+ var s = Parameters.LastOrDefault("");
+ var s2 = s.Split(' ', StringSplitOptions.TrimEntries).LastOrDefault("");
+ if (int.TryParse(s2, out int value))
+ return value;
+ return 0;
+ }
+ }
+
+
+ public HostTarget(ReceivedMessage other) : base(other)
+ {
+ Debug.Assert(MessageType == IrcMessageType.HOSTTARGET,
+ $"{nameof(HostTarget)} must have type {IrcMessageType.HOSTTARGET}" +
+ $" but has {MessageType}");
+ }
+ }
+}
diff --git a/TwitchIrcClient/IRC/Messages/Notice.cs b/TwitchIrcClient/IRC/Messages/Notice.cs
index cbb7c10..5fed21a 100644
--- a/TwitchIrcClient/IRC/Messages/Notice.cs
+++ b/TwitchIrcClient/IRC/Messages/Notice.cs
@@ -160,166 +160,4 @@ namespace TwitchIrcClient.IRC.Messages
usage_me,
usage_mod,
}
- //public enum NoticeId
- //{
- // AlreadyBanned,
- // AlreadyEmoteOnlyOff,
- // AlreadyEmoteOnlyOn,
- // AlreadyFollowersOff,
- // AlreadyFollowersOn,
- // AlreadyR9KOff,
- // AlreadyR9KOn,
- // AlreadySlowOff,
- // AlreadySlowOn,
- // AlreadySubsOff,
- // AlreadySubsOn,
- // AutohostReceive,
- // BadBanAdmin,
- // BadBanAnon,
- // BadBanBroadcaster,
- // BadBanMod,
- // BadBanSelf,
- // BadBanStaff,
- // BadCommercialError,
- // BadDeleteMessageBroadcaster,
- // BadDeleteMessageMod,
- // BadHostError,
- // BadHostHosting,
- // BadHostRateExceeded,
- // BadHostRejected,
- // BadHostSelf,
- // BadModBanned,
- // BadModMod,
- // BadSlowDuration,
- // BadTimeoutAdmin,
- // BadTimeoutAnon,
- // BadTimeoutBroadcaster,
- // BadTimeoutDuration,
- // BadTimeoutMod,
- // BadTimeoutSelf,
- // BadTimeoutStaff,
- // BadUnbanNoBan,
- // BadUnhostError,
- // BadUnmodMod,
- // BadVipGranteeBanned,
- // BadVipGranteeAlreadyVip,
- // BadVipMaxVipsReached,
- // BadVipAchievementIncomplete,
- // BadUnvipGranteeNotVip,
- // BanSuccess,
- // CmdsAvailable,
- // ColorChanged,
- // CommercialSuccess,
- // DeleteMessageSuccess,
- // DeleteStaffMessageSuccess,
- // EmoteOnlyOff,
- // EmoteOnlyOn,
- // FollowersOff,
- // FollowersOn,
- // FollowersOnZero,
- // HostOff,
- // HostOn,
- // HostReceive,
- // HostReceiveNoCount,
- // HostTargetWentOffline,
- // HostsRemaining,
- // InvalidUser,
- // ModSuccess,
- // MsgBanned,
- // MsgBadCharacters,
- // MsgChannelBlocked,
- // MsgChannelSuspended,
- // MsgDuplicate,
- // MsgEmoteonly,
- // MsgFollowersonly,
- // MsgFollowersonlyFollowed,
- // MsgFollowersonlyZero,
- // MsgR9K,
- // MsgRatelimit,
- // MsgRejected,
- // MsgRejectedMandatory,
- // MsgRequiresVerifiedPhoneNumber,
- // MsgSlowmode,
- // MsgSubsonly,
- // MsgSuspended,
- // MsgTimedout,
- // MsgVerifiedEmail,
- // NoHelp,
- // NoMods,
- // NoVips,
- // NotHosting,
- // NoPermission,
- // R9KOff,
- // R9KOn,
- // RaidErrorAlreadyRaiding,
- // RaidErrorForbidden,
- // RaidErrorSelf,
- // RaidErrorTooManyViewers,
- // RaidErrorUnexpected,
- // RaidNoticeMature,
- // RaidNoticeRestrictedChat,
- // RoomMods,
- // SlowOff,
- // SlowOn,
- // SubsOff,
- // SubsOn,
- // TimeoutNoTimeout,
- // TimeoutSuccess,
- // TosBan,
- // TurboOnlyColor,
- // UnavailableCommand,
- // UnbanSuccess,
- // UnmodSuccess,
- // UnraidErrorNoActiveRaid,
- // UnraidErrorUnexpected,
- // UnraidSuccess,
- // UnrecognizedCmd,
- // UntimeoutBanned,
- // UntimeoutSuccess,
- // UnvipSuccess,
- // UsageBan,
- // UsageClear,
- // UsageColor,
- // UsageCommercial,
- // UsageDisconnect,
- // UsageDelete,
- // UsageEmoteOnlyOff,
- // UsageEmoteOnlyOn,
- // UsageFollowersOff,
- // UsageFollowersOn,
- // UsageHelp,
- // UsageHost,
- // UsageMarker,
- // UsageMe,
- // UsageMod,
- // UsageMods,
- // UsageR9KOff,
- // UsageR9KOn,
- // UsageRaid,
- // UsageSlowOff,
- // UsageSlowOn,
- // UsageSubsOff,
- // UsageSubsOn,
- // UsageTimeout,
- // UsageUnban,
- // UsageUnhost,
- // UsageUnmod,
- // UsageUnraid,
- // UsageUntimeout,
- // UsageUnvip,
- // UsageUser,
- // UsageVip,
- // UsageVips,
- // UsageWhisper,
- // VipSuccess,
- // VipsSuccess,
- // WhisperBanned,
- // WhisperBannedRecipient,
- // WhisperInvalidLogin,
- // WhisperInvalidSelf,
- // WhisperLimitPerMin,
- // WhisperLimitPerSec,
- // WhisperRestricted,
- // WhisperRestrictedRecipient,
- //}
}
diff --git a/TwitchIrcClient/IRC/Messages/Privmsg.cs b/TwitchIrcClient/IRC/Messages/Privmsg.cs
index f8fefdf..5fcf219 100644
--- a/TwitchIrcClient/IRC/Messages/Privmsg.cs
+++ b/TwitchIrcClient/IRC/Messages/Privmsg.cs
@@ -18,8 +18,8 @@ namespace TwitchIrcClient.IRC.Messages
/// List of chat badges. Most badges have only 1 version, but some badges like
/// subscriber badges offer different versions of the badge depending on how
/// long the user has subscribed. To get the badge, use the Get Global Chat
- /// Badges and Get Channel Chat Badges APIs.Match the badge to the set-id field’s
- /// value in the response.Then, match the version to the id field in the list of versions.
+ /// Badges and Get Channel Chat Badges APIs. Match the badge to the set-id field’s
+ /// value in the response. Then, match the version to the id field in the list of versions.
///
public List Badges
{ get
@@ -79,6 +79,26 @@ namespace TwitchIrcClient.IRC.Messages
return value ?? "";
}
}
+ public IEnumerable Emotes
+ { get
+ {
+ var tag = TryGetTag("emotes");
+ foreach (var emote in tag.Split('/', StringSplitOptions.RemoveEmptyEntries))
+ {
+ var split = emote.Split(':', 2);
+ Debug.Assert(split.Length == 2);
+ var name = split[0];
+ foreach (var indeces in split[1].Split(','))
+ {
+ var split2 = indeces.Split('-');
+ if (!int.TryParse(split2[0], out int start) ||
+ !int.TryParse(split2[1], out int end))
+ throw new InvalidDataException();
+ yield return new Emote(name, start, end - start + 1);
+ }
+ }
+ }
+ }
///
/// An ID that uniquely identifies the message.
///
@@ -166,6 +186,54 @@ namespace TwitchIrcClient.IRC.Messages
/// A Boolean value that determines whether the user that sent the chat is a VIP.
///
public bool Vip => MessageTags.ContainsKey("vip");
+ ///
+ /// The level of the Hype Chat. Proper values are 1-10, or 0 if not a Hype Chat.
+ ///
+ public int HypeChatLevel
+ { get
+ {
+ var value = TryGetTag("pinned-chat-paid-level");
+ switch (value.ToUpper())
+ {
+ case "ONE":
+ return 1;
+ case "TWO":
+ return 2;
+ case "THREE":
+ return 3;
+ case "FOUR":
+ return 4;
+ case "FIVE":
+ return 5;
+ case "SIX":
+ return 6;
+ case "SEVEN":
+ return 7;
+ case "EIGHT":
+ return 8;
+ case "NINE":
+ return 9;
+ case "TEN":
+ return 10;
+ default:
+ return 0;
+ }
+ }
+ }
+ ///
+ /// The ISO 4217 alphabetic currency code the user has sent the Hype Chat in.
+ ///
+ public string HypeChatCurrency => TryGetTag("pinned-chat-paid-currency");
+ public decimal? HypeChatValue
+ { get
+ {
+ var numeric = TryGetTag("pinned-chat-paid-amount");
+ var exp = TryGetTag("pinned-chat-paid-exponent");
+ if (int.TryParse(numeric, out int d_numeric) && int.TryParse(exp, out int d_exp))
+ return d_numeric / ((decimal)Math.Pow(10, d_exp));
+ return null;
+ }
+ }
public bool FirstMessage => TryGetTag("first-msg") == "1";
public string ChatMessage => Parameters.Last();
public Privmsg(ReceivedMessage message) : base(message)
diff --git a/TwitchIrcClient/IRC/Messages/ReceivedMessage.cs b/TwitchIrcClient/IRC/Messages/ReceivedMessage.cs
index 153d780..958ff35 100644
--- a/TwitchIrcClient/IRC/Messages/ReceivedMessage.cs
+++ b/TwitchIrcClient/IRC/Messages/ReceivedMessage.cs
@@ -94,27 +94,23 @@ namespace TwitchIrcClient.IRC.Messages
message.Parameters.Add(spl_final[1]);
}
}
- switch (message.MessageType)
+ return message.MessageType switch
{
- case IrcMessageType.PRIVMSG:
- return new Privmsg(message);
- case IrcMessageType.CLEARCHAT:
- return new ClearChat(message);
- case IrcMessageType.CLEARMSG:
- return new ClearMsg(message);
- case IrcMessageType.NOTICE:
- return new Notice(message);
- case IrcMessageType.JOIN:
- return new Join(message);
- case IrcMessageType.PART:
- return new Part(message);
- case IrcMessageType.RPL_NAMREPLY:
- return new NamReply(message);
- case IrcMessageType.ROOMSTATE:
- return new Roomstate(message);
- default:
- return message;
- }
+ IrcMessageType.CLEARCHAT => new ClearChat(message),
+ IrcMessageType.CLEARMSG => new ClearMsg(message),
+ IrcMessageType.JOIN => new Join(message),
+ IrcMessageType.GLOBALUSERSTATE => new GlobalUserState(message),
+ IrcMessageType.HOSTTARGET => new HostTarget(message),
+ IrcMessageType.NOTICE => new Notice(message),
+ IrcMessageType.PART => new Part(message),
+ IrcMessageType.PRIVMSG => new Privmsg(message),
+ IrcMessageType.ROOMSTATE => new Roomstate(message),
+ IrcMessageType.RPL_NAMREPLY => new NamReply(message),
+ IrcMessageType.USERNOTICE => new UserNotice(message),
+ IrcMessageType.USERSTATE => new UserState(message),
+ IrcMessageType.WHISPER => new Whisper(message),
+ _ => message,
+ };
}
///
/// Tries to get the value of the tag.
diff --git a/TwitchIrcClient/IRC/Messages/UserNotice.cs b/TwitchIrcClient/IRC/Messages/UserNotice.cs
index 112cf81..e3a16b0 100644
--- a/TwitchIrcClient/IRC/Messages/UserNotice.cs
+++ b/TwitchIrcClient/IRC/Messages/UserNotice.cs
@@ -1,12 +1,19 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Data;
using System.Diagnostics;
using System.Drawing;
+using System.IO;
using System.Linq;
using System.Net.WebSockets;
+using System.Numerics;
using System.Text;
+using System.Threading.Channels;
using System.Threading.Tasks;
+using System.Xml.Linq;
+using static System.Net.Mime.MediaTypeNames;
+using static System.Runtime.InteropServices.JavaScript.JSType;
namespace TwitchIrcClient.IRC.Messages
{
@@ -16,7 +23,7 @@ namespace TwitchIrcClient.IRC.Messages
/// List of chat badges. Most badges have only 1 version, but some badges like
/// subscriber badges offer different versions of the badge depending on how
/// long the user has subscribed. To get the badge, use the Get Global Chat
- /// Badges and Get Channel Chat Badges APIs.Match the badge to the set-id field’s
+ /// Badges and Get Channel Chat Badges APIs. Match the badge to the set-id field’s
/// value in the response.Then, match the version to the id field in the list of versions.
///
public List Badges
@@ -52,6 +59,8 @@ namespace TwitchIrcClient.IRC.Messages
return System.Drawing.Color.FromArgb(r, g, b);
}
}
+ public string Channel => Parameters.FirstOrDefault("").TrimStart('#');
+ public string Message => Parameters.ElementAtOrDefault(1) ?? "";
///
/// The user’s display name. This tag may be empty if it is never set.
///
@@ -63,6 +72,13 @@ namespace TwitchIrcClient.IRC.Messages
return value ?? "";
}
}
+ public IEnumerable Emotes
+ { get
+ {
+ throw new NotImplementedException();
+
+ }
+ }
///
/// An ID that uniquely identifies the message.
///
@@ -70,6 +86,10 @@ namespace TwitchIrcClient.IRC.Messages
public UserNoticeType? UserNoticeType => Enum.TryParse(TryGetTag("msg-id"), out UserNoticeType type)
? type : null;
///
+ ///
+ ///
+ public string Login => TryGetTag("login");
+ ///
/// Whether the user is a moderator in this channel
///
public bool Moderator
@@ -118,24 +138,166 @@ namespace TwitchIrcClient.IRC.Messages
return value ?? "";
}
}
+ ///
+ ///
+ ///
+ public string SystemMessage => TryGetTag("system-msg");
+ ///
+ /// When the Twitch IRC server received the message
+ ///
+ public DateTime Timestamp
+ { get
+ {
+ if (double.TryParse(TryGetTag("tmi-sent-ts"), out double value))
+ return DateTime.UnixEpoch.AddMilliseconds(value);
+ throw new InvalidDataException();
+ }
+ }
+ ///
+ /// Included only with notices.
+ /// The display name of the broadcaster raiding this channel.
+ ///
+ public string RaidingChannelDisplayName => TryGetTag("msg-param-displayName");
+ ///
+ /// Included only with notices.
+ /// The login name of the broadcaster raiding this channel.
+ ///
+ public string RaidingChannelLogin => TryGetTag("msg-param-login");
+ ///
+ /// Included only with and notices.
+ /// The subscriptions promo, if any, that is ongoing (for example, Subtember 2018).
+ ///
+ public string SubscriptionPromoName => TryGetTag("msg-param-promo-name");
+ ///
+ /// Included only with and
+ /// notices.
+ /// The number of gifts the gifter has given during the promo indicated by .
+ ///
+ public int SubscriptionPromoCount => int.TryParse(TryGetTag("msg-param-promo-gift-total"),
+ out int value) ? value : 0;
+ ///
+ /// Included only with notices.
+ /// The display name of the subscription gift recipient.
+ ///
+ public string RecipientDisplayName => TryGetTag("msg-param-recipient-display-name");
+ ///
+ /// Included only with notices.
+ /// The user ID of the subscription gift recipient.
+ ///
+ public string RecipientId => TryGetTag("msg-param-recipient-id");
+ ///
+ /// Included only with notices.
+ /// The user name of the subscription gift recipient.
+ ///
+ public string RecipientUsername => TryGetTag("msg-param-recipient-user-name");
+ ///
+ /// Only Included in , ,
+ /// and .
+ /// Either "msg-param-cumulative-months" or "msg-param-months" depending
+ /// on the notice type.
+ ///
+ public int TotalMonths
+ { get
+ {
+ var s1 = TryGetTag("msg-param-cumulative-months");
+ var s2 = TryGetTag("msg-param-months");
+ if (int.TryParse(s1, out int value1))
+ return value1;
+ if (int.TryParse(s2, out int value2))
+ return value2;
+ return 0;
+ }
+ }
+ ///
+ /// Included only with and notices.
+ /// A Boolean value that indicates whether the user wants their streaks shared.
+ /// Is "false" for other message types.
+ ///
+ public bool ShouldShareStreak => TryGetTag("msg-param-should-share-streak")
+ == "1" ? true : false;
+ ///
+ /// Included only with and notices.
+ /// The number of consecutive months the user has subscribed.
+ /// This is zero(0) if is 0.
+ ///
+ public int StreakMonths => int.TryParse(TryGetTag("msg-param-streak-months"),
+ out int value) ? value : 0;
+ ///
+ /// Included only with ,
+ /// and notices.
+ ///
+ public SubType SubPlan
+ { get
+ {
+ switch (TryGetTag("msg-param-sub-plan").ToUpper())
+ {
+ case "PRIME":
+ return SubType.Prime;
+ case "1000":
+ return SubType.T1;
+ case "2000":
+ return SubType.T2;
+ case "3000":
+ return SubType.T3;
+ default:
+ return SubType.None;
+ }
+ }
+ }
+ ///
+ /// Included only with , ,
+ /// and notices.
+ /// The display name of the subscription plan. This may be a default name or one created
+ /// by the channel owner.
+ ///
+ public string SubPlanName => TryGetTag("msg-param-sub-plan-name");
+ ///
+ /// Included only with notices.
+ /// The number of viewers raiding this channel from the broadcaster’s channel.
+ ///
+ public int ViewerCount => int.TryParse(TryGetTag("msg-param-viewerCount"),
+ out int value) ? value : 0;
+ ///
+ /// The type of user sending the whisper message.
+ ///
public UserType UserType
{ get
{
if (!MessageTags.TryGetValue("user-type", out string? value))
- return UserType.Normal;
- switch (value)
+ return UserType.None;
+ switch (value.ToUpper())
{
- case "admin":
+ case "ADMIN":
return UserType.Admin;
- case "global_mod":
+ case "GLOBAL_MOD":
return UserType.GlobalMod;
- case "staff":
+ case "STAFF":
return UserType.Staff;
- default:
+ case "":
return UserType.Normal;
+ default:
+ throw new InvalidDataException();
}
}
}
+ ///
+ /// Included only with notices.
+ /// The name of the ritual being celebrated.
+ ///
+ public RitualType RitualType => Enum.TryParse(TryGetTag("msg-param-ritual-name"),
+ out RitualType rt) ? rt : RitualType.None;
+ //TODO possibly deprecate and add an int version in the future if all tiers are numeric
+ ///
+ /// Included only with notices.
+ /// The tier of the Bits badge the user just earned. For example, 100, 1000, or 10000.
+ ///
+ public string Threshold => TryGetTag("msg-param-threshold");
+ ///
+ /// Included only with notices.
+ /// The number of months gifted as part of a single, multi-month gift.
+ ///
+ public int GiftMonths => int.TryParse(TryGetTag("msg-param-gift-months"),
+ out int value) ? value : 0;
public UserNotice(ReceivedMessage message) : base(message)
{
Debug.Assert(MessageType == IrcMessageType.USERNOTICE,
@@ -145,16 +307,31 @@ namespace TwitchIrcClient.IRC.Messages
}
public enum UserNoticeType
{
- sub,
- resub,
- subgift,
- submysterygift,
- giftpaidupgrade,
- rewardgift,
- anongiftpaidupgrade,
- raid,
- unraid,
- ritual,
- bitsbadgetier,
+ sub = 0,
+ resub = 1,
+ subgift = 2,
+ submysterygift = 3,
+ giftpaidupgrade = 4,
+ rewardgift = 5,
+ anongiftpaidupgrade = 6,
+ raid = 7,
+ unraid = 8,
+ ritual = 9,
+ bitsbadgetier = 10,
+ }
+ public enum RitualType
+ {
+ new_chatter = 0,
+
+ None = int.MinValue,
+ }
+ public enum SubType
+ {
+ Prime = 0,
+ T1 = 1,
+ T2 = 2,
+ T3 = 3,
+
+ None = int.MinValue,
}
}
diff --git a/TwitchIrcClient/IRC/Messages/UserState.cs b/TwitchIrcClient/IRC/Messages/UserState.cs
new file mode 100644
index 0000000..1e7665b
--- /dev/null
+++ b/TwitchIrcClient/IRC/Messages/UserState.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TwitchIrcClient.IRC.Messages
+{
+ public class UserState : ReceivedMessage
+ {
+ public string Channel => Parameters.FirstOrDefault("").TrimStart('#');
+ ///
+ /// The color of the user’s name in the chat room. This is a hexadecimal
+ /// RGB color code in the form, #. This tag may be empty if it is never set.
+ ///
+ public Color? Color
+ { get
+ {
+ //should have format "#RRGGBB"
+ if (!MessageTags.TryGetValue("color", out string? value))
+ return null;
+ if (value.Length < 7)
+ return null;
+ int r = Convert.ToInt32(value.Substring(1, 2), 16);
+ int g = Convert.ToInt32(value.Substring(3, 2), 16);
+ int b = Convert.ToInt32(value.Substring(5, 2), 16);
+ return System.Drawing.Color.FromArgb(r, g, b);
+ }
+ }
+ ///
+ /// The user’s display name, escaped as described in the IRCv3 spec.
+ ///
+ public string DisplayName => TryGetTag("display-name");
+ ///
+ /// A comma-delimited list of IDs that identify the emote sets that the user has
+ /// access to. Is always set to at least zero (0). To access the emotes in the set,
+ /// use the Get Emote Sets API.
+ ///
+ ///
+ public IEnumerable EmoteSets
+ { get
+ {
+ var value = TryGetTag("emote-sets");
+ foreach (var s in value.Split(','))
+ {
+ if (int.TryParse(s, out int num))
+ yield return num;
+ else
+ throw new InvalidDataException();
+ }
+ }
+ }
+ ///
+ /// If a privmsg was sent, an ID that uniquely identifies the message.
+ ///
+ public string Id => TryGetTag("id");
+ ///
+ /// A Boolean value that determines whether the user is a moderator.
+ ///
+ public bool Moderator
+ { get
+ {
+ if (!MessageTags.TryGetValue("mod", out string? value))
+ return false;
+ return value == "1";
+ }
+ }
+ ///
+ /// Whether the user is subscribed to the channel
+ ///
+ public bool Subscriber
+ { get
+ {
+ if (!MessageTags.TryGetValue("subscriber", out string? value))
+ return false;
+ return value == "1";
+ }
+ }
+ ///
+ /// A Boolean value that indicates whether the user has site-wide commercial
+ /// free mode enabled
+ ///
+ public bool Turbo
+ { get
+ {
+ if (!MessageTags.TryGetValue("turbo", out string? value))
+ return false;
+ return value == "1";
+ }
+ }
+ public UserType UserType
+ { get
+ {
+ if (!MessageTags.TryGetValue("user-type", out string? value))
+ return UserType.Normal;
+ switch (value)
+ {
+ case "admin":
+ return UserType.Admin;
+ case "global_mod":
+ return UserType.GlobalMod;
+ case "staff":
+ return UserType.Staff;
+ default:
+ return UserType.Normal;
+ }
+ }
+ }
+
+ public UserState(ReceivedMessage other) : base(other)
+ {
+ Debug.Assert(MessageType == IrcMessageType.USERSTATE,
+ $"{nameof(UserState)} must have type {IrcMessageType.USERSTATE}" +
+ $" but has {MessageType}");
+ }
+ }
+}
diff --git a/TwitchIrcClient/IRC/Messages/Whisper.cs b/TwitchIrcClient/IRC/Messages/Whisper.cs
new file mode 100644
index 0000000..5efabc5
--- /dev/null
+++ b/TwitchIrcClient/IRC/Messages/Whisper.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TwitchIrcClient.IRC.Messages
+{
+ public class Whisper : ReceivedMessage
+ {
+
+ ///
+ /// List of chat badges. Most badges have only 1 version, but some badges like
+ /// subscriber badges offer different versions of the badge depending on how
+ /// long the user has subscribed. To get the badge, use the Get Global Chat
+ /// Badges and Get Channel Chat Badges APIs. Match the badge to the set-id field’s
+ /// value in the response.Then, match the version to the id field in the list of versions.
+ ///
+ public List Badges
+ { get
+ {
+ if (!MessageTags.TryGetValue("badges", out string? value))
+ return [];
+ if (value == null)
+ return [];
+ List badges = [];
+ foreach (var item in value.Split(',', StringSplitOptions.RemoveEmptyEntries))
+ {
+ var spl = item.Split('/', 2);
+ badges.Add(new Badge(spl[0], spl[1]));
+ }
+ return badges;
+ }
+ }
+ ///
+ /// The color of the user’s name in the chat room. This is a hexadecimal
+ /// RGB color code in the form, #. This tag may be empty if it is never set.
+ ///
+ public Color? Color
+ { get
+ {
+ //should have format "#RRGGBB"
+ if (!MessageTags.TryGetValue("color", out string? value))
+ return null;
+ if (value.Length < 7)
+ return null;
+ int r = Convert.ToInt32(value.Substring(1, 2), 16);
+ int g = Convert.ToInt32(value.Substring(3, 2), 16);
+ int b = Convert.ToInt32(value.Substring(5, 2), 16);
+ return System.Drawing.Color.FromArgb(r, g, b);
+ }
+ }
+ ///
+ /// The user’s display name. This tag may be empty if it is never set.
+ ///
+ public string DisplayName
+ { get
+ {
+ if (!MessageTags.TryGetValue("display-name", out string? value))
+ return "";
+ return value ?? "";
+ }
+ }
+ public IEnumerable Emotes
+ { get
+ {
+ var tag = TryGetTag("emotes");
+ foreach (var emote in tag.Split('/', StringSplitOptions.RemoveEmptyEntries))
+ {
+ var split = emote.Split(':', 2);
+ Debug.Assert(split.Length == 2);
+ var name = split[0];
+ foreach (var indeces in split[1].Split(','))
+ {
+ var split2 = indeces.Split('-');
+ if (!int.TryParse(split2[0], out int start) ||
+ !int.TryParse(split2[1], out int end))
+ throw new InvalidDataException();
+ yield return new Emote(name, start, end - start + 1);
+ }
+ }
+ }
+ }
+ ///
+ /// An ID that uniquely identifies the whisper message.
+ ///
+ public string MessageId => TryGetTag("message-id");
+ ///
+ /// An ID that uniquely identifies the whisper thread.
+ /// The ID is in the form, _.
+ ///
+ public string ThreadId => TryGetTag("thread-id");
+ ///
+ /// A Boolean value that indicates whether the user has site-wide commercial
+ /// free mode enabled
+ ///
+ public bool Turbo
+ { get
+ {
+ if (!MessageTags.TryGetValue("turbo", out string? value))
+ return false;
+ return value == "1";
+ }
+ }
+ ///
+ /// The ID of the user sending the whisper message.
+ ///
+ public string UserId => TryGetTag("user-id");
+ public string Message => Parameters.LastOrDefault("");
+ ///
+ /// The type of the user. Assumes a normal user if this is not provided or is invalid.
+ ///
+ public UserType UserType
+ { get
+ {
+ if (!MessageTags.TryGetValue("user-type", out string? value))
+ return UserType.Normal;
+ switch (value)
+ {
+ case "admin":
+ return UserType.Admin;
+ case "global_mod":
+ return UserType.GlobalMod;
+ case "staff":
+ return UserType.Staff;
+ default:
+ return UserType.Normal;
+ }
+ }
+ }
+ public Whisper(ReceivedMessage other) : base(other)
+ {
+ Debug.Assert(MessageType == IrcMessageType.WHISPER,
+ $"{nameof(Whisper)} must have type {IrcMessageType.WHISPER}" +
+ $" but has {MessageType}");
+ }
+ }
+}
diff --git a/TwitchIrcClientTests/ParserTest.cs b/TwitchIrcClientTests/ParserTest.cs
index e81cbe3..e672b58 100644
--- a/TwitchIrcClientTests/ParserTest.cs
+++ b/TwitchIrcClientTests/ParserTest.cs
@@ -2,6 +2,7 @@ using System.Drawing;
using System;
using TwitchIrcClient.IRC;
using TwitchIrcClient.IRC.Messages;
+using System.Diagnostics;
namespace TwitchIrcClientTests
{
@@ -9,24 +10,9 @@ namespace TwitchIrcClientTests
public class ParserTest
{
[TestMethod]
- public void TestSimpleMessages()
+ public void TestRoomstate()
{
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)
@@ -55,7 +41,11 @@ namespace TwitchIrcClientTests
{
Assert.Fail();
}
-
+ }
+ [TestMethod]
+ public void TestNamreply()
+ {
+ var NAMREPLY = ":justinfan7550.tmi.twitch.tv 353 justinfan7550 = #channelname :user1 user2 user3 user4 user5";
var _namReply = ReceivedMessage.Parse(NAMREPLY);
Assert.AreEqual(IrcMessageType.RPL_NAMREPLY, _namReply.MessageType);
if (_namReply is NamReply namReply)
@@ -68,7 +58,11 @@ namespace TwitchIrcClientTests
{
Assert.Fail();
}
-
+ }
+ [TestMethod]
+ public void TestJoin()
+ {
+ var JOIN = ":newuser!newuser@newuser.tmi.twitch.tv JOIN #channelname";
var _join = ReceivedMessage.Parse(JOIN);
Assert.AreEqual(IrcMessageType.JOIN, _join.MessageType);
if (_join is Join join)
@@ -80,7 +74,11 @@ namespace TwitchIrcClientTests
{
Assert.Fail();
}
-
+ }
+ [TestMethod]
+ public void TestPart()
+ {
+ var PART = ":leavinguser!leavinguser@leavinguser.tmi.twitch.tv PART #channelname";
var _part = ReceivedMessage.Parse(PART);
Assert.AreEqual(IrcMessageType.PART, _part.MessageType);
if (_part is Part part)
@@ -92,6 +90,25 @@ namespace TwitchIrcClientTests
{
Assert.Fail();
}
+ }
+ [TestMethod]
+ public void TestPrivmsg()
+ {
+ 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 ESCAPE = @"@escaped=\:\s\\\r\n\a\b\c PRIVMSG #channelname :message";
+ var EMOTES = @"@badge-info=subscriber/4;badges=subscriber/3;client-nonce=2cc8bb73f5d946b22ec2905c8ccdee7a;color=#1E90FF;" +
+ @"display-name=Ikatono;emote-only=1;emotes=emotesv2_4f3ee26e385b46aa88d5f45307489939:0-12,14-26/emotesv2_9046ad54f76f42389edb4cc828b1b057" +
+ @":28-35,37-44;first-msg=0;flags=;id=08424675-217f-44bc-b9c0-24e2e2dd5f33;mod=0;returning-chatter=0;room-id=230151386;" +
+ @"subscriber=1;tmi-sent-ts=1711136008625;turbo=0;user-id=24866530;user-type= :ikatono!ikatono@ikatono.tmi.twitch.tv " +
+ @"PRIVMSG #bajiru_en :bajiBUFFERING bajiBUFFERING bajiBONK bajiBONK";
var _priv = ReceivedMessage.Parse(PRIVMSG);
Assert.AreEqual(IrcMessageType.PRIVMSG, _priv.MessageType);
@@ -111,6 +128,7 @@ namespace TwitchIrcClientTests
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);
+ Assert.IsTrue(priv.Badges.SequenceEqual([new Badge("subscriber", "0")]));
}
else
{
@@ -134,8 +152,388 @@ namespace TwitchIrcClientTests
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);
+ Assert.IsTrue(cheer.Badges.SequenceEqual([
+ new Badge("subscriber", "9"),
+ new Badge("twitch-recap-2023", "1"),
+ ]));
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _escape = ReceivedMessage.Parse(ESCAPE);
+ Assert.AreEqual(IrcMessageType.PRIVMSG, _escape.MessageType);
+ if (_escape is Privmsg escape)
+ {
+ Assert.AreEqual("; \\\r\nabc", escape.MessageTags["escaped"]);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _emotes = ReceivedMessage.Parse(EMOTES);
+ Assert.AreEqual(IrcMessageType.PRIVMSG, _emotes.MessageType);
+ if (_emotes is Privmsg emotes)
+ {
+ Assert.IsTrue(emotes.Emotes.SequenceEqual([
+ new Emote("emotesv2_4f3ee26e385b46aa88d5f45307489939", 0, 12-0+1),
+ new Emote("emotesv2_4f3ee26e385b46aa88d5f45307489939", 14, 26-14+1),
+ new Emote("emotesv2_9046ad54f76f42389edb4cc828b1b057", 28, 35-28+1),
+ new Emote("emotesv2_9046ad54f76f42389edb4cc828b1b057", 37, 44-37+1),
+ ]));
+ }
+ else
+ {
+ Assert.Fail();
+ }
+ }
+ [TestMethod]
+ public void TestUserNotice()
+ {
+ //these 4 are examples given from Twitch's USERNOTICE tags page
+ var RESUB = @"@badge-info=;badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;" +
+ @"id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-cumulative-months=6;msg-param-streak-months=2;" +
+ @"msg-param-should-share-streak=1;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=12345678;subscriber=1;" +
+ @"system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=87654321;user-type=staff" +
+ @" :tmi.twitch.tv USERNOTICE #dallas :Great stream -- keep it up!";
+ var GIFTED = @"@badge-info=;badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;" +
+ @"id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;msg-param-months=1;" +
+ @"msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=55554444;msg-param-recipient-name=mr_woodchuck;" +
+ @"msg-param-sub-plan-name=House\sof\sNyoro~n;msg-param-sub-plan=1000;room-id=19571752;subscriber=0;" +
+ @"system-msg=TWW2\sgifted\sa\sTier\s1\ssub\sto\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=87654321;user-type=staff" +
+ @" :tmi.twitch.tv USERNOTICE #forstycup";
+ var RAID = @"@badge-info=;badges=turbo/1;color=#9ACD32;display-name=TestChannel;emotes=;id=3d830f12-795c-447d-af3c-ea05e40fbddb;" +
+ @"login=testchannel;mod=0;msg-id=raid;msg-param-displayName=TestChannel;msg-param-login=testchannel;msg-param-viewerCount=15;" +
+ @"room-id=33332222;subscriber=0;system-msg=15\sraiders\sfrom\sTestChannel\shave\sjoined\n!;tmi-sent-ts=1507246572675;turbo=1;" +
+ @"user-id=123456;user-type= :tmi.twitch.tv USERNOTICE #othertestchannel";
+ var NEWCHATTER = @"@badge-info=;badges=;color=;display-name=SevenTest1;emotes=30259:0-6;id=37feed0f-b9c7-4c3a-b475-21c6c6d21c3d;" +
+ @"login=seventest1;mod=0;msg-id=ritual;msg-param-ritual-name=new_chatter;room-id=87654321;subscriber=0;" +
+ @"system-msg=Seventoes\sis\snew\shere!;tmi-sent-ts=1508363903826;turbo=0;user-id=77776666;user-type=" +
+ @" :tmi.twitch.tv USERNOTICE #seventoes :HeyGuys";
+
+ var _resub = ReceivedMessage.Parse(RESUB);
+ Assert.AreEqual(IrcMessageType.USERNOTICE, _resub.MessageType);
+ if (_resub is UserNotice resub)
+ {
+ Assert.AreEqual(Color.FromArgb(0, 128, 0), resub.Color);
+ Assert.AreEqual("ronni", resub.DisplayName);
+ Assert.AreEqual("db25007f-7a18-43eb-9379-80131e44d633", resub.Id);
+ Assert.AreEqual("ronni", resub.Login);
+ Assert.IsFalse(resub.Moderator);
+ Assert.AreEqual(RitualType.None, resub.RitualType);
+ Assert.AreEqual(UserNoticeType.resub, resub.UserNoticeType);
+ Assert.AreEqual(6, resub.TotalMonths);
+ Assert.AreEqual(2, resub.StreakMonths);
+ Assert.IsTrue(resub.ShouldShareStreak);
+ Assert.AreEqual(SubType.Prime, resub.SubPlan);
+ Assert.AreEqual("Prime", resub.SubPlanName);
+ Assert.AreEqual("12345678", resub.RoomId);
+ Assert.IsTrue(resub.Subscriber);
+ Assert.AreEqual("ronni has subscribed for 6 months!", resub.SystemMessage);
+ Assert.AreEqual(new DateTime(2017, 10, 5, 23, 36, 12, 675, DateTimeKind.Utc),
+ resub.Timestamp);
+ Assert.IsTrue(resub.Turbo);
+ Assert.AreEqual("87654321", resub.UserId);
+ Assert.AreEqual(UserType.Staff, resub.UserType);
+ Assert.AreEqual("dallas", resub.Channel);
+ Assert.AreEqual("Great stream -- keep it up!", resub.Message);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _gifted = ReceivedMessage.Parse(GIFTED);
+ Assert.AreEqual(IrcMessageType.USERNOTICE, _gifted.MessageType);
+ if (_gifted is UserNotice gifted)
+ {
+ Assert.AreEqual(Color.FromArgb(0, 0, 255), gifted.Color);
+ Assert.AreEqual("TWW2", gifted.DisplayName);
+ Assert.AreEqual("e9176cd8-5e22-4684-ad40-ce53c2561c5e", gifted.Id);
+ Assert.AreEqual("tww2", gifted.Login);
+ Assert.IsFalse(gifted.Moderator);
+ Assert.AreEqual(RitualType.None, gifted.RitualType);
+ Assert.AreEqual(UserNoticeType.subgift, gifted.UserNoticeType);
+ Assert.AreEqual(1, gifted.TotalMonths);
+ Assert.AreEqual("Mr_Woodchuck", gifted.RecipientDisplayName);
+ //Twitch's example uses "msg-param-recipient-name" which doesn't appear anywhere
+ //else in the documentation. I believe this was inteded to be "msg-param-recipient-user-name"
+ //Assert.AreEqual("mr_woodchuck", gifted.RecipientUsername);
+ Assert.AreEqual("55554444", gifted.RecipientId);
+ Assert.AreEqual("House of Nyoro~n", gifted.SubPlanName);
+ Assert.AreEqual(SubType.T1, gifted.SubPlan);
+ Assert.AreEqual("19571752", gifted.RoomId);
+ Assert.IsFalse(gifted.Subscriber);
+ Assert.AreEqual("TWW2 gifted a Tier 1 sub to Mr_Woodchuck!", gifted.SystemMessage);
+ Assert.AreEqual(new DateTime(2018, 3, 16, 0, 17, 25, 153, DateTimeKind.Utc),
+ gifted.Timestamp);
+ Assert.IsFalse(gifted.Turbo);
+ Assert.AreEqual("87654321", gifted.UserId);
+ Assert.AreEqual(UserType.Staff, gifted.UserType);
+ Assert.AreEqual("forstycup", gifted.Channel);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _raid = ReceivedMessage.Parse(RAID);
+ Assert.AreEqual(IrcMessageType.USERNOTICE, _raid.MessageType);
+ if (_raid is UserNotice raid)
+ {
+ Assert.AreEqual(Color.FromArgb(154, 205, 50), raid.Color);
+ Assert.AreEqual("TestChannel", raid.DisplayName);
+ Assert.AreEqual("3d830f12-795c-447d-af3c-ea05e40fbddb", raid.Id);
+ Assert.AreEqual("testchannel", raid.Login);
+ Assert.IsFalse(raid.Moderator);
+ Assert.AreEqual(RitualType.None, raid.RitualType);
+ Assert.AreEqual(UserNoticeType.raid, raid.UserNoticeType);
+ Assert.AreEqual("TestChannel", raid.RaidingChannelDisplayName);
+ Assert.AreEqual("testchannel", raid.RaidingChannelLogin);
+ Assert.AreEqual(15, raid.ViewerCount);
+ Assert.AreEqual("33332222", raid.RoomId);
+ Assert.IsFalse(raid.Subscriber);
+ Assert.AreEqual("15 raiders from TestChannel have joined\n!", raid.SystemMessage);
+ Assert.AreEqual(new DateTime(2017, 10, 5, 23, 36, 12, 675, DateTimeKind.Utc),
+ raid.Timestamp);
+ Assert.IsTrue(raid.Turbo);
+ Assert.AreEqual("123456", raid.UserId);
+ Assert.AreEqual(UserType.Normal, raid.UserType);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _newchatter = ReceivedMessage.Parse(NEWCHATTER);
+ Assert.AreEqual(IrcMessageType.USERNOTICE, _newchatter.MessageType);
+ if (_newchatter is UserNotice newchatter)
+ {
+ Assert.AreEqual(null, newchatter.Color);
+ Assert.AreEqual("SevenTest1", newchatter.DisplayName);
+ Assert.AreEqual("37feed0f-b9c7-4c3a-b475-21c6c6d21c3d", newchatter.Id);
+ Assert.AreEqual("seventest1", newchatter.Login);
+ Assert.IsFalse(newchatter.Moderator);
+ Assert.AreEqual(RitualType.new_chatter, newchatter.RitualType);
+ Assert.AreEqual("87654321", newchatter.RoomId);
+ Assert.IsFalse(newchatter.Subscriber);
+ Assert.AreEqual("Seventoes is new here!", newchatter.SystemMessage);
+ Assert.AreEqual(new DateTime(2017, 10, 18, 21, 58, 23, 826, DateTimeKind.Utc),
+ newchatter.Timestamp);
+ Assert.IsFalse(newchatter.Turbo);
+ Assert.AreEqual("77776666", newchatter.UserId);
+ Assert.AreEqual(UserType.Normal, newchatter.UserType);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+ }
+ [TestMethod]
+ public void TestUserstate()
+ {
+ var USERSTATE = @"@badge-info=;badges=staff/1;color=#0D4200;display-name=ronni;" +
+ @"emote-sets=0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239;mod=1;subscriber=1;" +
+ @"turbo=1;user-type=staff :tmi.twitch.tv USERSTATE #dallas";
+
+ var _userstate = ReceivedMessage.Parse(USERSTATE);
+ Assert.AreEqual(IrcMessageType.USERSTATE, _userstate.MessageType);
+ if (_userstate is UserState userstate)
+ {
+ Assert.AreEqual("dallas", userstate.Channel);
+ Assert.AreEqual(Color.FromArgb(13, 66, 0), userstate.Color);
+ Assert.AreEqual("ronni", userstate.DisplayName);
+ Assert.IsTrue(userstate.EmoteSets.SequenceEqual([0, 33, 50, 237, 793, 2126, 3517, 4578, 5569, 9400, 10337, 12239]));
+ Assert.IsTrue(userstate.Moderator);
+ Assert.IsTrue(userstate.Subscriber);
+ Assert.IsTrue(userstate.Turbo);
+ Assert.AreEqual(UserType.Staff, userstate.UserType);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+ }
+ [TestMethod]
+ public void TestWhisper()
+ {
+ //Taken from a Twitch documentation example
+ //https://dev.twitch.tv/docs/irc/tags/#whisper-tags
+ var WHISPER = @"@badges=staff/1,bits-charity/1;color=#8A2BE2;display-name=PetsgomOO;emotes=;message-id=306;" +
+ @"thread-id=12345678_87654321;turbo=0;user-id=87654321;user-type=staff" +
+ @" :petsgomoo!petsgomoo@petsgomoo.tmi.twitch.tv WHISPER foo :hello";
+
+ var _whisper = ReceivedMessage.Parse(WHISPER);
+ Assert.AreEqual(IrcMessageType.WHISPER, _whisper.MessageType);
+ if (_whisper is Whisper whisper)
+ {
+ Assert.IsTrue(whisper.Badges.SequenceEqual([
+ new Badge("staff", "1"),
+ new Badge("bits-charity", "1"),
+ ]));
+ Assert.AreEqual(Color.FromArgb(138, 43, 226), whisper.Color);
+ Assert.AreEqual("PetsgomOO", whisper.DisplayName);
+ Assert.IsTrue(whisper.Emotes.SequenceEqual([]));
+ Assert.AreEqual("306", whisper.MessageId);
+ Assert.AreEqual("12345678_87654321", whisper.ThreadId);
+ Assert.IsFalse(whisper.Turbo);
+ Assert.AreEqual("87654321", whisper.UserId);
+ Assert.AreEqual(UserType.Staff, whisper.UserType);
+ Assert.AreEqual("hello", whisper.Message);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+ }
+ [TestMethod]
+ public void TestGlobalUserState()
+ {
+ var GLOBAL = @"@badge-info=subscriber/8;badges=subscriber/6;color=#0D4200;display-name=dallas;" +
+ @"emote-sets=0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239;turbo=0;user-id=12345678;" +
+ @"user-type=admin :tmi.twitch.tv GLOBALUSERSTATE";
+
+ var _global = ReceivedMessage.Parse(GLOBAL);
+ Assert.AreEqual(IrcMessageType.GLOBALUSERSTATE, _global.MessageType);
+ if (_global is GlobalUserState global)
+ {
+ Assert.IsTrue(global.BadgeInfo.SequenceEqual(["subscriber/8"]));
+ Assert.IsTrue(global.Badges.SequenceEqual([new Badge("subscriber", "6")]));
+ Assert.AreEqual(Color.FromArgb(13, 66, 0), global.Color);
+ Assert.AreEqual("dallas", global.DisplayName);
+ Assert.IsTrue(global.EmoteSets.SequenceEqual([
+ 0, 33, 50, 237, 793, 2126, 3517, 4578, 5569, 9400, 10337,
+ 12239]));
+ Assert.IsFalse(global.Turbo);
+ Assert.AreEqual("12345678", global.UserId);
+ Assert.AreEqual(UserType.Admin, global.UserType);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+ }
+ [TestMethod]
+ public void TestClearMsg()
+ {
+ var CLEARMSG = @"@login=ronni;room-id=;target-msg-id=abc-123-def;tmi-sent-ts=1642720582342" +
+ @" :tmi.twitch.tv CLEARMSG #dallas :HeyGuys";
+
+ var _clearmsg = ReceivedMessage.Parse(CLEARMSG);
+ Assert.AreEqual(IrcMessageType.CLEARMSG, _clearmsg.MessageType);
+ if (_clearmsg is ClearMsg clearmsg)
+ {
+ Assert.AreEqual("ronni", clearmsg.Login);
+ Assert.AreEqual("", clearmsg.RoomId);
+ Assert.AreEqual("abc-123-def", clearmsg.TargetMessageId);
+ Assert.AreEqual(new DateTime(2022, 1, 20, 23, 16, 22, 342, DateTimeKind.Utc),
+ clearmsg.Timestamp);
+ Assert.AreEqual("dallas", clearmsg.Channel);
+ Assert.AreEqual("HeyGuys", clearmsg.Message);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+ }
+ [TestMethod]
+ public void TestClearChat()
+ {
+ var PERMA = @"@room-id=12345678;target-user-id=87654321;tmi-sent-ts=1642715756806" +
+ @" :tmi.twitch.tv CLEARCHAT #dallas :ronni";
+ var CLEARCHAT = @"@room-id=12345678;tmi-sent-ts=1642715695392 :tmi.twitch.tv CLEARCHAT #dallas";
+ var TIMEOUT = @"@ban-duration=350;room-id=12345678;target-user-id=87654321;tmi-sent-ts=1642719320727" +
+ @" :tmi.twitch.tv CLEARCHAT #dallas :ronni";
+
+ var _perma = ReceivedMessage.Parse(PERMA);
+ Assert.AreEqual(IrcMessageType.CLEARCHAT, _perma.MessageType);
+ if (_perma is ClearChat perma)
+ {
+ Assert.AreEqual("12345678", perma.RoomId);
+ Assert.AreEqual("87654321", perma.TargetUserId);
+ Assert.AreEqual(new DateTime(2022, 1, 20, 21, 55, 56, 806, DateTimeKind.Utc),
+ perma.Timestamp);
+ Assert.AreEqual("dallas", perma.Channel);
+ Assert.AreEqual("ronni", perma.User);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _clearchat = ReceivedMessage.Parse(CLEARCHAT);
+ Assert.AreEqual(IrcMessageType.CLEARCHAT, _clearchat.MessageType);
+ if (_clearchat is ClearChat clearchat)
+ {
+ Assert.AreEqual("12345678", clearchat.RoomId);
+ Assert.AreEqual(new DateTime(2022, 1, 20, 21, 54, 55, 392),
+ clearchat.Timestamp);
+ Assert.AreEqual("dallas", clearchat.Channel);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _timeout = ReceivedMessage.Parse(TIMEOUT);
+ Assert.AreEqual(IrcMessageType.CLEARCHAT, _timeout.MessageType);
+ if (_timeout is ClearChat timeout)
+ {
+
+ }
+ else
+ {
+ Assert.Fail();
+ }
+ }
+ [TestMethod]
+ public void TestHostTarget()
+ {
+ var START = @":tmi.twitch.tv HOSTTARGET #abc :xyz 10";
+ var END = @":tmi.twitch.tv HOSTTARGET #abc :- 10";
+ //this should be valid based on the Twitch documentation but there
+ //doesn't seem to be a real use case
+ var NOCHAN = @":tmi.twitch.tv HOSTTARGET #abc : 10";
+
+ var _start = ReceivedMessage.Parse(START);
+ Assert.AreEqual(IrcMessageType.HOSTTARGET, _start.MessageType);
+ if (_start is HostTarget start)
+ {
+ Assert.AreEqual("abc", start.HostingChannel);
+ Assert.AreEqual("xyz", start.ChannelBeingHosted);
+ Assert.AreEqual(10, start.NumberOfViewers);
+ Assert.IsTrue(start.NowHosting);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _end = ReceivedMessage.Parse(END);
+ Assert.AreEqual(IrcMessageType.HOSTTARGET, _end.MessageType);
+ if (_end is HostTarget end)
+ {
+ Assert.AreEqual("abc", end.HostingChannel);
+ Assert.AreEqual("", end.ChannelBeingHosted);
+ Assert.IsFalse(end.NowHosting);
+ Assert.AreEqual(10, end.NumberOfViewers);
+ }
+ else
+ {
+ Assert.Fail();
+ }
+
+ var _nochan = ReceivedMessage.Parse(NOCHAN);
+ Assert.AreEqual(IrcMessageType.HOSTTARGET, _nochan.MessageType);
+ if (_nochan is HostTarget nochan)
+ {
+ Assert.AreEqual("abc", nochan.HostingChannel);
+ Assert.AreEqual("", nochan.ChannelBeingHosted);
+ Assert.IsTrue(nochan.NowHosting);
+ Assert.AreEqual(10, nochan.NumberOfViewers);
}
else
{