From e73a7b764a605a8eeb4b62be1df48ef0153def41 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 14 Jan 2024 21:53:34 +0100 Subject: [PATCH] GH-81: Add initial screen sharing test --- package.json | 2 + src-tauri/src/commands/settings_cmd.rs | 34 +++- src-tauri/src/connection/mod.rs | 1 + src-tauri/src/main.rs | 6 +- .../audio/processing/voice_activation.rs | 2 + src-tauri/src/utils/audio/recorder.rs | 23 +++ src-tauri/src/utils/server.rs | 7 + src/components/ChatInput.tsx | 3 +- src/components/ChatMessageContainer.tsx | 18 +- src/components/Sidebar.tsx | 69 +++++++- src/helper/ChatMessage.ts | 6 +- src/helper/MessageParser.tsx | 5 +- src/helper/webrtc/WebRTC.ts | 146 ++++++++++++++++ src/routes/Login.tsx | 44 ++++- yarn.lock | 161 +++++++++++++++++- 15 files changed, 511 insertions(+), 16 deletions(-) create mode 100644 src/helper/webrtc/WebRTC.ts diff --git a/package.json b/package.json index f488768..f6006e8 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "react-easy-crop": "^5.0.4", "react-redux": "^8.0.5", "react-router-dom": "^6.10.0", + "socket.io": "^4.7.4", + "socket.io-client": "^4.7.4", "tinycolor2": "^1.6.0" }, "devDependencies": { diff --git a/src-tauri/src/commands/settings_cmd.rs b/src-tauri/src/commands/settings_cmd.rs index 10e148f..acc6903 100644 --- a/src-tauri/src/commands/settings_cmd.rs +++ b/src-tauri/src/commands/settings_cmd.rs @@ -4,10 +4,11 @@ use std::{ sync::RwLock, }; +use reqwest::Identity; use tauri::State; use tracing::{info, trace}; -use crate::utils::{constants::get_project_dirs, server::Server}; +use crate::{utils::{constants::get_project_dirs, server::{Server, UserIdentity}}, errors::certificate_error::CertificateError}; use super::utils::settings::FrontendSettings; @@ -45,6 +46,7 @@ pub fn save_server( server_host: &str, server_port: u16, username: &str, + identity: Option, ) -> Result<(), String> { info!("Saving server: {server_host}:{server_port}"); let mut server_file = get_settings_file(SERVER_SETTINS_FILE)?; @@ -65,6 +67,7 @@ pub fn save_server( host: server_host.to_string(), port: server_port, username: username.to_string(), + identity, }); trace!("Server list: {:#?}", server_list); @@ -164,3 +167,32 @@ pub fn get_frontend_settings( Ok(settings_data) } + + +#[tauri::command] +pub fn get_identity_certs() -> Result, String> { + let project_dirs = get_project_dirs() + .ok_or_else(|| CertificateError::new("Unable to load project dir")).map_err(|e| format!("{:?}", e))?; + let data_dir = project_dirs.data_dir(); + + if !data_dir.exists() { + std::fs::create_dir_all(&data_dir).map_err(|e| format!("{:?}", e))?; + } + + let mut certs = Vec::new(); + + let dir_entries = fs::read_dir(&data_dir) + .map_err(|e| format!("Error reading directory: {}", e))?; + + for entry in dir_entries { + let entry = entry.map_err(|e| format!("Error reading directory entry: {}", e))?; + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + + if file_name_str.starts_with("cert_") && file_name_str.ends_with(".pem") { + certs.push(file_name_str.into_owned()); + } + } + + Ok(certs) +} \ No newline at end of file diff --git a/src-tauri/src/connection/mod.rs b/src-tauri/src/connection/mod.rs index fab7eb9..d73adce 100644 --- a/src-tauri/src/connection/mod.rs +++ b/src-tauri/src/connection/mod.rs @@ -109,6 +109,7 @@ impl Connection { .store_to_project_dir(true) .build()?; + //TODO: Check for ECDHE-RSA-AES256-GCM-SHA384 let socket = TcpStream::connect(server_uri).await?; let cx = TlsConnector::builder() .identity(certificate_store.get_client_certificate()?) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 884d1ef..3e86b09 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -34,7 +34,10 @@ use crate::commands::{ change_user_state, connect_to_server, crop_and_store_image, disable_audio_info, enable_audio_info, get_audio_devices, like_message, logout, send_message, set_audio_input_setting, set_audio_output_setting, set_user_image, - settings_cmd::{get_frontend_settings, get_server_list, save_frontend_settings, save_server}, + settings_cmd::{ + get_identity_certs, get_frontend_settings, get_server_list, save_frontend_settings, + save_server, + }, web_cmd::{get_open_graph_data_from_website, open_browser}, zip_cmd::{convert_to_base64, unzip_data_from_utf8, zip_data_to_utf8}, }; @@ -102,6 +105,7 @@ async fn main() { get_open_graph_data_from_website, save_frontend_settings, get_frontend_settings, + get_identity_certs, set_audio_input_setting, set_audio_output_setting, enable_audio_info, diff --git a/src-tauri/src/utils/audio/processing/voice_activation.rs b/src-tauri/src/utils/audio/processing/voice_activation.rs index 9abde0c..85805a3 100644 --- a/src-tauri/src/utils/audio/processing/voice_activation.rs +++ b/src-tauri/src/utils/audio/processing/voice_activation.rs @@ -104,11 +104,13 @@ impl VoiceActivation { max.map_or(Some(x), |max| Some(if x > max { x } else { max })) }) .unwrap_or_else(T::zero); // Apply the VAD logic + max_amplitude = if amplitude > max_amplitude { amplitude } else { max_amplitude }; + if vad.update(&litude) { // If the VAD is on, reset the fade out counter self.fade_out_count = 0; diff --git a/src-tauri/src/utils/audio/recorder.rs b/src-tauri/src/utils/audio/recorder.rs index 7968b70..6a77b23 100644 --- a/src-tauri/src/utils/audio/recorder.rs +++ b/src-tauri/src/utils/audio/recorder.rs @@ -8,6 +8,7 @@ use std::{ time::Duration, }; +use serde::de::IntoDeserializer; use tokio::sync::broadcast::{self, Receiver}; use tracing::{error, info, trace, warn}; @@ -23,6 +24,28 @@ use super::{ processing::voice_activation::{VoiceActivation, VoiceActivationType}, }; +struct GlobalMaxAvg { + max_avg: f32, + max_avg_count: u64, +} + +impl Default for GlobalMaxAvg { + fn default() -> Self { + Self { + max_avg: 0.0, + max_avg_count: 0, + } + } +} + +impl GlobalMaxAvg { + fn update(&mut self, value: f32) { + self.max_avg_count += 1; + self.max_avg = + (self.max_avg * (self.max_avg_count - 1) as f32 + value) / self.max_avg_count as f32; + } +} + pub struct Recorder { audio_thread: Option>, playing: Arc, diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index 47ff2ed..e00f91f 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -1,7 +1,14 @@ +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct UserIdentity { + pub name: String, + pub file: String, +} + #[derive(serde::Serialize, serde::Deserialize, Debug)] pub struct Server { pub description: String, pub host: String, pub port: u16, pub username: String, + pub identity: Option } diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index f54f647..1f80659 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -57,7 +57,8 @@ function ChatInput() { chatMessageHandler.sendCustomChatMessage("[[ Image too large ( " + formatBytes((reader.result as string).length) + " out of " + formatBytes(0x7fffff) + ") ]]", currentUser); return; } - let img = ''; + const legacyImageSize = 600; // Adapt image size for legacy clients + let img = ``; chatMessageHandler.sendCustomChatMessage(img, currentUser); }; } diff --git a/src/components/ChatMessageContainer.tsx b/src/components/ChatMessageContainer.tsx index 55444c4..e22cc82 100644 --- a/src/components/ChatMessageContainer.tsx +++ b/src/components/ChatMessageContainer.tsx @@ -1,4 +1,4 @@ -import { Avatar, Box, Card, CardContent, Grid, List } from "@mui/material"; +import { Avatar, Box, Card, CardContent, Grid, List, Typography } from "@mui/material"; import React, { ReactElement, useEffect, useMemo, useRef, useState } from "react"; import { MemoChatMessage } from "./ChatMessage"; import { useSelector } from "react-redux"; @@ -133,6 +133,21 @@ const ChatMessageContainer = (props: ChatMessageContainerProps) => { return popoverMap; }, [userIdToUserMap, userInfoAnchor]); + const emptyChatMessageContainer = useMemo(() => { + if (props.messages.length === 0) { + return ( + + + + Write something :) + + + + ); + } + return null; + }, [props.messages]); + return ( @@ -154,6 +169,7 @@ const ChatMessageContainer = (props: ChatMessageContainerProps) => { ))} + {emptyChatMessageContainer} {currentPopoverUserId && userIdToPopoverMap.get(currentPopoverUserId)}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 3e70e0c..9712931 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -3,18 +3,28 @@ import LogoutIcon from '@mui/icons-material/Logout'; import InfoIcon from '@mui/icons-material/Info'; import { invoke } from "@tauri-apps/api"; import { useNavigate } from "react-router-dom"; -import { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { LoadingButton } from "@mui/lab"; import './Sidebar.css' import ChannelViewer from "./ChannelViewer"; import CurrentUserInfo from "./CurrentUserInfo"; import SettingsIcon from '@mui/icons-material/Settings'; -import React from "react"; +import CastIcon from '@mui/icons-material/Cast'; +import { useSelector } from 'react-redux'; +import { RootState } from "../store/store"; +import { WebRTCStreamer, WebRTCViewer } from "../helper/webrtc/WebRTC"; function Sidebar() { - + const signalingServerUrl = "http://127.0.0.1:4000"; const navigate = useNavigate(); const [logoutInProgress, setLogoutInProgress] = useState(false); + const [showWebRtcWindow, setShowWebRtcWindow] = useState(false); + const [webRtcStreamer, setWebRtcStreamer] = useState(undefined); + const [webRtcViewer, setWebRtcViewer] = useState(undefined); + + const userList = useSelector((state: RootState) => state.reducer.userInfo); + let currentUserId = userList.currentUser?.id; + let currentChannelId = userList.currentUser?.channel_id; const triggerLogout = useCallback((event: React.MouseEvent) => { event.preventDefault(); @@ -30,10 +40,60 @@ function Sidebar() { navigate("/settings"); }, [navigate]); // add dependencies here + useEffect(() => { + const viewer = new WebRTCViewer(signalingServerUrl, currentUserId ?? 0, currentChannelId ?? 0); + viewer.listen(); + viewer.onStream((stream) => { + setShowWebRtcWindow(true); + }); + setWebRtcViewer(viewer); + return () => { + if (webRtcStreamer) { + webRtcStreamer.stop(); + } + viewer.stop(); + setShowWebRtcWindow(false); + } + }, []); + + const streamPreview = useMemo(() => { + if (showWebRtcWindow) { + return ( + + + ) + } + return null; + }, [webRtcStreamer]); + + + const castScreen = async (event: React.MouseEvent): Promise => { + + if (currentUserId === undefined || currentChannelId === undefined) { + return; + } + const rtc = new WebRTCStreamer(signalingServerUrl, currentUserId ?? 0, currentChannelId ?? 0); + setWebRtcStreamer(rtc); + setShowWebRtcWindow(true); + await rtc?.start(); + } + return ( + {streamPreview} @@ -41,6 +101,9 @@ function Sidebar() { + diff --git a/src/helper/ChatMessage.ts b/src/helper/ChatMessage.ts index a7bfc0c..aa509bb 100644 --- a/src/helper/ChatMessage.ts +++ b/src/helper/ChatMessage.ts @@ -22,10 +22,10 @@ export class ChatMessageHandler { invoke('send_message', { chatMessage: data, channelId: userInfo?.channel_id }); console.log("customChatMessage", data); this.pushChatMessage({ - actor: userInfo?.id || 0, + actor: userInfo?.id ?? 0, sender: { - user_id: userInfo?.id || 0, - user_name: userInfo?.name || 'unknown' + user_id: userInfo?.id ?? 0, + user_name: userInfo?.name ?? 'unknown' }, channel_id: [0], tree_id: [0], diff --git a/src/helper/MessageParser.tsx b/src/helper/MessageParser.tsx index 978c82a..be4a709 100644 --- a/src/helper/MessageParser.tsx +++ b/src/helper/MessageParser.tsx @@ -40,7 +40,7 @@ class DOMMessageParser { Array.from(this.document.querySelectorAll('a')).forEach(e => { let replaced = !this.replacementUrl.every(r => { if (e.getAttribute('href')?.match(r.regex)) { - let replaced = e.getAttribute('href')?.replace(r.regex, r.replacement) || ''; + let replaced = e.getAttribute('href')?.replace(r.regex, r.replacement) ?? ''; if (!r.inline) { e.setAttribute('href', replaced); @@ -71,6 +71,7 @@ class MessageParser { } parseDOM(dom: (value: DOMMessageParser) => DOMMessageParser) { + console.log("Parsing DOM"); this.input = dom(new DOMMessageParser(this.input)).build(); return this; @@ -111,7 +112,7 @@ class MessageParser { return this; } - waitAndExecute(timeStr: string, callback: (remainingStr: string) => void): void { + private waitAndExecute(timeStr: string, callback: (remainingStr: string) => void): void { let time = 0; let remainingStr = ''; let timeUnits = timeStr.split(' '); diff --git a/src/helper/webrtc/WebRTC.ts b/src/helper/webrtc/WebRTC.ts new file mode 100644 index 0000000..49df9c7 --- /dev/null +++ b/src/helper/webrtc/WebRTC.ts @@ -0,0 +1,146 @@ +import { Socket, io } from 'socket.io-client'; +import { DefaultEventsMap } from 'socket.io/dist/typed-events'; + +export class WebRTCStreamer { + private stream: MediaStream | null = null; + private signalingServerUrl: string; + private userId: number; + private roomId: number; + private socket: Socket | null = null; + private peerConnections: Map = new Map(); + private configuration: { iceServers: { urls: string; }[]; }; + + constructor(signalingServerUrl: string, userId: number, roomId: number) { + this.configuration = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] }; + this.signalingServerUrl = signalingServerUrl; + this.userId = userId; + this.roomId = roomId; + } + + private async requestScreenAccess() { + try { + // @ts-ignore + this.stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: "monitor" }, audio: false }); + } catch (error) { + console.error("Error: " + error); + } + } + + get getStream() { + return this.stream; + } + + async start() { + await this.requestScreenAccess(); + this.socket = io(`${this.signalingServerUrl}/ws?userId=${this.userId}&roomId=${this.roomId}`); + + this.socket?.on("answer", (id, description) => { + this.peerConnections.get(id)?.setRemoteDescription(description); + }); + + this.socket?.on("watcher", id => { + const peerConnection = new RTCPeerConnection(this.configuration); + this.peerConnections.set(id, peerConnection); + + if (this.stream) { + let stream = this.stream; + stream.getTracks().forEach(track => peerConnection.addTrack(track, stream)); + } + + peerConnection.onicecandidate = event => { + if (event.candidate) { + this.socket?.emit("candidate", id, event.candidate); + } + }; + + peerConnection + .createOffer() + .then(sdp => peerConnection.setLocalDescription(sdp)) + .then(() => { + this.socket?.emit("offer", id, peerConnection.localDescription); + }); + }); + + this.socket?.on("candidate", (id, candidate) => { + this.peerConnections.get(id)?.addIceCandidate(new RTCIceCandidate(candidate)); + }); + + this.socket?.on("disconnectPeer", id => { + this.peerConnections.get(id)?.close(); + this.peerConnections.delete(id); + }); + + this.socket.emit("broadcaster"); + + } + + + stop() { + this.socket?.disconnect(); + this.stream?.getTracks().forEach(track => track.stop()); + } +} + +export class WebRTCViewer { + private signalingServerUrl: string; + private userId: number; + private roomId: number; + private socket: Socket | null = null; + private configuration: { iceServers: { urls: string; }[]; }; + private stream: MediaStream | null = null; + private onStreamListener: ((stream: MediaStream) => void) | null = null; + + constructor(signalingServerUrl: string, userId: number, roomId: number) { + this.configuration = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] }; + this.userId = userId; + this.roomId = roomId; + this.signalingServerUrl = signalingServerUrl; + } + + listen() { + this.socket = io(`${this.signalingServerUrl}/ws?userId=${this.userId}&roomId=${this.roomId}`); + let peerConnection: RTCPeerConnection | null = null; + this.socket.on("offer", (id, description) => { + peerConnection = new RTCPeerConnection(this.configuration); + peerConnection + .setRemoteDescription(description) + .then(() => peerConnection?.createAnswer()) + .then(sdp => peerConnection?.setLocalDescription(sdp)) + .then(() => { + this.socket?.emit("answer", id, peerConnection?.localDescription); + }); + peerConnection.ontrack = event => { + if (this.onStreamListener) { + this.onStreamListener(event.streams[0]); + } + }; + peerConnection.onicecandidate = event => { + if (event.candidate) { + this.socket?.emit("candidate", id, event.candidate); + } + }; + }); + + + this.socket.on("candidate", (id, candidate) => { + peerConnection?.addIceCandidate(new RTCIceCandidate(candidate)) + .catch(e => console.error(e)); + }); + + this.socket.on("connect", () => { + this.socket?.emit("watcher"); + }); + + this.socket.on("broadcaster", () => { + this.socket?.emit("watcher"); + }); + } + + stop() { + this.socket?.disconnect(); + } + + onStream(callback: (stream: MediaStream) => void) { + this.onStreamListener = callback; + } +} \ No newline at end of file diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx index bab892f..903c180 100644 --- a/src/routes/Login.tsx +++ b/src/routes/Login.tsx @@ -1,7 +1,7 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import '../App.css'; import './styles/Login.css'; -import { Accordion, AccordionDetails, AccordionSummary, Alert, Avatar, Box, Container, Grid, LinearProgress, List, ListItem, ListItemAvatar, ListItemButton, ListItemIcon, ListItemText, TextField, Typography } from '@mui/material' +import { Accordion, AccordionDetails, AccordionSummary, Alert, Avatar, Box, Container, Grid, IconButton, LinearProgress, List, ListItem, ListItemAvatar, ListItemButton, ListItemIcon, ListItemText, MenuItem, Select, TextField, Tooltip, Typography } from '@mui/material' import LoadingButton from '@mui/lab/LoadingButton'; import { invoke } from '@tauri-apps/api/tauri' import { useLocation, useNavigate } from 'react-router-dom'; @@ -10,6 +10,8 @@ import { RootState } from '../store/store'; import React from 'react'; import StorageIcon from '@mui/icons-material/Storage'; import SendIcon from '@mui/icons-material/Send'; +import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; +import { n } from '@tauri-apps/api/fs-4bb77382'; interface ServerEntry { description: string, @@ -27,9 +29,12 @@ function Login() { const [server, setServer] = useState("magical.rocks"); const [port, setPort] = useState("64738"); const [username, setUsername] = useState("Endor"); + const [identity, setIdentity] = useState("none"); + const [identityCerts, setIdentityCerts] = useState(new Array()); const [connecting, setConnecting] = useState(false); const [errorInfo, setErrorInfo] = useState({ show: false, text: "" }); const [serverInfo, setServerInfo] = useState({ show: false, text: "" }); + const [showAdditionalOptions, setShowAdditionalOptions] = useState(false); const [serverList, setServerList] = useState([]); @@ -55,6 +60,11 @@ function Login() { }).catch(e => { console.log("error getting server list: ", e); }); + + invoke('get_identity_certs').then((e: any) => { + console.log("identity certs: ", e); + setIdentityCerts(e); + }); }, []); //TODO: We shouldn't just have a binary connected state, @@ -100,6 +110,27 @@ function Login() { let serverAddInfoBoxBox = serverInfo.show ? ({serverInfo.text}) : (
); let connectionLoading = connecting ? () : (
); + let additionalOptions = useMemo(() => { + if (showAdditionalOptions) { + return ( + + + + + + ) + } + return null; + }, [showAdditionalOptions, identity]); + return ( @@ -161,12 +192,21 @@ function Login() { setUsername(e.target.value)} /> + {additionalOptions} saveServer()}>Save + + + setShowAdditionalOptions(!showAdditionalOptions)} > + + + + + connect()} endIcon={}>Connect diff --git a/yarn.lock b/yarn.lock index 2a9afe8..2664b62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -633,11 +633,28 @@ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.5.0.tgz#57618e57942a5f0131374a9fdb0167e25a117fdc" integrity sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg== +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@tauri-apps/api@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.2.0.tgz#1f196b3e012971227f41b98214c846430a4eb477" integrity sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw== +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + "@types/dompurify@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.1.tgz#7c49ef3dca25b04fc75bafb783b256f93e6936a4" @@ -673,6 +690,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== +"@types/node@>=10.0.0": + version "20.11.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f" + integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ== + dependencies: + undici-types "~5.26.4" + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -772,6 +796,14 @@ magic-string "^0.27.0" react-refresh "^0.14.0" +accepts@~1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -788,6 +820,11 @@ babel-plugin-macros@^3.1.0: cosmiconfig "^7.0.0" resolve "^1.19.0" +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + browserslist@^4.21.3: version "4.21.5" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" @@ -852,6 +889,19 @@ convert-source-map@^1.5.0, convert-source-map@^1.7.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -881,7 +931,7 @@ dayjs@^1.11.7: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -debug@^4.1.0: +debug@^4.1.0, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -956,6 +1006,38 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.361.tgz#010ddd5e623470ab9d1bf776b009d11c3669a4e3" integrity sha512-VocVwjPp05HUXzf3xmL0boRn5b0iyqC7amtDww84Jb1QJNPBc7F69gJyEeXRoriLBC4a5pSyckdllrXAg4mmRA== +engine.io-client@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.3.tgz#4cf6fa24845029b238f83c628916d9149c399bc5" + integrity sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" + integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== + +engine.io@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc" + integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -1371,6 +1453,18 @@ material-colors@^1.2.1: resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -1381,6 +1475,11 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + node-releases@^2.0.8: version "2.0.10" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" @@ -1391,7 +1490,7 @@ normalize-wheel@^1.0.1: resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45" integrity sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA== -object-assign@^4.1.1: +object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -1665,6 +1764,44 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" + +socket.io-client@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.4.tgz#5f0e060ff34ac0a4b4c5abaaa88e0d1d928c64c8" + integrity sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.4.tgz#2401a2d7101e4bdc64da80b140d5d8b6a8c7738b" + integrity sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.5.2" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -1731,6 +1868,11 @@ typescript@^4.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + update-browserslist-db@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" @@ -1744,6 +1886,11 @@ use-sync-external-store@^1.0.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +vary@^1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + vite@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/vite/-/vite-4.2.1.tgz#6c2eb337b0dfd80a9ded5922163b94949d7fc254" @@ -1756,6 +1903,16 @@ vite@^4.2.0: optionalDependencies: fsevents "~2.3.2" +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"