psuedoinitial

actually I had a previous repo but my dumb ass let the real appsettings sneak in
This commit is contained in:
Adam R. Grey 2021-10-24 01:13:07 -04:00
parent 33898cb456
commit 3f717df1d4
48 changed files with 1466 additions and 0 deletions

1
.gitignore vendored
View File

@ -373,3 +373,4 @@ FodyWeavers.xsd
# Local History for Visual Studio Code # Local History for Visual Studio Code
.history/ .history/
appsettings.json

26
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

42
.vscode/tasks.json vendored Normal file
View File

@ -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"
}
]
}

12
Config.cs Normal file
View File

@ -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; }
}
}

40
OAuthTokenGetter.cs Normal file
View File

@ -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<OAuthToken>(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; }
}

51
Program.cs Normal file
View File

@ -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<Config>(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));
}
);})
);
}
}
}

269
TwitchEventSub/Receiver.cs Normal file
View File

@ -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<Subscription, Action<Twitchogram>> psuedoResources { get; set; }
= new Dictionary<Subscription, Action<Twitchogram>>();
private string hmacSecret = "";
private bool going = false;
private Uri publicUrl;
private Queue<string> twitchogrambuffer { get; set; }
private string clientId;
private HMACSHA256 hmacsha256 = null;
private List<Task> tasksToNotActuallyAwait = new List<Task>();
public Receiver(int port, string clientId, string clientSecret, string publicUrl)
{
this.clientId = clientId;
this.publicUrl = new Uri(publicUrl + "/twitcherize");
twitchogrambuffer = new Queue<string>();
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<Task<HttpResponseMessage>>();
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<c>(SubscribableTypes type, c condition, Action<Twitchogram> 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<byte[]>();
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<string>();
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}";
}
}
}

View File

@ -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.
}
}

View File

@ -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; }
}
}

View File

@ -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 {}
}

View File

@ -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 wont show up to viewers.
public bool is_paused { get; set; } //Is the reward currently paused. If true, viewers cant redeem.
public bool is_in_stock { get; set; } //Is the reward currently in stock. If false, viewers cant 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 isnt 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 isnt live or max_per_stream isnt enabled.
}
public class ChannelPointsCustomRewardRemove : ChannelPointsCustomRewardUpdate{}
public class ChannelPointsCustomRewardAdd : ChannelPointsCustomRewardUpdate{}
}

View File

@ -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; }
}
}

View File

@ -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.
}
}

View File

@ -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.
}
}

View File

@ -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.
}
}

View File

@ -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.
}
}

View File

@ -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 users 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 users 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; }
}
}

View File

@ -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.
}
}
}

View File

@ -0,0 +1,4 @@
namespace TwitchEventSub.Types
{
public class Event {}
}

View File

@ -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 { }
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace TwitchEventSub.Types.EventSubSubscription
{
public class Request
{
///<summary>
///use one of SubscribableTypes
///</summary>
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 }
}

View File

@ -0,0 +1,8 @@
namespace TwitchEventSub.Types.EventSubSubscription
{
public class Response
{
public Subscription subscription { get; set; }
public Event Event { get; set; }
}
}

View File

@ -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 notifications 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; }
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,15 @@
using System.Linq;
using System.Text.Json.Serialization;
namespace TwitchEventSub.Types.EventSubSubscription
{
///<summary>
/// don't deserialize me, go to SubscribableTypesTranslation and have it "refine" one for you
///</summary>
public class Twitchogram
{
public string challenge { get; set; }
public Subscription subscription { get; set; }
public Event Event { get; set; }
}
}

View File

