diff --git a/.gitignore b/.gitignore index 27ae6f1..5578290 100644 --- a/.gitignore +++ b/.gitignore @@ -373,3 +373,4 @@ FodyWeavers.xsd # Local History for Visual Studio Code .history/ +appsettings.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3ba46bb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net5.0/twitcher.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..083d814 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/twitcher.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/twitcher.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/twitcher.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Config.cs b/Config.cs new file mode 100644 index 0000000..9a08664 --- /dev/null +++ b/Config.cs @@ -0,0 +1,12 @@ +namespace twitcher +{ + public class Config + { + public string kafka_bootstrap { get; set; } + public string kafka_name { get; set; } + public int port { get; set; } + public string clientId { get; set; } + public string clientSecret { get; set; } + public string public_uri { get; set; } + } +} diff --git a/OAuthTokenGetter.cs b/OAuthTokenGetter.cs new file mode 100644 index 0000000..331d5c6 --- /dev/null +++ b/OAuthTokenGetter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.Net.Http; + +public class OAuthTokenGetter +{ + public static OAuthToken DoIt(HttpClient client, string clientId, string clientSecret, string[] scopes = null) + { + var scopeString = ""; + if (scopes != null && scopes.Length > 0) + { + scopeString = "&scope=" + string.Join(' ', scopes); + } + var postURI = $"https://id.twitch.tv/oauth2/token?client_id={clientId}&client_secret={clientSecret}&grant_type=client_credentials{scopeString}"; + //Console.WriteLine(postURI); + var r = client.PostAsync(postURI, null).Result; + + var response = ""; + using (var streamReader = new StreamReader(r.Content.ReadAsStream())) + { + response = streamReader.ReadToEnd(); + } + if(!r.IsSuccessStatusCode) + { + Console.Error.WriteLine(response); + throw new Exception(response); + } + //Console.WriteLine(response); + return JsonConvert.DeserializeObject(response); + } +} +public class OAuthToken +{ + public string access_token { get; set; } + public string refresh_token { get; set; } + public int expires_in { get; set; } + public string[] scope { get; set; } + public string token_type { get; set; } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..15b4ecf --- /dev/null +++ b/Program.cs @@ -0,0 +1,51 @@ +using franz; +using System; +using System.IO; +using System.Threading.Tasks; +using TwitchEventSub.Types.EventSubSubscription; +using Newtonsoft.Json; + +namespace twitcher +{ + class Program + { + public static TwitchEventSub.Receiver httpd; + public static Config twitcherConf; + //public static Telefranz tf; + static void Main(string[] args) + { + JsonConvert.DefaultSettings = () => { + var s = new JsonSerializerSettings(); + s.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); + return s; + }; + if (!File.Exists("appsettings.json")) + { + Console.Error.WriteLine("appsettings.json was not found!"); + twitcherConf = new Config(); + File.WriteAllText("appsettings.json", JsonConvert.SerializeObject(twitcherConf, Formatting.Indented)); + return; + } + twitcherConf = JsonConvert.DeserializeObject(File.ReadAllText("appsettings.json")); + + // Telefranz.Configure(name: twitcherConf.kafka_name, bootstrap_servers: twitcherConf.kafka_bootstrap); + // Task.WaitAll(Task.Delay(1000)); + // tf = Telefranz.Instance; + + //TODO: throw a request out somewhere asking for public url + //tf.ProduceMessage(new silver_messages.directorial.execute_command(){command = "ssl_expose", args = {twitcherConf.port.ToString()}}); + + httpd = new TwitchEventSub.Receiver(twitcherConf.port, twitcherConf.clientId, twitcherConf.clientSecret, twitcherConf.public_uri); + Task.WaitAll( + httpd.go(), + Task.Run(() => {httpd.Subscribe(SubscribableTypes.channel_follow, + new TwitchEventSub.Types.Conditions.ChannelFollow() { broadcaster_user_id = "12826" }, + (tg) => { + Console.WriteLine("I'm the handler for a twitchogram!"); + Console.WriteLine(JsonConvert.SerializeObject(tg)); + } + );}) + ); + } + } +} diff --git a/TwitchEventSub/Receiver.cs b/TwitchEventSub/Receiver.cs new file mode 100644 index 0000000..24c957f --- /dev/null +++ b/TwitchEventSub/Receiver.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using TwitchEventSub.Types.EventSubSubscription; +using System.Security.Cryptography; +using System.Linq; + +namespace TwitchEventSub +{ + public class Receiver + { + private OAuthToken token; + private HttpListener server; + static readonly HttpClient client = new HttpClient(); + public Dictionary> psuedoResources { get; set; } + = new Dictionary>(); + private string hmacSecret = ""; + private bool going = false; + private Uri publicUrl; + private Queue twitchogrambuffer { get; set; } + private string clientId; + private HMACSHA256 hmacsha256 = null; + private List tasksToNotActuallyAwait = new List(); + public Receiver(int port, string clientId, string clientSecret, string publicUrl) + { + this.clientId = clientId; + this.publicUrl = new Uri(publicUrl + "/twitcherize"); + twitchogrambuffer = new Queue(); + server = new HttpListener(); + server.Prefixes.Add($"http://*:{port}/"); + + authorizeClient(clientId, clientSecret); + + clearOldSubscriptions(); + + prepareHmac(); + } + + private void prepareHmac() + { + var r = new Random(); + var stringLength = r.Next(20, 100); + var charChoices = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + for (int i = 0; i < stringLength; i++) + { + this.hmacSecret += charChoices[r.Next(charChoices.Length)]; + } + this.hmacsha256 = new HMACSHA256(Encoding.ASCII.GetBytes(this.hmacSecret)); + } + + private void authorizeClient(string clientId, string clientSecret) + { + this.token = OAuthTokenGetter.DoIt(client, clientId, clientSecret); + Console.WriteLine($"got a token"); + client.DefaultRequestHeaders.Add("Client-ID", $"{this.clientId }"); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {this.token.access_token}"); + } + + private void clearOldSubscriptions() + { + var r = client.GetAsync("https://api.twitch.tv/helix/eventsub/subscriptions").Result; + + var responseText = ""; + using (var streamReader = new StreamReader(r.Content.ReadAsStream())) + { + responseText = streamReader.ReadToEnd(); + } + if(!r.IsSuccessStatusCode) + { + Console.Error.WriteLine(responseText); + throw new Exception(responseText); + } + + var listing = SubscribableTypesTranslation.RefineTwitchResponse(responseText); + + var deletes = new List>(); + foreach(var subscription in listing.data) + { + deletes.Add(client.DeleteAsync($"https://api.twitch.tv/helix/eventsub/subscriptions?id={subscription.id}")); + } + Task.WaitAll(deletes.ToArray()); + } + + public void Subscribe(SubscribableTypes type, c condition, Action handler) + where c : Types.Conditions.Condition + { + if (!going) + { + Task.Run(go); + } + var subreq = new Request() + { + condition = condition, + transport = new Transport() + { + callback = publicUrl, + secret = hmacSecret + }, + type = SubscribableTypesTranslation.Enum2String(type), + version = "1" + }; + var subAsString = JsonConvert.SerializeObject(subreq); + var content = new StringContent(subAsString, Encoding.UTF8, "application/json"); + var r = client.PostAsync("https://api.twitch.tv/helix/eventsub/subscriptions", content).Result; + + string responseText; + using (var streamReader = new StreamReader(r.Content.ReadAsStream())) + { + responseText = streamReader.ReadToEnd(); + } + var tr = SubscribableTypesTranslation.RefineTwitchResponse(responseText); + + var firstSubscription = tr.data?.FirstOrDefault(); + if(firstSubscription != null) + { + psuedoResources.Add(firstSubscription, handler); + } + } + public async Task go() + { + going = true; + server.Start(); + await Task.Run(() => + { + while (true) + { + HttpListenerContext context = server.GetContext(); + HttpListenerResponse response = context.Response; + + string page = context.Request.Url.LocalPath.Substring(1); + Console.WriteLine($"{DateTime.Now.ToShortTimeString()} page requested: {page}"); + var resp = "no resource"; + response.StatusCode = (int)HttpStatusCode.NotFound; + lock (psuedoResources) + { + if (page == "twitcherize") + { + byte[] rawIncomingBytes = new byte[context.Request.ContentLength64]; + var readBytes = context.Request.InputStream.Read(rawIncomingBytes, 0, (int)context.Request.ContentLength64); + + + if (VerifySignature(context.Request.Headers["Twitch-Eventsub-Message-Signature"], + context.Request.ContentEncoding, + context.Request.Headers["Twitch-Eventsub-Message-Id"], + context.Request.Headers["Twitch-Eventsub-Message-Timestamp"], + rawIncomingBytes)) + { + try + { + if (twitchogrambuffer.Contains(context.Request.Headers["Twitch-Eventsub-Message-Id"])) + { + Console.WriteLine("duplicate message received. Ignoring."); + } + else + { + var incomingText = context.Request.ContentEncoding.GetString(rawIncomingBytes); + //Console.WriteLine($"{context.Request.Headers["Twitch-Eventsub-Message-Id"]} looks new to me."); + lock (twitchogrambuffer) + { + var fromTwitch = SubscribableTypesTranslation.RefineTwitchogram(incomingText); + if (fromTwitch.challenge != null) + { + resp = fromTwitch.challenge; + Console.WriteLine("challenge responded to, should be subscribed"); + } + else if (fromTwitch.subscription.status == "authorization_revoked") + { + resp = "ok... :("; + Console.WriteLine($"auth revoked for {fromTwitch.subscription.type} ({fromTwitch.subscription.id})"); + } + else + { + var foundAHandler = false; + foreach (var kvp in psuedoResources) + { + if (kvp.Key.id == fromTwitch.subscription.id) + { + kvp.Value(fromTwitch); + resp = ":)"; + foundAHandler = true; + break; + } + } + if (!foundAHandler) + { + Console.WriteLine($"I don't have a handler for {fromTwitch.subscription.type} ({fromTwitch.subscription.id})? o_O"); + resp = ":S"; + } + } + } + tasksToNotActuallyAwait.Add(logMessageId(context.Request.Headers["Twitch-Eventsub-Message-Id"])); + response.StatusCode = (int)HttpStatusCode.OK; + } + } + catch (Exception e) + { + Console.Error.WriteLine("something went wrong calling resource."); + Console.Error.WriteLine(JsonConvert.SerializeObject(e)); + response.StatusCode = (int)HttpStatusCode.InternalServerError; + resp = "error :("; + } + } + else + { + Console.WriteLine("couldn't verify signature."); + response.StatusCode = (int)HttpStatusCode.Forbidden; + resp = "VoteNay"; + } + } + } + + Console.WriteLine($"returning status code {response.StatusCode}"); + byte[] buffer = Encoding.UTF8.GetBytes(resp); + response.ContentLength64 = buffer.Length; + Stream st = response.OutputStream; + st.Write(buffer, 0, buffer.Length); + response.Close(); + } + }); + going = false; + } + + private async Task logMessageId(string id) + { + lock(twitchogrambuffer) + { + twitchogrambuffer.Enqueue(id); + } + await Task.Delay(2000); + string x; + lock(twitchogrambuffer) + { + twitchogrambuffer.TryDequeue(out x); + } + } + + private bool VerifySignature(string signature, Encoding encoder, string messageId, string timestamp, byte[] rawIncomingBytes) + { + var hashables = new List(); + hashables.Add(encoder.GetBytes(messageId + timestamp).Concat(rawIncomingBytes).ToArray()); + hashables.Add(encoder.GetBytes(messageId.ToLower() + timestamp).Concat(rawIncomingBytes).ToArray()); + hashables.Add(encoder.GetBytes(messageId.ToLower() + timestamp.ToLower()).Concat(rawIncomingBytes).ToArray()); + hashables.Add(encoder.GetBytes(messageId + timestamp.ToLower()).Concat(rawIncomingBytes).ToArray()); + var computeds = new List(); + foreach(var hashable in hashables) + { + computeds.Add("sha256=" + Convert.ToHexString(hmacsha256.ComputeHash(hashable)).ToLower()); + // var hashed = computeds.Last(); + // if(signature?.ToLower() == hashed) + // { + // Console.Write(" * "); + // } + // else + // { + // Console.Write(" "); + // } + // Console.WriteLine(computeds.Last()); + } + return computeds.Contains(signature?.ToLower()); + //Console.WriteLine($"{signature?.ToLower()} == sha256={computed}?"); + //return signature?.ToLower() == $"sha256={computed}"; + } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelCheer.cs b/TwitchEventSub/Types/ChannelCheer.cs new file mode 100644 index 0000000..67639b7 --- /dev/null +++ b/TwitchEventSub/Types/ChannelCheer.cs @@ -0,0 +1,15 @@ +namespace TwitchEventSub.Types +{ + public class ChannelCheer : Event + { + public bool is_anonymous { get; set; } //Whether the user cheered anonymously or not. + public string user_id { get; set; } //The user ID for the user who cheered on the specified channel. This is null if is_anonymous is true. + public string user_login { get; set; } //The user login for the user who cheered on the specified channel. This is null if is_anonymous is true. + public string user_name { get; set; } //The user display name for the user who cheered on the specified channel. This is null if is_anonymous is true. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string message { get; set; } //The message sent with the cheer. + public int bits { get; set; } //The number of bits cheered. + } +} diff --git a/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomReward.cs b/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomReward.cs new file mode 100644 index 0000000..1b24da0 --- /dev/null +++ b/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomReward.cs @@ -0,0 +1,20 @@ +namespace TwitchEventSub.Types.ChannelPoints +{ + public class CustomChannelPointsReward + { + public string id { get; set; } + public string title { get; set; } + public int cost { get; set; } + public string prompt { get; set; } //The reward description. + } + public class GlobalCooldown + { + public bool is_enabled { get; set; } + public int seconds { get; set; } + } + public abstract class intSetting + { + public bool is_enabled { get; set; } + public int value { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomRewardRedemption.cs b/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomRewardRedemption.cs new file mode 100644 index 0000000..813862a --- /dev/null +++ b/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomRewardRedemption.cs @@ -0,0 +1,19 @@ +namespace TwitchEventSub.Types.ChannelPoints +{ + public class ChannelPointsCustomRewardRedemption: Event + { + public string id { get; set; } //The redemption identifier. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string user_id { get; set; } //User ID of the user that redeemed the reward. + public string user_login { get; set; } //Login of the user that redeemed the reward. + public string user_name { get; set; } //Display name of the user that redeemed the reward. + public string user_input { get; set; } //The user input provided. Empty string if not provided. + public string status { get; set; } //add defaults to unfulfilled, update will be fulfilled or canceled. Possible values are unknown, unfulfilled, fulfilled, and canceled. + public CustomChannelPointsReward reward { get; set; } //Basic information about the reward that was redeemed, at the time it was redeemed. + public string redeemed_at { get; set; } //RFC3339 timestamp of when the reward was redeemed. + } + public class ChannelPointsCustomRewardRedemptionAdd : ChannelPointsCustomRewardRedemption {} + public class ChannelPointsCustomRewardRedemptionUpdate : ChannelPointsCustomRewardRedemption {} +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomRewardUpdate.cs b/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomRewardUpdate.cs new file mode 100644 index 0000000..10e88b1 --- /dev/null +++ b/TwitchEventSub/Types/ChannelPoints/ChannelPointsCustomRewardUpdate.cs @@ -0,0 +1,28 @@ +namespace TwitchEventSub.Types.ChannelPoints +{ + public class ChannelPointsCustomRewardUpdate : Event + { + public string id { get; set; } //The reward identifier. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public bool is_enabled { get; set; } //Is the reward currently enabled. If false, the reward won’t show up to viewers. + public bool is_paused { get; set; } //Is the reward currently paused. If true, viewers can’t redeem. + public bool is_in_stock { get; set; } //Is the reward currently in stock. If false, viewers can’t redeem. + public string title { get; set; } //The reward title. + public int cost { get; set; } //The reward cost. + public string prompt { get; set; } //The reward description. + public bool is_user_input_required { get; set; } //Does the viewer need to enter information when redeeming the reward. + public bool should_redemptions_skip_request_queue { get; set; } //Should redemptions be set to fulfilled status immediately when redeemed and skip the request queue instead of the normal unfulfilled status. + public intSetting max_per_stream { get; set; } //Whether a maximum per stream is enabled and what the maximum is. + public intSetting max_per_user_per_stream { get; set; } //Whether a maximum per user per stream is enabled and what the maximum is. + public string background_color { get; set; } //Custom background color for the reward. Format: Hex with # prefix. Example: #FA1ED2. + public Image image { get; set; } //Set of custom images of 1x, 2x and 4x sizes for the reward. Can be null if no images have been uploaded. + public Image default_image { get; set; } //Set of default images of 1x, 2x and 4x sizes for the reward. + public GlobalCooldown global_cooldown { get; set; } //Whether a cooldown is enabled and what the cooldown is in seconds. + public string cooldown_expires_at { get; set; } //Timestamp of the cooldown expiration. null if the reward isn’t on cooldown. + public int redemptions_redeemed_current_stream { get; set; } //The number of redemptions redeemed during the current live stream. Counts against the max_per_stream limit. null if the broadcasters stream isn’t live or max_per_stream isn’t enabled. + } + public class ChannelPointsCustomRewardRemove : ChannelPointsCustomRewardUpdate{} + public class ChannelPointsCustomRewardAdd : ChannelPointsCustomRewardUpdate{} +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelPoints/Image.cs b/TwitchEventSub/Types/ChannelPoints/Image.cs new file mode 100644 index 0000000..53e7e0c --- /dev/null +++ b/TwitchEventSub/Types/ChannelPoints/Image.cs @@ -0,0 +1,11 @@ +using System; + +namespace TwitchEventSub.Types +{ + public class Image + { + public Uri url_1x { get; set; } //URL for the image at 1x size. + public Uri url_2x { get; set; } + public Uri url_4x { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelRaid.cs b/TwitchEventSub/Types/ChannelRaid.cs new file mode 100644 index 0000000..906947e --- /dev/null +++ b/TwitchEventSub/Types/ChannelRaid.cs @@ -0,0 +1,14 @@ + +namespace TwitchEventSub.Types +{ + public class ChannelRaid : Event + { + public string from_broadcaster_user_id { get; set; } //The broadcaster ID that created the raid. + public string from_broadcaster_user_login { get; set; } //The broadcaster login that created the raid. + public string from_broadcaster_user_name { get; set; } //The broadcaster display name that created the raid. + public string to_broadcaster_user_id { get; set; } //The broadcaster ID that received the raid. + public string to_broadcaster_user_login { get; set; } //The broadcaster login that received the raid. + public string to_broadcaster_user_name { get; set; } //The broadcaster display name that received the raid. + public int viewers { get; set; } //The number of viewers in the raid. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelSubscription/ChannelSubscribe.cs b/TwitchEventSub/Types/ChannelSubscription/ChannelSubscribe.cs new file mode 100644 index 0000000..f0b1096 --- /dev/null +++ b/TwitchEventSub/Types/ChannelSubscription/ChannelSubscribe.cs @@ -0,0 +1,14 @@ +namespace TwitchEventSub.Types.ChannelSubscription +{ + public class ChannelSubscribe : Event + { + public string user_id { get; set; } //The user ID for the user who subscribed to the specified channel. + public string user_login { get; set; } //The user login for the user who subscribed to the specified channel. + public string user_name { get; set; } //The user display name for the user who subscribed to the specified channel. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string tier { get; set; } //The tier of the subscription. Valid values are 1000, 2000, and 3000. + public bool is_gift { get; set; } //Whether the subscription is a gift. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionEnd.cs b/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionEnd.cs new file mode 100644 index 0000000..1f7439f --- /dev/null +++ b/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionEnd.cs @@ -0,0 +1,15 @@ +namespace TwitchEventSub.Types.ChannelSubscription +{ + //this event continues to be WILD. what POSSIBLE use can there be that isn't horrible? + public class ChannelSubscriptionEnd : Event + { + public string user_id { get; set; } //The user ID for the user whose subscription ended. + public string user_login { get; set; } //The user login for the user whose subscription ended. + public string user_name { get; set; } //The user display name for the user whose subscription ended. + public string broadcaster_user_id { get; set; } //The broadcaster user ID. + public string broadcaster_user_login { get; set; } //The broadcaster login. + public string broadcaster_user_name { get; set; } //The broadcaster display name. + public string tier { get; set; } //The tier of the subscription that ended. Valid values are 1000, 2000, and 3000. + public bool is_gift { get; set; } //Whether the subscription was a gift. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionGift.cs b/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionGift.cs new file mode 100644 index 0000000..d41f8b1 --- /dev/null +++ b/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionGift.cs @@ -0,0 +1,16 @@ +namespace TwitchEventSub.Types.ChannelSubscription +{ + public class ChannelSubscriptionGift : Event + { + public string user_id { get; set; } //The user ID of the user who sent the subscription gift. Set to null if it was an anonymous subscription gift. + public string user_login { get; set; } //The user login of the user who sent the gift. Set to null if it was an anonymous subscription gift. + public string user_name { get; set; } //The user display name of the user who sent the gift. Set to null if it was an anonymous subscription gift. + public string broadcaster_user_id { get; set; } //The broadcaster user ID. + public string broadcaster_user_login { get; set; } //The broadcaster login. + public string broadcaster_user_name { get; set; } //The broadcaster display name. + public int total { get; set; } //The number of subscriptions in the subscription gift. + public string tier { get; set; } //The tier of subscriptions in the subscription gift. + public int cumulative_total { get; set; } //The number of subscriptions gifted by this user in the channel. This value is null for anonymous gifts or if the gifter has opted out of sharing this information. + public bool is_anonymous { get; set; } //Whether the subscription gift was anonymous. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionMessage.cs b/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionMessage.cs new file mode 100644 index 0000000..ecb5960 --- /dev/null +++ b/TwitchEventSub/Types/ChannelSubscription/ChannelSubscriptionMessage.cs @@ -0,0 +1,28 @@ +namespace TwitchEventSub.Types.ChannelSubscription +{ + public class ChannelSubscriptionMessage : Event + { + public string user_id { get; set; } //The user ID of the user who sent a resubscription chat message. + public string user_login { get; set; } //The user login of the user who sent a resubscription chat message. + public string user_name { get; set; } //The user display name of the user who a resubscription chat message. + public string broadcaster_user_id { get; set; } //The broadcaster user ID. + public string broadcaster_user_login { get; set; } //The broadcaster login. + public string broadcaster_user_name { get; set; } //The broadcaster display name. + public string tier { get; set; } //The tier of the user’s subscription. + public Message message { get; set; } //An object that contains the resubscription message and emote information needed to recreate the message. + public int cumulative_months { get; set; } //The total number of months the user has been subscribed to the channel. + public int streak_months { get; set; } //The number of consecutive months the user’s current subscription has been active. This value is null if the user has opted out of sharing this information. + public int duration_months { get; set; } //The month duration of the subscription. + } + public class Message + { + public string text { get; set; } + public Emote[] emotes { get; set; } + } + public class Emote + { + public int begin { get; set; } //where the Emote starts in the text. + public int end { get; set; } //where the Emote ends in the text. + public string id { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/DropEntitlementGrant.cs b/TwitchEventSub/Types/DropEntitlementGrant.cs new file mode 100644 index 0000000..540b381 --- /dev/null +++ b/TwitchEventSub/Types/DropEntitlementGrant.cs @@ -0,0 +1,22 @@ +namespace TwitchEventSub.Types +{ + public class DropEntitlementGrant : Event + { + public string id { get; set; } //Individual event ID, as assigned by EventSub. Use this for de-duplicating messages. + public EntitlementObject[] data { get; set; } + + public class EntitlementObject + { + public string organization_id { get; set; } //The ID of the organization that owns the game that has Drops enabled. + public string category_id { get; set; } //Twitch category ID of the game that was being played when this benefit was entitled. + public string category_name { get; set; } //The category name. + public string campaign_id { get; set; } //The campaign this entitlement is associated with. + public string user_id { get; set; } //Twitch user ID of the user who was granted the entitlement. + public string user_name { get; set; } //The user display name of the user who was granted the entitlement. + public string user_login { get; set; } //The user login of the user who was granted the entitlement. + public string entitlement_id { get; set; } //Unique identifier of the entitlement. Use this to de-duplicate entitlements. + public string benefit_id { get; set; } //Identifier of the Benefit. + public string created_at { get; set; } //UTC timestamp in ISO format when this entitlement was granted on Twitch. + } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Event.cs b/TwitchEventSub/Types/Event.cs new file mode 100644 index 0000000..636f426 --- /dev/null +++ b/TwitchEventSub/Types/Event.cs @@ -0,0 +1,4 @@ +namespace TwitchEventSub.Types +{ + public class Event {} +} \ No newline at end of file diff --git a/TwitchEventSub/Types/EventSubSubscription/Condition.cs b/TwitchEventSub/Types/EventSubSubscription/Condition.cs new file mode 100644 index 0000000..4c4f450 --- /dev/null +++ b/TwitchEventSub/Types/EventSubSubscription/Condition.cs @@ -0,0 +1,81 @@ +using System; +using Newtonsoft.Json; + +namespace TwitchEventSub.Types.Conditions +{ + #region object oriented + public class Condition + { + public bool Similar(Condition condition) + { + return JsonConvert.SerializeObject(this) == JsonConvert.SerializeObject(condition); + } + } + + public abstract class TargetChannel : Condition + { + public string broadcaster_user_id { get; set; } + } + public class TargetExtension : Condition + { + public string extension_client_id { get; set; } + } + public class TargetSelf : Condition + { + public string client_id { get; set; } + } + public class TargetUser : Condition + { + public string user_id { get; set; } + } + public abstract class TargetCustomChannelPointsReward : Condition + { + public string broadcaster_user_id { get; set; } + public string reward_id { get; set; } + } + #endregion + public class ChannelBan : TargetChannel { } + public class ChannelSubscribe : TargetChannel { } + public class ChannelSubscriptionEnd : TargetChannel { } + public class ChannelSubscriptionGift : TargetChannel { } + public class ChannelSubscriptionMessage : TargetChannel { } + public class ChannelCheer : TargetChannel { } + public class ChannelUpdate : TargetChannel { } + public class ChannelFollow : TargetChannel { } + public class ChannelUnban : TargetChannel { } + public class ChannelModeratorAdd : TargetChannel { } + public class ChannelModeratorRemove : TargetChannel { } + public class ChannelPointsCustomRewardAdd : TargetChannel { } + public class ChannelPollBegin : TargetChannel { } + public class ChannelPollProgress : TargetChannel { } + public class ChannelPollEnd : TargetChannel { } + public class ChannelPredictionBegin : TargetChannel { } + public class ChannelPredictionProgress : TargetChannel { } + public class ChannelPredictionLock : TargetChannel { } + public class ChannelPredictionEnd : TargetChannel { } + public class Goals : TargetChannel { } + public class HypeTrainBegin : TargetChannel { } + public class HypeTrainProgress : TargetChannel { } + public class HypeTrainEnd : TargetChannel { } + public class StreamOnline : TargetChannel { } + public class StreamOffline : TargetChannel { } + public class ChannelRaid : Condition + { + public string from_broadcaster_user_id { get; set; } + public string to_broadcaster_user_id { get; set; } + } + public class ChannelPointsCustomRewardUpdate : TargetCustomChannelPointsReward { } + public class ChannelPointsCustomRewardRemove : TargetCustomChannelPointsReward { } + public class ChannelPointsCustomRewardRedemptionAdd : TargetCustomChannelPointsReward { } + public class ChannelPointsCustomRewardRedemptionUpdate : TargetCustomChannelPointsReward { } + public class DropEntitlementGrant : Condition + { + public string organization_id { get; set; } + public string category_id { get; set; } + public string campaign_id { get; set; } + } + public class ExtensionBitsTransactionCreate : TargetExtension { } + public class UserAuthorizationGrant : TargetSelf { } + public class UserAuthorizationRevoke : TargetSelf { } + public class UserUpdate : TargetUser { } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/EventSubSubscription/Request.cs b/TwitchEventSub/Types/EventSubSubscription/Request.cs new file mode 100644 index 0000000..74f571c --- /dev/null +++ b/TwitchEventSub/Types/EventSubSubscription/Request.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace TwitchEventSub.Types.EventSubSubscription +{ + public class Request + { + /// + ///use one of SubscribableTypes + /// + public string type { get; set; } + public string version { get; set; } + public Conditions.Condition condition { get; set; } + public Transport transport { get; set; } + } + public class Transport + { + public TransportMethod method { get; set; } //The transport method. Supported values: webhook. + public Uri callback { get; set; } //The callback URL where the notification should be sent. Must use https. Must use port 443. + public string secret { get; set; } //The secret used for verifying a signature. + } + public enum TransportMethod { webhook } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/EventSubSubscription/Response.cs b/TwitchEventSub/Types/EventSubSubscription/Response.cs new file mode 100644 index 0000000..02c4729 --- /dev/null +++ b/TwitchEventSub/Types/EventSubSubscription/Response.cs @@ -0,0 +1,8 @@ +namespace TwitchEventSub.Types.EventSubSubscription +{ + public class Response + { + public Subscription subscription { get; set; } + public Event Event { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/EventSubSubscription/Subscription.cs b/TwitchEventSub/Types/EventSubSubscription/Subscription.cs new file mode 100644 index 0000000..a070535 --- /dev/null +++ b/TwitchEventSub/Types/EventSubSubscription/Subscription.cs @@ -0,0 +1,25 @@ +using System; + +namespace TwitchEventSub.Types.EventSubSubscription +{ + public class Subscription + { + public string id { get; set; } //Your client ID. + public string type { get; set; } //The notification’s subscription type. + public string version { get; set; } //The version of the subscription. + public string status { get; set; } //The status of the subscription. + public int cost { get; set; } //How much the subscription counts against your limit. See Subscription Limits for more information. + public Conditions.Condition condition { get; set; } //Subscription-specific parameters. + public string created_at { get; set; } //The tChannelPointsime the notification was created. + } + internal class UnrefinedSubscription + { + public string id { get; set; } + public string type { get; set; } + public string version { get; set; } + public string status { get; set; } + public int cost { get; set; } + public string condition { get; set; } + public string created_at { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/EventSubSubscription/TwitchResponse.cs b/TwitchEventSub/Types/EventSubSubscription/TwitchResponse.cs new file mode 100644 index 0000000..0b98e86 --- /dev/null +++ b/TwitchEventSub/Types/EventSubSubscription/TwitchResponse.cs @@ -0,0 +1,12 @@ +using System; + +namespace TwitchEventSub.Types.EventSubSubscription +{ + public class TwitchResponse + { + public Subscription[] data { get; set; } + public int total { get; set; } + public int max_total_cost { get; set; } + public int total_cost { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/EventSubSubscription/Twitchogram.cs b/TwitchEventSub/Types/EventSubSubscription/Twitchogram.cs new file mode 100644 index 0000000..2a8e0cd --- /dev/null +++ b/TwitchEventSub/Types/EventSubSubscription/Twitchogram.cs @@ -0,0 +1,15 @@ +using System.Linq; +using System.Text.Json.Serialization; + +namespace TwitchEventSub.Types.EventSubSubscription +{ + /// + /// don't deserialize me, go to SubscribableTypesTranslation and have it "refine" one for you + /// + public class Twitchogram + { + public string challenge { get; set; } + public Subscription subscription { get; set; } + public Event Event { get; set; } + } +} diff --git a/TwitchEventSub/Types/EventSubSubscription/TypeTranslation.cs b/TwitchEventSub/Types/EventSubSubscription/TypeTranslation.cs new file mode 100644 index 0000000..d6ec4de --- /dev/null +++ b/TwitchEventSub/Types/EventSubSubscription/TypeTranslation.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace TwitchEventSub.Types.EventSubSubscription +{ + public enum SubscribableTypes + { + channel_update, //aka Channel Update, version 1. A broadcaster updates their channel properties e.g., category, title, mature flag, broadcast, or language. + channel_follow, //aka Channel Follow, version 1. A specified channel receives a follow. + channel_subscribe, //aka Channel Subscribe, version 1. A notification when a specified channel receives a subscriber. This does not include resubscribes. + channel_subscription_end, //aka Channel Subscription End, version 1. A notification when a subscription to the specified channel ends. + channel_subscription_gift, //aka Channel Subscription Gift, version 1. A notification when a viewer gives a gift subscription to one or more users in the specified channel. + channel_subscription_message, //aka Channel Subscription Message, version 1. A notification when a user sends a resubscription chat message in a specific channel. + channel_cheer, //aka Channel Cheer, version 1. A user cheers on the specified channel. + channel_raid, //aka Channel Raid, version 1. A broadcaster raids another broadcaster’s channel. + channel_ban, //aka Channel Ban, version 1. A viewer is banned from the specified channel. + channel_unban, //aka Channel Unban, version 1. A viewer is unbanned from the specified channel. + channel_moderator_add, //aka Channel Moderator Add, version 1. Moderator privileges were added to a user on a specified channel. + channel_moderator_remove, //aka Channel Moderator Remove, version 1. Moderator privileges were removed from a user on a specified channel. + channel_channel_points_custom_reward_add, //aka Channel Points Custom Reward Add, version 1. A custom channel points reward has been created for the specified channel. + channel_channel_points_custom_reward_update, //aka Channel Points Custom Reward Update, version 1. A custom channel points reward has been updated for the specified channel. + channel_channel_points_custom_reward_remove, //aka Channel Points Custom Reward Remove, version 1. A custom channel points reward has been removed from the specified channel. + channel_channel_points_custom_reward_redemption_add, //aka Channel Points Custom Reward Redemption Add, version 1. A viewer has redeemed a custom channel points reward on the specified channel. + channel_channel_points_custom_reward_redemption_update, //aka Channel Points Custom Reward Redemption Update, version 1. A redemption of a channel points custom reward has been updated for the specified channel. + channel_poll_begin, //aka Channel Poll Begin, version 1. A poll started on a specified channel. + channel_poll_progress, //aka Channel Poll Progress, version 1. Users respond to a poll on a specified channel. + channel_poll_end, //aka Channel Poll End, version 1. A poll ended on a specified channel. + channel_prediction_begin, //aka Channel Prediction Begin, version 1. A Prediction started on a specified channel. + channel_prediction_progress, //aka Channel Prediction Progress, version 1. Users participated in a Prediction on a specified channel. + channel_prediction_lock, //aka Channel Prediction Lock, version 1. A Prediction was locked on a specified channel. + channel_prediction_end, //aka Channel Prediction End, version 1. A Prediction ended on a specified channel. + drop_entitlement_grant, //aka Drop Entitlement Grant, version 1. An entitlement for a Drop is granted to a user. + extension_bits_transaction_create, //aka Extension Bits Transaction Create, version 1. A Bits transaction occurred for a specified Twitch Extension. + channel_goal_begin, //aka Goal Begin, version 1. Get notified when a broadcaster begins a goal. + channel_goal_progress, //aka Goal Progress, version 1. Get notified when progress (either positive or negative) is made towards a broadcaster’s goal. + channel_goal_end, //aka Goal End, version 1. Get notified when a broadcaster ends a goal. + channel_hype_train_begin, //aka Hype Train Begin, version 1. A Hype Train begins on the specified channel. + channel_hype_train_progress, //aka Hype Train Progress, version 1. A Hype Train makes progress on the specified channel. + channel_hype_train_end, //aka Hype Train End, version 1. A Hype Train ends on the specified channel. + stream_online, //aka Stream Online, version 1. The specified broadcaster starts a stream. + stream_offline, //aka Stream Offline, version 1. The specified broadcaster stops a stream. + user_authorization_grant, //aka User Authorization Grant, version 1. A user’s authorization has been granted to your client id. + user_authorization_revoke, //aka User Authorization Revoke, version 1. A user’s authorization has been revoked for your client id. + user_update //aka User Update, version 1. A user has updated their account. + } + internal class SubscribableTypesTranslation + { + public static string Enum2String(SubscribableTypes enumedType) + { + return table.Keys.FirstOrDefault(k => table[k] == enumedType); + } + public static SubscribableTypes String2Enum(string stringedType) + { + return table[stringedType]; + } + public static Twitchogram RefineTwitchogram(string raw) + { + var urT = JsonConvert.DeserializeObject(raw); + var refinedSubscription = new Subscription() + { + id = urT.subscription.id, + type = urT.subscription.type, + version = urT.subscription.version, + status = urT.subscription.status, + cost = urT.subscription.cost, + created_at = urT.subscription.created_at + }; + + var refinedTuple = TypedPieces(urT.subscription.type); + + refinedSubscription.condition = refinedTuple.Item1; + + var refinedTwitchogram = new Twitchogram() + { + challenge = urT.challenge, + subscription = refinedSubscription, + Event = refinedTuple.Item2 + }; + JsonConvert.PopulateObject(raw, refinedTwitchogram); + return refinedTwitchogram; + } + public static TwitchResponse RefineTwitchResponse(string raw) + { + var refinedResponse = JsonConvert.DeserializeObject(raw); + foreach(var d in refinedResponse.data) + { + d.condition = TypedCondition(d.type); + } + JsonConvert.PopulateObject(raw, refinedResponse); + + return refinedResponse; + } + public static Event TypedEvent(string stringType) + { + return TypedPieces(stringType).Item2; + } + public static Conditions.Condition TypedCondition(string stringType) + { + return TypedPieces(stringType).Item1; + } + public static Tuple TypedPieces(string stringType) + { + switch (String2Enum(stringType)) + { + case SubscribableTypes.channel_update: + return new Tuple( + new Conditions.ChannelUpdate(), + new Types.Offline.ChannelUpdate()); + case SubscribableTypes.channel_follow: + return new Tuple( + new Conditions.ChannelFollow(), + new Types.Offline.ChannelFollow()); + + case SubscribableTypes.channel_subscribe: + return new Tuple( + new Conditions.ChannelSubscribe(), + new Types.ChannelSubscription.ChannelSubscribe()); + + case SubscribableTypes.channel_subscription_end: + return new Tuple( + new Conditions.ChannelSubscriptionEnd(), + new Types.ChannelSubscription.ChannelSubscriptionEnd()); + + case SubscribableTypes.channel_subscription_gift: + return new Tuple( + new Conditions.ChannelSubscriptionGift(), + new Types.ChannelSubscription.ChannelSubscriptionGift()); + + case SubscribableTypes.channel_subscription_message: + return new Tuple( + new Conditions.ChannelSubscriptionMessage(), + new Types.ChannelSubscription.ChannelSubscriptionMessage()); + + case SubscribableTypes.channel_cheer: + return new Tuple( + new Conditions.ChannelCheer(), + new Types.ChannelCheer()); + + case SubscribableTypes.channel_raid: + return new Tuple( + new Conditions.ChannelRaid(), + new Types.ChannelRaid()); + + case SubscribableTypes.channel_ban: + return new Tuple( + new Conditions.ChannelBan(), + new Types.Moderation.ChannelBan()); + + case SubscribableTypes.channel_unban: + return new Tuple( + new Conditions.ChannelUnban(), + new Types.Moderation.ChannelUnban()); + + case SubscribableTypes.channel_moderator_add: + return new Tuple( + new Conditions.ChannelModeratorAdd(), + new Types.Moderation.ChannelModeratorAdd()); + + case SubscribableTypes.channel_moderator_remove: + return new Tuple( + new Conditions.ChannelModeratorRemove(), + new Types.Moderation.ChannelModeratorRemove()); + + case SubscribableTypes.channel_channel_points_custom_reward_add: + return new Tuple( + new Conditions.ChannelPointsCustomRewardAdd(), + new Types.ChannelPoints.ChannelPointsCustomRewardAdd()); + + case SubscribableTypes.channel_channel_points_custom_reward_update: + return new Tuple( + new Conditions.ChannelPointsCustomRewardUpdate(), + new Types.ChannelPoints.ChannelPointsCustomRewardUpdate()); + + case SubscribableTypes.channel_channel_points_custom_reward_remove: + return new Tuple( + new Conditions.ChannelPointsCustomRewardRemove(), + new Types.ChannelPoints.ChannelPointsCustomRewardRemove()); + + case SubscribableTypes.channel_channel_points_custom_reward_redemption_add: + return new Tuple( + new Conditions.ChannelPointsCustomRewardRedemptionAdd(), + new Types.ChannelPoints.ChannelPointsCustomRewardRedemptionAdd()); + + case SubscribableTypes.channel_channel_points_custom_reward_redemption_update: + return new Tuple( + new Conditions.ChannelPointsCustomRewardRedemptionUpdate(), + new Types.ChannelPoints.ChannelPointsCustomRewardRedemptionUpdate()); + + case SubscribableTypes.channel_poll_begin: + return new Tuple( + new Conditions.ChannelPollBegin(), + new Types.Poll.ChannelPollBegin()); + + case SubscribableTypes.channel_poll_progress: + return new Tuple( + new Conditions.ChannelPollProgress(), + new Types.Poll.ChannelPollProgress()); + + case SubscribableTypes.channel_poll_end: + return new Tuple( + new Conditions.ChannelPollEnd(), + new Types.Poll.ChannelPollEnd()); + + case SubscribableTypes.channel_prediction_begin: + return new Tuple( + new Conditions.ChannelPollBegin(), + new Types.Poll.ChannelPollBegin()); + + case SubscribableTypes.channel_prediction_progress: + return new Tuple( + new Conditions.ChannelPollProgress(), + new Types.Poll.ChannelPollProgress()); + + case SubscribableTypes.channel_prediction_lock: + return new Tuple( + new Conditions.ChannelPredictionLock(), + new Types.Prediction.ChannelPredictionLock()); + + case SubscribableTypes.channel_prediction_end: + return new Tuple( + new Conditions.ChannelPredictionEnd(), + new Types.Prediction.ChannelPredictionEnd()); + + case SubscribableTypes.drop_entitlement_grant: + return new Tuple( + new Conditions.DropEntitlementGrant(), + new Types.DropEntitlementGrant()); + + case SubscribableTypes.extension_bits_transaction_create: + return new Tuple( + new Conditions.ExtensionBitsTransactionCreate(), + new Types.ExtensionBitsTransactionCreate()); + + case SubscribableTypes.channel_goal_begin: + case SubscribableTypes.channel_goal_progress: + case SubscribableTypes.channel_goal_end: + return new Tuple( + new Conditions.Goals(), + new Types.Goals()); + + case SubscribableTypes.channel_hype_train_begin: + return new Tuple( + new Conditions.HypeTrainBegin(), + new Types.HypeTrain.HypeTrainBegin()); + + case SubscribableTypes.channel_hype_train_progress: + return new Tuple( + new Conditions.HypeTrainProgress(), + new Types.HypeTrain.HypeTrainProgress()); + + case SubscribableTypes.channel_hype_train_end: + return new Tuple( + new Conditions.HypeTrainEnd(), + new Types.HypeTrain.HypeTrainEnd()); + + case SubscribableTypes.stream_online: + return new Tuple( + new Conditions.StreamOnline(), + new Types.StreamOnline()); + + case SubscribableTypes.stream_offline: + return new Tuple( + new Conditions.StreamOffline(), + new Types.StreamOffline()); + + case SubscribableTypes.user_authorization_grant: + return new Tuple( + new Conditions.UserAuthorizationGrant(), + new Types.Offline.UserAuthorizationGrant()); + + case SubscribableTypes.user_authorization_revoke: + return new Tuple( + new Conditions.UserAuthorizationRevoke(), + new Types.Offline.UserAuthorizationRevoke()); + + case SubscribableTypes.user_update: + return new Tuple( + new Conditions.UserUpdate(), + new Types.Offline.UserUpdate()); + + default: + throw new ArgumentOutOfRangeException($"I've never heard of {stringType}. That's not real. I don't believe in that."); + } + } + private static readonly Dictionary table = new Dictionary() + { + {"channel.update", SubscribableTypes.channel_update}, + {"channel.follow", SubscribableTypes.channel_follow}, + {"channel.subscribe", SubscribableTypes.channel_subscribe}, + {"channel.subscription.end", SubscribableTypes.channel_subscription_end}, + {"channel.subscription.gift", SubscribableTypes.channel_subscription_gift}, + {"channel.subscription.message", SubscribableTypes.channel_subscription_message}, + {"channel.cheer", SubscribableTypes.channel_cheer}, + {"channel.raid", SubscribableTypes.channel_raid}, + {"channel.ban", SubscribableTypes.channel_ban}, + {"channel.unban", SubscribableTypes.channel_unban}, + {"channel.moderator.add", SubscribableTypes.channel_moderator_add}, + {"channel.moderator.remove", SubscribableTypes.channel_moderator_remove}, + {"channel.channel.points.custom.reward.add", SubscribableTypes.channel_channel_points_custom_reward_add}, + {"channel.channel.points.custom.reward.update", SubscribableTypes.channel_channel_points_custom_reward_update}, + {"channel.channel.points.custom.reward.remove", SubscribableTypes.channel_channel_points_custom_reward_remove}, + {"channel.channel.points.custom.reward.redemption.add", SubscribableTypes.channel_channel_points_custom_reward_redemption_add}, + {"channel.channel.points.custom.reward.redemption.update", SubscribableTypes.channel_channel_points_custom_reward_redemption_update}, + {"channel.poll.begin", SubscribableTypes.channel_poll_begin}, + {"channel.poll.progress", SubscribableTypes.channel_poll_progress}, + {"channel.poll.end", SubscribableTypes.channel_poll_end}, + {"channel.prediction.begin", SubscribableTypes.channel_prediction_begin}, + {"channel.prediction.progress", SubscribableTypes.channel_prediction_progress}, + {"channel.prediction.lock", SubscribableTypes.channel_prediction_lock}, + {"channel.prediction.end", SubscribableTypes.channel_prediction_end}, + {"drop.entitlement.grant", SubscribableTypes.drop_entitlement_grant}, + {"extension.bits.transaction.create", SubscribableTypes.extension_bits_transaction_create}, + {"channel.goal.begin", SubscribableTypes.channel_goal_begin}, + {"channel.goal.progress", SubscribableTypes.channel_goal_progress}, + {"channel.goal.end", SubscribableTypes.channel_goal_end}, + {"channel.hype.train.begin", SubscribableTypes.channel_hype_train_begin}, + {"channel.hype.train.progress", SubscribableTypes.channel_hype_train_progress}, + {"channel.hype.train.end", SubscribableTypes.channel_hype_train_end}, + {"stream.online", SubscribableTypes.stream_online}, + {"stream.offline", SubscribableTypes.stream_offline}, + {"user.authorization.grant", SubscribableTypes.user_authorization_grant}, + {"user.authorization.revoke", SubscribableTypes.user_authorization_revoke}, + {"user.update", SubscribableTypes.user_update} + }; + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/ExtensionBitsTransaction.cs b/TwitchEventSub/Types/ExtensionBitsTransaction.cs new file mode 100644 index 0000000..b297939 --- /dev/null +++ b/TwitchEventSub/Types/ExtensionBitsTransaction.cs @@ -0,0 +1,23 @@ +namespace TwitchEventSub.Types +{ + public class ExtensionBitsTransactionCreate : Event + { + public string extension_client_id { get; set; } //Client ID of the extension. + public string id { get; set; } //Transaction ID. + public string broadcaster_user_id { get; set; } //The transaction’s broadcaster ID. + public string broadcaster_user_login { get; set; } //The transaction’s broadcaster login. + public string broadcaster_user_name { get; set; } //The transaction’s broadcaster display name. + public string user_id { get; set; } //The transaction’s user ID. + public string user_login { get; set; } //The transaction’s user login. + public string user_name { get; set; } //The transaction’s user display name. + public ExtensionProduct product { get; set; } //Additional extension product information. + } + + public class ExtensionProduct + { + public string name { get; set; } + public int bits { get; set; } //involved in the transaction + public string sku { get; set; } //Unique identifier for the product acquired. + public bool in_development { get; set; } //Flag indicating if the product is in development. If in_development is true, bits will be 0. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Goals.cs b/TwitchEventSub/Types/Goals.cs new file mode 100644 index 0000000..a3b53c1 --- /dev/null +++ b/TwitchEventSub/Types/Goals.cs @@ -0,0 +1,18 @@ +namespace TwitchEventSub.Types +{ + public class Goals : Event + { + public string id { get; set; } //An ID that identifies this event. + public string broadcaster_user_id { get; set; } //An ID that uniquely identifies the broadcaster. + public string broadcaster_user_name { get; set; } //The broadcaster’s display name. + public string broadcaster_user_login { get; set; } //The broadcaster’s user handle. + public GoalType type { get; set; } //The type of goal. Possible values are: followers, subscriptions + public string description { get; set; } //maximum of 40 characters + public bool? is_achieved { get; set; } //whether the broadcaster achieved their goal. Only the channel.goal.end event includes this + public int current_amount { get; set; } + public int target_amount { get; set; } + public string started_at { get; set; } //The UTC timestamp in RFC 3339 format, which indicates when the broadcaster created the goal. + public string ended_at { get; set; } //The UTC timestamp in RFC 3339 format, which indicates when the broadcaster ended the goal. Only the channel.goal.end event includes this field. + } + public enum GoalType { followers, subscriptions } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/HypeTrain/Contribution.cs b/TwitchEventSub/Types/HypeTrain/Contribution.cs new file mode 100644 index 0000000..75b4aa5 --- /dev/null +++ b/TwitchEventSub/Types/HypeTrain/Contribution.cs @@ -0,0 +1,12 @@ +namespace TwitchEventSub.Types.HypeTrain +{ + public class Contribution + { + public string user_id { get; set; } //The ID of the user. + public string user_login { get; set; } //The login of the user. + public string user_name { get; set; } //The display name of the user. + public ContributionType type { get; set; } //Type of contribution. Valid values include bits, subscription. + public int total { get; set; } + } + public enum ContributionType { unknown, bits, subscription } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/HypeTrain/HypeTrain.cs b/TwitchEventSub/Types/HypeTrain/HypeTrain.cs new file mode 100644 index 0000000..4b76284 --- /dev/null +++ b/TwitchEventSub/Types/HypeTrain/HypeTrain.cs @@ -0,0 +1,32 @@ +namespace TwitchEventSub.Types.HypeTrain +{ + public abstract class HypeTrainEvents : Event + { + public string id { get; set; } //The Hype Train ID. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public int level { get; set; } //The current level of the Hype Train. + public int total { get; set; } //Total points contributed to the Hype Train. + public Contribution top_contributions { get; set; } //The contributors with the most points contributed. + public string started_at { get; set; } //The time when the Hype Train started. + } + + public class HypeTrainProgress: HypeTrainEvents + { + + public int progress { get; set; } //The number of points contributed to the Hype Train at the current level. + public int goal { get; set; } //The number of points required to reach the next level. + public Contribution last_contribution { get; set; } //The most recent contribution. + public string expires_at { get; set; } //The time when the Hype Train expires. The expiration is extended when the Hype Train reaches a new level. + } + public class HypeTrainBegin : HypeTrainProgress + { + new private int level{get;set;} = 1; + } + public class HypeTrainEnd : HypeTrainEvents + { + public string ended_at { get; set; } //The time when the Hype Train ended. + public string cooldown_ends_at { get; set; } //The time when the Hype Train cooldown ends so that the next Hype Train can start. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Moderation/ChannelBan.cs b/TwitchEventSub/Types/Moderation/ChannelBan.cs new file mode 100644 index 0000000..c33f3f6 --- /dev/null +++ b/TwitchEventSub/Types/Moderation/ChannelBan.cs @@ -0,0 +1,19 @@ +namespace TwitchEventSub.Types.Moderation +{ + public class ChannelBan : Event + { + public string user_id { get; set; } + public string user_login { get; set; } + public string user_name { get; set; } + public string broadcaster_user_id { get; set; } + public string broadcaster_user_login { get; set; } + public string broadcaster_user_name { get; set; } + public string moderator_user_id { get; set; } + public string moderator_user_login { get; set; } + public string moderator_user_name { get; set; } + public string reason { get; set; } + public string ends_at { get; set; } + public bool is_permanent { get; set; } + } + +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Moderation/ChannelModeratorAdd.cs b/TwitchEventSub/Types/Moderation/ChannelModeratorAdd.cs new file mode 100644 index 0000000..d4f209e --- /dev/null +++ b/TwitchEventSub/Types/Moderation/ChannelModeratorAdd.cs @@ -0,0 +1,12 @@ +namespace TwitchEventSub.Types.Moderation +{ + public class ChannelModeratorAdd : Event + { + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string user_id { get; set; } //The user ID of the new moderator. + public string user_login { get; set; } //The user login of the new moderator. + public string user_name { get; set; } //The display name of the new moderator. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Moderation/ChannelModeratorRemove.cs b/TwitchEventSub/Types/Moderation/ChannelModeratorRemove.cs new file mode 100644 index 0000000..aeb28ab --- /dev/null +++ b/TwitchEventSub/Types/Moderation/ChannelModeratorRemove.cs @@ -0,0 +1,12 @@ +namespace TwitchEventSub.Types.Moderation +{ + public class ChannelModeratorRemove : Event + { + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string user_id { get; set; } //The user ID of the removed moderator. + public string user_login { get; set; } //The user login of the removed moderator. + public string user_name { get; set; } //The display name of the removed moderator. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Moderation/ChannelUnban.cs b/TwitchEventSub/Types/Moderation/ChannelUnban.cs new file mode 100644 index 0000000..37ff051 --- /dev/null +++ b/TwitchEventSub/Types/Moderation/ChannelUnban.cs @@ -0,0 +1,16 @@ +namespace TwitchEventSub.Types.Moderation +{ + + public class ChannelUnban : Event + { + public string user_id { get; set; } //The user id for the user who was unbanned on the specified channel. + public string user_login { get; set; } //The user login for the user who was unbanned on the specified channel. + public string user_name { get; set; } //The user display name for the user who was unbanned on the specified channel. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string moderator_user_id { get; set; } //The user ID of the issuer of the unban. + public string moderator_user_login { get; set; } //The user login of the issuer of the unban. + public string moderator_user_name { get; set; } //The user name of the issuer of the unban. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Offline/ChannelFollow.cs b/TwitchEventSub/Types/Offline/ChannelFollow.cs new file mode 100644 index 0000000..ffd43e4 --- /dev/null +++ b/TwitchEventSub/Types/Offline/ChannelFollow.cs @@ -0,0 +1,13 @@ +namespace TwitchEventSub.Types.Offline +{ + public class ChannelFollow : Event + { + public string user_id { get; set; } //The user ID for the user now following the specified channel. + public string user_login { get; set; } //The user login for the user now following the specified channel. + public string user_name { get; set; } //The user display name for the user now following the specified channel. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string followed_at { get; set; } //RFC3339 timestamp of when the follow occurred. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Offline/ChannelUpdate.cs b/TwitchEventSub/Types/Offline/ChannelUpdate.cs new file mode 100644 index 0000000..50b381d --- /dev/null +++ b/TwitchEventSub/Types/Offline/ChannelUpdate.cs @@ -0,0 +1,14 @@ +namespace TwitchEventSub.Types.Offline +{ + public class ChannelUpdate : Event + { + public string broadcaster_user_id { get; set; } //The broadcaster’s user ID. + public string broadcaster_user_login { get; set; } //The broadcaster’s user login. + public string broadcaster_user_name { get; set; } //The broadcaster’s user display name. + public string title { get; set; } //The channel’s stream title. + public string language { get; set; } //The channel’s broadcast language. + public string category_id { get; set; } //The channel’s category ID. + public string category_name { get; set; } //The category name. + public bool is_mature { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Offline/README.txt b/TwitchEventSub/Types/Offline/README.txt new file mode 100644 index 0000000..2e1c745 --- /dev/null +++ b/TwitchEventSub/Types/Offline/README.txt @@ -0,0 +1 @@ +stuff that is likely to happen or at least makes sense offline? i can't think of a good name \ No newline at end of file diff --git a/TwitchEventSub/Types/Offline/UserAuthorizationUpdate.cs b/TwitchEventSub/Types/Offline/UserAuthorizationUpdate.cs new file mode 100644 index 0000000..594e427 --- /dev/null +++ b/TwitchEventSub/Types/Offline/UserAuthorizationUpdate.cs @@ -0,0 +1,12 @@ +namespace TwitchEventSub.Types.Offline +{ + public class UserAuthorizationUpdate : Event + { + public string client_id { get; set; } //The client_id of the application that was granted user access. + public string user_id { get; set; } //The user id for the user who has granted authorization for your client id. + public string user_login { get; set; } //The user login for the user who has granted authorization for your client id. + public string user_name { get; set; } //The user display name for the user who has granted authorization for your client id. + } + public class UserAuthorizationGrant : UserAuthorizationUpdate { } + public class UserAuthorizationRevoke : UserAuthorizationUpdate { } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Offline/UserUpdate.cs b/TwitchEventSub/Types/Offline/UserUpdate.cs new file mode 100644 index 0000000..10ce583 --- /dev/null +++ b/TwitchEventSub/Types/Offline/UserUpdate.cs @@ -0,0 +1,11 @@ +namespace TwitchEventSub.Types.Offline +{ + public class UserUpdate : Event + { + public string user_id { get; set; } + public string user_login { get; set; } + public string user_name { get; set; } //The user’s user display name. + public string email { get; set; } //The user’s email. Only included if you have the user:read:email scope for the user. + public string description { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Poll/Poll.cs b/TwitchEventSub/Types/Poll/Poll.cs new file mode 100644 index 0000000..29f38db --- /dev/null +++ b/TwitchEventSub/Types/Poll/Poll.cs @@ -0,0 +1,31 @@ +namespace TwitchEventSub.Types.Poll +{ + public class PollEvent : Event + { + public string id { get; set; } //ID of the poll. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string title { get; set; } //Question displayed for the poll. + public PollChoice[] choices { get; set; } //An array of choices for the poll. + public VotingMethod bits_voting { get; set; } //The Bits voting settings for the poll. + public VotingMethod channel_points_voting { get; set; } //The Channel Points voting settings for the poll. + public string started_at { get; set; } //The time the poll started. + } + public class ChannelPollBegin : PollEvent + { + public string ends_at { get; set; } //The time the poll will end. + } + public class ChannelPollProgress : PollEvent + { + public string ends_at { get; set; } //The time the poll will end. + } + public class ChannelPollEnd: PollEvent + { + public ChannelPollStatus status { get; set; } //The status of the poll. Valid values are completed, archived, and terminated. + public string ended_at { get; set; } //The time the poll ended. + } + + public enum ChannelPollStatus { completed, archived, terminated } + +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Poll/PollChoice.cs b/TwitchEventSub/Types/Poll/PollChoice.cs new file mode 100644 index 0000000..6487ff1 --- /dev/null +++ b/TwitchEventSub/Types/Poll/PollChoice.cs @@ -0,0 +1,11 @@ +namespace TwitchEventSub.Types.Poll +{ + public class PollChoice + { + public string id { get; set; } + public string title { get; set; } //Text displayed for the choice. + public int bits_votes { get; set; } //Number of votes received via Bits. + public int channel_points_votes { get; set; } //Number of votes received via Channel Points. + public int votes { get; set; } //Total number of votes received for the choice across all methods of voting. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Poll/VotingMethod.cs b/TwitchEventSub/Types/Poll/VotingMethod.cs new file mode 100644 index 0000000..da53cdf --- /dev/null +++ b/TwitchEventSub/Types/Poll/VotingMethod.cs @@ -0,0 +1,8 @@ +namespace TwitchEventSub.Types.Poll +{ + public class VotingMethod + { + public bool is_enabled { get; set; } + public int amount_per_vote { get; set; } + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Prediction/ChannelPredictionUpdate.cs b/TwitchEventSub/Types/Prediction/ChannelPredictionUpdate.cs new file mode 100644 index 0000000..abafa31 --- /dev/null +++ b/TwitchEventSub/Types/Prediction/ChannelPredictionUpdate.cs @@ -0,0 +1,20 @@ +namespace TwitchEventSub.Types.Prediction +{ + public class ChannelPredictionProgress : Event + { + public string id { get; set; } //Channel Points Prediction ID. + public string broadcaster_user_id { get; set; } //The requested broadcaster ID. + public string broadcaster_user_login { get; set; } //The requested broadcaster login. + public string broadcaster_user_name { get; set; } //The requested broadcaster display name. + public string title { get; set; } //Title for the Channel Points Prediction. + public Outcome[] outcomes { get; set; } //An array of outcomes for the Channel Points Prediction. Includes top_predictors. + public string started_at { get; set; } //The time the Channel Points Prediction started. + public string locks_at { get; set; } //The time the Channel Points Prediction will automatically lock. + } + public class ChannelPredictionBegin : ChannelPredictionProgress {} + public class ChannelPredictionLock : ChannelPredictionProgress {} + public class ChannelPredictionEnd : ChannelPredictionProgress + { + public string winning_outcome_id { get; set; } //docs say it's a string. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Prediction/Outcome.cs b/TwitchEventSub/Types/Prediction/Outcome.cs new file mode 100644 index 0000000..f24ab80 --- /dev/null +++ b/TwitchEventSub/Types/Prediction/Outcome.cs @@ -0,0 +1,12 @@ +namespace TwitchEventSub.Types.Prediction +{ + public class Outcome + { + public string id { get; set; } //The outcome ID. + public string title { get; set; } //The outcome title. + public string color { get; set; } //The color for the outcome. Valid values are pink and blue. + public int users { get; set; } //The number of users who used Channel Points on this outcome. + public int channel_points { get; set; } //The total number of Channel Points used on this outcome. + public Predictor[] top_predictors { get; set; } //An array of up to 10 users who used the most Channel Points on this outcome. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/Prediction/Predictor.cs b/TwitchEventSub/Types/Prediction/Predictor.cs new file mode 100644 index 0000000..d7cddc6 --- /dev/null +++ b/TwitchEventSub/Types/Prediction/Predictor.cs @@ -0,0 +1,11 @@ +namespace TwitchEventSub.Types.Prediction +{ + public class Predictor + { + public string user_id { get; set; } //The ID of the user. + public string user_login { get; set; } //The login of the user. + public string user_name { get; set; } //The display name of the user. + public int channel_points_won { get; set; } //The number of Channel Points won. This value is always null in the event payload for Prediction progress and Prediction lock. This value is 0 if the outcome did not win or if the Prediction was canceled and Channel Points were refunded. + public int channel_points_used { get; set; } //The number of Channel Points used to participate in the Prediction. + } +} \ No newline at end of file diff --git a/TwitchEventSub/Types/StreamActivity.cs b/TwitchEventSub/Types/StreamActivity.cs new file mode 100644 index 0000000..ab867a9 --- /dev/null +++ b/TwitchEventSub/Types/StreamActivity.cs @@ -0,0 +1,17 @@ +namespace TwitchEventSub.Types +{ + public class StreamActivity : Event + { + public string broadcaster_user_id { get; set; } //The broadcaster’s user id. + public string broadcaster_user_login { get; set; } //The broadcaster’s user login. + public string broadcaster_user_name { get; set; } //The broadcaster’s user display name. + } + public class StreamOffline : StreamActivity {} + + public class StreamOnline : StreamActivity + { + public string id { get; set; } //The id of the stream. + public string type { get; set; } //The stream type. Valid values are: live, playlist, watch_party, premiere, rerun. + public string started_at { get; set; } //The timestamp at which the stream went online at. + } +} \ No newline at end of file diff --git a/appsettings.example.json b/appsettings.example.json new file mode 100644 index 0000000..5e0d272 --- /dev/null +++ b/appsettings.example.json @@ -0,0 +1,8 @@ +{ + "kafka_bootstrap": "parice.franz:9092", + "kafka_name": "twitcher", + "port": 8420, + "clientId": "plz? :>", + "clientSecret": "not synesthesia. It'll come to me. the one where you believe there is no reality, only perception. that's The Secret.", + "public_uri": "https://google.com" +} \ No newline at end of file diff --git a/twitcher.csproj b/twitcher.csproj new file mode 100644 index 0000000..eb67e86 --- /dev/null +++ b/twitcher.csproj @@ -0,0 +1,14 @@ + + + + Exe + net5.0 + $(RestoreSources);../packages/nuget/;https://api.nuget.org/v3/index.json + + + + + + + +