// Copyright(c) 2019-2022 pypy, Natsumi and individual contributors. // All rights reserved. // // This work is licensed under the terms of the MIT license. // For a copy, see . using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Drawing; using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using CefSharp; using librsync.net; using Microsoft.Toolkit.Uwp.Notifications; using Microsoft.Win32; using NLog; namespace VRCX { public partial class AppApi { public static readonly AppApi Instance; private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly MD5 _hasher = MD5.Create(); private static bool dialogOpen; static AppApi() { Instance = new AppApi(); ProcessMonitor.Instance.ProcessStarted += Instance.OnProcessStateChanged; ProcessMonitor.Instance.ProcessExited += Instance.OnProcessStateChanged; } public void Init() { // Create Instance before Cef tries to bind it } /// /// Computes the MD5 hash of the file represented by the specified base64-encoded string. /// /// The base64-encoded string representing the file. /// The MD5 hash of the file as a base64-encoded string. public string MD5File(string Blob) { var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length); using (var md5 = MD5.Create()) { var md5Hash = md5.ComputeHash(fileData); return Convert.ToBase64String(md5Hash); } } public string ResizeImageToFitLimits(string base64data) { return Convert.ToBase64String(ResizeImageToFitLimits(Convert.FromBase64String(base64data))); } public byte[] ResizeImageToFitLimits(byte[] imageData, int maxWidth = 2000, int maxHeight = 2000, long maxSize = 10_000_000) { using var fileMemoryStream = new MemoryStream(imageData); var image = new Bitmap(fileMemoryStream); // for APNG, check if image is png format and less than maxSize if (image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) && imageData.Length < maxSize && image.Width <= maxWidth && image.Height <= maxHeight) { return imageData; } if (image.Width > maxWidth) { var sizingFactor = image.Width / (double)maxWidth; var newHeight = (int)Math.Round(image.Height / sizingFactor); image = new Bitmap(image, maxWidth, newHeight); } if (image.Height > maxHeight) { var sizingFactor = image.Height / (double)maxHeight; var newWidth = (int)Math.Round(image.Width / sizingFactor); image = new Bitmap(image, newWidth, maxHeight); } SaveToFileToUpload(); for (int i = 0; i < 250 && imageData.Length > maxSize; i++) { SaveToFileToUpload(); if (imageData.Length < maxSize) break; int newWidth; int newHeight; if (image.Width > image.Height) { newWidth = image.Width - 25; newHeight = (int)Math.Round(image.Height / (image.Width / (double)newWidth)); } else { newHeight = image.Height - 25; newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight)); } image = new Bitmap(image, newWidth, newHeight); } if (imageData.Length > maxSize) { throw new Exception("Failed to get image into target filesize."); } return imageData; void SaveToFileToUpload() { using var imageSaveMemoryStream = new MemoryStream(); image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png); imageData = imageSaveMemoryStream.ToArray(); } } /// /// Computes the signature of the file represented by the specified base64-encoded string using the librsync library. /// /// The base64-encoded string representing the file. /// The signature of the file as a base64-encoded string. public string SignFile(string Blob) { var fileData = Convert.FromBase64String(Blob); using (var sig = Librsync.ComputeSignature(new MemoryStream(fileData))) using (var memoryStream = new MemoryStream()) { sig.CopyTo(memoryStream); var sigBytes = memoryStream.ToArray(); return Convert.ToBase64String(sigBytes); } } /// /// Returns the length of the file represented by the specified base64-encoded string. /// /// The base64-encoded string representing the file. /// The length of the file in bytes. public string FileLength(string Blob) { var fileData = Convert.FromBase64String(Blob); return fileData.Length.ToString(); } /// /// Shows the developer tools for the main browser window. /// public void ShowDevTools() { MainForm.Instance.Browser.ShowDevTools(); } /// /// Deletes all cookies from the global cef cookie manager. /// public void DeleteAllCookies() { Cef.GetGlobalCookieManager().DeleteCookies(); } /// /// Opens the specified URL in the default browser. /// /// The URL to open. public void OpenLink(string url) { if (url.StartsWith("http://") || url.StartsWith("https://")) { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } } // broken since adding ExecuteVrFeedFunction( // public void ShowVRForm() // { // try // { // MainForm.Instance.BeginInvoke(new MethodInvoker(() => // { // if (VRForm.Instance == null) // { // new VRForm().Show(); // } // })); // } // catch // { // } // } public void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand) { VRCXVR.Instance.SetActive(active, hmdOverlay, wristOverlay, menuButton, overlayHand); } public void RefreshVR() { VRCXVR.Instance.Restart(); } public void RestartVR() { VRCXVR.Instance.Restart(); } public void SetZoom(double zoomLevel) { MainForm.Instance.Browser.SetZoomLevel(zoomLevel); } public async Task GetZoom() { return await MainForm.Instance.Browser.GetZoomLevelAsync(); } /// /// Retrieves an image from the VRChat API and caches it for future use. The function will return the cached image if it already exists. /// /// The URL of the image to retrieve. /// The ID of the file associated with the image. /// The version of the file associated with the image. /// A string representing the file location of the cached image. public async Task GetImage(string url, string fileId, string version) { return await ImageCache.GetImage(url, fileId, version); } /// /// Displays a desktop notification with the specified bold text, optional text, and optional image. /// /// The bold text to display in the notification. /// The optional text to display in the notification. /// The optional image to display in the notification. public void DesktopNotification(string BoldText, string Text = "", string Image = "") { try { ToastContentBuilder builder = new ToastContentBuilder(); if (Uri.TryCreate(Image, UriKind.Absolute, out Uri uri)) builder.AddAppLogoOverride(uri); if (!string.IsNullOrEmpty(BoldText)) builder.AddText(BoldText); if (!string.IsNullOrEmpty(Text)) builder.AddText(Text); builder.Show(); } catch (System.AccessViolationException ex) { logger.Warn(ex, "Unable to send desktop notification"); } catch (Exception ex) { logger.Error(ex, "Unknown error when sending desktop notification"); } } /// /// Restarts the VRCX application for an update by launching a new process with the upgrade argument and exiting the current process. /// public void RestartApplication(bool isUpgrade) { var args = new List(); if (isUpgrade) args.Add(StartupArgs.VrcxLaunchArguments.IsUpgradePrefix); if (StartupArgs.LaunchArguments.IsDebug) args.Add(StartupArgs.VrcxLaunchArguments.IsDebugPrefix); if (!string.IsNullOrWhiteSpace(StartupArgs.LaunchArguments.ConfigDirectory)) args.Add($"{StartupArgs.VrcxLaunchArguments.ConfigDirectoryPrefix}={StartupArgs.LaunchArguments.ConfigDirectory}"); if (!string.IsNullOrWhiteSpace(StartupArgs.LaunchArguments.ProxyUrl)) args.Add($"{StartupArgs.VrcxLaunchArguments.ProxyUrlPrefix}={StartupArgs.LaunchArguments.ProxyUrl}"); var vrcxProcess = new Process { StartInfo = new ProcessStartInfo { FileName = Path.Combine(Program.BaseDirectory, "VRCX.exe"), Arguments = string.Join(' ', args), UseShellExecute = true, WorkingDirectory = Program.BaseDirectory } }; vrcxProcess.Start(); Environment.Exit(0); } /// /// Checks if the VRCX update executable exists in the AppData directory. /// /// True if the update executable exists, false otherwise. public bool CheckForUpdateExe() { if (File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe"))) return true; return false; } /// /// Sends an IPC packet to announce the start of VRCX. /// public void IPCAnnounceStart() { IPCServer.Send(new IPCPacket { Type = "VRCXLaunch", MsgType = "VRCXLaunch" }); } /// /// Sends an IPC packet with a specified message type and data. /// /// The message type to send. /// The data to send. public void SendIpc(string type, string data) { IPCServer.Send(new IPCPacket { Type = "VrcxMessage", MsgType = type, Data = data }); } public void ExecuteAppFunction(string function, string json) { if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame) MainForm.Instance.Browser.ExecuteScriptAsync($"$app.{function}", json); } public void ExecuteVrFeedFunction(string function, string json) { if (VRCXVR._wristOverlay == null) return; if (VRCXVR._wristOverlay.IsLoading) VRCXVR.Instance.Restart(); VRCXVR._wristOverlay.ExecuteScriptAsync($"$app.{function}", json); } public void ExecuteVrOverlayFunction(string function, string json) { if (VRCXVR._hmdOverlay == null) return; if (VRCXVR._hmdOverlay.IsLoading) VRCXVR.Instance.Restart(); VRCXVR._hmdOverlay.ExecuteScriptAsync($"$app.{function}", json); } /// /// Gets the launch command from the startup arguments and clears the launch command. /// /// The launch command. public string GetLaunchCommand() { var command = StartupArgs.LaunchArguments.LaunchCommand; StartupArgs.LaunchArguments.LaunchCommand = string.Empty; return command; } /// /// Focuses the main window of the VRCX application. /// public void FocusWindow() { MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); })); } /// /// Returns the file path of the custom user CSS file, if it exists. /// /// The file path of the custom user CSS file, or an empty string if it doesn't exist. public string CustomCssPath() { var output = string.Empty; var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.css"); if (File.Exists(filePath)) output = filePath; return output; } /// /// Returns the file path of the custom user js file, if it exists. /// /// The file path of the custom user js file, or an empty string if it doesn't exist. public string CustomScriptPath() { var output = string.Empty; var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.js"); if (File.Exists(filePath)) output = filePath; return output; } public string CurrentCulture() { return CultureInfo.CurrentCulture.ToString(); } public string CurrentLanguage() { return CultureInfo.InstalledUICulture.Name; } public string GetVersion() { return Program.Version; } /// /// Returns whether or not the VRChat client was last closed gracefully. According to the log file, anyway. /// /// True if the VRChat client was last closed gracefully, false otherwise. public bool VrcClosedGracefully() { return LogWatcher.Instance.VrcClosedGracefully; } public void ChangeTheme(int value) { WinformThemer.SetGlobalTheme(value); } public void DoFunny() { WinformThemer.DoFunny(); } /// /// Returns a color value derived from the given user ID. /// This is, essentially, and is used for, random colors. /// /// The user ID to derive the color value from. /// A color value derived from the given user ID. public int GetColourFromUserID(string userId) { var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId)); return (hash[3] << 8) | hash[4]; } /// /// Returns a dictionary of color values derived from the given list of user IDs. /// /// The list of user IDs to derive the color values from. /// A dictionary of color values derived from the given list of user IDs. public Dictionary GetColourBulk(List userIds) { var output = new Dictionary(); foreach (string userId in userIds) { output.Add(userId, GetColourFromUserID(userId)); } return output; } /// /// Retrieves the current text from the clipboard. /// /// The current text from the clipboard. public string GetClipboard() { var clipboard = string.Empty; var thread = new Thread(() => clipboard = Clipboard.GetText()); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); return clipboard; } /// /// Sets whether or not the application should start up automatically with Windows. /// /// True to enable automatic startup, false to disable it. public void SetStartup(bool enabled) { try { using (var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true)) { if (enabled) { var path = Application.ExecutablePath; key.SetValue("VRCX", $"\"{path}\" --startup"); } else { key.DeleteValue("VRCX", false); } } } catch { } } // what the fuck even is this // refactor when // #AppApiLivesDontMatter public void SetAppLauncherSettings(bool enabled, bool killOnExit) { AutoAppLaunchManager.Instance.Enabled = enabled; AutoAppLaunchManager.Instance.KillChildrenOnExit = killOnExit; } /// /// Copies an image file to the clipboard if it exists and is of a supported image file type. /// /// The path to the image file to copy to the clipboard. public void CopyImageToClipboard(string path) { // check if the file exists and is any image file type if (File.Exists(path) && (path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg") || path.EndsWith(".gif") || path.EndsWith(".bmp") || path.EndsWith(".webp"))) { MainForm.Instance.BeginInvoke(new MethodInvoker(() => { var image = Image.FromFile(path); // Clipboard.SetImage(image); var data = new DataObject(); data.SetData(DataFormats.Bitmap, image); data.SetFileDropList(new StringCollection { path }); Clipboard.SetDataObject(data, true); })); } } /// /// Flashes the window of the main form. /// public void FlashWindow() { MainForm.Instance.BeginInvoke(new MethodInvoker(() => { WinformThemer.Flash(MainForm.Instance); })); } /// /// Sets the user agent string for the browser. /// public void SetUserAgent() { using (var client = MainForm.Instance.Browser.GetDevToolsClient()) { _ = client.Network.SetUserAgentOverrideAsync(Program.Version); } } public string GetFileBase64(string path) { if (File.Exists(path)) { return Convert.ToBase64String(File.ReadAllBytes(path)); } return null; } } }