@ -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 broadcasters 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 broadcasters 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 users authorization has been granted to your client id.
user_authorization_revoke, //aka User Authorization Revoke, version 1. A users 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<Twitchogram>(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<TwitchResponse>(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<Conditions.Condition, Event> TypedPieces(string stringType)
{
switch (String2Enum(stringType))
{
case SubscribableTypes.channel_update:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelUpdate(),
new Types.Offline.ChannelUpdate());
case SubscribableTypes.channel_follow:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelFollow(),
new Types.Offline.ChannelFollow());
case SubscribableTypes.channel_subscribe:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelSubscribe(),
new Types.ChannelSubscription.ChannelSubscribe());
case SubscribableTypes.channel_subscription_end:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelSubscriptionEnd(),
new Types.ChannelSubscription.ChannelSubscriptionEnd());
case SubscribableTypes.channel_subscription_gift:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelSubscriptionGift(),
new Types.ChannelSubscription.ChannelSubscriptionGift());
case SubscribableTypes.channel_subscription_message:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelSubscriptionMessage(),
new Types.ChannelSubscription.ChannelSubscriptionMessage());
case SubscribableTypes.channel_cheer:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelCheer(),
new Types.ChannelCheer());
case SubscribableTypes.channel_raid:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelRaid(),
new Types.ChannelRaid());
case SubscribableTypes.channel_ban:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelBan(),
new Types.Moderation.ChannelBan());
case SubscribableTypes.channel_unban:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelUnban(),
new Types.Moderation.ChannelUnban());
case SubscribableTypes.channel_moderator_add:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelModeratorAdd(),
new Types.Moderation.ChannelModeratorAdd());
case SubscribableTypes.channel_moderator_remove:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelModeratorRemove(),
new Types.Moderation.ChannelModeratorRemove());
case SubscribableTypes.channel_channel_points_custom_reward_add:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPointsCustomRewardAdd(),
new Types.ChannelPoints.ChannelPointsCustomRewardAdd());
case SubscribableTypes.channel_channel_points_custom_reward_update:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPointsCustomRewardUpdate(),
new Types.ChannelPoints.ChannelPointsCustomRewardUpdate());
case SubscribableTypes.channel_channel_points_custom_reward_remove:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPointsCustomRewardRemove(),
new Types.ChannelPoints.ChannelPointsCustomRewardRemove());
case SubscribableTypes.channel_channel_points_custom_reward_redemption_add:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPointsCustomRewardRedemptionAdd(),
new Types.ChannelPoints.ChannelPointsCustomRewardRedemptionAdd());
case SubscribableTypes.channel_channel_points_custom_reward_redemption_update:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPointsCustomRewardRedemptionUpdate(),
new Types.ChannelPoints.ChannelPointsCustomRewardRedemptionUpdate());
case SubscribableTypes.channel_poll_begin:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPollBegin(),
new Types.Poll.ChannelPollBegin());
case SubscribableTypes.channel_poll_progress:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPollProgress(),
new Types.Poll.ChannelPollProgress());
case SubscribableTypes.channel_poll_end:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPollEnd(),
new Types.Poll.ChannelPollEnd());
case SubscribableTypes.channel_prediction_begin:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPollBegin(),
new Types.Poll.ChannelPollBegin());
case SubscribableTypes.channel_prediction_progress:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPollProgress(),
new Types.Poll.ChannelPollProgress());
case SubscribableTypes.channel_prediction_lock:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPredictionLock(),
new Types.Prediction.ChannelPredictionLock());
case SubscribableTypes.channel_prediction_end:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ChannelPredictionEnd(),
new Types.Prediction.ChannelPredictionEnd());
case SubscribableTypes.drop_entitlement_grant:
return new Tuple<Conditions.Condition, Event>(
new Conditions.DropEntitlementGrant(),
new Types.DropEntitlementGrant());
case SubscribableTypes.extension_bits_transaction_create:
return new Tuple<Conditions.Condition, Event>(
new Conditions.ExtensionBitsTransactionCreate(),
new Types.ExtensionBitsTransactionCreate());
case SubscribableTypes.channel_goal_begin:
case SubscribableTypes.channel_goal_progress:
case SubscribableTypes.channel_goal_end:
return new Tuple<Conditions.Condition, Event>(
new Conditions.Goals(),
new Types.Goals());
case SubscribableTypes.channel_hype_train_begin:
return new Tuple<Conditions.Condition, Event>(
new Conditions.HypeTrainBegin(),
new Types.HypeTrain.HypeTrainBegin());
case SubscribableTypes.channel_hype_train_progress:
return new Tuple<Conditions.Condition, Event>(
new Conditions.HypeTrainProgress(),
new Types.HypeTrain.HypeTrainProgress());
case SubscribableTypes.channel_hype_train_end:
return new Tuple<Conditions.Condition, Event>(
new Conditions.HypeTrainEnd(),
new Types.HypeTrain.HypeTrainEnd());
case SubscribableTypes.stream_online:
return new Tuple<Conditions.Condition, Event>(
new Conditions.StreamOnline(),
new Types.StreamOnline());
case SubscribableTypes.stream_offline:
return new Tuple<Conditions.Condition, Event>(
new Conditions.StreamOffline(),
new Types.StreamOffline());
case SubscribableTypes.user_authorization_grant:
return new Tuple<Conditions.Condition, Event>(
new Conditions.UserAuthorizationGrant(),
new Types.Offline.UserAuthorizationGrant());
case SubscribableTypes.user_authorization_revoke:
return new Tuple<Conditions.Condition, Event>(
new Conditions.UserAuthorizationRevoke(),
new Types.Offline.UserAuthorizationRevoke());
case SubscribableTypes.user_update:
return new Tuple<Conditions.Condition, Event>(
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<string, SubscribableTypes> table = new Dictionary<string, SubscribableTypes>()
{
{"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}
};
}
}

