using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TwitchChatAPI.Enums;
using TwitchChatAPI.Helpers;
using TwitchChatAPI.MonoBehaviours;
using TwitchChatAPI.Objects;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("Zehs")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright © 2025 Zehs")]
[assembly: AssemblyDescription("Add Twitch chat integration to your Unity game mods! Subscribe to events like Messages, Cheers, Subs, and Raids. No Twitch authentication or connections required.")]
[assembly: AssemblyFileVersion("2.0.0.0")]
[assembly: AssemblyInformationalVersion("2.0.0+8d20404d44337407c8c2c06658bf8873112aeb86")]
[assembly: AssemblyProduct("TwitchChatAPI")]
[assembly: AssemblyTitle("TwitchChatAPI")]
[assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/ZehsTeam/TwitchChatAPI")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("2.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
namespace TwitchChatAPI
{
public static class API
{
public static string Channel => TwitchChat.Channel;
public static ConnectionState ConnectionState => TwitchChat.ConnectionState;
public static IReadOnlyCollection<TwitchUser> Users => UserHelper.Users.Values;
public static event Action<ConnectionState> OnConnectionStateChanged;
public static event Action OnConnect;
public static event Action OnDisconnect;
public static event Action<TwitchMessage> OnMessage;
public static event Action<TwitchCheerEvent> OnCheer;
public static event Action<TwitchSubEvent> OnSub;
public static event Action<TwitchRaidEvent> OnRaid;
public static event Action<TwitchRoomState> OnRoomStateUpdate;
public static void Connect()
{
TwitchChat.Connect();
}
public static void Connect(string channel)
{
TwitchChat.Connect(channel);
}
public static void Disconnect()
{
TwitchChat.Disconnect();
}
public static bool TryGetUserByUsername(string username, out TwitchUser twitchUser)
{
return UserHelper.TryGetUserByUsername(username, out twitchUser);
}
public static bool TryGetUserByUserId(string userId, out TwitchUser twitchUser)
{
return UserHelper.TryGetUserByUserId(userId, out twitchUser);
}
public static TwitchUser[] GetUsersSeenWithin(TimeSpan timeSpan)
{
return UserHelper.GetUsersSeenWithin(timeSpan);
}
internal static void InvokeOnConnectionStateChanged(ConnectionState state)
{
MainThreadDispatcher.Enqueue(delegate
{
API.OnConnectionStateChanged?.Invoke(state);
});
}
internal static void InvokeOnConnect()
{
MainThreadDispatcher.Enqueue(delegate
{
API.OnConnect?.Invoke();
});
}
internal static void InvokeOnDisconnect()
{
MainThreadDispatcher.Enqueue(delegate
{
API.OnDisconnect?.Invoke();
});
}
internal static void InvokeOnMessage(TwitchMessage message)
{
MainThreadDispatcher.Enqueue(delegate
{
API.OnMessage?.Invoke(message);
});
}
internal static void InvokeOnSub(TwitchSubEvent subEvent)
{
MainThreadDispatcher.Enqueue(delegate
{
API.OnSub?.Invoke(subEvent);
});
}
internal static void InvokeOnCheer(TwitchCheerEvent cheerEvent)
{
MainThreadDispatcher.Enqueue(delegate
{
API.OnCheer?.Invoke(cheerEvent);
});
}
internal static void InvokeOnRaid(TwitchRaidEvent raidEvent)
{
MainThreadDispatcher.Enqueue(delegate
{
API.OnRaid?.Invoke(raidEvent);
});
}
internal static void InvokeOnRoomStateUpdate(TwitchRoomState roomState)
{
MainThreadDispatcher.Enqueue(delegate
{
API.OnRoomStateUpdate?.Invoke(roomState);
});
}
}
internal static class ConfigManager
{
public static ConfigFile ConfigFile { get; private set; }
public static ConfigEntry<bool> ExtendedLogging { get; private set; }
public static ConfigEntry<bool> TwitchChat_Enabled { get; private set; }
public static ConfigEntry<string> TwitchChat_Channel { get; private set; }
public static void Initialize(ConfigFile configFile)
{
ConfigFile = configFile;
BindConfigs();
}
private static void BindConfigs()
{
ExtendedLogging = ConfigFile.Bind<bool>("General", "ExtendedLogging", false, "Enable extended logging.");
TwitchChat_Enabled = ConfigFile.Bind<bool>("Twitch Chat", "Enabled", true, "Enable/Disable the connection to Twitch chat.");
TwitchChat_Channel = ConfigFile.Bind<string>("Twitch Chat", "Channel", "", "Your Twitch channel username.");
TwitchChat_Enabled.SettingChanged += delegate
{
TwitchChat.HandleEnabledChanged();
};
TwitchChat_Channel.SettingChanged += delegate
{
TwitchChat.HandleChannelChanged();
};
}
}
internal static class Logger
{
public static ManualLogSource ManualLogSource { get; private set; }
public static void Initialize(ManualLogSource manualLogSource)
{
ManualLogSource = manualLogSource;
}
public static void LogDebug(object data)
{
Log((LogLevel)32, data);
}
public static void LogInfo(object data, bool extended = false)
{
Log((LogLevel)16, data, extended);
}
public static void LogWarning(object data, bool extended = false)
{
Log((LogLevel)4, data, extended);
}
public static void LogError(object data, bool extended = false)
{
Log((LogLevel)2, data, extended);
}
public static void LogFatal(object data, bool extended = false)
{
Log((LogLevel)1, data, extended);
}
public static void Log(LogLevel logLevel, object data, bool extended = false)
{
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
if (!extended || IsExtendedLoggingEnabled())
{
ManualLogSource manualLogSource = ManualLogSource;
if (manualLogSource != null)
{
manualLogSource.Log(logLevel, data);
}
}
}
public static bool IsExtendedLoggingEnabled()
{
if (ConfigManager.ExtendedLogging == null)
{
return false;
}
return ConfigManager.ExtendedLogging.Value;
}
}
[BepInPlugin("TwitchChatAPI", "TwitchChatAPI", "2.0.0")]
public class Plugin : BaseUnityPlugin
{
public static Plugin Instance { get; private set; }
internal static ConfigFile Config { get; private set; }
internal static JsonSave GlobalSave { get; private set; }
private void Awake()
{
Instance = this;
Logger.Initialize(Logger.CreateLogSource("TwitchChatAPI"));
Logger.LogInfo("TwitchChatAPI has awoken!");
Config = Utils.CreateGlobalConfigFile((BaseUnityPlugin)(object)this);
GlobalSave = new JsonSave(Utils.GetPluginPersistentDataPath(), "GlobalSave");
ConfigManager.Initialize(Config);
MainThreadDispatcher.Initialize();
TwitchChat.Initialize();
}
}
internal static class TwitchChat
{
public const string ServerIP = "irc.chat.twitch.tv";
public const int ServerPort = 6667;
private static ConnectionState _connectionState = ConnectionState.None;
private static TcpClient _client;
private static NetworkStream _stream;
private static StreamReader _reader;
private static StreamWriter _writer;
private static CancellationTokenSource _cts;
private static bool _isReconnecting;
private static Task _reconnectTask;
private static CancellationTokenSource _reconnectCts;
private static int _reconnectDelay = 5000;
private static bool _explicitDisconnect;
private static readonly object _connectionLock = new object();
private static readonly SemaphoreSlim _readLock = new SemaphoreSlim(1, 1);
public static bool Enabled => ConfigManager.TwitchChat_Enabled.Value;
public static string Channel => ConfigManager.TwitchChat_Channel.Value.Trim();
public static ConnectionState ConnectionState
{
get
{
return _connectionState;
}
private set
{
if (_connectionState != value)
{
_connectionState = value;
API.InvokeOnConnectionStateChanged(_connectionState);
}
}
}
public static void Initialize()
{
Application.quitting += OnApplicationQuit;
if (Enabled)
{
Connect();
}
}
public static void Connect()
{
Task.Run((Func<Task?>)ConnectAsync);
}
public static void Connect(string channel)
{
ConfigManager.TwitchChat_Channel.Value = channel;
Connect();
}
public static async Task ConnectAsync()
{
lock (_connectionLock)
{
if (_isReconnecting)
{
CancelReconnect();
}
_explicitDisconnect = false;
_isReconnecting = false;
}
if (!Enabled)
{
Logger.LogError("Failed to connect to Twitch chat. Twitch chat has been disabled in the config settings.");
return;
}
if (ConnectionState == ConnectionState.Connecting)
{
Logger.LogWarning("Twitch chat is already connecting.");
return;
}
if (ConnectionState == ConnectionState.Connected)
{
Disconnect();
}
if (!UserHelper.IsValidUsername(Channel))
{
Logger.LogWarning("Failed to start Twitch chat connection: Invalid or empty channel name.");
return;
}
Logger.LogInfo("Establishing connection to Twitch chat...");
ConnectionState = ConnectionState.Connecting;
try
{
_cts = new CancellationTokenSource();
_client = new TcpClient();
await _client.ConnectAsync("irc.chat.twitch.tv", 6667);
_stream = _client.GetStream();
_reader = new StreamReader(_stream);
_writer = new StreamWriter(_stream)
{
AutoFlush = true
};
await _writer.WriteLineAsync("NICK justinfan123");
await _writer.WriteLineAsync("CAP REQ :twitch.tv/tags");
await _writer.WriteLineAsync("CAP REQ :twitch.tv/commands");
await _writer.WriteLineAsync("JOIN #" + Channel);
ConnectionState = ConnectionState.Connected;
_explicitDisconnect = false;
Logger.LogInfo("Successfully connected to Twitch chat " + Channel + ".");
API.InvokeOnConnect();
await Task.Run((Func<Task?>)ListenAsync, _cts.Token);
}
catch (Exception arg)
{
ConnectionState = ConnectionState.Disconnected;
_explicitDisconnect = false;
Logger.LogError($"Failed to connect to Twitch chat {Channel}. {arg}");
ScheduleReconnect();
}
}
public static void Disconnect()
{
lock (_connectionLock)
{
_explicitDisconnect = true;
CancelReconnect();
if (ConnectionState != ConnectionState.Connected && ConnectionState != ConnectionState.Connecting)
{
Logger.LogInfo("Twitch chat is not connected or already disconnecting.");
return;
}
ConnectionState = ConnectionState.Disconnecting;
_cts?.Cancel();
DisconnectStreams();
ConnectionState = ConnectionState.Disconnected;
Logger.LogInfo("Twitch chat connection stopped.");
API.InvokeOnDisconnect();
}
}
private static void DisconnectStreams()
{
_writer?.Dispose();
_reader?.Dispose();
_stream?.Dispose();
_client?.Close();
_writer = null;
_reader = null;
_stream = null;
_client = null;
}
private static void ScheduleReconnect()
{
lock (_connectionLock)
{
if (!Enabled || _explicitDisconnect || _isReconnecting)
{
return;
}
Logger.LogInfo($"Reconnection to Twitch chat will be attempted in {_reconnectDelay / 1000} seconds.");
_isReconnecting = true;
_reconnectCts = new CancellationTokenSource();
_reconnectTask = Task.Delay(_reconnectDelay, _reconnectCts.Token).ContinueWith((Func<Task, Task>)async delegate(Task task)
{
if (!task.IsCanceled)
{
lock (_connectionLock)
{
_isReconnecting = false;
}
if (Enabled)
{
Logger.LogInfo("Attempting to reconnect to Twitch chat...");
await ConnectAsync();
}
}
});
}
}
private static void CancelReconnect()
{
lock (_connectionLock)
{
if (_reconnectTask != null && !_reconnectTask.IsCompleted)
{
_reconnectCts?.Cancel();
_reconnectTask = null;
}
_isReconnecting = false;
}
}
private static async Task ListenAsync()
{
if (ConnectionState != ConnectionState.Connected || _reader == null)
{
return;
}
try
{
while (_cts != null && !_cts.Token.IsCancellationRequested)
{
lock (_connectionLock)
{
if (_reader == null)
{
break;
}
}
string text = await SafeReadLineAsync(_cts.Token);
if (text != null)
{
if (text.StartsWith("PING"))
{
Logger.LogInfo("Received PING, sending PONG...", extended: true);
await (_writer?.WriteLineAsync("PONG :tmi.twitch.tv") ?? Task.CompletedTask).ConfigureAwait(continueOnCapturedContext: false);
}
else
{
MessageHelper.ProcessMessage(text);
}
}
}
}
catch (TaskCanceledException)
{
Logger.LogInfo("Twitch chat listen task canceled.");
}
catch (OperationCanceledException)
{
Logger.LogInfo("Twitch chat listen task canceled.");
}
catch (Exception arg)
{
Logger.LogError($"Twitch chat listen task failed. {arg}");
ScheduleReconnect();
}
finally
{
lock (_connectionLock)
{
ConnectionState = ConnectionState.Disconnected;
}
}
}
private static async Task<string> SafeReadLineAsync(CancellationToken cancellationToken)
{
await _readLock.WaitAsync(cancellationToken);
try
{
Task<string> readTask = _reader.ReadLineAsync();
if (await Task.WhenAny(new Task[2]
{
readTask,
Task.Delay(-1, cancellationToken)
}).ConfigureAwait(continueOnCapturedContext: false) == readTask)
{
return await readTask.ConfigureAwait(continueOnCapturedContext: false);
}
throw new OperationCanceledException(cancellationToken);
}
finally
{
_readLock.Release();
}
}
private static void OnApplicationQuit()
{
Logger.LogInfo("Application is quitting. Disconnecting Twitch chat...");
Disconnect();
}
public static void HandleEnabledChanged()
{
if (Enabled)
{
Connect();
}
else
{
Disconnect();
}
}
public static void HandleChannelChanged()
{
if (Enabled)
{
Connect();
}
}
}
internal static class Utils
{
public static string GetPluginPersistentDataPath()
{
return Path.Combine(Application.persistentDataPath, "TwitchChatAPI");
}
public static ConfigFile CreateConfigFile(BaseUnityPlugin plugin, string path, string name = null, bool saveOnInit = false)
{
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Expected O, but got Unknown
BepInPlugin metadata = MetadataHelper.GetMetadata((object)plugin);
if (name == null)
{
name = metadata.GUID;
}
name += ".cfg";
return new ConfigFile(Path.Combine(path, name), saveOnInit, metadata);
}
public static ConfigFile CreateLocalConfigFile(BaseUnityPlugin plugin, string name = null, bool saveOnInit = false)
{
return CreateConfigFile(plugin, Paths.ConfigPath, name, saveOnInit);
}
public static ConfigFile CreateGlobalConfigFile(BaseUnityPlugin plugin, string name = null, bool saveOnInit = false)
{
string pluginPersistentDataPath = GetPluginPersistentDataPath();
if (name == null)
{
name = "global";
}
return CreateConfigFile(plugin, pluginPersistentDataPath, name, saveOnInit);
}
}
public static class MyPluginInfo
{
public const string PLUGIN_GUID = "TwitchChatAPI";
public const string PLUGIN_NAME = "TwitchChatAPI";
public const string PLUGIN_VERSION = "2.0.0";
}
}
namespace TwitchChatAPI.Objects
{
internal class JsonSave
{
private JObject _data;
public string DirectoryPath { get; private set; }
public string FileName { get; private set; }
public string FilePath => Path.Combine(DirectoryPath, FileName);
public JsonSave(string directoryPath, string fileName)
{
DirectoryPath = directoryPath;
FileName = fileName;
_data = ReadFile();
}
public bool KeyExists(string key)
{
if (_data == null)
{
Logger.LogError("KeyExists: Data is null. Ensure the save file is properly loaded.");
return false;
}
return _data.ContainsKey(key);
}
public T Load<T>(string key, T defaultValue = default(T), bool readFile = false)
{
if (TryLoad<T>(key, out var value, readFile))
{
return value;
}
return defaultValue;
}
public bool TryLoad<T>(string key, out T value, bool readFile = false)
{
//IL_0057: Expected O, but got Unknown
value = default(T);
if (readFile)
{
_data = ReadFile();
}
if (_data == null)
{
Logger.LogError("Load: Data is null. Returning default value for key: " + key + ".");
return false;
}
JToken val = default(JToken);
if (_data.TryGetValue(key, ref val))
{
try
{
value = val.ToObject<T>();
return true;
}
catch (JsonException val2)
{
JsonException val3 = val2;
Logger.LogError("Load: JSON Conversion Error for key: " + key + ". " + ((Exception)(object)val3).Message);
}
catch (ArgumentNullException ex)
{
Logger.LogError("Load: Argument Null Error for key: " + key + ". " + ex.Message);
}
catch (Exception ex2)
{
Logger.LogError("Load: Unexpected Error for key: " + key + ". " + ex2.Message);
}
return false;
}
Logger.LogWarning("Load: Key '" + key + "' does not exist. Returning default value.", extended: true);
return false;
}
public bool Save<T>(string key, T value)
{
if (_data == null)
{
Logger.LogError("Save: Data is null. Cannot save key: " + key + ".");
return false;
}
try
{
JToken val = JToken.FromObject((object)value);
if (_data.ContainsKey(key))
{
_data[key] = val;
}
else
{
_data.Add(key, val);
}
return WriteFile(_data);
}
catch (Exception ex)
{
Logger.LogError("Save: Error saving key: " + key + ". " + ex.Message);
return false;
}
}
private JObject ReadFile()
{
//IL_0070: Expected O, but got Unknown
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Expected O, but got Unknown
//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
//IL_00bf: Expected O, but got Unknown
try
{
if (!File.Exists(FilePath))
{
Logger.LogWarning("ReadFile: Save file does not exist at \"" + FilePath + "\". Initializing with an empty file.", extended: true);
return new JObject();
}
using FileStream stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
using StreamReader streamReader = new StreamReader(stream, Encoding.UTF8);
return JObject.Parse(streamReader.ReadToEnd());
}
catch (JsonException val)
{
JsonException val2 = val;
Logger.LogError("ReadFile: JSON Parsing Error for file: \"" + FilePath + "\". " + ((Exception)(object)val2).Message);
}
catch (Exception ex)
{
Logger.LogError("ReadFile: Unexpected Error for file: \"" + FilePath + "\". " + ex.Message);
}
return new JObject();
}
private bool WriteFile(JObject data)
{
try
{
if (!Directory.Exists(DirectoryPath))
{
Directory.CreateDirectory(DirectoryPath);
}
File.WriteAllText(FilePath, ((object)data).ToString(), Encoding.UTF8);
return true;
}
catch (Exception ex)
{
Logger.LogError("WriteFile: Unexpected Error for file: \"" + FilePath + "\". " + ex.Message);
}
return false;
}
}
internal class JsonSaveValue<T> : ObservableValue<T>
{
public JsonSave JsonSave { get; private set; }
public string Key { get; private set; }
public T DefaultValue { get; private set; }
public bool ReadFile { get; private set; }
public bool HasValue
{
get
{
T value;
return TryLoad(out value);
}
}
public JsonSaveValue(JsonSave jsonSave, string key, T defaultValue = default(T), bool readFile = false)
: base(default(T))
{
JsonSave = jsonSave;
Key = key;
DefaultValue = defaultValue;
ReadFile = readFile;
CustomValueGetter = Load;
CustomValueSetter = Save;
}
private T Load()
{
return JsonSave.Load(Key, DefaultValue, ReadFile);
}
private bool TryLoad(out T value)
{
return JsonSave.TryLoad<T>(Key, out value, ReadFile);
}
private void Save(T value)
{
if (!object.Equals(value, base.Value))
{
JsonSave.Save(Key, value);
}
}
}
internal class ObservableValue<T>
{
protected T _value;
protected Func<T> CustomValueGetter;
protected Action<T> CustomValueSetter;
public T Value
{
get
{
return GetValue();
}
set
{
SetValue(value);
}
}
public event Action<T> OnValueChanged;
public ObservableValue(T initialValue = default(T))
{
_value = initialValue;
}
private T GetValue()
{
if (CustomValueGetter != null)
{
_value = CustomValueGetter();
}
return _value;
}
private void SetValue(T value)
{
if (!object.Equals(_value, value))
{
_value = value;
CustomValueSetter?.Invoke(value);
this.OnValueChanged?.Invoke(value);
}
}
}
public abstract class TwitchEvent
{
public string Channel { get; set; }
public TwitchUser User { get; set; }
public string Message { get; set; }
public Dictionary<string, string> Tags { get; set; }
public TwitchEvent RemoveTags()
{
Tags = new Dictionary<string, string>();
return this;
}
}
public class TwitchSubEvent : TwitchEvent
{
public SubType Type { get; set; }
public SubTier Tier { get; set; }
public int CumulativeMonths { get; set; }
public string RecipientUser { get; set; }
public int GiftCount { get; set; }
[Obsolete("Use Type instead.", true)]
public SubType SubType => Type;
[Obsolete("Use CumulativeMonths instead.", true)]
public int Months => CumulativeMonths;
[Obsolete("Use SubTier.Prime instead.", true)]
public bool IsPrime => Tier == SubTier.Prime;
}
public class TwitchCheerEvent : TwitchEvent
{
public int CheerAmount { get; set; }
}
public class TwitchRaidEvent : TwitchEvent
{
public int ViewerCount { get; set; }
}
public struct TwitchMessage
{
public string Channel { get; set; }
public TwitchUser User { get; set; }
public string Message { get; set; }
public Dictionary<string, string> Tags { get; set; }
public TwitchMessage RemoveTags()
{
TwitchMessage result = this;
result.Tags = new Dictionary<string, string>();
return result;
}
}
public struct TwitchRoomState
{
public string Channel { get; set; }
public bool IsEmoteOnly { get; set; }
public bool IsFollowersOnly { get; set; }
public bool IsR9K { get; set; }
public bool IsSlowMode { get; set; }
public bool IsSubsOnly { get; set; }
public Dictionary<string, string> Tags { get; set; }
public TwitchRoomState RemoveTags()
{
TwitchRoomState result = this;
result.Tags = new Dictionary<string, string>();
return result;
}
}
public struct TwitchUser : IEquatable<TwitchUser>
{
public string UserId { get; set; }
public string Username { get; set; }
public string DisplayName { get; set; }
public string Color { get; set; }
public bool IsVIP { get; set; }
public bool IsSubscriber { get; set; }
public bool IsModerator { get; set; }
public bool IsBroadcaster { get; set; }
public override bool Equals(object obj)
{
if (obj is TwitchUser other)
{
return Equals(other);
}
return false;
}
public bool Equals(TwitchUser other)
{
return string.Equals(UserId, other.UserId, StringComparison.Ordinal);
}
public override int GetHashCode()
{
return UserId?.GetHashCode() ?? 0;
}
public static bool operator ==(TwitchUser left, TwitchUser right)
{
return left.Equals(right);
}
public static bool operator !=(TwitchUser left, TwitchUser right)
{
return !left.Equals(right);
}
}
}
namespace TwitchChatAPI.MonoBehaviours
{
public class MainThreadDispatcher : MonoBehaviour
{
private static readonly Queue<Action> _actions = new Queue<Action>();
public static MainThreadDispatcher Instance { get; private set; }
public static void Initialize()
{
//IL_0013: Unknown result type (might be due to invalid IL or missing references)
//IL_0018: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Expected O, but got Unknown
if (!((Object)(object)Instance != (Object)null))
{
GameObject val = new GameObject("TwitchChatAPI MainThreadDispatcher")
{
hideFlags = (HideFlags)61
};
Object.DontDestroyOnLoad((Object)(object)val);
val.AddComponent<MainThreadDispatcher>();
}
}
private void Awake()
{
if ((Object)(object)Instance != (Object)null && (Object)(object)Instance != (Object)(object)this)
{
Object.Destroy((Object)(object)((Component)this).gameObject);
return;
}
Instance = this;
Logger.LogInfo("Spawned \"" + ((Object)((Component)this).gameObject).name + "\"", extended: true);
}
private void Update()
{
lock (_actions)
{
while (_actions.Count > 0)
{
_actions.Dequeue()?.Invoke();
}
}
}
public static void Enqueue(Action action)
{
lock (_actions)
{
_actions.Enqueue(action);
}
}
}
}
namespace TwitchChatAPI.Helpers
{
internal static class MessageHelper
{
public static void ProcessMessage(string message)
{
try
{
if (message.StartsWith("@") && message.Contains("PRIVMSG"))
{
ProcessMessage_PRIVMSG(message);
}
else if (message.StartsWith("@") && message.Contains("USERNOTICE"))
{
ProcessMessage_USERNOTICE(message);
}
else if (message.StartsWith("@") && message.Contains("ROOMSTATE"))
{
ProcessMessage_ROOMSTATE(message);
}
else
{
Logger.LogInfo("Unhandled RAW message: " + message, extended: true);
}
}
catch (Exception arg)
{
Logger.LogError($"Failed to process message:\n\n{message}\n\nError: {arg}");
}
}
private static void ProcessMessage_PRIVMSG(string message)
{
try
{
string text = message.Split(' ')[0].Substring(1);
Dictionary<string, string> dictionary = text.Split(';').ToDictionary((string tag) => tag.Split('=')[0], delegate(string tag)
{
if (!tag.Contains('='))
{
return string.Empty;
}
int num3 = tag.IndexOf('=') + 1;
return tag.Substring(num3, tag.Length - num3);
});
string text2 = message.Split("PRIVMSG")[1];
string channel = text2.Split(' ', StringSplitOptions.RemoveEmptyEntries)[0].Trim().TrimStart('#');
string text3 = string.Empty;
int num = text2.IndexOf(':');
if (num != -1)
{
string text4 = text2;
int num2 = num + 1;
text3 = text4.Substring(num2, text4.Length - num2).Trim();
}
TwitchUser twitchUser = GetTwitchUser(channel, dictionary);
if (!twitchUser.Equals(default(TwitchUser)))
{
UserHelper.UpdateUser(twitchUser);
if (dictionary.ContainsKey("bits"))
{
ProcessMessage_PRIVMSG_Cheer(message, channel, twitchUser, text3, dictionary);
return;
}
TwitchMessage twitchMessage = default(TwitchMessage);
twitchMessage.Channel = channel;
twitchMessage.User = twitchUser;
twitchMessage.Message = text3;
twitchMessage.Tags = dictionary;
TwitchMessage message2 = twitchMessage;
API.InvokeOnMessage(message2);
}
}
catch (Exception arg)
{
Logger.LogError($"Failed to process PRIVMSG message:\n\n{message}\n\nError: {arg}");
}
}
private static void ProcessMessage_PRIVMSG_Cheer(string message, string channel, TwitchUser twitchUser, string chatMessage, Dictionary<string, string> tags)
{
try
{
string[] value = new string[25]
{
"Cheer", "cheerwhal", "Corgo", "uni", "ShowLove", "Party", "SeemsGood", "Pride", "Kappa", "FrankerZ",
"HeyGuys", "DansGame", "EleGiggle", "TriHard", "Kreygasm", "4Head", "SwiftRage", "NotLikeThis", "FailFish", "VoHiYo",
"PJSalt", "MrDestructoid", "bday", "RIPCheer", "Shamrock"
};
string pattern = "\\b(" + string.Join("|", value) + ")\\d+\\b";
chatMessage = Regex.Replace(chatMessage, pattern, string.Empty, RegexOptions.IgnoreCase).Trim();
TwitchCheerEvent twitchCheerEvent = new TwitchCheerEvent
{
Channel = channel,
User = twitchUser,
Message = chatMessage,
Tags = tags,
CheerAmount = int.Parse(tags.GetValueOrDefault("bits", "0"))
};
Logger.LogInfo("RAW cheer message: " + message, extended: true);
Logger.LogInfo($"[!] Cheer event: {twitchCheerEvent.User.DisplayName} cheered {twitchCheerEvent.CheerAmount} bits!\n{JsonConvert.SerializeObject((object)twitchCheerEvent, (Formatting)1)}", extended: true);
API.InvokeOnCheer(twitchCheerEvent);
}
catch (Exception arg)
{
Logger.LogError($"Failed to process PRIVMSG message:\n\n{message}\n\nError: {arg}");
}
}
private static void ProcessMessage_USERNOTICE(string message)
{
try
{
string text = message.Split(' ')[0].Substring(1);
Dictionary<string, string> dictionary = text.Split(';').ToDictionary((string tag) => tag.Split('=')[0], delegate(string tag)
{
if (!tag.Contains('='))
{
return string.Empty;
}
int num3 = tag.IndexOf('=') + 1;
return tag.Substring(num3, tag.Length - num3);
});
string valueOrDefault = dictionary.GetValueOrDefault("msg-id", string.Empty);
if (string.IsNullOrEmpty(valueOrDefault))
{
Logger.LogError("Failed to process USERNOTICE message:\n\n" + message);
return;
}
string text2 = message.Split("USERNOTICE")[1];
string channel = text2.Split(' ', StringSplitOptions.RemoveEmptyEntries)[0].Trim().TrimStart('#');
string chatMessage = string.Empty;
int num = text2.IndexOf(':');
if (num != -1)
{
string text3 = text2;
int num2 = num + 1;
chatMessage = text3.Substring(num2, text3.Length - num2).Trim();
}
TwitchUser twitchUser = GetTwitchUser(channel, dictionary);
if (!twitchUser.Equals(default(TwitchUser)))
{
UserHelper.UpdateUser(twitchUser);
switch (valueOrDefault)
{
case "sub":
case "resub":
case "subgift":
case "submysterygift":
ProcessMessage_USERNOTICE_Sub(message, channel, twitchUser, chatMessage, dictionary);
break;
case "raid":
ProcessMessage_USERNOTICE_Raid(message, channel, twitchUser, dictionary);
break;
default:
Logger.LogInfo("Unhandled USERNOTICE message: " + message, extended: true);
break;
}
}
}
catch (Exception arg)
{
Logger.LogError($"Failed to process USERNOTICE message:\n\n{message}\n\nError: {arg}");
}
}
private static void ProcessMessage_USERNOTICE_Sub(string message, string channel, TwitchUser twitchUser, string chatMessage, Dictionary<string, string> tags)
{
try
{
string valueOrDefault = tags.GetValueOrDefault("msg-id", string.Empty);
if (string.IsNullOrEmpty(valueOrDefault))
{
Logger.LogError("Failed to process USERNOTICE message: " + message);
return;
}
SubType subType = SubType.Sub;
switch (valueOrDefault)
{
case "resub":
subType = SubType.Resub;
break;
case "subgift":
subType = SubType.SubGift;
break;
case "submysterygift":
subType = SubType.SubMysteryGift;
break;
}
if (subType == SubType.SubGift && tags.ContainsKey("msg-param-community-gift-id"))
{
Logger.LogInfo("Skipping subgift since it originates from a submysterygift. Message: " + message, extended: true);
return;
}
SubTier tier = SubTier.One;
if (tags.TryGetValue("msg-param-sub-plan", out var value) && !string.IsNullOrEmpty(value))
{
switch (value)
{
case "Prime":
tier = SubTier.Prime;
break;
case "1000":
tier = SubTier.One;
break;
case "2000":
tier = SubTier.Two;
break;
case "3000":
tier = SubTier.Three;
break;
}
}
TwitchSubEvent twitchSubEvent = new TwitchSubEvent
{
Channel = channel,
User = twitchUser,
Message = chatMessage,
Tags = tags,
Type = subType,
Tier = tier,
CumulativeMonths = int.Parse(tags.GetValueOrDefault("msg-param-cumulative-months", "0")),
RecipientUser = tags.GetValueOrDefault("msg-param-recipient-display-name", string.Empty),
GiftCount = int.Parse(tags.GetValueOrDefault("msg-param-mass-gift-count", "0"))
};
Logger.LogInfo("RAW subscription message: " + message, extended: true);
Logger.LogInfo("[!] Subscription event: \n" + JsonConvert.SerializeObject((object)twitchSubEvent, (Formatting)1), extended: true);
API.InvokeOnSub(twitchSubEvent);
}
catch (Exception arg)
{
Logger.LogError($"Failed to process USERNOTICE message:\n\n{message}\n\nError: {arg}");
}
}
private static void ProcessMessage_USERNOTICE_Raid(string message, string channel, TwitchUser twitchUser, Dictionary<string, string> tags)
{
try
{
TwitchRaidEvent twitchRaidEvent = new TwitchRaidEvent
{
Channel = channel,
User = twitchUser,
Message = string.Empty,
Tags = tags,
ViewerCount = int.Parse(tags.GetValueOrDefault("msg-param-viewerCount", "0"))
};
Logger.LogInfo("RAW raid message: " + message, extended: true);
Logger.LogInfo($"[!] Raid detected: {twitchRaidEvent.User.DisplayName} is raiding with {twitchRaidEvent.ViewerCount} viewers!\n{JsonConvert.SerializeObject((object)twitchRaidEvent, (Formatting)1)}", extended: true);
API.InvokeOnRaid(twitchRaidEvent);
}
catch (Exception arg)
{
Logger.LogError($"Failed to process USERNOTICE message:\n\n{message}\n\nError: {arg}");
}
}
private static void ProcessMessage_ROOMSTATE(string message)
{
try
{
string text = message.Split(' ')[0].Substring(1);
Dictionary<string, string> dictionary = text.Split(';').ToDictionary((string tag) => tag.Split('=')[0], (string tag) => (!tag.Contains('=')) ? "" : tag.Split('=')[1]);
string channel = message.Split("ROOMSTATE")[1].Trim().TrimStart('#');
TwitchRoomState twitchRoomState = default(TwitchRoomState);
twitchRoomState.Channel = channel;
twitchRoomState.IsEmoteOnly = dictionary.ContainsKey("emote-only") && dictionary["emote-only"] == "1";
twitchRoomState.IsFollowersOnly = dictionary.ContainsKey("followers-only") && dictionary["followers-only"] != "-1";
twitchRoomState.IsR9K = dictionary.ContainsKey("r9k") && dictionary["r9k"] == "1";
twitchRoomState.IsSlowMode = dictionary.ContainsKey("slow") && dictionary["slow"] != "0";
twitchRoomState.IsSubsOnly = dictionary.ContainsKey("subs-only") && dictionary["subs-only"] == "1";
twitchRoomState.Tags = dictionary;
TwitchRoomState twitchRoomState2 = twitchRoomState;
Logger.LogInfo("RAW roomstate message: " + message, extended: true);
Logger.LogInfo("[!] Room state change detected: \n" + JsonConvert.SerializeObject((object)twitchRoomState2, (Formatting)1), extended: true);
API.InvokeOnRoomStateUpdate(twitchRoomState2);
}
catch (Exception arg)
{
Logger.LogError($"Failed to process ROOMSTATE message:\n\n{message}\n\nError: {arg}");
}
}
private static TwitchUser GetTwitchUser(string channel, Dictionary<string, string> tags)
{
try
{
string valueOrDefault = tags.GetValueOrDefault("display-name", "Anonymous");
string text = tags.GetValueOrDefault("color", "#FFFFFF");
if (string.IsNullOrEmpty(text))
{
text = "#FFFFFF";
}
TwitchUser result = default(TwitchUser);
result.UserId = tags.GetValueOrDefault("user-id", "0");
result.Username = valueOrDefault.ToLower();
result.DisplayName = valueOrDefault;
result.Color = text;
result.IsVIP = tags.TryGetValue("vip", out var value) && value == "1";
result.IsSubscriber = tags.TryGetValue("subscriber", out var value2) && value2 == "1";
result.IsModerator = tags.TryGetValue("mod", out var value3) && value3 == "1";
result.IsBroadcaster = valueOrDefault.Equals(channel, StringComparison.OrdinalIgnoreCase);
return result;
}
catch (Exception arg)
{
Logger.LogError($"Failed to get TwitchUser: {arg}");
return default(TwitchUser);
}
}
}
internal static class UserHelper
{
public static Dictionary<string, TwitchUser> Users { get; private set; } = new Dictionary<string, TwitchUser>();
public static Dictionary<string, string> UsernameToUserId { get; private set; } = new Dictionary<string, string>();
public static Dictionary<string, float> TimeLastSeen { get; private set; } = new Dictionary<string, float>();
public static bool TryGetUserByUsername(string username, out TwitchUser twitchUser)
{
if (UsernameToUserId.TryGetValue(username.ToLower(), out var value) && Users.TryGetValue(value, out twitchUser))
{
return true;
}
twitchUser = default(TwitchUser);
return false;
}
public static bool TryGetUserByUserId(string userId, out TwitchUser twitchUser)
{
return Users.TryGetValue(userId, out twitchUser);
}
public static TwitchUser[] GetUsersSeenWithin(TimeSpan timeSpan)
{
float realtimeSinceStartup = Time.realtimeSinceStartup;
float minTime = realtimeSinceStartup - (float)timeSpan.TotalSeconds;
TwitchUser value;
return (from entry in TimeLastSeen
where entry.Value >= minTime
select (!Users.TryGetValue(entry.Key, out value)) ? default(TwitchUser) : value into user
where true
select user).ToArray();
}
public static void UpdateUser(TwitchUser twitchUser)
{
if (!twitchUser.Equals(default(TwitchUser)))
{
Users[twitchUser.UserId] = twitchUser;
string key = twitchUser.Username.ToLower();
if (UsernameToUserId.TryGetValue(key, out var value) && value != twitchUser.UserId)
{
UsernameToUserId.Remove(key);
}
UsernameToUserId[key] = twitchUser.UserId;
TimeLastSeen[twitchUser.UserId] = Time.realtimeSinceStartup;
}
}
public static bool IsValidUsername(string username)
{
if (string.IsNullOrWhiteSpace(username))
{
return false;
}
Regex regex = new Regex("^[A-Za-z0-9][A-Za-z0-9_]{3,24}$");
if (regex.IsMatch(username))
{
return !username.StartsWith("_");
}
return false;
}
}
}
namespace TwitchChatAPI.Extensions
{
public static class TwitchUserExtensions
{
public static string GetDisplayNameWithColor(this TwitchUser twitchUser)
{
return "<color=" + twitchUser.Color + ">" + twitchUser.DisplayName + "</color>";
}
public static string GetDisplayNameWithColor(this TwitchUser twitchUser, Func<string, string> colorParser)
{
string text = ((colorParser != null) ? colorParser(twitchUser.Color) : twitchUser.Color);
return "<color=" + text + ">" + twitchUser.DisplayName + "</color>";
}
}
}
namespace TwitchChatAPI.Enums
{
public enum ConnectionState
{
None,
Connecting,
Connected,
Disconnecting,
Disconnected
}
public enum SubTier
{
Prime,
One,
Two,
Three
}
public enum SubType
{
Sub,
Resub,
SubGift,
SubMysteryGift
}
}
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
internal sealed class IgnoresAccessChecksToAttribute : Attribute
{
public IgnoresAccessChecksToAttribute(string assemblyName)
{
}
}
}
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
[ExcludeFromCodeCoverage]
[DebuggerNonUserCode]
internal sealed class MemberNotNullAttribute : Attribute
{
public string[] Members { get; }
public MemberNotNullAttribute(string member)
{
Members = new string[1] { member };
}
public MemberNotNullAttribute(params string[] members)
{
Members = members;
}
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
[ExcludeFromCodeCoverage]
[DebuggerNonUserCode]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
public bool ReturnValue { get; }
public string[] Members { get; }
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = new string[1] { member };
}
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
}
}