diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..460f9b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/TwitchDesktopNotifications/bin +/TwitchDesktopNotifications/obj +/.vs +/TwitchDesktopNotifications/App.config diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..5958f75 --- /dev/null +++ b/Readme.md @@ -0,0 +1,30 @@ +# Twitch Notify +## not affiliated with Twitch or Amazon + +[![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)](https://travis-ci.org/joemccann/dillinger) + +## Installation + Twitch Notify Requires [.NET 6 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.13-windows-x64-installer). + + +## Development + +Want to contribute? Great! + +Project is built using Visual Studios 2022, must have Windows 10.0.17763 SDK installed + +You must create a `App.config` file inside `TwitchDesktopNotifications` project. +```xml + + + + + + + +``` + +## License + +MIT +**Free Software, Hell Yeah!** diff --git a/TwitchDesktopNotifications.sln b/TwitchDesktopNotifications.sln new file mode 100644 index 0000000..5726db7 --- /dev/null +++ b/TwitchDesktopNotifications.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchDesktopNotifications", "TwitchDesktopNotifications\TwitchDesktopNotifications.csproj", "{323F3EBC-C1B1-4DDE-9069-5A65EDA4EB16}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {323F3EBC-C1B1-4DDE-9069-5A65EDA4EB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {323F3EBC-C1B1-4DDE-9069-5A65EDA4EB16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {323F3EBC-C1B1-4DDE-9069-5A65EDA4EB16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {323F3EBC-C1B1-4DDE-9069-5A65EDA4EB16}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FE540A21-E905-4833-8D6A-01F7B31156C7} + EndGlobalSection +EndGlobal diff --git a/TwitchDesktopNotifications/Assets/icon.ico b/TwitchDesktopNotifications/Assets/icon.ico new file mode 100644 index 0000000..bdbac49 Binary files /dev/null and b/TwitchDesktopNotifications/Assets/icon.ico differ diff --git a/TwitchDesktopNotifications/Assets/twitch.png b/TwitchDesktopNotifications/Assets/twitch.png new file mode 100644 index 0000000..b1e0e4a Binary files /dev/null and b/TwitchDesktopNotifications/Assets/twitch.png differ diff --git a/TwitchDesktopNotifications/Core/Notification.cs b/TwitchDesktopNotifications/Core/Notification.cs new file mode 100644 index 0000000..cc33c0b --- /dev/null +++ b/TwitchDesktopNotifications/Core/Notification.cs @@ -0,0 +1,85 @@ +using Microsoft.Toolkit.Uwp.Notifications; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Windows.Foundation.Collections; +using System.Diagnostics; +using System.ComponentModel; + +namespace TwitchDesktopNotifications.Core +{ + internal class Notification + { + private Notification() { + ToastNotificationManagerCompat.OnActivated += toastArgs => + { + // Obtain the arguments from the notification + ToastArguments args = ToastArguments.Parse(toastArgs.Argument); + + try + { + Process myProcess = new Process(); + myProcess.StartInfo.UseShellExecute = true; + myProcess.StartInfo.FileName = args["streamerUrl"]; + myProcess.Start(); + }catch(Exception ex) { } + }; + } + + private static Notification Instance; + + public static Notification GetInstance() + { + if(Instance == null) + { + Instance = new Notification(); + } + return Instance; + } + + public void sendNotification(String streamerName, String streamerUrl, String profilePic, String streamThumbnail, String title) + { + String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify"); + + streamThumbnail = streamThumbnail.Replace("{width}", 260.ToString()).Replace("{height}", 147.ToString()); + + // download there profile picture + string fileNameProfilePic = profilePic.Split("/").Last(); + (new WebClient()).DownloadFile(new Uri(profilePic), FilePath+"/"+ fileNameProfilePic); + + // download there profile picture + string fileNameThumbnailPic = streamThumbnail.Split("/").Last(); + (new WebClient()).DownloadFile(new Uri(streamThumbnail), + FilePath + "/" + fileNameThumbnailPic + ); + + var builder = new ToastContentBuilder() + .AddArgument("streamerUrl", streamerUrl) + .AddText(streamerName + " is now live on Twitch") + .AddHeroImage(new Uri("file://" + (FilePath + "/" + fileNameThumbnailPic).Replace("\\", "/"))) + .AddAppLogoOverride(new Uri("file://" + (FilePath + "/" + fileNameProfilePic).Replace("\\", "/")), ToastGenericAppLogoCrop.Circle) + .AddButton(new ToastButton() + .SetContent("Watch " + streamerName) + .AddArgument("action", "watch") + .SetBackgroundActivation()) + .AddButton(new ToastButton() + .SetContent("Dismiss") + .AddArgument("action", "nothing") + .SetBackgroundActivation()); + + if(title != "") { + builder.AddText(title); + } + builder.Show(toast => + { + toast.ExpirationTime = DateTime.Now.AddSeconds(15); + }); + } + + + } +} diff --git a/TwitchDesktopNotifications/Core/TwitchFetcher.cs b/TwitchDesktopNotifications/Core/TwitchFetcher.cs new file mode 100644 index 0000000..2646c72 --- /dev/null +++ b/TwitchDesktopNotifications/Core/TwitchFetcher.cs @@ -0,0 +1,212 @@ +using ABI.System; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using System.Web; +using TwitchDesktopNotifications.JsonStructure; +using TwitchDesktopNotifications.JsonStructure.Helix; +using Windows.ApplicationModel.Background; + +namespace TwitchDesktopNotifications.Core +{ + internal class TwitchFetcher + { + private TwitchFetcher() { } + + public static TwitchFetcher instance { get; private set; } + + string TwitchClientID = ConfigurationManager.AppSettings["TwitchClientID"]; + string TwitchClientSecret = ConfigurationManager.AppSettings["TwitchClientSecret"]; + + List currentlyLive = null; + + public string guid { get; private set; } + + public static TwitchFetcher GetInstance() + { + if(instance == null) + { + instance = new TwitchFetcher(); + } + return instance; + } + + private byte[] buildPostData(Dictionary postData) + { + string content = ""; + foreach (var pair in postData) + { + content += HttpUtility.UrlEncode(pair.Key) + "=" + HttpUtility.UrlEncode(pair.Value) + "&"; + } + content = content.TrimEnd('&'); + return Encoding.UTF8.GetBytes(content); + } + + private T MakeRequest(string endpoint) + { + + if (DataStore.GetInstance().Store.Authentication.ExpiresAsDate <= DateTime.UtcNow) + { + Refresh(); + } + + WebRequest request = WebRequest.Create("https://api.twitch.tv/" + endpoint); + request.Method = "GET"; + request.Headers[HttpRequestHeader.Authorization] = String.Format("Bearer {0}", DataStore.GetInstance().Store.Authentication.AccessToken); + request.Headers["Client-ID"] = TwitchClientID; + WebResponse response = request.GetResponse(); + Stream dataStream = response.GetResponseStream(); + StreamReader reader = new StreamReader(dataStream); + string responseFromServer = reader.ReadToEnd(); + reader.Close(); + dataStream.Close(); + response.Close(); + + return JsonSerializer.Deserialize(responseFromServer); + } + + public void FetchCurrentUser() + { + try + { + DataStore.GetInstance().Store.UserData = MakeRequest("helix/users").Data[0]; + DataStore.GetInstance().Save(); + }catch(System.Exception ex) + { + Environment.Exit(1); + } + } + + public UserData FetchUserData(string user_id) + { + try + { + return MakeRequest("helix/users?id=" + user_id).Data[0]; + }catch(System.Exception ex) + { + Environment.Exit(1); + } + return null; + } + + public void GetLiveFollowingUsers() + { + try + { + bool isFinished = false; + if (DataStore.GetInstance().Store.UserData == null) + { + FetchCurrentUser(); + } + + string QueryUrl = "helix/streams/followed?first=100&user_id=" + DataStore.GetInstance().Store.UserData.UserId; + Streams following = MakeRequest(QueryUrl); + + if (currentlyLive != null) + { + following.Data.ForEach(x => + { + bool found = false; + + foreach (StreamsData sd in currentlyLive) + { + if (sd.UserId == x.UserId) found = true; + } + + if (!found) + { + UserData streamer = FetchUserData(x.UserId); + Notification.GetInstance().sendNotification(streamer.DisplayName, "https://twitch.tv/" + streamer.UserName, streamer.ProfileImage, x.ThumbnailImg, x.Title); + } + }); + } + + currentlyLive = following.Data; + }catch(System.Exception ex) + { + Environment.Exit(1); + } +} + + public void Refresh() + { + Dictionary postData = new Dictionary(); + + postData["client_id"] = TwitchClientID; + postData["client_secret"] = TwitchClientSecret; + postData["grant_type"] = "refresh_token"; + postData["refresh_token"] = DataStore.GetInstance().Store.Authentication.RefreshToken; + + byte[] byteArray = buildPostData(postData); + + WebRequest request = WebRequest.Create("https://id.twitch.tv/oauth2/token"); + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded"; + request.ContentLength = byteArray.Length; + Stream dataStream = request.GetRequestStream(); + dataStream.Write(byteArray, 0, byteArray.Length); + dataStream.Close(); + WebResponse response = request.GetResponse(); + dataStream = response.GetResponseStream(); + StreamReader reader = new StreamReader(dataStream); + string responseFromServer = reader.ReadToEnd(); + reader.Close(); + dataStream.Close(); + response.Close(); + + DataStore.GetInstance().Store.Authentication = JsonSerializer.Deserialize(responseFromServer); + DateTime unixStart = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc); + DataStore.GetInstance().Store.Authentication.ExpiresAt = (long)Math.Floor((DateTime.Now.AddSeconds(DataStore.GetInstance().Store.Authentication.ExpiresSeconds) - unixStart).TotalMilliseconds); + DataStore.GetInstance().Save(); + } + + async public void BeginConnection() + { + guid = Guid.NewGuid().ToString(); + WebServer.GetInstance().TwitchState = guid; + Process myProcess = new Process(); + myProcess.StartInfo.UseShellExecute = true; + myProcess.StartInfo.FileName = String.Format("https://id.twitch.tv/oauth2/authorize?&redirect_uri=http://localhost:32584/twitchRedirect&scope=user:read:subscriptions%20user:read:follows%20user:read:email%20openid&response_type=code&state={0}&nonce={1}&client_id={2}", guid, guid, TwitchClientID); + myProcess.Start(); + } + + public string endConnection(string code) + { + Dictionary postData = new Dictionary(); + + postData["client_id"] = TwitchClientID; + postData["client_secret"] = TwitchClientSecret; + postData["grant_type"] = "authorization_code"; + postData["redirect_uri"] = "http://localhost:32584/twitchRedirect"; + postData["code"] = code; + + byte[] byteArray = buildPostData(postData); + + WebRequest request = WebRequest.Create("https://id.twitch.tv/oauth2/token"); + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded"; + request.ContentLength = byteArray.Length; + Stream dataStream = request.GetRequestStream(); + dataStream.Write(byteArray, 0, byteArray.Length); + dataStream.Close(); + WebResponse response = request.GetResponse(); + dataStream = response.GetResponseStream(); + StreamReader reader = new StreamReader(dataStream); + string responseFromServer = reader.ReadToEnd(); + reader.Close(); + dataStream.Close(); + response.Close(); + return responseFromServer; + } + } +} diff --git a/TwitchDesktopNotifications/Core/WebServer.cs b/TwitchDesktopNotifications/Core/WebServer.cs new file mode 100644 index 0000000..85c50b5 --- /dev/null +++ b/TwitchDesktopNotifications/Core/WebServer.cs @@ -0,0 +1,112 @@ +using System.Net; +using System.Text; +using System.Web; +using System.IO; + +namespace TwitchDesktopNotifications.Core +{ + internal class WebServer + { + public int Port = 32584; + + private HttpListener listener; + + private static WebServer Instance; + + public static WebServer GetInstance() + { + if(Instance == null) + { + Instance= new WebServer(); + } + return Instance; + } + + public String TwitchCode { get; private set; } + public String TwitchState { get; set; } + + public void Start() + { + listener = new HttpListener(); + listener.Prefixes.Add("http://127.0.0.1:" + Port.ToString() + "/"); + listener.Prefixes.Add("http://localhost:" + Port.ToString() + "/"); + listener.Start(); + new Thread(new ThreadStart(ThreadManagedServer)).Start(); + } + + public event EventHandler CodeRecived; + + public void Stop() + { + listener.Stop(); + } + + private void RespondConnection(HttpListenerRequest request, HttpListenerResponse response) + { + var query = HttpUtility.ParseQueryString(request.Url.Query); + if (request.HttpMethod == "GET" && query["state"] == this.TwitchState) + { + this.TwitchCode = query["code"]; + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = "text/html"; + response.OutputStream.Write(Encoding.ASCII.GetBytes("Twitch Connected!