View File

@ -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 transactions broadcaster ID.
public string broadcaster_user_login { get; set; } //The transactions broadcaster login.
public string broadcaster_user_name { get; set; } //The transactions broadcaster display name.
public string user_id { get; set; } //The transactions user ID.
public string user_login { get; set; } //The transactions user login.
public string user_name { get; set; } //The transactions 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.
}
}

View File

@ -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 broadcasters display name.
public string broadcaster_user_login { get; set; } //The broadcasters 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 }
}

View File

@ -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 }
}

View File

@ -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.
}
}

View File

@ -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; }
}
}

View File

@ -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.
}
}

View File

@ -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.
}
}

View File

@ -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.
}
}

View File

@ -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.
}
}

View File

@ -0,0 +1,14 @@
namespace TwitchEventSub.Types.Offline
{
public class ChannelUpdate : Event
{
public string broadcaster_user_id { get; set; } //The broadcasters user ID.
public string broadcaster_user_login { get; set; } //The broadcasters user login.
public string broadcaster_user_name { get; set; } //The broadcasters user display name.
public string title { get; set; } //The channels stream title.
public string language { get; set; } //The channels broadcast language.
public string category_id { get; set; } //The channels category ID.
public string category_name { get; set; } //The category name.
public bool is_mature { get; set; }
}
}

View File

@ -0,0 +1 @@
stuff that is likely to happen or at least makes sense offline? i can't think of a good name

View File

@ -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 { }
}

View File

@ -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 users user display name.
public string email { get; set; } //The users email. Only included if you have the user:read:email scope for the user.
public string description { get; set; }
}
}

View File

@ -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 }
}

View File

@ -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.
}
}

View File

@ -0,0 +1,8 @@
namespace TwitchEventSub.Types.Poll
{
public class VotingMethod
{
public bool is_enabled { get; set; }
public int amount_per_vote { get; set; }
}
}

View File

@ -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.
}
}

View File

@ -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.
}
}

View File

@ -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.
}
}

View File

@ -0,0 +1,17 @@
namespace TwitchEventSub.Types
{
public class StreamActivity : Event
{
public string broadcaster_user_id { get; set; } //The broadcasters user id.
public string broadcaster_user_login { get; set; } //The broadcasters user login.
public string broadcaster_user_name { get; set; } //The broadcasters 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.
}
}

8
appsettings.example.json Normal file
View File

@ -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"
}

14
twitcher.csproj Normal file
View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RestoreSources>$(RestoreSources);../packages/nuget/;https://api.nuget.org/v3/index.json</RestoreSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="silvermeddlists.franz" Version="0.0.5" />
</ItemGroup>
</Project>