mirror of
https://github.com/Ikatono/TwitchIrcClient.git
synced 2025-10-29 04:56:12 -05:00
staging work then abandoning the project, dealing with Twitch APIs isn't worth the effor
This commit is contained in:
145
TwitchIrcClient/ApiClient/ApiClient.cs
Normal file
145
TwitchIrcClient/ApiClient/ApiClient.cs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
using Microsoft.VisualBasic;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TwitchIrcClient.ApiClient.Messages;
|
||||||
|
using TwitchIrcClient.Authentication;
|
||||||
|
using TwitchIrcClient.EventSub;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.ApiClient
|
||||||
|
{
|
||||||
|
public class ApiClient : IDisposable
|
||||||
|
{
|
||||||
|
private readonly HttpClient Client = new();
|
||||||
|
private readonly CancellationTokenSource TokenSource = new();
|
||||||
|
public CancellationToken CancellationToken => TokenSource.Token;
|
||||||
|
public string? ClientId { get; set; }
|
||||||
|
|
||||||
|
public async Task<EventSubResponse?> CreateWebsocketSubscriptionAsync(EventSubWebsocketClient eswClient,
|
||||||
|
UserAccessAuthentication auth, string type, string version,
|
||||||
|
IDictionary<string, string> condition)
|
||||||
|
{
|
||||||
|
var req = new EventSubRequest(type, version, condition,
|
||||||
|
ApiTransport.MakeForWebsocket(
|
||||||
|
eswClient.SessionId ?? throw new InvalidOperationException(
|
||||||
|
"no session id, did websocket connection fail?")));
|
||||||
|
using var content = JsonContent.Create(req);
|
||||||
|
content.Headers.Add("Authorization", $"Bearer: {auth.Token}");
|
||||||
|
content.Headers.Add("Client-Id", auth.ClientId);
|
||||||
|
using var resp = await Client.PostAsync("https://api.twitch.tv/helix/eventsub/subscriptions",
|
||||||
|
content, CancellationToken);
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
return null;
|
||||||
|
var j_resp = JsonSerializer.Deserialize<EventSubResponse>(await resp.Content.ReadAsStringAsync());
|
||||||
|
if (j_resp is null)
|
||||||
|
return null;
|
||||||
|
eswClient.TotalCost = j_resp.TotalCost;
|
||||||
|
eswClient.TotalSubscriptions = j_resp.Total;
|
||||||
|
eswClient.MaximumCost = j_resp.MaxTotalCost;
|
||||||
|
return j_resp;
|
||||||
|
}
|
||||||
|
public async Task<bool> DeleteWebSocketSubscriptionAsync(UserAccessAuthentication auth, string id)
|
||||||
|
{
|
||||||
|
using var req = new HttpRequestMessage();
|
||||||
|
req.RequestUri = new Uri($"https://api.twitch.tv/helix/eventsub/subscriptions?id={id}");
|
||||||
|
req.Headers.Add("Authorization", $"Bearer: {auth.Token}");
|
||||||
|
req.Headers.Add("Client-Id", auth.ClientId);
|
||||||
|
var resp = await Client.SendAsync(req);
|
||||||
|
return resp.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
public async IAsyncEnumerable<EventSubResponseItem> GetWebsocketSubscriptionsAsync(
|
||||||
|
UserAccessAuthentication auth, EventSubSubscriptionStatus? status = null,
|
||||||
|
string? type = null, string? userId = null)
|
||||||
|
{
|
||||||
|
var attrs = new Dictionary<string, string>();
|
||||||
|
if (status is EventSubSubscriptionStatus _status)
|
||||||
|
attrs["status"] = EventSubSubscriptionStatusConverter.Convert(_status);
|
||||||
|
if (type is string _type)
|
||||||
|
attrs["type"] = _type;
|
||||||
|
if (userId is string _userId)
|
||||||
|
attrs["status"] = _userId;
|
||||||
|
if (attrs.Count >= 2)
|
||||||
|
throw new ArgumentException("cannot set more that 1 filter parameter");
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
using var req = new HttpRequestMessage();
|
||||||
|
req.RequestUri = new Uri("https://api.twitch.tv/helix/eventsub/subscriptions?"
|
||||||
|
+ string.Join(',', attrs.Select(p => $"{p.Key}={p.Value}")));
|
||||||
|
req.Headers.Add("Authorization", $"Bearer {auth.Token}");
|
||||||
|
req.Headers.Add("Client-Id", auth.ClientId);
|
||||||
|
using var resp = await Client.SendAsync(req, CancellationToken);
|
||||||
|
if (CancellationToken.IsCancellationRequested || resp is null)
|
||||||
|
yield break;
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
yield break;
|
||||||
|
var esslr = JsonSerializer.Deserialize<EventSubSubscriptionListResponse>(
|
||||||
|
await resp.Content.ReadAsStringAsync());
|
||||||
|
if (esslr is null)
|
||||||
|
yield break;
|
||||||
|
foreach (var item in esslr.Data)
|
||||||
|
yield return item;
|
||||||
|
if (esslr.Pagination is EventSubSubscriptionListResponsePagination pagination)
|
||||||
|
attrs["cursor"] = pagination.Cursor;
|
||||||
|
else
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async Task<DcfCodeMessage?> GetDcfTokenAsync(string clientId,
|
||||||
|
IEnumerable<string> scopes, AuthorizationCallback callback)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(callback, nameof(callback));
|
||||||
|
ArgumentNullException.ThrowIfNullOrWhiteSpace(clientId, nameof(clientId));
|
||||||
|
using var req = new HttpRequestMessage();
|
||||||
|
req.Content = new FormUrlEncodedContent([
|
||||||
|
new ("client_id", clientId),
|
||||||
|
new ("scopes", string.Join(' ', scopes)),
|
||||||
|
]);
|
||||||
|
req.RequestUri = new Uri("https://id.twitch.tv/oauth2/device");
|
||||||
|
using var resp = await Client.SendAsync(req, CancellationToken);
|
||||||
|
if (resp is null)
|
||||||
|
return null;
|
||||||
|
var dcm = JsonSerializer.Deserialize<DcfCodeMessage>(await resp.Content.ReadAsStringAsync());
|
||||||
|
if (dcm is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
using TcpListener listener = new();
|
||||||
|
var auth_task = callback.Invoke(dcm.VerificationUri);
|
||||||
|
if (auth_task is null)
|
||||||
|
return null;
|
||||||
|
var success = await auth_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IDisposable
|
||||||
|
private bool disposedValue;
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!disposedValue)
|
||||||
|
{
|
||||||
|
TokenSource.Cancel();
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
Client.Dispose();
|
||||||
|
}
|
||||||
|
disposedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
#endregion //IDisposable
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Callback for user to authorize the app
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
/// <returns>true if successful</returns>
|
||||||
|
public delegate Task<bool> AuthorizationCallback(string url);
|
||||||
|
}
|
||||||
53
TwitchIrcClient/ApiClient/Messages/ApiTransport.cs
Normal file
53
TwitchIrcClient/ApiClient/Messages/ApiTransport.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TwitchIrcClient.EventSub.Messages;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.ApiClient.Messages
|
||||||
|
{
|
||||||
|
public record class ApiTransport
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonConverter(typeof(TwitchTransportTypeConverter))]
|
||||||
|
[JsonPropertyName("method")]
|
||||||
|
public TwitchTransportType Method { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("callback")]
|
||||||
|
public string? Callback { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("secret")]
|
||||||
|
public string? Secret { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("session_id")]
|
||||||
|
public string? SessionId { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("conduit_id")]
|
||||||
|
public string? ConduitId { get; set; }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public ApiTransport(TwitchTransportType method)
|
||||||
|
{
|
||||||
|
Method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApiTransport MakeForWebhook(string callback, string secret)
|
||||||
|
=> new(TwitchTransportType.Webhook)
|
||||||
|
{
|
||||||
|
Callback = callback,
|
||||||
|
Secret = secret,
|
||||||
|
};
|
||||||
|
public static ApiTransport MakeForWebsocket(string sessionId)
|
||||||
|
=> new(TwitchTransportType.Websocket)
|
||||||
|
{
|
||||||
|
SessionId = sessionId,
|
||||||
|
};
|
||||||
|
public static ApiTransport MakeForConduit(string conduitId)
|
||||||
|
=> new(TwitchTransportType.Conduit)
|
||||||
|
{
|
||||||
|
ConduitId = conduitId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
27
TwitchIrcClient/ApiClient/Messages/EventSubQueryRequest.cs
Normal file
27
TwitchIrcClient/ApiClient/Messages/EventSubQueryRequest.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TwitchIrcClient.EventSub.Messages;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.ApiClient.Messages
|
||||||
|
{
|
||||||
|
public record class EventSubQueryRequest
|
||||||
|
{
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonConverter(typeof(EventSubSubscriptionStatusConverter))]
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public EventSubSubscriptionStatus? Status { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string? Type { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("user_id")]
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("after")]
|
||||||
|
public string? After { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
36
TwitchIrcClient/ApiClient/Messages/EventSubRequest.cs
Normal file
36
TwitchIrcClient/ApiClient/Messages/EventSubRequest.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.ApiClient.Messages
|
||||||
|
{
|
||||||
|
public record class EventSubRequest
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string Version { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("condition")]
|
||||||
|
public Dictionary<string, string> Condition;
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("transport")]
|
||||||
|
public ApiTransport Transport { get; set; }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public EventSubRequest(string type, string version,
|
||||||
|
IEnumerable<KeyValuePair<string, string>> condition,
|
||||||
|
ApiTransport transport)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Version = version;
|
||||||
|
Condition = condition.ToDictionary();
|
||||||
|
Transport = transport;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
TwitchIrcClient/ApiClient/Messages/EventSubResponse.cs
Normal file
52
TwitchIrcClient/ApiClient/Messages/EventSubResponse.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.ApiClient.Messages
|
||||||
|
{
|
||||||
|
public record class EventSubResponseItem
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string Version { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("condition")]
|
||||||
|
public Dictionary<string, string> Condition { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("created_at")]
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("transport")]
|
||||||
|
public Dictionary<string, string> Transport { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("cost")]
|
||||||
|
public int Cost { get; set; }
|
||||||
|
}
|
||||||
|
public record class EventSubResponse
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public EventSubResponseItem[] Data { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("total")]
|
||||||
|
public int Total { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("total_cost")]
|
||||||
|
public int TotalCost { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("max_total_cost")]
|
||||||
|
public int MaxTotalCost { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.ApiClient.Messages
|
||||||
|
{
|
||||||
|
public record class EventSubSubscriptionListResponsePagination
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("cursor")]
|
||||||
|
public string Cursor { get; set; }
|
||||||
|
}
|
||||||
|
public record class EventSubSubscriptionListResponse
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public EventSubResponseItem[] Data { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("total")]
|
||||||
|
public int Total { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("total_cost")]
|
||||||
|
public int TotalCost { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("max_total_cost")]
|
||||||
|
public int MaxTotalCost { get; set; }
|
||||||
|
[JsonPropertyName("pagination")]
|
||||||
|
public EventSubSubscriptionListResponsePagination? Pagination { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.ApiClient.Messages
|
||||||
|
{
|
||||||
|
public enum EventSubSubscriptionStatus
|
||||||
|
{
|
||||||
|
Enabled = 0,
|
||||||
|
WebhookCallbackVerificationPending = 1,
|
||||||
|
WebhookCallbackVerificationFailed = 2,
|
||||||
|
NotificationFailuresExceeded = 3,
|
||||||
|
AuthorizationRevoked = 4,
|
||||||
|
ModeratorRemoved = 5,
|
||||||
|
UserRemoved = 6,
|
||||||
|
VersionRemoved = 7,
|
||||||
|
BetaMaintenance = 8,
|
||||||
|
WebsocketDisconnected = 9,
|
||||||
|
WebsocketFailedPingPong = 10,
|
||||||
|
WebsocketReceivedInboundTraffic = 11,
|
||||||
|
WebsocketConnectionUnused = 12,
|
||||||
|
WebsocketInternalError = 13,
|
||||||
|
WebsocketNetworkTimeout = 14,
|
||||||
|
WebsocketNetworkError = 15,
|
||||||
|
}
|
||||||
|
public class EventSubSubscriptionStatusConverter : JsonConverter<EventSubSubscriptionStatus>
|
||||||
|
{
|
||||||
|
private static readonly IList<KeyValuePair<EventSubSubscriptionStatus, string>> ConversionList =
|
||||||
|
[
|
||||||
|
new (EventSubSubscriptionStatus.Enabled, "enabled"),
|
||||||
|
new (EventSubSubscriptionStatus.WebhookCallbackVerificationPending, "webhook_callback_verification_pending"),
|
||||||
|
new (EventSubSubscriptionStatus.WebhookCallbackVerificationFailed, "webhook_callback_verification_failed"),
|
||||||
|
new (EventSubSubscriptionStatus.NotificationFailuresExceeded, "notification_failures_exceeded"),
|
||||||
|
new (EventSubSubscriptionStatus.AuthorizationRevoked, "authorization_revoked"),
|
||||||
|
new (EventSubSubscriptionStatus.ModeratorRemoved, "moderator_removed"),
|
||||||
|
new (EventSubSubscriptionStatus.UserRemoved, "user_removed"),
|
||||||
|
new (EventSubSubscriptionStatus.VersionRemoved, "version_removed"),
|
||||||
|
new (EventSubSubscriptionStatus.BetaMaintenance, "beta_maintenance"),
|
||||||
|
new (EventSubSubscriptionStatus.WebsocketDisconnected, "websocket_disconnected"),
|
||||||
|
new (EventSubSubscriptionStatus.WebsocketFailedPingPong, "websocket_failed_ping_pong"),
|
||||||
|
new (EventSubSubscriptionStatus.WebsocketReceivedInboundTraffic, "websocket_received_inbound_traffic"),
|
||||||
|
new (EventSubSubscriptionStatus.WebsocketConnectionUnused, "websocket_connection_unused"),
|
||||||
|
new (EventSubSubscriptionStatus.WebsocketInternalError, "websocket_internal_error"),
|
||||||
|
new (EventSubSubscriptionStatus.WebsocketNetworkTimeout, "websocket_network_timeout"),
|
||||||
|
new (EventSubSubscriptionStatus.WebsocketNetworkError, "websocket_network_error"),
|
||||||
|
];
|
||||||
|
private static readonly IList<KeyValuePair<string, EventSubSubscriptionStatus>> InverseConversionList =
|
||||||
|
ConversionList.Select<KeyValuePair<EventSubSubscriptionStatus, string>,
|
||||||
|
KeyValuePair<string, EventSubSubscriptionStatus>>(p => new(p.Value, p.Key)).ToList();
|
||||||
|
|
||||||
|
public static string Convert(EventSubSubscriptionStatus status)
|
||||||
|
=> ConversionList.First(p => p.Key == status).Value;
|
||||||
|
public static EventSubSubscriptionStatus Convert(string status)
|
||||||
|
=> InverseConversionList.First(p => p.Key == status).Value;
|
||||||
|
public override EventSubSubscriptionStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> Convert(reader.GetString());
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, EventSubSubscriptionStatus value, JsonSerializerOptions options)
|
||||||
|
=> writer.WriteStringValue(Convert(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
28
TwitchIrcClient/Authentication/DcfCodeMessage.cs
Normal file
28
TwitchIrcClient/Authentication/DcfCodeMessage.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.Authentication
|
||||||
|
{
|
||||||
|
public record class DcfCodeMessage
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("device_code")]
|
||||||
|
public string DeviceCode { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("expires_in")]
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("interval")]
|
||||||
|
public int Interval { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("user_code")]
|
||||||
|
public string UserCode { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("verification_uri")]
|
||||||
|
public string VerificationUri { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
TwitchIrcClient/Authentication/UserAccessAuthentication.cs
Normal file
10
TwitchIrcClient/Authentication/UserAccessAuthentication.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.Authentication
|
||||||
|
{
|
||||||
|
public record class UserAccessAuthentication(string Token, string ClientId);
|
||||||
|
}
|
||||||
134
TwitchIrcClient/EventSub/EventSubWebsocketClient.cs
Normal file
134
TwitchIrcClient/EventSub/EventSubWebsocketClient.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TwitchIrcClient.IRC.Messages;
|
||||||
|
using TwitchIrcClient.IRC;
|
||||||
|
using TwitchIrcClient.EventSub.Messages;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub
|
||||||
|
{
|
||||||
|
public class EventSubWebsocketClient : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ClientWebSocket Socket = new();
|
||||||
|
private readonly CancellationTokenSource TokenSource = new();
|
||||||
|
public CancellationToken CancelToken => TokenSource.Token;
|
||||||
|
public readonly HttpClient Client = new();
|
||||||
|
public string? SessionId { get; private set; }
|
||||||
|
public string? ReconnectUrl { get; private set; }
|
||||||
|
public int? TotalSubscriptions { get; internal set; }
|
||||||
|
public int? TotalCost { get; internal set; }
|
||||||
|
public int? MaximumCost { get; internal set; }
|
||||||
|
|
||||||
|
public async Task<bool> ConnectAsync()
|
||||||
|
{
|
||||||
|
const string url = "wss://eventsub.wss.twitch.tv/ws?keepalive_timeout_seconds=600";
|
||||||
|
await Socket.ConnectAsync(new Uri(url), CancelToken);
|
||||||
|
if (CancelToken.IsCancellationRequested)
|
||||||
|
return false;
|
||||||
|
if (Socket.State != WebSocketState.Open)
|
||||||
|
return false;
|
||||||
|
System_ReceivedWelcome += (sender, e) =>
|
||||||
|
{
|
||||||
|
if (sender is EventSubWebsocketClient esc)
|
||||||
|
{
|
||||||
|
esc.SessionId = e.Welcome.Payload.Session.Id;
|
||||||
|
esc.ReconnectUrl = e.Welcome.Payload.Session.ReconnectUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_ = Task.Run(HandleIncomingMessages, CancelToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleIncomingMessages()
|
||||||
|
{
|
||||||
|
var arr = new byte[8 * 1024];
|
||||||
|
var buffer = new List<byte>();
|
||||||
|
Task? prevTask = null;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var resp = await Socket.ReceiveAsync(arr, CancelToken);
|
||||||
|
if (CancelToken.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
buffer.AddRange(arr.Take(resp.Count));
|
||||||
|
if (resp.EndOfMessage)
|
||||||
|
{
|
||||||
|
var str = Encoding.UTF8.GetString(buffer.ToArray());
|
||||||
|
//events get their own task so future messages aren't delayed
|
||||||
|
//use ContinueWith to preserve otder of incoming messages
|
||||||
|
prevTask = prevTask?.IsCompleted ?? true
|
||||||
|
? Task.Run(() => DoIncomingMessage(str), CancelToken)
|
||||||
|
: prevTask.ContinueWith((_, _) => DoIncomingMessage(str), CancelToken);
|
||||||
|
buffer.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private event EventHandler<EventSubKeepaliveEventArgs>? System_ReceivedKeepalive;
|
||||||
|
private event EventHandler<EventSubNotificationEventArgs>? System_ReceivedNotification;
|
||||||
|
private event EventHandler<EventSubReconnectEventArgs>? System_ReceivedReconnect;
|
||||||
|
private event EventHandler<EventSubRevocationEventArgs>? System_ReceivedRevocation;
|
||||||
|
private event EventHandler<EventSubWelcomeEventArgs>? System_ReceivedWelcome;
|
||||||
|
|
||||||
|
public event EventHandler<EventSubKeepaliveEventArgs>? ReceivedKeepalive;
|
||||||
|
public event EventHandler<EventSubNotificationEventArgs>? ReceivedNotification;
|
||||||
|
public event EventHandler<EventSubReconnectEventArgs>? ReceivedReconnect;
|
||||||
|
public event EventHandler<EventSubRevocationEventArgs>? ReceivedRevocation;
|
||||||
|
public event EventHandler<EventSubWelcomeEventArgs>? ReceivedWelcome;
|
||||||
|
private void DoIncomingMessage(string message)
|
||||||
|
{
|
||||||
|
var esm = EventSubMessage.Parse(message);
|
||||||
|
switch (esm)
|
||||||
|
{
|
||||||
|
case EventSubKeepalive keepalive:
|
||||||
|
System_ReceivedKeepalive?.Invoke(this, new EventSubKeepaliveEventArgs(keepalive));
|
||||||
|
ReceivedKeepalive?.Invoke(this, new EventSubKeepaliveEventArgs(keepalive));
|
||||||
|
break;
|
||||||
|
case EventSubNotification notification:
|
||||||
|
System_ReceivedNotification?.Invoke(this, new EventSubNotificationEventArgs(notification));
|
||||||
|
ReceivedNotification?.Invoke(this, new EventSubNotificationEventArgs(notification));
|
||||||
|
break;
|
||||||
|
case EventSubReconnect reconnect:
|
||||||
|
System_ReceivedReconnect?.Invoke(this, new EventSubReconnectEventArgs(reconnect));
|
||||||
|
ReceivedReconnect?.Invoke(this, new EventSubReconnectEventArgs(reconnect));
|
||||||
|
break;
|
||||||
|
case EventSubRevocation revocation:
|
||||||
|
System_ReceivedRevocation?.Invoke(this, new EventSubRevocationEventArgs(revocation));
|
||||||
|
ReceivedRevocation?.Invoke(this, new EventSubRevocationEventArgs(revocation));
|
||||||
|
break;
|
||||||
|
case EventSubWelcome welcome:
|
||||||
|
System_ReceivedWelcome?.Invoke(this, new EventSubWelcomeEventArgs(welcome));
|
||||||
|
ReceivedWelcome?.Invoke(this, new EventSubWelcomeEventArgs(welcome));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidDataException("invalid message type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region dispose
|
||||||
|
private bool disposedValue;
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!disposedValue)
|
||||||
|
{
|
||||||
|
TokenSource.Cancel();
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
Socket?.Dispose();
|
||||||
|
Client?.Dispose();
|
||||||
|
}
|
||||||
|
disposedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
#endregion //dispose
|
||||||
|
}
|
||||||
|
}
|
||||||
28
TwitchIrcClient/EventSub/Messages/Emote.cs
Normal file
28
TwitchIrcClient/EventSub/Messages/Emote.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public record class Emote
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("begin")]
|
||||||
|
public int Begin { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("end")]
|
||||||
|
public int End { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
public Emote(int begin, int end, string id)
|
||||||
|
{
|
||||||
|
Begin = begin;
|
||||||
|
End = end;
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
TwitchIrcClient/EventSub/Messages/EventSubKeepalive.cs
Normal file
21
TwitchIrcClient/EventSub/Messages/EventSubKeepalive.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public class EventSubKeepalive : EventSubMessage
|
||||||
|
{
|
||||||
|
public override EventSubMessageType MessageType => EventSubMessageType.Keepalive;
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("metadata")]
|
||||||
|
public EventSubMessageBaseMetadata Metadata { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("payload")]
|
||||||
|
public JsonObject Payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
65
TwitchIrcClient/EventSub/Messages/EventSubMessage.cs
Normal file
65
TwitchIrcClient/EventSub/Messages/EventSubMessage.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public class EventSubMessageBaseMetadata
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("message_id")]
|
||||||
|
public string MessageId { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("message_type")]
|
||||||
|
public string MessageType { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("message_timestamp")]
|
||||||
|
public DateTime MessageTime { get; set; }
|
||||||
|
}
|
||||||
|
public abstract class EventSubMessage
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public abstract EventSubMessageType MessageType { get; }
|
||||||
|
public static EventSubMessage Parse(string json)
|
||||||
|
{
|
||||||
|
var node = JsonNode.Parse(json);
|
||||||
|
if (!(node?["metadata"]?["message_type"]?.AsValue().TryGetValue(out string? value) ?? false))
|
||||||
|
throw new ArgumentException("invalid json", nameof(json));
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
"session_welcome" => JsonSerializer.Deserialize<EventSubWelcome>(node),
|
||||||
|
"session_keepalive" => JsonSerializer.Deserialize<EventSubKeepalive>(node),
|
||||||
|
"notification" => JsonSerializer.Deserialize<EventSubNotification>(node),
|
||||||
|
"session_reconnect" => JsonSerializer.Deserialize<EventSubReconnect>(node),
|
||||||
|
"revocation" => JsonSerializer.Deserialize<EventSubRevocation>(node),
|
||||||
|
_ => (EventSubMessage?)null,
|
||||||
|
} ?? throw new ArgumentException("invalid json", nameof(json));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class EventSubNotificationEventArgs(EventSubNotification notification) : EventArgs
|
||||||
|
{
|
||||||
|
public EventSubNotification Notification = notification;
|
||||||
|
}
|
||||||
|
public class EventSubKeepaliveEventArgs(EventSubKeepalive keepalive) : EventArgs
|
||||||
|
{
|
||||||
|
public EventSubKeepalive Keepalive = keepalive;
|
||||||
|
}
|
||||||
|
public class EventSubReconnectEventArgs(EventSubReconnect reconnect) : EventArgs
|
||||||
|
{
|
||||||
|
public EventSubReconnect Reconnect = reconnect;
|
||||||
|
}
|
||||||
|
public class EventSubRevocationEventArgs(EventSubRevocation revocation) : EventArgs
|
||||||
|
{
|
||||||
|
public EventSubRevocation Revocation = revocation;
|
||||||
|
}
|
||||||
|
public class EventSubWelcomeEventArgs(EventSubWelcome welcome) : EventArgs
|
||||||
|
{
|
||||||
|
public EventSubWelcome Welcome = welcome;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
TwitchIrcClient/EventSub/Messages/EventSubMessageType.cs
Normal file
33
TwitchIrcClient/EventSub/Messages/EventSubMessageType.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public enum EventSubMessageType
|
||||||
|
{
|
||||||
|
Welcome = 0,
|
||||||
|
Keepalive = 1,
|
||||||
|
Notification = 2,
|
||||||
|
Reconnect = 3,
|
||||||
|
Revocation = 4,
|
||||||
|
}
|
||||||
|
internal static class EventSubMessageTypeHelper
|
||||||
|
{
|
||||||
|
public static EventSubMessageType Parse(string s)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(s);
|
||||||
|
return s.ToLower() switch
|
||||||
|
{
|
||||||
|
"session_welcome" => EventSubMessageType.Welcome,
|
||||||
|
"session_keepalive" => EventSubMessageType.Keepalive,
|
||||||
|
"notification" => EventSubMessageType.Notification,
|
||||||
|
"session_reconnect" => EventSubMessageType.Reconnect,
|
||||||
|
"revocation" => EventSubMessageType.Revocation,
|
||||||
|
_ => throw new ArgumentException("invalid string", nameof(s)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
TwitchIrcClient/EventSub/Messages/EventSubNotification.cs
Normal file
77
TwitchIrcClient/EventSub/Messages/EventSubNotification.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public class EventSubNotificationMetadata : EventSubMessageBaseMetadata
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("subscription_type")]
|
||||||
|
public string SubscriptionType { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("subscription_version")]
|
||||||
|
public string SubscriptionVersion { get; set; }
|
||||||
|
}
|
||||||
|
public class EventSubNotificationTransport
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("method")]
|
||||||
|
public string Method { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("session_id")]
|
||||||
|
public string SessionId { get; set; }
|
||||||
|
}
|
||||||
|
public class EventSubNotificationSubscription
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string Version { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("cost")]
|
||||||
|
public int Cost { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("condition")]
|
||||||
|
public object Condition { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("transport")]
|
||||||
|
public EventSubNotificationTransport Transport { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("created_at")]
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
public class EventSubNotificationPayload
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("subscription")]
|
||||||
|
public EventSubNotificationSubscription Subscription { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("event")]
|
||||||
|
public JsonObject Event { get; set; }
|
||||||
|
}
|
||||||
|
public class EventSubNotification : EventSubMessage
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public override EventSubMessageType MessageType => EventSubMessageType.Notification;
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("metadata")]
|
||||||
|
public EventSubNotificationMetadata Metadata { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("payload")]
|
||||||
|
public EventSubNotificationPayload Payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TwitchIrcClient.IRC.Messages;
|
||||||
|
using TwitchIrcClient.IRC;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public delegate void EventSubMessageCallback(EventSubWebsocketClient origin, EventSubNotification message);
|
||||||
|
public readonly record struct EventSubNotificationCallbackItem(
|
||||||
|
EventSubMessageCallback Callback,
|
||||||
|
IReadOnlyCollection<string>? CallbackTypes)
|
||||||
|
{
|
||||||
|
public bool TryCall(EventSubWebsocketClient origin, EventSubNotification message)
|
||||||
|
{
|
||||||
|
if (CallbackTypes?.Contains(message.Metadata.MessageType) ?? true)
|
||||||
|
{
|
||||||
|
Callback(origin, message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
TwitchIrcClient/EventSub/Messages/EventSubReconnect.cs
Normal file
45
TwitchIrcClient/EventSub/Messages/EventSubReconnect.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public class EventSubReconnectPayloadSession
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("keepalive_timeout_seconds")]
|
||||||
|
public int? KeepaliveTimeoutSeconds { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("reconnect_url")]
|
||||||
|
public string ReconnectUrl { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("connected_at")]
|
||||||
|
public DateTime ConnectedAt { get; set; }
|
||||||
|
}
|
||||||
|
public class EventSubReconnectPayload
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("session")]
|
||||||
|
public EventSubReconnectPayloadSession Session { get; set; }
|
||||||
|
}
|
||||||
|
public class EventSubReconnect : EventSubMessage
|
||||||
|
{
|
||||||
|
public override EventSubMessageType MessageType => EventSubMessageType.Reconnect;
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("metadata")]
|
||||||
|
public EventSubMessageBaseMetadata Metadata { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("payload")]
|
||||||
|
public EventSubReconnectPayload Payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
27
TwitchIrcClient/EventSub/Messages/EventSubRevocation.cs
Normal file
27
TwitchIrcClient/EventSub/Messages/EventSubRevocation.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public class EventSubRevocationPayload
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("subscription")]
|
||||||
|
public EventSubNotificationSubscription Subscription { get; set; }
|
||||||
|
}
|
||||||
|
public class EventSubRevocation : EventSubMessage
|
||||||
|
{
|
||||||
|
public override EventSubMessageType MessageType => EventSubMessageType.Revocation;
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("metadata")]
|
||||||
|
public EventSubNotificationMetadata Metadata { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("payload")]
|
||||||
|
public EventSubRevocationPayload Payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
//this will fail for "Channel Moderate Event" because of the complicated "Condition" field
|
||||||
|
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
||||||
|
public record class EventSubSubscriptionRequest
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string Version { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("condition")]
|
||||||
|
public Dictionary<string,string> Condition { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("transport")]
|
||||||
|
public Dictionary<string, string> Transport { get; set; }
|
||||||
|
public EventSubSubscriptionRequest(string type, string version,
|
||||||
|
Dictionary<string, string> condition,
|
||||||
|
Dictionary<string, string> transport)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Version = version;
|
||||||
|
Condition = condition;
|
||||||
|
Transport = transport;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
TwitchIrcClient/EventSub/Messages/EventSubTransport.cs
Normal file
95
TwitchIrcClient/EventSub/Messages/EventSubTransport.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public record class EventSubTransport
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The transport method. Possible values are:
|
||||||
|
/// webhook
|
||||||
|
/// websocket
|
||||||
|
/// </summary>
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonConverter(typeof(TwitchTransportTypeConverter))]
|
||||||
|
[JsonPropertyName("method")]
|
||||||
|
public TwitchTransportType Method { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The callback URL where the notifications are sent. The URL must use the HTTPS protocol and port 443.
|
||||||
|
/// See <see href="https://dev.twitch.tv/docs/eventsub/handling-webhook-events#processing-an-event"/>.
|
||||||
|
/// Specify this field only if <see cref="Method"/> is set to <see cref="TwitchTransportType.Webhook"/>.
|
||||||
|
/// NOTE: Redirects are not followed.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("callback")]
|
||||||
|
public string? Callback { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("secret")]
|
||||||
|
public string? Secret { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("session_id")]
|
||||||
|
public string? SessionId { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("connected_at")]
|
||||||
|
public DateTime? ConnectedAt { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
[JsonPropertyName("disconnected_at")]
|
||||||
|
public DateTime? DisconnectedAt { get; set; }
|
||||||
|
|
||||||
|
public EventSubTransport()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
private EventSubTransport(TwitchTransportType method,
|
||||||
|
string? callback, string? secret, string? sessionId,
|
||||||
|
DateTime? connectedAt, DateTime? disconnectedAt)
|
||||||
|
{
|
||||||
|
Method = method;
|
||||||
|
Callback = callback;
|
||||||
|
Secret = secret;
|
||||||
|
SessionId = sessionId;
|
||||||
|
ConnectedAt = connectedAt;
|
||||||
|
DisconnectedAt = disconnectedAt;
|
||||||
|
}
|
||||||
|
public static EventSubTransport MakeWebhook(string callback, string secret)
|
||||||
|
=> new EventSubTransport(TwitchTransportType.Webhook,
|
||||||
|
callback, secret, null, null, null);
|
||||||
|
public static EventSubTransport MakeWebsocket(string sessionId, DateTime? connectedAt,
|
||||||
|
DateTime? disconnectedAt) => new EventSubTransport(TwitchTransportType.Websocket,
|
||||||
|
null, null, sessionId, connectedAt, disconnectedAt);
|
||||||
|
}
|
||||||
|
public enum TwitchTransportType
|
||||||
|
{
|
||||||
|
Webhook = 0,
|
||||||
|
Websocket = 1,
|
||||||
|
Conduit = 2,
|
||||||
|
}
|
||||||
|
public class TwitchTransportTypeConverter : JsonConverter<TwitchTransportType>
|
||||||
|
{
|
||||||
|
public override TwitchTransportType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> reader.GetString() switch
|
||||||
|
{
|
||||||
|
"webhook" => TwitchTransportType.Webhook,
|
||||||
|
"websocket" => TwitchTransportType.Websocket,
|
||||||
|
"conduit" => TwitchTransportType.Conduit,
|
||||||
|
_ => throw new JsonException(),
|
||||||
|
};
|
||||||
|
public override void Write(Utf8JsonWriter writer, TwitchTransportType value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value switch
|
||||||
|
{
|
||||||
|
TwitchTransportType.Webhook => "webhook",
|
||||||
|
TwitchTransportType.Websocket => "websocket",
|
||||||
|
TwitchTransportType.Conduit => "conduit",
|
||||||
|
_ => throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(TwitchTransportType)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
TwitchIrcClient/EventSub/Messages/EventSubWelcome.cs
Normal file
21
TwitchIrcClient/EventSub/Messages/EventSubWelcome.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace TwitchIrcClient.EventSub.Messages
|
||||||
|
{
|
||||||
|
public class EventSubWelcome : EventSubMessage
|
||||||
|
{
|
||||||
|
public override EventSubMessageType MessageType => EventSubMessageType.Welcome;
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("metadata")]
|
||||||
|
public EventSubMessageBaseMetadata Metadata { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
[JsonPropertyName("payload")]
|
||||||
|
public EventSubReconnectPayload Payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
|
||||||
|
"message_type": "session_keepalive",
|
||||||
|
"message_timestamp": "2023-07-19T10:11:12.634234626Z"
|
||||||
|
},
|
||||||
|
"payload": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"message_id": "befa7b53-d79d-478f-86b9-120f112b044e",
|
||||||
|
"message_type": "notification",
|
||||||
|
"message_timestamp": "2022-11-16T10:11:12.464757833Z",
|
||||||
|
"subscription_type": "automod.message.hold",
|
||||||
|
"subscription_version": "1"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"subscription": {
|
||||||
|
"id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4",
|
||||||
|
"type": "automod.message.hold",
|
||||||
|
"version": "beta",
|
||||||
|
"status": "enabled",
|
||||||
|
"cost": 0,
|
||||||
|
"condition": {
|
||||||
|
"broadcaster_user_id": "1337",
|
||||||
|
"moderator_user_id": "9001"
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"method": "websocket",
|
||||||
|
"session_id": "123456789"
|
||||||
|
},
|
||||||
|
"created_at": "2023-04-11T10:11:12.123Z"
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"broadcaster_user_id": "1337",
|
||||||
|
"broadcaster_user_name": "blah",
|
||||||
|
"broadcaster_user_login": "blahblah",
|
||||||
|
"user_id": "456789012",
|
||||||
|
"user_name": "baduser",
|
||||||
|
"user_login": "baduserbla",
|
||||||
|
"message_id": "bad-message-id",
|
||||||
|
"message": "This is a bad message… ",
|
||||||
|
"level": 5,
|
||||||
|
"category": "aggressive",
|
||||||
|
"held_at": "2022-12-02T15:00:00.00Z",
|
||||||
|
"fragments": {
|
||||||
|
"emotes": [
|
||||||
|
{
|
||||||
|
"text": "badtextemote1",
|
||||||
|
"id": "emote-123",
|
||||||
|
"set-id": "set-emote-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "badtextemote2",
|
||||||
|
"id": "emote-234",
|
||||||
|
"set-id": "set-emote-2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cheermotes": [
|
||||||
|
{
|
||||||
|
"text": "badtextcheermote1",
|
||||||
|
"amount": 1000,
|
||||||
|
"prefix": "prefix",
|
||||||
|
"tier": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
|
||||||
|
"message_type": "session_reconnect",
|
||||||
|
"message_timestamp": "2022-11-18T09:10:11.634234626Z"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"session": {
|
||||||
|
"id": "AQoQexAWVYKSTIu4ec_2VAxyuhAB",
|
||||||
|
"status": "reconnecting",
|
||||||
|
"keepalive_timeout_seconds": null,
|
||||||
|
"reconnect_url": "wss://eventsub.wss.twitch.tv?...",
|
||||||
|
"connected_at": "2022-11-16T10:11:12.634234626Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"message_id": "84c1e79a-2a4b-4c13-ba0b-4312293e9308",
|
||||||
|
"message_type": "revocation",
|
||||||
|
"message_timestamp": "2022-11-16T10:11:12.464757833Z",
|
||||||
|
"subscription_type": "channel.follow",
|
||||||
|
"subscription_version": "1"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"subscription": {
|
||||||
|
"id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4",
|
||||||
|
"status": "authorization_revoked",
|
||||||
|
"type": "channel.follow",
|
||||||
|
"version": "1",
|
||||||
|
"cost": 1,
|
||||||
|
"condition": {
|
||||||
|
"broadcaster_user_id": "12826"
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"method": "websocket",
|
||||||
|
"session_id": "AQoQexAWVYKSTIu4ec_2VAxyuhAB"
|
||||||
|
},
|
||||||
|
"created_at": "2022-11-16T10:11:12.464757833Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"message_id": "96a3f3b5-5dec-4eed-908e-e11ee657416c",
|
||||||
|
"message_type": "session_welcome",
|
||||||
|
"message_timestamp": "2023-07-19T14:56:51.634234626Z"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"session": {
|
||||||
|
"id": "AQoQILE98gtqShGmLD7AM6yJThAB",
|
||||||
|
"status": "connected",
|
||||||
|
"connected_at": "2023-07-19T14:56:51.616329898Z",
|
||||||
|
"keepalive_timeout_seconds": 10,
|
||||||
|
"reconnect_url": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
TwitchIrcClientTests/EventSubJsonTest.cs
Normal file
106
TwitchIrcClientTests/EventSubJsonTest.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TwitchIrcClient.EventSub.Messages;
|
||||||
|
|
||||||
|
namespace TwitchIrcClientTests
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
[DeploymentItem("EventSubExampleJson")]
|
||||||
|
public class EventSubJsonTest
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEventSubNotification()
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText("EventSubNotification_AutomodMessageHold.json");
|
||||||
|
var automodMessageHold = EventSubMessage.Parse(text);
|
||||||
|
Assert.IsInstanceOfType<EventSubNotification>(automodMessageHold);
|
||||||
|
var esn = (EventSubNotification)automodMessageHold;
|
||||||
|
Assert.AreEqual("befa7b53-d79d-478f-86b9-120f112b044e", esn.Metadata.MessageId);
|
||||||
|
Assert.AreEqual(EventSubMessageType.Notification, esn.MessageType);
|
||||||
|
//test accuracy to a millisecond
|
||||||
|
Assert.AreEqual(new DateTime(2022, 11, 16, 10, 11, 12, 464, 578, DateTimeKind.Utc).Ticks,
|
||||||
|
esn.Metadata.MessageTime.Ticks, 10000);
|
||||||
|
Assert.AreEqual("automod.message.hold", esn.Metadata.SubscriptionType);
|
||||||
|
Assert.AreEqual("1", esn.Metadata.SubscriptionVersion);
|
||||||
|
Assert.AreEqual("f1c2a387-161a-49f9-a165-0f21d7a4e1c4", esn.Payload.Subscription.Id);
|
||||||
|
Assert.AreEqual("automod.message.hold", esn.Payload.Subscription.Type);
|
||||||
|
Assert.AreEqual("beta", esn.Payload.Subscription.Version);
|
||||||
|
Assert.AreEqual("enabled", esn.Payload.Subscription.Status);
|
||||||
|
Assert.AreEqual(0, esn.Payload.Subscription.Cost);
|
||||||
|
Assert.AreEqual("websocket", esn.Payload.Subscription.Transport.Method);
|
||||||
|
Assert.AreEqual("123456789", esn.Payload.Subscription.Transport.SessionId);
|
||||||
|
}
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEventSubWelcome()
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText("EventSubWelcome.json");
|
||||||
|
var welcome = EventSubMessage.Parse(text);
|
||||||
|
Assert.IsInstanceOfType<EventSubWelcome>(welcome);
|
||||||
|
var esw = (EventSubWelcome)welcome;
|
||||||
|
Assert.AreEqual("96a3f3b5-5dec-4eed-908e-e11ee657416c", esw.Metadata.MessageId);
|
||||||
|
Assert.AreEqual(EventSubMessageType.Welcome, esw.MessageType);
|
||||||
|
Assert.AreEqual(new DateTime(2023, 7, 19, 14, 56, 51, 634, 234, DateTimeKind.Utc).Ticks,
|
||||||
|
esw.Metadata.MessageTime.Ticks, 10000);
|
||||||
|
}
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEventSubRevocation()
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText("EventSubRevocation_ChannelFollow.json");
|
||||||
|
var channelFollow = EventSubMessage.Parse(text);
|
||||||
|
Assert.IsInstanceOfType<EventSubRevocation>(channelFollow);
|
||||||
|
var esr = (EventSubRevocation)channelFollow;
|
||||||
|
Assert.AreEqual("84c1e79a-2a4b-4c13-ba0b-4312293e9308", esr.Metadata.MessageId);
|
||||||
|
Assert.AreEqual(EventSubMessageType.Revocation, esr.MessageType);
|
||||||
|
Assert.AreEqual(new DateTime(2022, 11, 16, 10, 11, 12, 464, 757, DateTimeKind.Utc).Ticks,
|
||||||
|
esr.Metadata.MessageTime.Ticks, 10000);
|
||||||
|
Assert.AreEqual("channel.follow", esr.Metadata.SubscriptionType);
|
||||||
|
Assert.AreEqual("1", esr.Metadata.SubscriptionVersion);
|
||||||
|
var sub = esr.Payload.Subscription;
|
||||||
|
Assert.AreEqual("f1c2a387-161a-49f9-a165-0f21d7a4e1c4", sub.Id);
|
||||||
|
Assert.AreEqual("authorization_revoked", sub.Status);
|
||||||
|
Assert.AreEqual("channel.follow", sub.Type);
|
||||||
|
Assert.AreEqual("1", sub.Version);
|
||||||
|
Assert.AreEqual(1, sub.Cost);
|
||||||
|
Assert.AreEqual("websocket", sub.Transport.Method);
|
||||||
|
Assert.AreEqual("AQoQexAWVYKSTIu4ec_2VAxyuhAB", sub.Transport.SessionId);
|
||||||
|
Assert.AreEqual(new DateTime(2022, 11, 16, 10, 11, 12, 464, 757, DateTimeKind.Utc).Ticks,
|
||||||
|
esr.Payload.Subscription.CreatedAt.Ticks, 10000);
|
||||||
|
}
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEventSubReconnect()
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText("EventSubReconnect.json");
|
||||||
|
var reconnect = EventSubMessage.Parse(text);
|
||||||
|
Assert.IsInstanceOfType<EventSubReconnect>(reconnect);
|
||||||
|
var esr = (EventSubReconnect)reconnect;
|
||||||
|
Assert.AreEqual("84c1e79a-2a4b-4c13-ba0b-4312293e9308", esr.Metadata.MessageId);
|
||||||
|
Assert.AreEqual(EventSubMessageType.Reconnect, esr.MessageType);
|
||||||
|
Assert.AreEqual(new DateTime(2022, 11, 18, 9, 10, 11, 634, 234, DateTimeKind.Utc).Ticks,
|
||||||
|
esr.Metadata.MessageTime.Ticks, 10000);
|
||||||
|
Assert.AreEqual("AQoQexAWVYKSTIu4ec_2VAxyuhAB", esr.Payload.Session.Id);
|
||||||
|
Assert.AreEqual("reconnecting", esr.Payload.Session.Status);
|
||||||
|
Assert.IsNull(esr.Payload.Session.KeepaliveTimeoutSeconds);
|
||||||
|
Assert.AreEqual("wss://eventsub.wss.twitch.tv?...", esr.Payload.Session.ReconnectUrl);
|
||||||
|
Assert.AreEqual(new DateTime(2022, 11, 16, 10, 11, 12, 634, 234, DateTimeKind.Utc).Ticks,
|
||||||
|
esr.Payload.Session.ConnectedAt.Ticks, 10000);
|
||||||
|
}
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEventSubKeepalive()
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText("EventSubKeepalive.json");
|
||||||
|
var keepalive = EventSubMessage.Parse(text);
|
||||||
|
Assert.IsInstanceOfType<EventSubKeepalive>(keepalive);
|
||||||
|
var esk = (EventSubKeepalive)keepalive;
|
||||||
|
Assert.AreEqual("84c1e79a-2a4b-4c13-ba0b-4312293e9308", esk.Metadata.MessageId);
|
||||||
|
Assert.AreEqual(EventSubMessageType.Keepalive, esk.MessageType);
|
||||||
|
Assert.AreEqual(new DateTime(2023, 7, 19, 10, 11, 12, 634, 234, DateTimeKind.Utc).Ticks,
|
||||||
|
esk.Metadata.MessageTime.Ticks, 10000);
|
||||||
|
Assert.IsFalse(esk.Payload.Any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ using System.Diagnostics;
|
|||||||
namespace TwitchIrcClientTests
|
namespace TwitchIrcClientTests
|
||||||
{
|
{
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class ParserTest
|
public class IrcParserTest
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestRoomstate()
|
public void TestRoomstate()
|
||||||
@@ -24,4 +24,22 @@
|
|||||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="EventSubExampleJson\EventSubKeepalive.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="EventSubExampleJson\EventSubNotification_AutomodMessageHold.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="EventSubExampleJson\EventSubReconnect.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="EventSubExampleJson\EventSubRevocation_ChannelFollow.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="EventSubExampleJson\EventSubWelcome.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user