Twitch Desktop Notification

Twitch has been success fully connected. Please close this tab.

")); + response.OutputStream.Close(); + CodeRecived?.Invoke(this, new EventArgs()); + } + else + {; + response.StatusCode = (int)HttpStatusCode.Forbidden; + response.ContentType = "text/html"; + response.OutputStream.Write(Encoding.ASCII.GetBytes("State Missmatch

State Missmatch

State does not match up preventing XSS.

")); + response.OutputStream.Close(); + } + } + + private void RespondFavicon(HttpListenerResponse response) + { + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = "image/x-icon"; + response.OutputStream.Write(File.ReadAllBytes("Assets/icon.ico")); + response.OutputStream.Close(); + } + + private void processRequestThread(object? obj) + { + HttpListenerContext context = (HttpListenerContext)obj; + HttpListenerRequest request = context.Request; + + if (request.Url.AbsolutePath == "/favicon.ico") + { + RespondFavicon(context.Response); + } + else if (request.Url.AbsolutePath == "/twitchRedirect") + { + RespondConnection(request, context.Response); + } + else + { + HttpListenerResponse response = context.Response; + response.StatusCode = (int)HttpStatusCode.NotFound; + response.ContentType = "text/html"; + response.OutputStream.Write(Encoding.ASCII.GetBytes("Not Found

Not Found

File not found

")); + response.OutputStream.Close(); + } + } + + private void ThreadManagedServer() + { + while (listener.IsListening) + { + try + { + HttpListenerContext context = listener.GetContext(); + ParameterizedThreadStart pts = new ParameterizedThreadStart(processRequestThread); + pts.Invoke(context); + } + catch (Exception e) + { + } + } + } + } +} diff --git a/TwitchDesktopNotifications/DataStore.cs b/TwitchDesktopNotifications/DataStore.cs new file mode 100644 index 0000000..79c98f1 --- /dev/null +++ b/TwitchDesktopNotifications/DataStore.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using TwitchDesktopNotifications.JsonStructure; +using System.IO; + +namespace TwitchDesktopNotifications +{ + internal class DataStore + { + private DataStore() { } + + public static DataStore Instance { get; private set; } + private Store _store; + public JsonStructure.Store Store { + get { + if (_store == null) + { + Load(); + } + return _store; + } + private set { + _store = value; + } + } + + public bool isLoaded { get; private set; } + + public static DataStore GetInstance() + { + if(Instance == null) + { + Instance = new DataStore(); + } + return Instance; + } + + public void Save() + { + String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify"); + String FileName = "store.json"; + + string fileContent = JsonSerializer.Serialize(Store); + Directory.CreateDirectory(FilePath); + File.WriteAllText(FilePath + "/" + FileName, fileContent); + } + + public void Load() { + String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify"); + String FileName = "store.json"; + + Directory.CreateDirectory(FilePath); + + if(File.Exists(FilePath+"/"+ FileName)) { + + string fileContent = File.ReadAllText(FilePath+"/"+ FileName); + Store = JsonSerializer.Deserialize(fileContent); + } + else + { + Store = new JsonStructure.Store(); + } + isLoaded= true; + } + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Authentication.cs b/TwitchDesktopNotifications/JsonStructure/Authentication.cs new file mode 100644 index 0000000..34bef48 --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Authentication.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TwitchDesktopNotifications.JsonStructure +{ + internal class Authentication + { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresSeconds { get; set; } + + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } + + [JsonPropertyName("scope")] + public List Scopes { get; set; } + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } + + [JsonPropertyName("expiresAt")] + public long ExpiresAt { get; set; } + + [JsonIgnore] + public DateTime ExpiresAsDate { + get + { + return (new DateTime(1970, 1, 1)).AddMilliseconds(ExpiresAt); + } + set + { + DateTime unixStart = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc); + ExpiresAt = (long)Math.Floor((value.ToUniversalTime() - unixStart).TotalMilliseconds); + } + } + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Helix/Pagination.cs b/TwitchDesktopNotifications/JsonStructure/Helix/Pagination.cs new file mode 100644 index 0000000..d0a0896 --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Helix/Pagination.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TwitchDesktopNotifications.JsonStructure.Helix +{ + internal class Pagination + { + public Pagination() { } + + [JsonPropertyName("cursor")] + public string Cursor { get; set; } + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Helix/Streams.cs b/TwitchDesktopNotifications/JsonStructure/Helix/Streams.cs new file mode 100644 index 0000000..138c4c0 --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Helix/Streams.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TwitchDesktopNotifications.JsonStructure.Helix +{ + internal class Streams + { + public Streams() { } + + [JsonPropertyName("data")] + public List Data { get; set; } + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Helix/StreamsData.cs b/TwitchDesktopNotifications/JsonStructure/Helix/StreamsData.cs new file mode 100644 index 0000000..5de71fc --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Helix/StreamsData.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TwitchDesktopNotifications.JsonStructure.Helix +{ + internal class StreamsData + { + public StreamsData() { } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + + [JsonPropertyName("user_name")] + public string DisplayName { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("thumbnail_url")] + public string ThumbnailImg { get; set; } + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Helix/User.cs b/TwitchDesktopNotifications/JsonStructure/Helix/User.cs new file mode 100644 index 0000000..4d48c5d --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Helix/User.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TwitchDesktopNotifications.JsonStructure.Helix +{ + internal class User + { + public User() { } + + [JsonPropertyName("data")] + public List Data { get; set; } + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Helix/UserData.cs b/TwitchDesktopNotifications/JsonStructure/Helix/UserData.cs new file mode 100644 index 0000000..5994dc9 --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Helix/UserData.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TwitchDesktopNotifications.JsonStructure.Helix +{ + internal class UserData + { + public UserData() { } + + [JsonPropertyName("id")] + public string UserId { get; set; } + + [JsonPropertyName("login")] + public string UserName { get; set; } + + [JsonPropertyName("display_name")] + public string DisplayName { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("broadcaster_type")] + public string BroadcasterType { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("profile_image_url")] + public string ProfileImage { get; set; } + + [JsonPropertyName("offline_image_url")] + public string OfflineImage { get; set; } + + [JsonPropertyName("view_count")] + public int TotalViewers { get; set; } + + [JsonPropertyName("email")] + public string EMail { get; set; } + + [JsonPropertyName("created_at")] + public string CreatedISODate { get; set; } + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Helix/UserFollows.cs b/TwitchDesktopNotifications/JsonStructure/Helix/UserFollows.cs new file mode 100644 index 0000000..c067826 --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Helix/UserFollows.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TwitchDesktopNotifications.JsonStructure.Helix +{ + internal class UserFollows + { + public UserFollows() { } + + [JsonPropertyName("data")] + public List results { get; set; } + + [JsonPropertyName("pagination")] + public Pagination Pagination { get; set; } + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Helix/UsersFollowsData.cs b/TwitchDesktopNotifications/JsonStructure/Helix/UsersFollowsData.cs new file mode 100644 index 0000000..8733105 --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Helix/UsersFollowsData.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TwitchDesktopNotifications.JsonStructure.Helix +{ + internal class UsersFollowsData + { + public UsersFollowsData() { } + + [JsonPropertyName("from_id")] + public string FromID { get; set; } + + [JsonPropertyName("from_login")] + public string FromUserName { get; set; } + + [JsonPropertyName("from_name")] + public string FromDisplayName { get; set; } + + [JsonPropertyName("to_id")] + public string ToID { get; set; } + + [JsonPropertyName("to_login")] + public string ToUserName { get; set; } + + [JsonPropertyName("to_name")] + public string ToDisplayName { get; set; } + + [JsonPropertyName("followed_at")] + public string FollowedISODateTime { get; set; } + + } +} diff --git a/TwitchDesktopNotifications/JsonStructure/Store.cs b/TwitchDesktopNotifications/JsonStructure/Store.cs new file mode 100644 index 0000000..2d59f42 --- /dev/null +++ b/TwitchDesktopNotifications/JsonStructure/Store.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using TwitchDesktopNotifications.JsonStructure.Helix; + +namespace TwitchDesktopNotifications.JsonStructure +{ + internal class Store + { + public Store() { } + + [JsonPropertyName("authentication")] + public Authentication Authentication { get; set; } + + [JsonPropertyName("user_data")] + public UserData UserData { get; set; } + } +} diff --git a/TwitchDesktopNotifications/Program.cs b/TwitchDesktopNotifications/Program.cs new file mode 100644 index 0000000..ceefc03 --- /dev/null +++ b/TwitchDesktopNotifications/Program.cs @@ -0,0 +1,109 @@ +// See https://aka.ms/new-console-template for more information +using System.Drawing; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Windows.Controls; +using System.Windows.Forms; +using System.Windows.Media; +using TwitchDesktopNotifications; +using TwitchDesktopNotifications.Core; +using TwitchDesktopNotifications.JsonStructure; + +internal class Program +{ + + static bool isConnecting = false; + static WebServer ws = WebServer.GetInstance(); + + private static NotifyIcon notifyIcon; + private static ContextMenuStrip cms; + + + public static void Ws_CodeRecived(object? sender, EventArgs e) + { + ws.CodeRecived -= Ws_CodeRecived; + + string response = TwitchFetcher.GetInstance().endConnection(((WebServer)sender).TwitchCode); + + if (!DataStore.GetInstance().isLoaded) + { + DataStore.GetInstance().Load(); + } + + DataStore.GetInstance().Store.Authentication = JsonSerializer.Deserialize(response); + + DateTime unixStart = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc); + DataStore.GetInstance().Store.Authentication.ExpiresAt = (long)Math.Floor((DateTime.Now.AddSeconds(DataStore.GetInstance().Store.Authentication.ExpiresSeconds) - unixStart).TotalMilliseconds); + DataStore.GetInstance().Save(); + + isConnecting = false; + ws.Stop(); + } + + protected static void Reconnect_Click(object? sender, System.EventArgs e) + { + TriggerAuthentication(); + } + + protected static void Quit_Click(object? sender, System.EventArgs e) + { + notifyIcon.Visible = false; + notifyIcon.Dispose(); + Environment.Exit(0); + } + + private async static void TriggerAuthentication() + { + ws.CodeRecived += Ws_CodeRecived; + ws.Start(); + isConnecting = true; + TwitchFetcher.GetInstance().BeginConnection(); + if (DataStore.GetInstance().Store.Authentication == null) + { + var timerForCrash = new PeriodicTimer(TimeSpan.FromSeconds(10)); + await timerForCrash.WaitForNextTickAsync(); + if (isConnecting) + { + MessageBox.Show("Twitch Connection not authenticated Exiting for saftey.", "Twitch Notify"); + notifyIcon.Visible = false; + notifyIcon.Dispose(); + Environment.Exit(1); + } + } + } + + private static async Task Main(string[] args) + { + var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + + notifyIcon = new NotifyIcon(); + notifyIcon.Icon = new Icon("Assets/icon.ico"); + notifyIcon.Text = "Notify"; + + cms = new ContextMenuStrip(); + + cms.Items.Add(new ToolStripMenuItem("Reconnect", null, new EventHandler(Reconnect_Click))); + cms.Items.Add(new ToolStripSeparator()); + cms.Items.Add(new ToolStripMenuItem("Quit", null, new EventHandler(Quit_Click), "Quit")); + + notifyIcon.ContextMenuStrip = cms; + notifyIcon.Visible = true; + + if (DataStore.GetInstance().Store.Authentication == null) + { + TriggerAuthentication(); + } + + new Thread(() => + { + while (true) + { + Thread.Sleep(10000); + TwitchFetcher.GetInstance().GetLiveFollowingUsers(); + } + }).Start(); + + Application.Run(); + + } +} \ No newline at end of file diff --git a/TwitchDesktopNotifications/TwitchDesktopNotifications.csproj b/TwitchDesktopNotifications/TwitchDesktopNotifications.csproj new file mode 100644 index 0000000..48de116 --- /dev/null +++ b/TwitchDesktopNotifications/TwitchDesktopNotifications.csproj @@ -0,0 +1,33 @@ + + + + WinExe + net6.0-windows10.0.17763.0 + enable + enable + Assets\icon.ico + Twitch Notify + KeareanGaming + Twitch Notify + True + True + + + + + + + + + + + + + Always + + + Always + + + +