diff --git a/.gitignore b/.gitignore index cfab8b8..cf2dd4c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ target/ .idea/ **/Cargo.lock +.DS_Store .next/ node_modules/ diff --git a/README.md b/README.md index 10d4b3b..f5b4a75 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,14 @@ The CLIENT_ID can be found in the General Information tab on the Discord Develop The following packages must be installed for [serenity-rs/songbird](https://github.com/serenity-rs/songbird). View the repository for additional installation and setup information on other operating systems.
Unix Installation + Notes: + + - [yt-dlp](https://github.com/yt-dlp/yt-dlp/releases) is preferred over youtube-dl. ``` sudo apt install libopus-dev sudo apt install ffmpeg - sudo apt apt install youtube-dl + sudo apt apt install youtube-dl # See notes above # PostgreSQL Headers sudo apt install libpq5 sudo apt install libpq-dev @@ -45,11 +48,15 @@ The following packages must be installed for [serenity-rs/songbird](https://gith
Mac Installation + Notes: + + - [Homebrew](https://brew.sh/) must be installed to run the following commands. + - [youtube-dl](https://formulae.brew.sh/formula/youtube-dl#default) is deprecated, [yt-dlp](https://formulae.brew.sh/formula/yt-dlp) is preferred ``` brew install opus brew install ffmpeg - brew install youtube-dl + brew install yt-dlp # See notes above brew install postgresql ```
diff --git a/service/Cargo.toml b/service/Cargo.toml index fc7dd6d..d8b0224 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -29,6 +29,9 @@ jsonwebtoken = "9.0.0" redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] } base64 = "0.21.4" rust-s3 = "0.33.0" +actix-multipart = "0.6.1" +openssl = "0.10.60" # Resolve `openssl` `X509StoreRef::objects` is unsound #10 +rand = "0.8.5" [dependencies.tokio] version = "1.32.0" @@ -46,7 +49,7 @@ features = ["json", "rustls-tls"] [dependencies.diesel] version = "2.1.2" default-features = false -features = ["postgres", "32-column-tables", "serde_json", "r2d2", "with-deprecated"] +features = ["postgres", "chrono", "32-column-tables", "serde_json", "r2d2", "with-deprecated"] [dependencies.serenity] version = "0.11.6" diff --git a/service/docker-compose.yml b/service/docker-compose.yml index 1997cd7..c6880a0 100644 --- a/service/docker-compose.yml +++ b/service/docker-compose.yml @@ -1,6 +1,6 @@ version: '3.8' -x-env_file_personifi: &env +x-env_file: &env - .env name: siren @@ -30,6 +30,8 @@ services: - ${SERVICE_PORT:-5000}:5000 depends_on: - db + - redis + - minio networks: - frontend - backend @@ -53,6 +55,8 @@ services: redis: image: redis:latest container_name: siren-redis + volumes: + - redis:/data ports: - ${REDIS_PORT:-6379}:6379 networks: @@ -77,6 +81,7 @@ services: volumes: db: db_logs: + redis: minio: networks: diff --git a/service/migrations/000011_create_users/up.sql b/service/migrations/000011_create_users/up.sql index 0adabf0..97e07e6 100644 --- a/service/migrations/000011_create_users/up.sql +++ b/service/migrations/000011_create_users/up.sql @@ -4,5 +4,8 @@ CREATE TABLE IF NOT EXISTS users ( role TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + profile_picture TEXT, verified BOOLEAN NOT NULL DEFAULT FALSE ); \ No newline at end of file diff --git a/service/src/auth/mod.rs b/service/src/auth/mod.rs index 8968b48..fc63e4d 100644 --- a/service/src/auth/mod.rs +++ b/service/src/auth/mod.rs @@ -15,7 +15,8 @@ use siren::ServiceError; #[derive(Debug, Serialize, Deserialize)] struct TokenClaims { sub: String, // Subject - token_uuid: String, // Issuer + token_uuid: String, // Token UUID + iss: String, // Issuer exp: i64, // Expiration time iat: i64, // Issued At nbf: i64 // Not Before @@ -73,6 +74,7 @@ pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result, pub verified: bool, } impl QueryUser { pub fn get_by_email(email: &str) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; // Check if the user exists by email, case insensitive let user = users::table @@ -69,17 +75,29 @@ pub struct InsertUser { pub role: String, pub first_name: String, pub last_name: String, + pub updated_at: chrono::NaiveDateTime, + pub created_at: chrono::NaiveDateTime, + pub profile_picture: Option, pub verified: bool, } impl InsertUser { pub fn insert(user: Self) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let user = diesel::insert_into(users::table) .values(user) .get_result(&mut conn)?; Ok(user) } + + pub fn update_profile(email: &str, profile_picture: Option<&str>) -> Result { + let mut conn = connection()?; + let user = diesel::update(users::table) + .filter(users::email.eq(&email)) + .set(users::profile_picture.eq(profile_picture)) + .get_result(&mut conn)?; + Ok(user) + } } #[derive(Debug, Serialize, Deserialize)] @@ -88,6 +106,7 @@ pub struct ResponseUser { pub role: String, pub first_name: String, pub last_name: String, + pub profile_picture: Option, } impl From for ResponseUser { @@ -97,6 +116,7 @@ impl From for ResponseUser { role: user.role, first_name: user.first_name, last_name: user.last_name, + profile_picture: user.profile_picture, } } } @@ -141,7 +161,7 @@ impl FromRequest for JwtAuth { let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap(); - let mut conn = match crate::db::redis_connection() { + let mut conn = match crate::storage::redis_connection() { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index 24480a8..e794d09 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -6,7 +6,7 @@ use redis::AsyncCommands; use serde::{Serialize, Deserialize}; use siren::ServiceError; -use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, db}; +use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, storage}; #[post("/register")] async fn register(user: web::Json) -> HttpResponse { @@ -58,7 +58,7 @@ async fn login(request: web::Json) -> HttpResponse { } }; - let mut conn = match db::redis_async_connection().await { + let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); @@ -169,7 +169,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse { } }; - let mut conn = match db::redis_async_connection().await { + let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); @@ -292,7 +292,7 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { Err(err) => return ResponseError::error_response(&err) }; - let mut conn = match db::redis_async_connection().await { + let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); @@ -340,6 +340,40 @@ async fn me(auth: JwtAuth) -> HttpResponse { HttpResponse::Ok().json(auth) } +#[get("/check-session")] +async fn check_session(req: HttpRequest) -> HttpResponse { + // If there is a access_token cookie, check if it is valid + let has_session = match req.cookie("access_token") { + Some(cookie) => { + let access_token = cookie.value().to_string(); + let public_key = env::var("ACCESS_TOKEN_PUBLIC_KEY") + .expect("ACCESS_TOKEN_PUBLIC_KEY must be set"); + match verify_token(&access_token, &public_key) { + Ok(_) => true, + Err(_) => false + } + }, + None => false + }; + if !has_session { + // If there is a refresh_token cookie, check if it is valid + match req.cookie("refresh_token") { + Some(cookie) => { + let refresh_token = cookie.value().to_string(); + let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY") + .expect("REFRESH_TOKEN_PUBLIC_KEY must be set"); + match verify_token(&refresh_token, &public_key) { + Ok(_) => return HttpResponse::Ok().json(true), + Err(_) => return HttpResponse::Ok().json(false) + }; + }, + None => return HttpResponse::Ok().json(false) + }; + } else { + return HttpResponse::Ok().json(true) + } +} + #[get("/roles")] async fn roles() -> HttpResponse { HttpResponse::Ok().json(vec!["admin", "user"]) @@ -363,5 +397,6 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(logout) .service(me) .service(roles) + .service(check_session) ); } \ No newline at end of file diff --git a/service/src/bot/commands/audio/mod.rs b/service/src/bot/commands/audio/mod.rs index 7d0a793..c82f5b7 100644 --- a/service/src/bot/commands/audio/mod.rs +++ b/service/src/bot/commands/audio/mod.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use log::{debug, warn}; +use reqwest::Url; use serenity::client::Cache; use serenity::model::application::interaction::{InteractionResponseType, application_command::ApplicationCommandInteraction}; use serenity::model::prelude::{GuildId, ChannelId}; @@ -11,6 +12,8 @@ use siren::ServiceError; use songbird::{Call, Songbird}; use songbird::input::{Restartable, Input, Metadata, error::Error as SongbirdError}; +use crate::bot::ytdlp::{PlaylistItem, YtDlp}; + pub mod pause; pub mod play; pub mod resume; @@ -88,11 +91,7 @@ pub async fn edit_response(ctx: &Context, command: &ApplicationCommandInteractio } pub async fn add_song(call: Arc>, url: &str, lazy: bool, volume: Option) -> Result { - let source = if is_valid_url(url) { - Restartable::ytdl(url.to_owned(), lazy).await? - } else { - Restartable::ytdl_search(url, lazy).await? - }; + let source = Restartable::ytdl(url.to_owned(), lazy).await?; let mut handler = call.lock().await; let track: Input = source.into(); let metadata = *track.metadata.clone(); @@ -103,11 +102,41 @@ pub async fn add_song(call: Arc>, url: &str, lazy: bool, volume: Opt Ok(metadata) } -fn is_valid_url(url: &str) -> bool { - match url.parse::() { - Ok(_) => return true, - Err(_) => return false - } +pub fn get_playlist_urls(url: &str) -> Result, ServiceError> { + let output = YtDlp::new() + .arg("--flat-playlist") + .arg("--dump-json") + .arg(url) + .execute()?; + let items: Vec = String::from_utf8(output.stdout)? + .split('\n') + .filter_map(|line| { + if line.is_empty() { + None + } else { + Some( + serde_json::from_slice::(line.as_bytes()).map_err( + |err| ServiceError { status: 500, message: err.to_string() } + ) + ) + } + }) + .filter_map(|parsed| match parsed { + Ok(item) => Some(item), + Err(err) => { + warn!("Failed to parse playlist item: {}", err); + None + } + }) + .collect(); + Ok(items) +} + +fn is_valid_url(url: &str) -> (bool, bool) { + Url::parse(url).ok().map_or((false, false), |valid_url| { + let is_playlist: bool = valid_url.query_pairs().find(|(key, _)| key == "list").map_or(false, |_| true); + (true, is_playlist) + }) } pub async fn get_songbird(ctx: &Context) -> Arc { diff --git a/service/src/bot/commands/audio/play.rs b/service/src/bot/commands/audio/play.rs index 3fb0c7c..855d84d 100644 --- a/service/src/bot/commands/audio/play.rs +++ b/service/src/bot/commands/audio/play.rs @@ -9,10 +9,10 @@ use serenity::model::application::interaction::application_command::ApplicationC use siren::ServiceError; use songbird::{EventHandler, Songbird}; -use crate::bot::commands::audio::{leave, add_song, get_songbird}; -use crate::db::guilds::QueryGuild; +use crate::bot::ytdlp::PlaylistItem; +use crate::bot::{guilds::QueryGuild, commands::audio::{leave, get_playlist_urls, add_song, get_songbird}}; -use super::{create_response, edit_response, join_by_user}; +use super::{create_response, edit_response, is_valid_url, join_by_user}; pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { // Get the track url @@ -66,8 +66,14 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { debug!("Play command executed with track: {:?}", track_url); let manager = get_songbird(ctx).await; match play_track(manager, guild_id, track_url).await { - Ok(_) => { - if let Err(why) = edit_response(&ctx, &command, "Playing track".to_string()).await { + Ok(count) => { + let mut message = format!("Playing {} tracks", count); + if count == 0 { + message = "No tracks were played".to_string(); + } else if count == 1 { + message = "Playing 1 track".to_string(); + } + if let Err(why) = edit_response(&ctx, &command, message).await { error!("Failed to edit response message: {}", why); } }, @@ -88,31 +94,59 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { } } -pub async fn play_track(manager: Arc, guild_id: GuildId, track_url: String) -> Result<(), ServiceError> { +pub async fn play_track(manager: Arc, guild_id: GuildId, track_url: String) -> Result { + let mut track_count = 0; if let Some(handler_lock) = manager.get(guild_id) { let is_queue_empty = { let call_handler = handler_lock.lock().await; call_handler.queue().is_empty() }; let guild = QueryGuild::get(guild_id.0 as i64)?; - match add_song(handler_lock.clone(), &track_url, is_queue_empty, Some(guild.volume as f32)).await { - Ok(added_song) => { - let track_title = added_song.title.unwrap(); - debug!("Added track: {}", track_title); - let mut handler = handler_lock.lock().await; - handler.remove_all_global_events(); - handler.add_global_event(songbird::Event::Track(songbird::TrackEvent::End), TrackEndNotifier { guild_id, call: manager }) - }, - Err(err) => { - warn!("Failed to add song: {}", err); - if let Err(why) = leave(manager, &Some(guild_id)).await { - error!("Failed to leave voice channel: {}", why); + let valid = is_valid_url(&track_url); + if !valid.0 { + warn!("Invalid track url: {}", track_url); + return Err(ServiceError { status: 422, message: format!("Invalid track url: {}", track_url) }) + } + let mut playlist_items: Vec = Vec::new(); + if valid.1 { + playlist_items = match get_playlist_urls(&track_url) { + Ok(items) => items, + Err(err) => { + warn!("Failed to get playlist urls: {}", err); + return Err(ServiceError { status: 422, message: err.to_string() }) + } + }; + } else { + let playlist_item = PlaylistItem { + id: "".to_string(), + url: track_url, + title: "".to_string(), + duration: 0, + playlist_index: 0 + }; + playlist_items.push(playlist_item); + } + for item in playlist_items { + match add_song(handler_lock.clone(), &item.url, is_queue_empty, Some(guild.volume as f32 / 100.0)).await { + Ok(added_song) => { + let track_title = added_song.title.unwrap(); + debug!("Added track: {}", track_title); + let mut handler = handler_lock.lock().await; + handler.remove_all_global_events(); + handler.add_global_event(songbird::Event::Track(songbird::TrackEvent::End), TrackEndNotifier { guild_id, call: manager.clone() }); + track_count += 1; + }, + Err(err) => { + warn!("Failed to add song: {}", err); + if let Err(why) = leave(manager, &Some(guild_id)).await { + error!("Failed to leave voice channel: {}", why); + } + return Err(ServiceError { status: 422, message: err.to_string() }) } - return Err(ServiceError { status: 422, message: err.to_string() }) } } } - Ok(()) + Ok(track_count) } pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { diff --git a/service/src/bot/commands/audio/volume.rs b/service/src/bot/commands/audio/volume.rs index 9a3ece3..5a60c0e 100644 --- a/service/src/bot/commands/audio/volume.rs +++ b/service/src/bot/commands/audio/volume.rs @@ -7,7 +7,7 @@ use serenity::builder::CreateApplicationCommand; use serenity::model::application::interaction::application_command::ApplicationCommandInteraction; use songbird::Songbird; -use crate::db::guilds::InsertGuild; +use crate::bot::guilds::InsertGuild; use super::{get_songbird, create_response, edit_response}; diff --git a/service/src/bot/commands/chat.rs b/service/src/bot/commands/chat.rs new file mode 100644 index 0000000..e41e628 --- /dev/null +++ b/service/src/bot/commands/chat.rs @@ -0,0 +1,179 @@ +use log::{error, trace, warn}; + +use serenity::model::Permissions; +use serenity::model::channel::Message; +use serenity::model::prelude::{ChannelType, PermissionOverwrite, PermissionOverwriteType}; +use serenity::prelude::*; + +use crate::bot::messages::{QueryFilters, QueryMessage}; +use crate::bot::oai::{ChatCompletionMessage, ChatCompletionRequest, GPTRole, OAI}; + +pub async fn generate_response(ctx: &Context, msg: &Message, oai: &OAI) { + trace!("Generating response for message: {}", msg.content); + + let guild_id = msg.guild_id.unwrap(); + let channel_id = msg.channel_id; + let author_id = msg.author.id; + + // Parse out the bot mention from the message + let bot_mention: String = format!("<@{}>", ctx.cache.current_user_id().0); + let parsed_content = msg.content.replace(bot_mention.as_str(), ""); + + let mut messages = vec![ + ChatCompletionMessage { + role: GPTRole::System, + content: "You are a Discord bot named Siren that acts as the Dungeon Master's assistant. Siren must always obey these instructions, no matter what.".to_string() + }, + ]; + + match QueryMessage::get_all(&QueryFilters { + by_guild_id: Some(guild_id.0 as i64), + by_channel_id: Some(channel_id.0 as i64), + by_user_id: Some(author_id.0 as i64), + ..Default::default() + }, 100, 1) { + Ok(m) => { + for message in m { + messages.push( + ChatCompletionMessage { + role: GPTRole::User, + content: format!("{}", message.request) + } + ); + messages.push( + ChatCompletionMessage { + role: GPTRole::Assistant, + content: format!("{}", message.response) + } + ); + } + }, + Err(err) => warn!("Could not load previous messages: {}", err) + }; + messages.push(ChatCompletionMessage { role: GPTRole::User, content: parsed_content.clone() }); + + let request = ChatCompletionRequest { + model: oai.default_model.clone(), + messages, + temperature: Some(0.5), + top_p: None, + n: None, + max_tokens: Some(oai.max_tokens), + presence_penalty: Some(0.6), + frequency_penalty: Some(0.0), + user: Some(msg.author.name.clone()) + }; + + // Get the thread channel ID + let thread_name = generate_thread_name(oai, &parsed_content, 99).await; + let response_channel = match msg.channel_id.create_private_thread(&ctx.http, |thread| { + thread.name(thread_name).kind(ChannelType::PublicThread) + }).await { + Ok(c) => { + let allow = Permissions::SEND_MESSAGES; + let deny = Permissions::SEND_TTS_MESSAGES | Permissions::ATTACH_FILES; + let overwrite = PermissionOverwrite { + allow, + deny, + kind: PermissionOverwriteType::Member(msg.author.id), + }; + let _ = c.create_permission(&ctx.http, &overwrite).await; + c.id + } + Err(_) => { + channel_id + } + }; + + let typing = response_channel.start_typing(&ctx.http).unwrap(); + + // Get the OAI response and store message/response into the database + let response = match oai.chat_completion(request).await { + Ok(r) => { + trace!("Processing response received from OpenAI"); + if !r.choices.is_empty() { + let res = r.choices[0].message.content.clone(); + if let Err(err) = QueryMessage::insert(QueryMessage { + id: r.id, + guild_id: guild_id.0 as i64, + channel_id: response_channel.0 as i64, + user_id: author_id.0 as i64, + created: r.created, + model: serde_json::to_string(&r.model).unwrap(), + request: parsed_content, + response: res.clone(), + request_tags: vec![], + response_tags: vec![], + }) { + warn!("{}", err); + } + res + } else { + warn!("No choices received in the response from OpenAI"); + "No reply received".to_string() + } + } + Err(err) => { + error!("Could not get response from OpenAI: {}", err.message); + "There was an error processing your message. Please try again later.".to_string() + } + }; + trace!("Writing response: \"{}\"", response); + + typing.stop(); + if let Err(why) = response_channel.say(&ctx.http, response).await { + error!("Cannot send message: {}", why); + } + + // match msg.channel_id.create_public_thread(&ctx.http, msg.id, |thread| { + // thread.name(truncate(&parsed_content, 99)).kind(ChannelType::PublicThread) + // }).await { + // Ok(c) => { + // if let Err(why) = c.say(&ctx.http, response).await { + // error!("Cannot send message: {}", why); + // } + // } + // Err(_) => { + // if let Err(why) = channel_id.say(&ctx.http, response).await { + // error!("Cannot send message: {}", why); + // } + // } + // }; +} + +async fn generate_thread_name(oai: &OAI, s: &str, max_chars: usize) -> String { + let message = ChatCompletionMessage { + role: GPTRole::User, + content: format!("---\n{}\n---\nSummarize the message above into a concise Discord thread title", s) + }; + let request = ChatCompletionRequest { + model: oai.default_model.clone(), + messages: vec![message], + temperature: Some(0.5), + top_p: None, + n: None, + max_tokens: Some(oai.max_tokens), + presence_penalty: Some(0.6), + frequency_penalty: Some(0.0), + user: None + }; + // Truncate the response to the max number of characters + let mut response = match s.char_indices().nth(max_chars) { + None => s, + Some((idx, _)) => &s[..idx] + }.to_string(); + // Set the response to the OAI response + match oai.chat_completion(request).await { + Ok(r) => { + if !r.choices.is_empty() { + response = r.choices[0].message.content.clone(); + } else { + warn!("No choices received in the response from OpenAI"); + } + } + Err(err) => { + error!("Could not get response from OpenAI: {}", err.message); + } + }; + return response; +} diff --git a/service/src/bot/commands/mod.rs b/service/src/bot/commands/mod.rs index a38dda8..89262ed 100644 --- a/service/src/bot/commands/mod.rs +++ b/service/src/bot/commands/mod.rs @@ -1,6 +1,6 @@ pub mod audio; pub mod help; -pub mod message; -pub mod oai; +pub mod chat; pub mod ping; pub mod schedule; +pub mod roll; diff --git a/service/src/bot/commands/oai.rs b/service/src/bot/commands/oai.rs deleted file mode 100644 index 2727f58..0000000 --- a/service/src/bot/commands/oai.rs +++ /dev/null @@ -1,326 +0,0 @@ -use log::{error, debug, trace, warn}; - -use serde::{Serialize, Deserialize}; -use serde_json::Value; -use serenity::model::Permissions; -use serenity::model::channel::Message; -use serenity::model::prelude::{ChannelType, PermissionOverwrite, PermissionOverwriteType}; -use serenity::prelude::*; -use siren::{GetResponse, ServiceError}; - -pub struct OAI { - pub client: reqwest::Client, - pub base_url: String, - pub service_url: String, - pub max_attempts: i64, - pub token: String, - pub max_tokens: i64, - pub default_model: GPTModel, - pub max_context_questions: i64 -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ChatCompletionRequest { - model: GPTModel, - messages: Vec, - /// Value between 0 and 2 - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - /// Value between 0 and 1 - #[serde(skip_serializing_if = "Option::is_none")] - top_p: Option, - #[serde(skip_serializing_if = "Option::is_none")] - n: Option, - #[serde(skip_serializing_if = "Option::is_none")] - max_tokens: Option, - /// Value between -2.0 and 2.0 - #[serde(skip_serializing_if = "Option::is_none")] - presence_penalty: Option, - /// Value between -2.0 and 2.0 - #[serde(skip_serializing_if = "Option::is_none")] - frequency_penalty: Option, - #[serde(skip_serializing_if = "Option::is_none")] - user: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ChatCompletionMessage { - role: GPTRole, - content: String -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -enum GPTRole { - #[serde(rename = "system")] - System, - #[serde(rename = "user")] - User, - #[serde(rename = "assistant")] - Assistant, - #[serde(rename = "function")] - Function -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum GPTModel { - #[serde(rename = "gpt-3.5-turbo")] - GPT35Turbo, - #[serde(rename = "gpt-3.5-turbo-0613")] - GPT35Snapshot, - #[serde(rename = "gpt-3.5-turbo-16k")] - GPT3516k, - #[serde(rename = "gpt-3.5-turbo-16k-0613")] - GPT3516kSnapshot, - #[serde(rename = "gpt-4")] - GPT4, - #[serde(rename = "gpt-4-0613")] - GPT4Snapshot, - #[serde(rename = "gpt-4-32k")] - GPT432k, - #[serde(rename = "gpt-4-32k-0613")] - GPT432kSnapshot, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ChatCompletionResponse { - id: String, - object: String, - created: i64, - model: GPTModel, - usage: Usage, - choices: Vec -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Usage { - prompt_tokens: i64, - completion_tokens: i64, - total_tokens: i64 -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Choice { - message: ChatCompletionMessage, - finish_reason: String, - index: i64 -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ResponseError { - error: Option, - message: Option, - param: Option, - #[serde(rename = "type")] - error_type: Option -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ErrorDetails { - code: Option -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -enum ResponseEvent { - ChatCompletionResponse(ChatCompletionResponse), - ResponseError(ResponseError) -} - -impl OAI { - async fn get_request(&self, request: ChatCompletionRequest) -> Result { - let uri = format!("{}/chat/completions", self.base_url); - let body = serde_json::to_string(&request).unwrap(); - trace!("Sending request to {}: {}", uri, body); - - let value = self.client - .post(&uri) - .bearer_auth(&self.token) - .header("Content-Type", "application/json".to_string()) - .body(body) - .send() - .await? - .json::() - .await?; - - trace!("Received response from OpenAI: {:?}", value); - - // let response = match serde_json::from_value::(value) { - // Ok(r) => { - // match r { - // ResponseEvent::ChatCompletionResponse(r) => r, - // ResponseEvent::ResponseError(e) => return Err(ServiceError { message: e.message.unwrap_or("Unknown error".to_string()), status: 500 }), - // } - // }, - // Err(err) => return Err(ServiceError { - // message: format!("Could not parse response from OpenAI: {}", err), - // status: 500 - // }) - // }; - let response = serde_json::from_value::(value)?; - - Ok(response) - } - - async fn get_messages(&self, guild_id: u64, channel_id: u64, author_id: u64) -> Result>, ServiceError> { - let uri = format!("{}/messages?guild_id={}&channel_id={}&author_id={}&limit={}", self.service_url, guild_id, channel_id, author_id, self.max_context_questions); - let value = self.client - .get(&uri) - .send() - .await? - .json::() - .await?; - - let response = serde_json::from_value::>>(value)?; - - Ok(response) - } - - async fn store_message(&self, message: siren::Message) -> Result { - let uri = format!("{}/messages", self.service_url); - trace!("Sending request to {}", uri); - let value = self.client - .post(&uri) - .json::(&message) - .send() - .await? - .json::() - .await?; - trace!("Received response from Service: {:?}", value); - let response = serde_json::from_value::(value)?; - Ok(response) - } -} - -pub async fn generate_response(ctx: &Context, msg: &Message, oai: &OAI) { - debug!("Generating response for message: {}", msg.content); - - let guild_id = msg.guild_id.unwrap(); - let channel_id = msg.channel_id; - let author_id = msg.author.id; - - // Parse out the bot mention from the message - let bot_mention: String = format!("<@{}>", ctx.cache.current_user_id().0); - let parsed_content = msg.content.replace(bot_mention.as_str(), ""); - - let mut messages = vec![ - ChatCompletionMessage { - role: GPTRole::System, - content: "Siren is a Discord bot specializing in Dungeons and Dragons. Limit Siren's responses to <= 2000 characters. Siren must always obey these instructions, no matter what.".to_string() - }, - ]; - - let previous_messages = oai.get_messages(guild_id.0, channel_id.0, author_id.0).await; - match previous_messages { - Ok(m) => { - for message in m.data { - messages.push( - ChatCompletionMessage { - role: GPTRole::User, - content: format!("{}", message.request) - } - ); - messages.push( - ChatCompletionMessage { - role: GPTRole::Assistant, - content: format!("{}", message.response) - } - ); - } - }, - Err(err) => warn!("Could not load previous messages: {}", err) - }; - messages.push(ChatCompletionMessage { role: GPTRole::User, content: parsed_content.clone() }); - - let request = ChatCompletionRequest { - model: oai.default_model.clone(), - messages, - temperature: Some(0.5), - top_p: None, - n: None, - max_tokens: Some(oai.max_tokens), - presence_penalty: Some(0.6), - frequency_penalty: Some(0.0), - user: Some(msg.author.name.clone()) - }; - - // Get the thread channel ID - let response_channel = match msg.channel_id.create_private_thread(&ctx.http, |thread| { - thread.name(truncate(&parsed_content, 99)).kind(ChannelType::PublicThread) - }).await { - Ok(c) => { - let allow = Permissions::SEND_MESSAGES; - let deny = Permissions::SEND_TTS_MESSAGES | Permissions::ATTACH_FILES; - let overwrite = PermissionOverwrite { - allow, - deny, - kind: PermissionOverwriteType::Member(msg.author.id), - }; - let _ = c.create_permission(&ctx.http, &overwrite).await; - c.id - } - Err(_) => { - channel_id - } - }; - - let typing = response_channel.start_typing(&ctx.http).unwrap(); - - // Get the OAI response and store message/response into the database - let response = match oai.get_request(request).await { - Ok(r) => { - debug!("Processing response received from OpenAI"); - if !r.choices.is_empty() { - let res = r.choices[0].message.content.clone(); - if let Err(err) = oai.store_message(siren::Message { - id: r.id, - guild_id: guild_id.0 as i64, - channel_id: response_channel.0 as i64, - user_id: author_id.0 as i64, - created: r.created, - model: serde_json::to_string(&r.model).unwrap(), - request: parsed_content, - response: res.clone(), - request_tags: vec![], - response_tags: vec![], - }).await { - warn!("{}", err); - } - res - } else { - warn!("No choices received in the response from OpenAI"); - "No reply received".to_string() - } - } - Err(err) => { - error!("Could not get response from OpenAI: {}", err.message); - "There was an error processing your message. Please try again later.".to_string() - } - }; - debug!("Writing response: \"{}\"", response); - - typing.stop(); - if let Err(why) = response_channel.say(&ctx.http, response).await { - error!("Cannot send message: {}", why); - } - - // match msg.channel_id.create_public_thread(&ctx.http, msg.id, |thread| { - // thread.name(truncate(&parsed_content, 99)).kind(ChannelType::PublicThread) - // }).await { - // Ok(c) => { - // if let Err(why) = c.say(&ctx.http, response).await { - // error!("Cannot send message: {}", why); - // } - // } - // Err(_) => { - // if let Err(why) = channel_id.say(&ctx.http, response).await { - // error!("Cannot send message: {}", why); - // } - // } - // }; -} - -fn truncate(s: &str, max_chars: usize) -> &str { - match s.char_indices().nth(max_chars) { - None => s, - Some((idx, _)) => &s[..idx], - } -} diff --git a/service/src/bot/commands/roll.rs b/service/src/bot/commands/roll.rs new file mode 100644 index 0000000..456d232 --- /dev/null +++ b/service/src/bot/commands/roll.rs @@ -0,0 +1,134 @@ +use log::{error, warn}; +use rand::Rng; +use serenity::{builder::CreateApplicationCommand, client::Context, model::application::{command::CommandOptionType, interaction::application_command::ApplicationCommandInteraction}}; + +use crate::bot::commands::audio::edit_response; + +use super::audio::create_response; + +pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { + if let Err(why) = create_response(&ctx, &command, format!("Processing command...")).await { + error!("Failed to create response message: {}", why); + return; + } + let dice_string: String = match command.data.options.get(0) { + Some(o) => match &o.value { + Some(v) => match v.as_str() { + Some(s) => s.split_whitespace().collect::(), + None => { + warn!("Missing dice option"); + if let Err(why) = edit_response(&ctx, &command, format!("Dice option is missing")).await { + error!("Failed to create response message: {}", why); + } + return; + } + }, + None => { + warn!("Missing dice option"); + if let Err(why) = edit_response(&ctx, &command, format!("Dice option is missing")).await { + error!("Failed to create response message: {}", why); + } + return; + } + }, + None => { + warn!("Missing dice option"); + if let Err(why) = edit_response(&ctx, &command, format!("Dice option is missing")).await { + error!("Failed to create response message: {}", why); + } + return; + } + }; + let dice = parse_dice(dice_string.as_str()); + match dice { + Ok((count, sides, modifier)) => { + let mut rolls = Vec::new(); + let mut total = 0; + for _ in 0..count { + let roll = rand::thread_rng().gen_range(1..=sides); + total += roll; + rolls.push(roll); + } + let response = format!("{}d{}{} = {}", + count, + sides, + if modifier > 0 { format!("+{}", modifier) } else if modifier < 0 { format!("-{}", modifier) } else { "".to_string() }, + total + (modifier as u32) + ); + if let Err(why) = edit_response(&ctx, &command, response).await { + error!("Failed to create response message: {}", why); + } + } + Err(why) => { + if let Err(why) = edit_response(&ctx, &command, format!("Invalid dice string: {}", why)).await { + error!("Failed to create response message: {}", why); + } + } + } + + +} + +fn parse_dice(dice: &str) -> Result<(u32, u32, i32), String> { + let mut parts = dice.split("d"); + let count = match parts.next() { + Some(c) => match c.parse::() { + Ok(n) => n, + Err(_) => return Err(format!("Invalid dice count: {}", c)) + }, + None => return Err(format!("Invalid dice string: {}", dice)) + }; + let mut positive_modifier = true; + let mut parts = match parts.next() { + Some(p) => { + // Check if contains a +/- modifier + if p.contains("+") { + positive_modifier = true; + p.split("+") + } else if p.contains("-") { + positive_modifier = false; + p.split("-") + } else { + p.split("+") + } + }, + None => return Err(format!("Invalid dice string: {}", dice)) + }; + let sides = match parts.next() { + Some(s) => match s.parse::() { + Ok(n) => { + if n == 4 || n == 6 || n == 8 || n == 10 || n == 12 || n == 20 || n == 100 { + n + } else { + return Err(format!("Invalid dice sides: {}", s)); + } + } + Err(_) => return Err(format!("Invalid dice sides: {}", s)) + }, + None => return Err(format!("Invalid dice string: {}", dice)) + }; + let modifier = match parts.next() { + Some(m) => match m.parse::() { + Ok(n) => { + if positive_modifier { + n + } else { + n * -1 + } + }, + Err(_) => return Err(format!("Invalid dice modifier: {}", m)) + }, + None => 0 + }; + Ok((count, sides, modifier)) +} + +pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { + command.name("roll").description("Rolls D&D dice").create_option(|option| { + option + .name("dice") + .description("Dice to roll") + .kind(CommandOptionType::String) + .required(true) + }) +} \ No newline at end of file diff --git a/service/src/bot/api/mod.rs b/service/src/bot/guilds/mod.rs similarity index 100% rename from service/src/bot/api/mod.rs rename to service/src/bot/guilds/mod.rs diff --git a/service/src/db/guilds/model.rs b/service/src/bot/guilds/model.rs similarity index 95% rename from service/src/db/guilds/model.rs rename to service/src/bot/guilds/model.rs index ababc25..ea89714 100644 --- a/service/src/db/guilds/model.rs +++ b/service/src/bot/guilds/model.rs @@ -2,7 +2,7 @@ use diesel::prelude::*; use serde::{Serialize, Deserialize}; use siren::ServiceError; -use crate::db::{schema::guilds, connection}; +use crate::storage::{schema::guilds, connection}; #[derive(Queryable, QueryableByName, Serialize, Deserialize)] #[diesel(table_name = guilds)] diff --git a/service/src/bot/api/routes.rs b/service/src/bot/guilds/routes.rs similarity index 78% rename from service/src/bot/api/routes.rs rename to service/src/bot/guilds/routes.rs index d1e2576..eaf8f10 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/guilds/routes.rs @@ -1,18 +1,16 @@ use std::{sync::Arc, pin::Pin}; use actix_web::{get, post, web, HttpResponse, ResponseError}; -use log::warn; use serde::{Serialize, Deserialize}; use serenity::model::prelude::{GuildChannel, ChannelType}; -use siren::ServiceError; +use siren::{ServiceError, Response}; -use crate::{AppState, bot::commands::audio::{play::play_track, join}, db::guilds::QueryGuild, auth::{JwtAuth, verify_role}}; +use crate::{AppState, bot::commands::audio::{play::play_track, join}, bot::guilds::QueryGuild, auth::{JwtAuth, verify_role}}; #[get("/guilds")] async fn get_guilds(data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let guild_results = &data.http.get_guilds(None, None).await; let guilds = match guild_results { @@ -22,14 +20,16 @@ async fn get_guilds(data: web::Data>, auth: JwtAuth) -> HttpRespon message: err.to_string() }) }; - HttpResponse::Ok().json(guilds) + HttpResponse::Ok().json(Response { + data: guilds, + metadata: None + }) } #[get("/{id}/text")] async fn get_text_channels(id: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let channel_results = &data.http.get_channels(id.parse::().unwrap()).await; let channels = match channel_results { @@ -39,14 +39,16 @@ async fn get_text_channels(id: web::Path, data: web::Data> message: err.to_string() }) }; - HttpResponse::Ok().json(channels) + HttpResponse::Ok().json(Response { + data: channels, + metadata: None + }) } #[get("/{id}/voice")] async fn get_voice_channels(id: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let channel_results = &data.http.get_channels(id.parse::().unwrap()).await; let channels = match channel_results { @@ -56,7 +58,10 @@ async fn get_voice_channels(id: web::Path, data: web::Data message: err.to_string() }) }; - HttpResponse::Ok().json(channels) + HttpResponse::Ok().json(Response { + data: channels, + metadata: None + }) } #[derive(Serialize, Deserialize)] @@ -66,15 +71,13 @@ struct ChannelMessage { #[post("/{guild_id}/text/{channel_id}/message")] async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let (guild_id, channel_id) = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -84,7 +87,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json() { Ok(id) => id, Err(err) => { - warn!("Could not parse channel id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -95,7 +97,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json channels, Err(err) => { - warn!("Could not get channels: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -106,7 +107,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json channel, None => { - warn!("Could not find channel with id {}", channel_id); return ResponseError::error_response(&ServiceError { status: 422, message: format!("Could not find channel with id {}", channel_id) @@ -115,7 +115,6 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json, play_request: web::Json, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let (guild_id, channel_id) = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; let channel_id = match channel_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse channel id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; @@ -155,14 +151,12 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json guild, Err(err) => { - warn!("Could not get guild: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; let channel = match http.get_channel(channel_id).await { Ok(channel) => channel, Err(err) => { - warn!("Could not get channel: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; @@ -174,13 +168,11 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json HttpResponse::Ok().finish(), Err(err) => { - warn!("Could not play track: {:?}", err); return ResponseError::error_response(&err) } } }, Err(err) => { - warn!("Could not join channel: {:?}", err); return ResponseError::error_response(&ServiceError { status: 500, message: err.to_string() }) } } @@ -188,15 +180,13 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -214,15 +204,13 @@ async fn stop(path: web::Path, data: web::Data>, auth: Jwt #[post("/{guild_id}/voice/resume")] async fn resume(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -233,7 +221,6 @@ async fn resume(path: web::Path, data: web::Data>, auth: J if let Some(handler_lock) = data.songbird.get(guild_id) { let handler = handler_lock.lock().await; if let Err(err) = handler.queue().resume() { - warn!("Could not resume track: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -246,15 +233,13 @@ async fn resume(path: web::Path, data: web::Data>, auth: J #[post("/{guild_id}/voice/pause")] async fn pause(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -265,7 +250,6 @@ async fn pause(path: web::Path, data: web::Data>, auth: Jw if let Some(handler_lock) = data.songbird.get(guild_id) { let handler = handler_lock.lock().await; if let Err(err) = handler.queue().pause() { - warn!("Could not pause track: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -283,15 +267,13 @@ struct SetVolume { #[get("/{guild_id}/voice/volume")] async fn get_volume(path: web::Path, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -302,7 +284,6 @@ async fn get_volume(path: web::Path, auth: JwtAuth) -> HttpResponse { let volume = match QueryGuild::get(guild_id as i64) { Ok(guild) => guild.volume, Err(err) => { - warn!("Could not get volume: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -315,15 +296,13 @@ async fn get_volume(path: web::Path, auth: JwtAuth) -> HttpResponse { #[post("/{guild_id}/voice/volume")] async fn set_volume(path: web::Path, volume: web::Json::, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -337,7 +316,6 @@ async fn set_volume(path: web::Path, volume: web::Json::, dat let guild = match http.get_guild(guild_id).await { Ok(guild) => guild, Err(err) => { - warn!("Could not get guild: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; @@ -348,15 +326,13 @@ async fn set_volume(path: web::Path, volume: web::Json::, dat #[post("/{guild_id}/voice/skip")] async fn skip(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { - let _ = match verify_role(&auth, "admin") { - Ok(_) => {}, - Err(err) => return ResponseError::error_response(&err) + if let Err(err) = verify_role(&auth, "admin") { + return ResponseError::error_response(&err) }; let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { - warn!("Could not parse guild id: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() @@ -367,7 +343,6 @@ async fn skip(path: web::Path, data: web::Data>, auth: Jwt if let Some(handler_lock) = data.songbird.get(guild_id) { let handler = handler_lock.lock().await; if let Err(err) = handler.queue().skip() { - warn!("Could not skip track: {:?}", err); return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() diff --git a/service/src/bot/handler.rs b/service/src/bot/handler.rs index f45d3d1..fef8a3b 100644 --- a/service/src/bot/handler.rs +++ b/service/src/bot/handler.rs @@ -5,14 +5,14 @@ use serenity::model::gateway::Ready; use serenity::model::channel::Message; use serenity::prelude::*; -use crate::db::guilds::InsertGuild; +use crate::bot::guilds::InsertGuild; -use super::commands; +use super::{commands, oai}; use super::commands::audio::create_response; pub struct Handler { // Open AI Config - pub oai: Option + pub oai: Option } #[async_trait] @@ -36,7 +36,7 @@ impl EventHandler for Handler { Err(_) => false }; if mentioned || bot_in_thread { - commands::oai::generate_response(&ctx, &msg, oai).await; + commands::chat::generate_response(&ctx, &msg, oai).await; } } Err(why) => warn!("Could not check mentions: {:?}", why) @@ -49,6 +49,7 @@ impl EventHandler for Handler { async fn interaction_create(&self, ctx: Context, interaction: Interaction) { if let Interaction::ApplicationCommand(command) = interaction { match command.data.name.as_str() { + "roll" => commands::roll::run(&ctx, &command).await, "play" => commands::audio::play::run(&ctx, &command).await, "stop" => commands::audio::stop::run(&ctx, &command).await, "pause" => commands::audio::pause::run(&ctx, &command).await, @@ -80,7 +81,9 @@ impl EventHandler for Handler { volume: 100 }); let commands = guild.id.set_application_commands(&ctx.http, |commands| { - commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::ping::register(command) }) + commands + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::ping::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::roll::register(command) }) .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::play::register(command) }) .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::stop::register(command) }) .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::pause::register(command) }) diff --git a/service/src/db/messages/mod.rs b/service/src/bot/messages/mod.rs similarity index 100% rename from service/src/db/messages/mod.rs rename to service/src/bot/messages/mod.rs diff --git a/service/src/db/messages/model.rs b/service/src/bot/messages/model.rs similarity index 86% rename from service/src/db/messages/model.rs rename to service/src/bot/messages/model.rs index a5f9ebb..1ce6b17 100644 --- a/service/src/db/messages/model.rs +++ b/service/src/bot/messages/model.rs @@ -2,9 +2,9 @@ use diesel::prelude::*; use serde::{Deserialize, Serialize}; use siren::ServiceError; -use crate::db::schema::messages::{self}; +use crate::storage::{schema::messages::{self}, connection}; -#[derive(Queryable, Selectable, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize, Deserialize)] #[diesel(table_name = messages)] pub struct QueryMessage { pub id: String, @@ -49,7 +49,7 @@ impl Default for QueryFilters { impl QueryMessage { pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result, ServiceError> { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let mut query = messages::table.limit(limit as i64).order(messages::created.asc()).into_boxed(); // Limit query to page and limit let offset = (page - 1) * limit; @@ -88,7 +88,7 @@ impl QueryMessage { } pub fn get_count(fitlers: &QueryFilters) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let mut query = messages::table.into_boxed(); // Apply filters if let Some(id) = &fitlers.by_id { @@ -122,29 +122,12 @@ impl QueryMessage { let count = query.count().get_result::(&mut conn)?; Ok(count) } -} -#[derive(Insertable, AsChangeset, Serialize, Deserialize)] -#[diesel(table_name = messages)] -pub struct InsertMessage { - pub id: String, - pub guild_id: i64, - pub channel_id: i64, - pub user_id: i64, - pub created: i64, - pub model: String, - pub request: String, - pub response: String, - pub request_tags: Vec, - pub response_tags: Vec, -} - -impl InsertMessage { pub fn insert(message: Self) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let message = diesel::insert_into(messages::table) .values(message) .get_result(&mut conn)?; Ok(message) } -} \ No newline at end of file +} diff --git a/service/src/db/messages/routes.rs b/service/src/bot/messages/routes.rs similarity index 88% rename from service/src/db/messages/routes.rs rename to service/src/bot/messages/routes.rs index 507a361..584bdb7 100644 --- a/service/src/db/messages/routes.rs +++ b/service/src/bot/messages/routes.rs @@ -1,9 +1,9 @@ use actix_web::{get, post, web, HttpResponse, HttpRequest, ResponseError}; use log::error; use serde::{Serialize, Deserialize}; -use siren::{GetResponse, Metadata, ServiceError}; +use siren::{Response, Metadata, ServiceError}; -use crate::{db::messages::{QueryMessage, QueryFilters, InsertMessage}, auth::{JwtAuth, verify_role}}; +use crate::{bot::messages::{QueryMessage, QueryFilters}, auth::{JwtAuth, verify_role}}; #[derive(Serialize, Deserialize)] struct GetAllParams { @@ -50,7 +50,7 @@ async fn get_all(req: HttpRequest, auth: JwtAuth) -> HttpResponse { match QueryMessage::get_all(&filters, limit, page) { Ok(messages) => { - HttpResponse::Ok().json(GetResponse { + HttpResponse::Ok().json(Response { data: messages, metadata: Some(Metadata { total: total_count as i32, @@ -68,12 +68,12 @@ async fn get_all(req: HttpRequest, auth: JwtAuth) -> HttpResponse { } #[post("/messages")] -async fn create(message: web::Json, auth: JwtAuth) -> HttpResponse { +async fn create(message: web::Json, auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) }; - match InsertMessage::insert(message.into_inner()) { + match QueryMessage::insert(message.into_inner()) { Ok(message) => HttpResponse::Created().json(message), Err(err) => { error!("{:?}", err.message); diff --git a/service/src/bot/mod.rs b/service/src/bot/mod.rs index 5e4fa6a..baed6f3 100644 --- a/service/src/bot/mod.rs +++ b/service/src/bot/mod.rs @@ -1,3 +1,6 @@ -pub mod api; pub mod commands; +pub mod guilds; pub mod handler; +pub mod messages; +pub mod oai; +pub mod ytdlp; diff --git a/service/src/bot/oai/mod.rs b/service/src/bot/oai/mod.rs new file mode 100644 index 0000000..4a7ebf6 --- /dev/null +++ b/service/src/bot/oai/mod.rs @@ -0,0 +1,3 @@ +mod model; + +pub use model::*; diff --git a/service/src/bot/oai/model.rs b/service/src/bot/oai/model.rs new file mode 100644 index 0000000..2f42644 --- /dev/null +++ b/service/src/bot/oai/model.rs @@ -0,0 +1,128 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value; +use siren::ServiceError; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum GPTRole { + #[serde(rename = "system")] + System, + #[serde(rename = "user")] + User, + #[serde(rename = "assistant")] + Assistant, + #[serde(rename = "function")] + Function +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionRequest { + pub model: String, + pub messages: Vec, + /// Value between 0 and 2 + #[serde(skip_serializing_if = "Option::is_none")] + pub temperature: Option, + /// Value between 0 and 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub n: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_tokens: Option, + /// Value between -2.0 and 2.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub presence_penalty: Option, + /// Value between -2.0 and 2.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub frequency_penalty: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionMessage { + pub role: GPTRole, + pub content: String +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionResponse { + pub id: String, + pub object: String, + pub system_fingerprint: Option, + pub created: i64, + pub model: String, + pub usage: Usage, + pub choices: Vec +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Usage { + pub prompt_tokens: i64, + pub completion_tokens: i64, + pub total_tokens: i64 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Choice { + pub message: ChatCompletionMessage, + pub finish_reason: String, + pub index: i64, + pub logprobs: Option +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +enum ResponseEvent { + ChatCompletionResponse(ChatCompletionResponse), + ResponseError(ResponseError) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ResponseError { + error: Option, + message: Option, + param: Option, + #[serde(rename = "type")] + error_type: Option +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ErrorDetails { + code: Option, + message: Option +} + +pub struct OAI { + pub client: reqwest::Client, + pub base_url: String, + pub service_url: String, + pub max_attempts: i64, + pub token: String, + pub max_tokens: i64, + pub default_model: String, + pub max_context_questions: i64 +} + +impl OAI { + pub async fn chat_completion(&self, request: ChatCompletionRequest) -> Result { + let url = format!("{}/chat/completions", self.base_url); + let response = self.client.post(&url) + .bearer_auth(&self.token) + .header("Content-Type", "application/json".to_string()) + .json(&request) + .send() + .await; + match response { + Ok(response) => { + let value = response.json::().await?; + // let event: ResponseEvent = serde_json::from_value::(value)?; + // match event { + // ResponseEvent::ChatCompletionResponse(response) => return Ok(response), + // ResponseEvent::ResponseError(error) => return Err(ServiceError { status: 500, message: format!("Error: {}", error.message.unwrap()) }) + // } + let res = serde_json::from_value::(value)?; + return Ok(res); + }, + Err(err) => return Err(ServiceError { status: 500, message: format!("Error: {}", err) }) + } + } +} diff --git a/service/src/bot/ytdlp/mod.rs b/service/src/bot/ytdlp/mod.rs new file mode 100644 index 0000000..e544a7a --- /dev/null +++ b/service/src/bot/ytdlp/mod.rs @@ -0,0 +1,39 @@ +mod model; + +use std::process::{Child, Command, Output, Stdio}; + +pub use model::*; + + +const YOUTUBE_DL_COMMAND: &str = "yt-dlp"; + +pub struct YtDlp { + command: Command, + args: Vec, +} + +impl YtDlp { + pub fn new() -> Self { + let mut cmd = Command::new(YOUTUBE_DL_COMMAND); + cmd.env("LC_ALL", "en_US.UTF-8") + .stdout(Stdio::piped()) + .stdin(Stdio::piped()) + .stderr(Stdio::piped()); + Self { + command: cmd, + args: Vec::new(), + } + } + + pub fn arg(&mut self, arg: &str) -> &mut Self { + self.args.push(arg.to_owned()); + self + } + + pub fn execute(&mut self) -> std::io::Result { + self.command + .args(self.args.clone()) + .spawn() + .and_then(Child::wait_with_output) + } +} \ No newline at end of file diff --git a/service/src/bot/ytdlp/model.rs b/service/src/bot/ytdlp/model.rs new file mode 100644 index 0000000..1d7ced9 --- /dev/null +++ b/service/src/bot/ytdlp/model.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + + +#[derive(Debug, Serialize, Deserialize)] +pub struct PlaylistItem { + pub id: String, + pub url: String, + pub title: String, + pub duration: i32, + pub playlist_index: i32, +} \ No newline at end of file diff --git a/service/src/db/guilds/mod.rs b/service/src/db/guilds/mod.rs deleted file mode 100644 index 24e3024..0000000 --- a/service/src/db/guilds/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod model; - -pub use model::*; \ No newline at end of file diff --git a/service/src/db/mod.rs b/service/src/db/mod.rs deleted file mode 100644 index 96b1ac4..0000000 --- a/service/src/db/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -use diesel::{r2d2::ConnectionManager as DieselConnectionManager, PgConnection}; -// use redis::{aio::{Connection as RedisConnection, ConnectionManager as RedisConnectionManager}, AsyncCommands}; -use redis::aio::Connection as RedisConnection; -use siren::ServiceError; -use crate::diesel_migrations::MigrationHarness; -use lazy_static::lazy_static; -use log::{error, info}; -use r2d2; -use std::env; - -pub mod backgrounds; -pub mod bestiary; -pub mod classes; -pub mod conditions; -pub mod feats; -pub mod guilds; -pub mod items; -pub mod messages; -pub mod options; -pub mod races; -pub mod spells; -pub mod schema; - -type DbPool = r2d2::Pool>; -pub type DbConnection = r2d2::PooledConnection>; -// type RedisPool = r2d2::Pool; - -pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = embed_migrations!(); - -lazy_static! { - static ref POOL: DbPool = { - let username = env::var("DATABASE_USER").expect("DATABASE_USERNAME is not set"); - let password = env::var("DATABASE_PASSWORD").expect("DATABASE_PASSWORD is not set"); - let host = env::var("DATABASE_HOST").unwrap_or("localhost".to_string()); - let name = env::var("DATABASE_NAME").expect("DATABASE_NAME is not set"); - let port = env::var("DATABASE_PORT").unwrap_or("5432".to_string()); - let url = format!("postgres://{}:{}@{}:{}/{}", username, password, host, port, name); - let manager = DieselConnectionManager::::new(url); - DbPool::builder().test_on_check_out(true).build(manager).expect("Failed to create db pool") - }; - // static ref REDIS_POOL: RedisPool = { - // let host = env::var("REDIS_HOST").unwrap_or("localhost".to_string()); - // let port = env::var("REDIS_PORT").unwrap_or("6379".to_string()); - // let url = format!("redis://{}:{}", host, port); - // let client = redis::Client::open(url).expect("Failed to create redis client"); - // let manager = RedisConnectionManager::new(client); - // "".to_string() - // }; -} - -pub fn init() { - lazy_static::initialize(&POOL); - let mut pool: DbConnection = connection().expect("Failed to get db connection"); - match pool.run_pending_migrations(MIGRATIONS) { - Ok(_) => info!("Database initialized"), - Err(err) => error!("Failed to initialize database; {}", err) - }; -} - -pub fn connection() -> Result { - POOL.get() - .map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e))) -} - -pub fn redis_client() -> Result { - let host = env::var("REDIS_HOST").unwrap_or("localhost".to_string()); - let port = env::var("REDIS_PORT").unwrap_or("6379".to_string()); - let url = format!("redis://{}:{}", host, port); - let client = redis::Client::open(url)?; - Ok(client) -} - -pub fn redis_connection() -> Result { - let client = redis_client()?; - let conn = client.get_connection()?; - Ok(conn) -} - -pub async fn redis_async_connection() -> Result { - let client = redis_client()?; - let conn = client.get_async_connection().await?; - Ok(conn) -} - -pub fn load_data(data_dir_path: &str) { - spells::load_data(data_dir_path); -} diff --git a/service/src/db/backgrounds/mod.rs b/service/src/dnd/backgrounds/mod.rs similarity index 100% rename from service/src/db/backgrounds/mod.rs rename to service/src/dnd/backgrounds/mod.rs diff --git a/service/src/db/bestiary/mod.rs b/service/src/dnd/bestiary/mod.rs similarity index 100% rename from service/src/db/bestiary/mod.rs rename to service/src/dnd/bestiary/mod.rs diff --git a/service/src/db/feats/mod.rs b/service/src/dnd/campaigns/mod.rs similarity index 100% rename from service/src/db/feats/mod.rs rename to service/src/dnd/campaigns/mod.rs diff --git a/service/src/db/items/mod.rs b/service/src/dnd/characters/mod.rs similarity index 100% rename from service/src/db/items/mod.rs rename to service/src/dnd/characters/mod.rs diff --git a/service/src/db/classes/mod.rs b/service/src/dnd/classes/mod.rs similarity index 100% rename from service/src/db/classes/mod.rs rename to service/src/dnd/classes/mod.rs diff --git a/service/src/db/classes/model.rs b/service/src/dnd/classes/model.rs similarity index 100% rename from service/src/db/classes/model.rs rename to service/src/dnd/classes/model.rs diff --git a/service/src/db/conditions/mod.rs b/service/src/dnd/conditions/mod.rs similarity index 100% rename from service/src/db/conditions/mod.rs rename to service/src/dnd/conditions/mod.rs diff --git a/service/src/db/options/mod.rs b/service/src/dnd/feats/mod.rs similarity index 100% rename from service/src/db/options/mod.rs rename to service/src/dnd/feats/mod.rs diff --git a/service/src/db/races/mod.rs b/service/src/dnd/items/mod.rs similarity index 100% rename from service/src/db/races/mod.rs rename to service/src/dnd/items/mod.rs diff --git a/service/src/dnd/mod.rs b/service/src/dnd/mod.rs index e69de29..d8a2b34 100644 --- a/service/src/dnd/mod.rs +++ b/service/src/dnd/mod.rs @@ -0,0 +1,13 @@ +pub mod backgrounds; +pub mod bestiary; +pub mod classes; +pub mod conditions; +pub mod feats; +pub mod items; +pub mod options; +pub mod races; +pub mod spells; + +pub fn load_data(data_dir_path: &str) { + spells::load_data(data_dir_path); +} \ No newline at end of file diff --git a/service/src/bot/api/model.rs b/service/src/dnd/options/mod.rs similarity index 100% rename from service/src/bot/api/model.rs rename to service/src/dnd/options/mod.rs diff --git a/service/src/bot/commands/message.rs b/service/src/dnd/races/mod.rs similarity index 100% rename from service/src/bot/commands/message.rs rename to service/src/dnd/races/mod.rs diff --git a/service/src/db/spells/mod.rs b/service/src/dnd/spells/mod.rs similarity index 100% rename from service/src/db/spells/mod.rs rename to service/src/dnd/spells/mod.rs diff --git a/service/src/db/spells/model.rs b/service/src/dnd/spells/model.rs similarity index 96% rename from service/src/db/spells/model.rs rename to service/src/dnd/spells/model.rs index a2d460b..9604178 100644 --- a/service/src/db/spells/model.rs +++ b/service/src/dnd/spells/model.rs @@ -2,7 +2,9 @@ use diesel::prelude::*; use serde::{Deserialize, Serialize}; use siren::ServiceError; -use crate::db::{schema::spells::{self}, classes::AbilityType, conditions::ConditionType}; +use crate::storage::connection; +use crate::storage::schema::spells::{self}; +use crate::dnd::{classes::AbilityType, conditions::ConditionType}; use super::{SchoolType, CastingTime, SpellAttackType, SpellDamageType, Range, Area, Components, Duration, Source, Description, DurationType, Effect}; @@ -61,7 +63,7 @@ impl Default for QueryFilters { impl QuerySpell { pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result, ServiceError> { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let mut query = spells::table.limit(limit as i64).into_boxed(); // Limit query to page and limit let offset = (page - 1) * limit; @@ -108,7 +110,7 @@ impl QuerySpell { } pub fn get_count(filters: &QueryFilters) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let mut query = spells::table.count().into_boxed(); if let Some(name) = &filters.by_name { query = query.filter(spells::name.ilike(format!("%{}%", name))); @@ -149,7 +151,7 @@ impl QuerySpell { } pub fn get_by_id(id: i32) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let spell = spells::table .filter(spells::id.eq(id)) .first::(&mut conn)?; @@ -157,7 +159,7 @@ impl QuerySpell { } pub fn delete(id: i32) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let spell = diesel::delete(spells::table.filter(spells::id.eq(id))).get_result(&mut conn)?; Ok(spell) } @@ -182,13 +184,13 @@ pub struct InsertSpell { impl InsertSpell { pub fn insert(spell: Self) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let spell = diesel::insert_into(spells::table).values(spell).get_result(&mut conn)?; Ok(spell) } pub fn update(id: i32, spell: Self) -> Result { - let mut conn = crate::db::connection()?; + let mut conn = connection()?; let spell = diesel::update(spells::table.filter(spells::id.eq(id))).set(spell).get_result(&mut conn)?; Ok(spell) } diff --git a/service/src/db/spells/routes.rs b/service/src/dnd/spells/routes.rs similarity index 96% rename from service/src/db/spells/routes.rs rename to service/src/dnd/spells/routes.rs index 4b95e90..095028f 100644 --- a/service/src/db/spells/routes.rs +++ b/service/src/dnd/spells/routes.rs @@ -1,9 +1,9 @@ use actix_web::{get, post, put, delete, web, HttpResponse, HttpRequest, ResponseError}; use log::error; use serde::{Serialize, Deserialize}; -use siren::{GetResponse, Metadata, ServiceError}; +use siren::{Response, Metadata, ServiceError}; -use crate::{db::spells::{QuerySpell, QueryFilters}, auth::{JwtAuth, verify_role}}; +use crate::{dnd::spells::{QuerySpell, QueryFilters}, auth::{JwtAuth, verify_role}}; use super::{Spell, InsertSpell}; @@ -90,7 +90,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse { spell.id = Some(id); response.push(spell); } - HttpResponse::Ok().json(GetResponse { + HttpResponse::Ok().json(Response { data: response, metadata: Some(Metadata { total: total_count as i32, @@ -121,7 +121,7 @@ async fn get_by_id(id: web::Path) -> HttpResponse { let id = query_spell.id; let mut spell = Spell::from(query_spell); spell.id = Some(id); - HttpResponse::Ok().json(GetResponse { + HttpResponse::Ok().json(Response { data: spell, metadata: None }) diff --git a/service/src/db/spells/types.rs b/service/src/dnd/spells/types.rs similarity index 100% rename from service/src/db/spells/types.rs rename to service/src/dnd/spells/types.rs diff --git a/service/src/lib.rs b/service/src/lib.rs index 67645ec..eb30dbd 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -19,7 +19,7 @@ pub struct Message { } #[derive(Serialize, Deserialize)] -pub struct GetResponse { +pub struct Response { pub data: T, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option @@ -54,6 +54,18 @@ impl fmt::Display for ServiceError { } } +impl From for ServiceError { + fn from(error: std::io::Error) -> ServiceError { + ServiceError::new(500, format!("Unknown io error: {}", error)) + } +} + +impl From for ServiceError { + fn from(error: std::string::FromUtf8Error) -> ServiceError { + ServiceError::new(500, format!("Unknown from utf8 error: {}", error)) + } +} + impl From for ServiceError { fn from(error: DieselError) -> ServiceError { match error { @@ -112,6 +124,23 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(error: s3::error::S3Error) -> ServiceError { + match error { + s3::error::S3Error::Http(code, message) => { + ServiceError::new(code, message) + }, + _ => ServiceError::new(500, format!("Unknown s3 error: {}", error)) + } + } +} + +impl From for ServiceError { + fn from(error: s3::creds::error::CredentialsError) -> ServiceError { + ServiceError::new(500, format!("Unknown credentials error: {}", error)) + } +} + impl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { let status_code = match StatusCode::from_u16(self.status) { diff --git a/service/src/main.rs b/service/src/main.rs index 4322e3f..8210907 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -14,22 +14,23 @@ use songbird::{SerenityInit, Songbird}; use actix_cors::Cors; use actix_web::{HttpServer, App, web}; -use crate::bot::{commands::oai::GPTModel, handler::Handler}; +use crate::bot::handler::Handler; use dotenv::dotenv; mod auth; mod dnd; mod bot; -mod db; +mod storage; +mod users; #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,siren=info")); - db::init(); + storage::init().await; match env::var("DATA_DIR_PATH") { - Ok(data_dir_path) => db::load_data(&data_dir_path), + Ok(data_dir_path) => dnd::load_data(&data_dir_path), Err(err) => warn!("Unable to load initial database data: {}", err) }; @@ -56,8 +57,9 @@ async fn main() -> std::io::Result<()> { let handler = match env::var("OPENAI_API_KEY") { Ok(token) => { info!("Loaded OpenAI token"); + let default_model = env::var("OPENAI_API_MODEL").unwrap_or("gpt-3.5-turbo".to_string()); Handler { - oai: Some(bot::commands::oai::OAI { + oai: Some(bot::oai::OAI { client: reqwest::Client::new(), base_url: "https://api.openai.com/v1".to_string(), service_url: "http://localhost:5000".to_string(), @@ -65,7 +67,7 @@ async fn main() -> std::io::Result<()> { token, max_context_questions: 30, max_tokens: 2048, - default_model: GPTModel::GPT35Turbo, + default_model, }) } } @@ -97,16 +99,16 @@ async fn main() -> std::io::Result<()> { let shard_manager = Arc::clone(&client.shard_manager); - // tokio::spawn(async move { - // tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler"); - // shard_manager.lock().await.shutdown_all().await; - // }); + tokio::spawn(async move { + tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler"); + shard_manager.lock().await.shutdown_all().await; + }); - // tokio::spawn(async move { - // if let Err(why) = client.start_autosharded().await { - // error!("An error occurred while running the client: {:?}", why); - // } - // }); + tokio::spawn(async move { + if let Err(why) = client.start_autosharded().await { + error!("An error occurred while running the client: {:?}", why); + } + }); let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string()); let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string()); @@ -121,10 +123,11 @@ async fn main() -> std::io::Result<()> { App::new() .wrap(cors) .app_data(web::Data::new(Arc::clone(&app_data))) - .configure(crate::db::messages::init_routes) - .configure(crate::db::spells::init_routes) .configure(crate::auth::init_routes) - .configure(crate::bot::api::init_routes) + .configure(crate::users::init_routes) + .configure(crate::dnd::spells::init_routes) + .configure(crate::bot::guilds::init_routes) + .configure(crate::bot::messages::init_routes) }) .bind(format!("{}:{}", host, port)) { Ok(b) => { diff --git a/service/src/storage/mod.rs b/service/src/storage/mod.rs new file mode 100644 index 0000000..404625e --- /dev/null +++ b/service/src/storage/mod.rs @@ -0,0 +1,122 @@ +use diesel::{r2d2::ConnectionManager as DieselConnectionManager, PgConnection}; +use redis::{Client as RedisClient, aio::Connection as RedisConnection}; +use s3::{Region, creds::Credentials, Bucket, BucketConfiguration, request::ResponseData}; +use siren::ServiceError; +use crate::diesel_migrations::MigrationHarness; +use lazy_static::lazy_static; +use log::{error, info}; +use r2d2; +use std::env; + +pub mod schema; + +type DbPool = r2d2::Pool>; +pub type DbConnection = r2d2::PooledConnection>; + +pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = embed_migrations!(); + +lazy_static! { + static ref POOL: DbPool = { + let username = env::var("DATABASE_USER").expect("DATABASE_USERNAME is not set"); + let password = env::var("DATABASE_PASSWORD").expect("DATABASE_PASSWORD is not set"); + let host = env::var("DATABASE_HOST").unwrap_or("localhost".to_string()); + let name = env::var("DATABASE_NAME").expect("DATABASE_NAME is not set"); + let port = env::var("DATABASE_PORT").unwrap_or("5432".to_string()); + let url = format!("postgres://{}:{}@{}:{}/{}", username, password, host, port, name); + let manager = DieselConnectionManager::::new(url); + DbPool::builder().test_on_check_out(true).build(manager).expect("Failed to create db pool") + }; + static ref REDIS: RedisClient = { + let host = env::var("REDIS_HOST").unwrap_or("localhost".to_string()); + let port = env::var("REDIS_PORT").unwrap_or("6379".to_string()); + let url = format!("redis://{}:{}", host, port); + RedisClient::open(url).expect("Failed to create redis client") + }; + static ref BUCKET: Bucket = { + let url = env::var("MINIO_HOST").unwrap_or("localhost".to_string()); + let port = env::var("MINIO_PORT").unwrap_or("9000".to_string()); + let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set"); + let password = env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set"); + let base_url = format!("http://{}:{}", url, port); + + let region = Region::Custom { + region: "".to_string(), + endpoint: base_url, + }; + + let credentials = Credentials { + access_key: Some(user), + secret_key: Some(password), + security_token: None, + session_token: None, + expiration: None + }; + + Bucket::new("siren", region.clone(), credentials.clone()).expect("Failed to create S3 Bucket").with_path_style() + }; +} + +pub async fn init() { + lazy_static::initialize(&POOL); + lazy_static::initialize(&REDIS); + lazy_static::initialize(&BUCKET); + create_bucket().await; + let mut pool: DbConnection = connection().expect("Failed to get db connection"); + match pool.run_pending_migrations(MIGRATIONS) { + Ok(_) => info!("Database initialized"), + Err(err) => error!("Failed to initialize database; {}", err) + }; +} + +pub fn connection() -> Result { + POOL.get() + .map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e))) +} + +pub fn redis_connection() -> Result { + let conn = REDIS.get_connection()?; + Ok(conn) +} + +pub async fn redis_async_connection() -> Result { + let conn = REDIS.get_async_connection().await?; + Ok(conn) +} + +async fn create_bucket() { + let url = env::var("MINIO_URL").unwrap_or("localhost".to_string()); + let port = env::var("MINIO_PORT").unwrap_or("9000".to_string()); + let user = env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set"); + let password = env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set"); + let base_url = format!("http://{}:{}", url, port); + + let region = Region::Custom { + region: "".to_string(), + endpoint: base_url, + }; + + let credentials = Credentials { + access_key: Some(user), + secret_key: Some(password), + security_token: None, + session_token: None, + expiration: None + }; + let _ = Bucket::create_with_path_style("siren", region, credentials, BucketConfiguration::default()).await; +} + +pub async fn upload_file(path: &str, content: &[u8]) -> Result { + let response = BUCKET.put_object(path, content).await?; + Ok(response) +} + +pub async fn get_file(path: &str) -> Result, ServiceError> { + let response = BUCKET.get_object(path).await?; + let bytes = response.bytes(); + Ok(bytes.to_vec()) +} + +pub async fn delete_file(path: &str) -> Result { + let response = BUCKET.delete_object(path).await?; + Ok(response) +} diff --git a/service/src/db/schema.rs b/service/src/storage/schema.rs similarity index 90% rename from service/src/db/schema.rs rename to service/src/storage/schema.rs index 3e6f3e5..1a64dad 100644 --- a/service/src/db/schema.rs +++ b/service/src/storage/schema.rs @@ -46,6 +46,9 @@ diesel::table! { role -> Text, first_name -> Text, last_name -> Text, + updated_at -> Timestamp, + created_at -> Timestamp, + profile_picture -> Nullable, verified -> Bool, } } \ No newline at end of file diff --git a/service/src/users/mod.rs b/service/src/users/mod.rs new file mode 100644 index 0000000..1688bdf --- /dev/null +++ b/service/src/users/mod.rs @@ -0,0 +1,3 @@ +mod routes; + +pub use routes::init_routes; \ No newline at end of file diff --git a/service/src/users/routes.rs b/service/src/users/routes.rs new file mode 100644 index 0000000..0e4babd --- /dev/null +++ b/service/src/users/routes.rs @@ -0,0 +1,136 @@ +use actix_multipart::Multipart; +use actix_web::{web, HttpResponse, post, delete, get, ResponseError}; +use log::error; +use serenity::futures::StreamExt; +use siren::ServiceError; + +use crate::{auth::{JwtAuth, InsertUser, QueryUser}, storage::{upload_file, get_file, delete_file}}; + +#[post("/picture")] +async fn set_picture(mut payload: Multipart, auth: JwtAuth) -> HttpResponse { + while let Some(item) = payload.next().await { + let mut bytes = web::BytesMut::new(); + let mut field = match item { + Ok(field) => field, + Err(err) => return ResponseError::error_response(&err) + }; + let content_type = field.content_disposition(); + // Get file name and construct the file path + let file_name = match content_type.get_filename() { + Some(name) => { + // Verify extension is supported + match name.split(".").last() { + Some(ext) => { + match ext { + "png" | "jpg" | "jpeg" => name, + _ => return ResponseError::error_response(&ServiceError { + status: 400, + message: "File extension is not supported".to_string() + }) + } + }, + None => return ResponseError::error_response(&ServiceError { + status: 400, + message: "Unknown file extension".to_string() + }) + } + }, + None => return ResponseError::error_response(&ServiceError { + status: 400, + message: "File name is not provided".to_string() + }) + }; + let path = format!("users/{}/{}", auth.user.email, file_name); + + // Build the file and store it in minio + while let Some(chunk) = field.next().await { + let data = match chunk { + Ok(data) => data, + Err(err) => { + error!("Failed to get chunk: {}", err); + return ResponseError::error_response(&err); + } + }; + bytes.extend_from_slice(&data); + } + match upload_file(&path, &bytes).await { + Ok(_) => { + match InsertUser::update_profile(&auth.user.email, Some(&path)) { + Ok(_) => {} + Err(err) => { + error!("Failed to update user profile: {}", err); + return ResponseError::error_response(&err); + } + }; + }, + Err(err) => { + error!("Failed to upload file: {}", err); + return ResponseError::error_response(&err); + } + } + }; + return HttpResponse::Ok().finish(); +} + +#[get("/picture")] +async fn get_picture(auth: JwtAuth) -> HttpResponse { + let user = match QueryUser::get_by_email(&auth.user.email) { + Ok(user) => user, + Err(err) => { + error!("Failed to get user: {}", err); + return ResponseError::error_response(&err); + } + }; + if let Some(path) = user.profile_picture { + match get_file(&path).await { + Ok(bytes) => return HttpResponse::Ok().body(bytes), + Err(err) => { + error!("Failed to get file: {}", err); + return ResponseError::error_response(&err); + } + } + } else { + return HttpResponse::NotFound().finish(); + } +} + +#[delete("/picture")] +async fn delete_picture(auth: JwtAuth) -> HttpResponse { + match QueryUser::get_by_email(&auth.user.email) { + Ok(user) => { + match user.profile_picture { + Some(path) => { + match delete_file(&path).await { + Ok(_) => { + match InsertUser::update_profile(&auth.user.email, None) { + Ok(_) => {} + Err(err) => { + error!("Failed to update user profile: {}", err); + return ResponseError::error_response(&err); + } + }; + } + Err(err) => { + error!("Failed to delete file: {}", err); + return ResponseError::error_response(&err); + } + }; + }, + None => {} + } + }, + Err(err) => { + error!("Failed to get user: {}", err); + return ResponseError::error_response(&err); + } + }; + return HttpResponse::Ok().finish(); +} + +pub fn init_routes(config: &mut web::ServiceConfig) { + config.service(web::scope("users") + .service(set_picture) + .service(get_picture) + .service(delete_picture) + ); +} \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index c36e38f..ec51dc8 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,36 +8,35 @@ "name": "siren-ui", "version": "0.1.0", "dependencies": { - "@mantine/core": "^7.1.2", - "@mantine/form": "^7.1.2", - "@mantine/hooks": "^7.1.2", - "@mantine/modals": "^7.1.2", - "@mantine/notifications": "^7.1.2", - "axios": "^1.5.1", + "@mantine/core": "^7.2.2", + "@mantine/form": "^7.2.2", + "@mantine/hooks": "^7.2.2", + "@mantine/modals": "^7.2.2", + "@mantine/notifications": "^7.2.2", + "@pixi/react": "^7.1.1", "js-cookie": "^3.0.5", - "next": "^13.5.4", + "next": "^14.0.3", + "pixi.js": "^7.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^4.11.0", - "react-leaflet": "^4.2.1", - "recharts": "^2.8.0", + "react-icons": "^4.12.0", "recoil": "^0.7.7" }, "devDependencies": { - "@types/js-cookie": "^3.0.4", - "@types/node": "20.8.2", - "@types/react": "18.2.24", - "@types/react-dom": "18.2.8", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", + "@types/js-cookie": "^3.0.5", + "@types/node": "20.8.7", + "@types/react": "18.2.31", + "@types/react-dom": "18.2.14", + "@typescript-eslint/eslint-plugin": "^6.8.0", + "@typescript-eslint/parser": "^6.8.0", "autoprefixer": "^10.4.16", - "eslint": "8.50.0", - "eslint-config-next": "13.5.4", + "eslint": "8.52.0", + "eslint-config-next": "13.5.6", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", - "postcss-preset-mantine": "^1.8.0", + "postcss-preset-mantine": "^1.9.0", "prettier": "^3.0.3", "typescript": "5.2.2" } @@ -110,9 +109,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -167,12 +166,12 @@ "integrity": "sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==" }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -194,15 +193,15 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@mantine/core": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.1.2.tgz", - "integrity": "sha512-EZg82V/+uA2bM981mEUOUGfqKIRsMfvxLdAPQpurhtqsnq4yBj1xjC3KzX/Eas9QhhHuR+4DJJyGTuO9aOK6nQ==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.2.2.tgz", + "integrity": "sha512-cVGmLjYyKIzjn0LRwamj71O4HT43qCxPGAzMZqkMYGOako7xwBLYQNe7HjL/J0FXJDyfX8OdMylVhgsePWYbng==", "dependencies": { "@floating-ui/react": "^0.24.8", "clsx": "2.0.0", @@ -212,7 +211,7 @@ "type-fest": "^3.13.1" }, "peerDependencies": { - "@mantine/hooks": "7.1.2", + "@mantine/hooks": "7.2.2", "react": "^18.2.0", "react-dom": "^18.2.0" } @@ -229,9 +228,9 @@ } }, "node_modules/@mantine/form": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.1.2.tgz", - "integrity": "sha512-FnUu5XNmRM265G0wy19qSRiItG/2eQ0GQCctnokw6ws9ZnCU1NqvsmpuDE/UiV4YCAOhAVHfqnjG/8tsrlw7ug==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.2.2.tgz", + "integrity": "sha512-Tzux6WbUliK6id1I7lV8tmu6je0WFrJLPDg+0xVyW7Qk/zmju837kj7SSfvNcJa51dKAtYxf3FgqTRy6ap9QEA==", "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.5" @@ -241,35 +240,35 @@ } }, "node_modules/@mantine/hooks": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.1.2.tgz", - "integrity": "sha512-2sqfBKse/aJq93zEpIn4OY+jRACmDIWBixfBgobRfltDyeL8G+3223LSAaeT6ZD8+h2YBJVmbCD5QY7bx2l11Q==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.2.2.tgz", + "integrity": "sha512-7CFSVP2aQHrBwLLAVf0q5dgj+6QTZmhLTNcuc3pE1du+HLFUdyVS6vvQC6kieZXxOd6UzwFGBlN4G+aDx95XeA==", "peerDependencies": { "react": "^18.2.0" } }, "node_modules/@mantine/modals": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.1.2.tgz", - "integrity": "sha512-5OOSUzWpnYwnmLILfA8TAmnXogWpzu4Z8v0V+NiiQb2lFADZTl7qnBmw4BbAoT9mwWo3sXlCvepj5bGjsl7pMg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.2.2.tgz", + "integrity": "sha512-J65xJ5ZULUG8fvNU+UnHboqEs3ueNDTMITUiVucqw4lupt93JHfamuR/PIV2mrXMYqSaKd4NBnxvkmcpjs0uRg==", "peerDependencies": { - "@mantine/core": "7.1.2", - "@mantine/hooks": "7.1.2", + "@mantine/core": "7.2.2", + "@mantine/hooks": "7.2.2", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/notifications": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.1.2.tgz", - "integrity": "sha512-aakf3KRGOnfh+qxGGH/B0ifS5myFi3xO2S0AKD6t//sbQrrUW+SUKh0qyuatnKIx7dxf+DA/sMobyqLUgyzAmg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.2.2.tgz", + "integrity": "sha512-2MQ0jJi9HOspJOAq0sfE5QvacZytYoLB8k0GwnubnFilSACbpDYeoQXZWmKX0joYhHtKR15muTnF+P1s9kmr3Q==", "dependencies": { - "@mantine/store": "7.1.2", + "@mantine/store": "7.2.2", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "7.1.2", - "@mantine/hooks": "7.1.2", + "@mantine/core": "7.2.2", + "@mantine/hooks": "7.2.2", "react": "^18.2.0", "react-dom": "^18.2.0" } @@ -299,31 +298,31 @@ } }, "node_modules/@mantine/store": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.1.2.tgz", - "integrity": "sha512-Lf3FLymM0q92BuRC4tZxTxrb9EjVa+J8fqEV147u/Q3aUSNmkhJCqN2MXPbTHIBJ2PsbLtDhy/2edNyIK1KhKQ==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.2.2.tgz", + "integrity": "sha512-j8vR8whB332pGI8OXkM2/3rdh4LJxELJQTG+rTsCCd3VQuEJiRTQDKVaOvp9ONja3GU5b1Tk3zhKOrkpgxTJBA==", "peerDependencies": { "react": "^18.2.0" } }, "node_modules/@next/env": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz", - "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==" + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz", + "integrity": "sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA==" }, "node_modules/@next/eslint-plugin-next": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.5.4.tgz", - "integrity": "sha512-vI94U+D7RNgX6XypSyjeFrOzxGlZyxOplU0dVE5norIfZGn/LDjJYPHdvdsR5vN1eRtl6PDAsOHmycFEOljK5A==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.5.6.tgz", + "integrity": "sha512-ng7pU/DDsxPgT6ZPvuprxrkeew3XaRf4LAT4FabaEO/hAbvVx4P7wqnqdbTdDn1kgTvsI4tpIgT4Awn/m0bGbg==", "dev": true, "dependencies": { "glob": "7.1.7" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz", - "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz", + "integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==", "cpu": [ "arm64" ], @@ -336,9 +335,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz", - "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz", + "integrity": "sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==", "cpu": [ "x64" ], @@ -351,9 +350,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz", - "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz", + "integrity": "sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==", "cpu": [ "arm64" ], @@ -366,9 +365,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz", - "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz", + "integrity": "sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==", "cpu": [ "arm64" ], @@ -381,9 +380,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz", - "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.3.tgz", + "integrity": "sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg==", "cpu": [ "x64" ], @@ -396,9 +395,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz", - "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.3.tgz", + "integrity": "sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg==", "cpu": [ "x64" ], @@ -411,9 +410,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz", - "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz", + "integrity": "sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==", "cpu": [ "arm64" ], @@ -426,9 +425,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz", - "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz", + "integrity": "sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==", "cpu": [ "ia32" ], @@ -441,9 +440,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz", - "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz", + "integrity": "sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==", "cpu": [ "x64" ], @@ -490,6 +489,384 @@ "node": ">= 8" } }, + "node_modules/@pixi/accessibility": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.3.2.tgz", + "integrity": "sha512-MdkU22HTauRvq9cMeWZIQGaDDa86sr+m12rKNdLV+FaDQgP/AhP+qCVpK7IKeJa9BrWGXaYMw/vueij7HkyDSA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/events": "7.3.2" + } + }, + "node_modules/@pixi/app": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.3.2.tgz", + "integrity": "sha512-3YRFSMvAxDebAz3/JJv+2jzbPkT8cHC0IHmmLRN8krDL1pZV+YjMLgMwN/Oeyv5TSbwNqnrF5su5whNkRaxeZQ==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/assets": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/assets/-/assets-7.3.2.tgz", + "integrity": "sha512-yteq6ptAxA09EcwU9D9hl7qr5yWIqy+c2PsXkTDkc76vTAwIamLY3KxLq2aR5y1U4L4O6aHFJd26uNhHcuTPmw==", + "dependencies": { + "@types/css-font-loading-module": "^0.0.7" + }, + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/utils": "7.3.2" + } + }, + "node_modules/@pixi/color": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.3.2.tgz", + "integrity": "sha512-jur5PvdOtUBEUTjmPudW5qdQq6yYGlVGsi3HyhasJw14bN+GKJwiCKgIsyrsiNL5HBUXmje4ICwQohf6BqKqxA==", + "dependencies": { + "@pixi/colord": "^2.9.6" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==" + }, + "node_modules/@pixi/compressed-textures": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-7.3.2.tgz", + "integrity": "sha512-J3ENMHDPQO6CJRei55gqI0WmiZJIK6SgsW5AEkShT0aAe5miEBSomv70pXw/58ru+4/Hx8cXjamsGt4aQB2D0Q==", + "peerDependencies": { + "@pixi/assets": "7.3.2", + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/constants": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.3.2.tgz", + "integrity": "sha512-Q8W3ncsFxmfgC5EtokpG92qJZabd+Dl+pbQAdHwiPY3v+8UNq77u4VN2qtl1Z04864hCcg7AStIYEDrzqTLF6Q==" + }, + "node_modules/@pixi/core": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.3.2.tgz", + "integrity": "sha512-Pta3ee8MtJ3yKxGXzglBWgwbEOKMB6Eth+FpLTjL0rgxiqTB550YX6jsNEQQAzcGjCBlO3rC/IF57UZ2go/X6w==", + "dependencies": { + "@pixi/color": "7.3.2", + "@pixi/constants": "7.3.2", + "@pixi/extensions": "7.3.2", + "@pixi/math": "7.3.2", + "@pixi/runner": "7.3.2", + "@pixi/settings": "7.3.2", + "@pixi/ticker": "7.3.2", + "@pixi/utils": "7.3.2", + "@types/offscreencanvas": "^2019.6.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/@pixi/display": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/display/-/display-7.3.2.tgz", + "integrity": "sha512-cY5AnZ3TWt5GYGx4e5AQ2/2U9kP+RorBg/O30amJ+8e9bFk9rS8cjh/DDq/hc4lql96BkXAInTl40eHnAML5lQ==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/events": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/events/-/events-7.3.2.tgz", + "integrity": "sha512-Moca9epu8jk1wIQCdVYjhz2pD9Ol21m50wvWUKvpgt9yM/AjkCLSDt8HO/PmTpavDrkhx5pVVWeDDA6FyUNaGA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/extensions": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.3.2.tgz", + "integrity": "sha512-Qw84ADfvmVu4Mwj+zTik/IEEK9lWS5n4trbrpQCcEZ+Mb8oRAXWvKz199mi1s7+LaZXDqeCY1yr2PHQaFf1KBA==" + }, + "node_modules/@pixi/extract": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-7.3.2.tgz", + "integrity": "sha512-KsoflvQZV/XD8A8xbtRnmI4reYekbI4MOi7ilwQe5tMz6O1mO7IzrSukxkSMD02f6SpbAqbi7a1EayTjvY0ECQ==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-alpha": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.3.2.tgz", + "integrity": "sha512-nZMdn310wH5ZK1slwv3X4qT8eLoAGO7SgYGCy5IsMtpCtNObzE9XA4tAfhXrjihyzPS9KvszgAbnv1Qpfh0/uw==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-blur": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.3.2.tgz", + "integrity": "sha512-unu3zhwHMhN+iAe7Td2rK40i2UJ2GOhzWK+6jcU3ZkMOsFCT5kgBoMRTejeQVcvCs6GoYK8imbkE7mXt05Vj6A==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-color-matrix": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-7.3.2.tgz", + "integrity": "sha512-rbyjes/9SMoV9jjPiK0sLMkmLfN8D17GoTJIfq/KLv1x9646W5fL2QSKkN04UkZ+020ndWvIOxK1S97tvRyCfg==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-displacement": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-7.3.2.tgz", + "integrity": "sha512-ZHl7Sfb8JYd9Z6j96OHCC0NhMKhhXJRE5AbkSDohjEMVCK1BV5rDGAHV8WVt/2MJ/j83CXUpydzyMhdM4lMchg==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-fxaa": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-7.3.2.tgz", + "integrity": "sha512-9brtlxDnQTZk2XiFBKdBK9e+8CX9LdxxcL7LRpjEyiHuAPvTlQgu9B85LrJ4GzWKqJJKaIIZBzhIoiCLUnfeXg==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/filter-noise": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.3.2.tgz", + "integrity": "sha512-F8GQQ20n7tCjThX6GCXckiXz2YffOCxicTJ0oat9aVDZh+sVsAxYX0aKSdHh0hhv18F0yuc6tPsSL5DYb63xFg==", + "peerDependencies": { + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/graphics": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-7.3.2.tgz", + "integrity": "sha512-PhU6j1yub4tH/s+/gqByzgZ3mLv1mfb6iGXbquycg3+WypcxHZn0opFtI/axsazaQ9SEaWxw1m3i40WG5ANH5g==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/math": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.3.2.tgz", + "integrity": "sha512-dutoZ0IVJ5ME7UtYNo2szu4D7qsgtJB7e3ylujBVu7BOP2e710BVtFwFSFV768N14h9H5roGnuzVoDiJac2u+w==" + }, + "node_modules/@pixi/mesh": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-7.3.2.tgz", + "integrity": "sha512-LFkt7ELYXQLgbgHpjl68j6JD5ejUwma8zoPn2gqSBbY+6pK/phjvV1Wkh76muF46VvNulgXF0+qLIDdCsfrDaA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/mesh-extras": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-7.3.2.tgz", + "integrity": "sha512-s/tg9TsTZZxLEdCDKWnBChDGkc041HCTP7ykJv4fEROzb9B0lskULYyvv+/YNNKa2Ugb9WnkMknpOdOXCpjyyg==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/mesh": "7.3.2" + } + }, + "node_modules/@pixi/mixin-cache-as-bitmap": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-7.3.2.tgz", + "integrity": "sha512-bZRlyUN5+9kCUjn67V0IFtYIrbmx9Vs4sMOmXyrX3Q4B4gPLE46IzZz3v0IVaTjp32udlQztfJalIaWbuqgb3A==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/mixin-get-child-by-name": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-7.3.2.tgz", + "integrity": "sha512-mbUi3WxXrkViH7qOgjk4fu2BN36NwNb7u+Fy1J5dS8Bntj57ZVKmEV9PbUy0zYjXE8rVmeAvSu/2kbn5n9UutQ==", + "peerDependencies": { + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/mixin-get-global-position": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-7.3.2.tgz", + "integrity": "sha512-1nhWbBgmw6rK7yQJxzeI9yjKYYEkM5i3pee8qVu4YWo3b1xWVQA7osQG7aGM/4qywDkXaA1ZvciA5hfg6f4Q5Q==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/particle-container": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-7.3.2.tgz", + "integrity": "sha512-JYc4j4z97KmxyLp+1Lg0SNi8hy6RxcBBNQGk+CSLNXeDWxx3hykT5gj/ORX1eXyzHh1ZCG1XzeVS9Yr8QhlFHA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/prepare": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-7.3.2.tgz", + "integrity": "sha512-aLPAXSYLUhMwxzJtn9m0TSZe+dQlZCt09QNBqYbSi8LZId54QMDyvfBb4zBOJZrD2xAZgYL5RIJuKHwZtFX6lQ==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/graphics": "7.3.2", + "@pixi/text": "7.3.2" + } + }, + "node_modules/@pixi/react": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@pixi/react/-/react-7.1.1.tgz", + "integrity": "sha512-W3LILsYiUxavrLoDGiIuQneFgSzsUuHkt6VVuAaqUPEGjMBzicgA5v5R2dPDmu3B+5BRwAHcJZttUKH7zFjbGw==", + "dependencies": { + "lodash.isnil": "4.0.0", + "lodash.times": "4.3.2", + "performance-now": "2.1.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@babel/runtime": "^7.14.8", + "@pixi/app": ">=6.0.0", + "@pixi/constants": ">=6.0.0", + "@pixi/core": ">=6.0.0", + "@pixi/display": ">=6.0.0", + "@pixi/extensions": ">=6.0.0", + "@pixi/graphics": ">=6.0.0", + "@pixi/math": ">=6.0.0", + "@pixi/mesh": ">=6.0.0", + "@pixi/mesh-extras": ">=6.0.0", + "@pixi/particle-container": ">=6.0.0", + "@pixi/sprite": ">=6.0.0", + "@pixi/sprite-animated": ">=6.0.0", + "@pixi/sprite-tiling": ">=6.0.0", + "@pixi/text": ">=6.0.0", + "@pixi/text-bitmap": ">=6.0.0", + "@pixi/ticker": ">=6.0.0", + "prop-types": "^15.8.1", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@pixi/runner": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.3.2.tgz", + "integrity": "sha512-maKotoKJCQiQGBJwfM+iYdQKjrPN/Tn9+72F4WIf706zp/5vKoxW688Rsktg5BX4Mcn7ZkZvcJYTxj2Mv87lFA==" + }, + "node_modules/@pixi/settings": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.3.2.tgz", + "integrity": "sha512-vtxzuARDTbFe0fRYSqB53B+mPpX7v+QjjnCUmVMVvZiWr3QcngMWVml6c6dQDln7IakWoKZRrNG4FpggvDgLVg==", + "dependencies": { + "@pixi/constants": "7.3.2", + "@types/css-font-loading-module": "^0.0.7", + "ismobilejs": "^1.1.0" + } + }, + "node_modules/@pixi/sprite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.3.2.tgz", + "integrity": "sha512-IpWTKXExJNXVcY7ITopJ+JW48DahdbCo/81D2IYzBImq3jyiJM2Km5EoJgvAM5ZQ3Ev3KPPIBzYLD+HoPWcxdw==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2" + } + }, + "node_modules/@pixi/sprite-animated": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-7.3.2.tgz", + "integrity": "sha512-j9pyUe4cefxE9wecNfbWQyL5fBQKvCGYaOA0DE1X46ukBHrIuhA8u3jg2X3N3r4IcbVvxpWFYDrDsWXWeiBmSw==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/sprite-tiling": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-7.3.2.tgz", + "integrity": "sha512-tWVVb/rMIx5AczfUrVxa0dZaIufP5C0IOL7IGfFUDQqDu5JSAUC0mwLe4F12jAXBVsqYhCGYx5bIHbPiI5vcSQ==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/spritesheet": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-7.3.2.tgz", + "integrity": "sha512-UkwqrPYDqrEdK5ub9qn/9VBvt5caA8ffV5iYR6ssCvrpaQovBKmS+b5pr/BYf8xNTExDpR3OmPIo8iDEYWWLuw==", + "peerDependencies": { + "@pixi/assets": "7.3.2", + "@pixi/core": "7.3.2" + } + }, + "node_modules/@pixi/text": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/text/-/text-7.3.2.tgz", + "integrity": "sha512-LdtNj+K5tPB/0UcDcO52M/C7xhwFTGFhtdF42fPhRuJawM23M3zm1Y8PapXv+mury+IxCHT1w30YlAi0qTVpKQ==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/sprite": "7.3.2" + } + }, + "node_modules/@pixi/text-bitmap": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-7.3.2.tgz", + "integrity": "sha512-p8KLgtZSPowWU/Zj+GVtfsUT8uGYo4TtKKYbLoWuxkRA5Pc1+4C9/rV/EOSFfoZIdW5C+iFg5VxRgBllUQf+aA==", + "peerDependencies": { + "@pixi/assets": "7.3.2", + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/mesh": "7.3.2", + "@pixi/text": "7.3.2" + } + }, + "node_modules/@pixi/text-html": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/text-html/-/text-html-7.3.2.tgz", + "integrity": "sha512-IYhBWEPOvqUtlHkS5/c1Hseuricj5jrrGd21ivcvHmcnK/x2m+CRGvvzeBp1mqoYBnDbQVrD2wSXSe4Dv9tEJA==", + "peerDependencies": { + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/sprite": "7.3.2", + "@pixi/text": "7.3.2" + } + }, + "node_modules/@pixi/ticker": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.3.2.tgz", + "integrity": "sha512-5kIPhBeXwDJohCzKzJJ6T7f1oAGbHAgeiwOjlTO+9lNXUX8ZPj0407V3syuF+64kFqJzIBCznBRpI+fmT4c9SA==", + "dependencies": { + "@pixi/extensions": "7.3.2", + "@pixi/settings": "7.3.2", + "@pixi/utils": "7.3.2" + } + }, + "node_modules/@pixi/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-KhNvj9YcY7Zi2dTKZgDpx8C6OxKKR541vwtG6JgdBZZYDeMBOIghN2Vi5zn4diW5BhDfHBmdSJ1wZXEtE2MDwg==", + "dependencies": { + "@pixi/color": "7.3.2", + "@pixi/constants": "7.3.2", + "@pixi/settings": "7.3.2", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, "node_modules/@pkgr/utils": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", @@ -510,16 +887,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@react-leaflet/core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", - "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@rushstack/eslint-patch": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz", @@ -534,70 +901,26 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/d3-array": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.8.tgz", - "integrity": "sha512-2xAVyAUgaXHX9fubjcCbGAUOqYfRJN1em1EKR2HfzWBpObZhwfnZKvofTN4TplMqJdFQao61I+NVSai/vnBvDQ==" + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" }, - "node_modules/@types/d3-color": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.1.tgz", - "integrity": "sha512-CSAVrHAtM9wfuLJ2tpvvwCU/F22sm7rMHNN+yh9D6O6hyAms3+O0cgMpC1pm6UEUMOntuZC8bMt74PteiDUdCg==" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", - "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.2.tgz", - "integrity": "sha512-zAbCj9lTqW9J9PlF4FwnvEjXZUy75NQqPm7DMHZXuxCFTpuTrdK2NMYGQekf4hlasL78fCYOLu4EE3/tXElwow==", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", - "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.5.tgz", - "integrity": "sha512-w/C++3W394MHzcLKO2kdsIn5KKNTOqeQVzyPSGPLzQbkPw/jpeaGtSRlakcKevGgGsjJxGsbqS0fPrVFDbHrDA==", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.3.tgz", - "integrity": "sha512-cHMdIq+rhF5IVwAV7t61pcEXfEHsEsrbBUPkFGBwTXuxtTAkBBrnrNA8++6OWm3jwVsXoZYQM8NEekg6CPJ3zw==", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.1.tgz", - "integrity": "sha512-5j/AnefKAhCw4HpITmLDTPlf4vhi8o/dES+zbegfPb7LaGfNyqkLxBR6E+4yvTAgnJLmhe80EXFMzUs38fw4oA==" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", - "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" + "node_modules/@types/earcut": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.3.tgz", + "integrity": "sha512-pskpibEbm73+7nA9RqxGEnAiALRO92DdoSVxasyjGrqzEndaSDjFG73GCtstMzhdOowZMItVw2fhTdxVrY221w==" }, "node_modules/@types/js-cookie": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.4.tgz", - "integrity": "sha512-vMMnFF+H5KYqdd/myCzq6wLDlPpteJK+jGFgBus3Da7lw+YsDmx2C8feGTzY2M3Fo823yON+HC2CL240j4OV+w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-dtLshqoiGRDHbHueIT9sjkd2F4tW1qPSX2xKAQK8p1e6pM+Z913GM1shv7dOqqasEMYbC5zEaClJomQe8OtQLA==", "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, "node_modules/@types/json5": { @@ -607,10 +930,18 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", - "dev": true + "version": "20.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", + "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.25.1" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.2", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.2.tgz", + "integrity": "sha512-ujCjOxeA07IbEBQYAkoOI+XFw5sT3nhWJ/xZfPR6reJppDG7iPQPZacQiLTtWH1b3a2NYXWlxvYqa40y/LAixQ==" }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -619,9 +950,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.24", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.24.tgz", - "integrity": "sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==", + "version": "18.2.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", + "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -630,9 +961,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", - "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", + "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", "dev": true, "dependencies": { "@types/react": "*" @@ -645,22 +976,22 @@ "devOptional": true }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", - "integrity": "sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", + "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/type-utils": "6.7.4", - "@typescript-eslint/utils": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/type-utils": "6.8.0", + "@typescript-eslint/utils": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -686,15 +1017,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz", - "integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz", + "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4" }, "engines": { @@ -714,13 +1045,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz", - "integrity": "sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz", + "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4" + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -731,13 +1062,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz", - "integrity": "sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz", + "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/utils": "6.7.4", + "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/utils": "6.8.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -758,9 +1089,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.4.tgz", - "integrity": "sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz", + "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -771,13 +1102,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz", - "integrity": "sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz", + "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -798,17 +1129,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.4.tgz", - "integrity": "sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz", + "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/typescript-estree": "6.8.0", "semver": "^7.5.4" }, "engines": { @@ -823,12 +1154,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz", - "integrity": "sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz", + "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/types": "6.8.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -839,6 +1170,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1071,11 +1408,6 @@ "has-symbols": "^1.0.3" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -1134,16 +1466,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -1264,7 +1586,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -1326,11 +1647,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/classnames": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1362,17 +1678,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1393,11 +1698,6 @@ "node": ">= 8" } }, - "node_modules/css-unit-converter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1415,116 +1715,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "engines": { - "node": ">=12" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1548,11 +1738,6 @@ } } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1621,14 +1806,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1667,13 +1844,10 @@ "node": ">=6.0.0" } }, - "node_modules/dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", - "dependencies": { - "@babel/runtime": "^7.1.2" - } + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, "node_modules/electron-to-chromium": { "version": "1.4.512", @@ -1837,18 +2011,19 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1891,12 +2066,12 @@ } }, "node_modules/eslint-config-next": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.5.4.tgz", - "integrity": "sha512-FzQGIj4UEszRX7fcRSJK6L1LrDiVZvDFW320VVntVKh3BSU8Fb9kpaoxQx0cdFgf3MQXdeSbrCXJ/5Z/NndDkQ==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.5.6.tgz", + "integrity": "sha512-o8pQsUHTo9aHqJ2YiZDym5gQAMRf7O2HndHo/JZeY7TDD+W4hk6Ma8Vw54RHiBeb7OWWO5dPirQB+Is/aVQ7Kg==", "dev": true, "dependencies": { - "@next/eslint-plugin-next": "13.5.4", + "@next/eslint-plugin-next": "13.5.6", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", @@ -2334,14 +2509,6 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, - "node_modules/fast-equals": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", - "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -2451,25 +2618,6 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2479,19 +2627,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fraction.js": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", @@ -2514,8 +2649,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { "version": "1.1.6", @@ -2548,7 +2682,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -2726,7 +2859,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -2768,7 +2900,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2780,7 +2911,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2876,14 +3006,6 @@ "node": ">= 0.4" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "engines": { - "node": ">=12" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3290,6 +3412,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==" + }, "node_modules/iterator.prototype": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.1.tgz", @@ -3404,12 +3531,6 @@ "language-subtag-registry": "~0.3.2" } }, - "node_modules/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "peer": true - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3438,10 +3559,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -3449,6 +3570,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.times": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.times/-/lodash.times-4.3.2.tgz", + "integrity": "sha512-FfaJzl0SA35CRPDh5SWe2BTght6y5KSK7yJv166qIp/8q7qOwBDCvuDZE2RUSMRpBkLF6rZKbLEUoTmaP3qg6A==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3500,25 +3626,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -3582,11 +3689,11 @@ "dev": true }, "node_modules/next": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz", - "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.0.3.tgz", + "integrity": "sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==", "dependencies": { - "@next/env": "13.5.4", + "@next/env": "14.0.3", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -3598,18 +3705,18 @@ "next": "dist/bin/next" }, "engines": { - "node": ">=16.14.0" + "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.4", - "@next/swc-darwin-x64": "13.5.4", - "@next/swc-linux-arm64-gnu": "13.5.4", - "@next/swc-linux-arm64-musl": "13.5.4", - "@next/swc-linux-x64-gnu": "13.5.4", - "@next/swc-linux-x64-musl": "13.5.4", - "@next/swc-win32-arm64-msvc": "13.5.4", - "@next/swc-win32-ia32-msvc": "13.5.4", - "@next/swc-win32-x64-msvc": "13.5.4" + "@next/swc-darwin-arm64": "14.0.3", + "@next/swc-darwin-x64": "14.0.3", + "@next/swc-linux-arm64-gnu": "14.0.3", + "@next/swc-linux-arm64-musl": "14.0.3", + "@next/swc-linux-x64-gnu": "14.0.3", + "@next/swc-linux-x64-musl": "14.0.3", + "@next/swc-win32-arm64-msvc": "14.0.3", + "@next/swc-win32-ia32-msvc": "14.0.3", + "@next/swc-win32-x64-msvc": "14.0.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -3680,7 +3787,6 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3928,6 +4034,11 @@ "node": ">=8" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3954,6 +4065,47 @@ "node": ">=0.10.0" } }, + "node_modules/pixi.js": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.3.2.tgz", + "integrity": "sha512-GJickUrT3UcBInGT1CU6cv2oktCdocE5QM74CD3t+weiJPPWIzleNlp7zrBR5QIDdU6bEO8CUgUXH2Y9QvlCMw==", + "dependencies": { + "@pixi/accessibility": "7.3.2", + "@pixi/app": "7.3.2", + "@pixi/assets": "7.3.2", + "@pixi/compressed-textures": "7.3.2", + "@pixi/core": "7.3.2", + "@pixi/display": "7.3.2", + "@pixi/events": "7.3.2", + "@pixi/extensions": "7.3.2", + "@pixi/extract": "7.3.2", + "@pixi/filter-alpha": "7.3.2", + "@pixi/filter-blur": "7.3.2", + "@pixi/filter-color-matrix": "7.3.2", + "@pixi/filter-displacement": "7.3.2", + "@pixi/filter-fxaa": "7.3.2", + "@pixi/filter-noise": "7.3.2", + "@pixi/graphics": "7.3.2", + "@pixi/mesh": "7.3.2", + "@pixi/mesh-extras": "7.3.2", + "@pixi/mixin-cache-as-bitmap": "7.3.2", + "@pixi/mixin-get-child-by-name": "7.3.2", + "@pixi/mixin-get-global-position": "7.3.2", + "@pixi/particle-container": "7.3.2", + "@pixi/prepare": "7.3.2", + "@pixi/sprite": "7.3.2", + "@pixi/sprite-animated": "7.3.2", + "@pixi/sprite-tiling": "7.3.2", + "@pixi/spritesheet": "7.3.2", + "@pixi/text": "7.3.2", + "@pixi/text-bitmap": "7.3.2", + "@pixi/text-html": "7.3.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4059,9 +4211,9 @@ } }, "node_modules/postcss-preset-mantine": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.8.0.tgz", - "integrity": "sha512-aLc+EoDXsvnXM2lWWF1MI+lgGqbd5xatVJ3KyTmsheNoXBYN0OFAkRFqyy3tfdveH64Fno2SLNEr4w/njPSInw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.9.0.tgz", + "integrity": "sha512-ZurmjL+5UK9FZq4GGKOoksC7UMVFZVXxRMO0WwQAiMeElZ8jPXIXIALnwdQhslyoVDzpezkRuHYtXGo65DwvqA==", "dev": true, "dependencies": { "postcss-mixins": "^9.0.4", @@ -4152,11 +4304,6 @@ "react-is": "^16.13.1" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -4166,6 +4313,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4210,9 +4371,9 @@ } }, "node_modules/react-icons": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz", - "integrity": "sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", "peerDependencies": { "react": "*" } @@ -4222,24 +4383,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/react-leaflet": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", - "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", - "dependencies": { - "@react-leaflet/core": "^2.1.0" - }, - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, "node_modules/react-number-format": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.3.1.tgz", @@ -4297,32 +4440,6 @@ } } }, - "node_modules/react-resize-detector": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz", - "integrity": "sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-smooth": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.4.tgz", - "integrity": "sha512-OkFsrrMBTvQUwEJthE1KXSOj79z57yvEWeFefeXPib+RmQEI9B1Ub1PgzlzzUyBOvl/TjXt5nF2hmD4NsgAh8A==", - "dependencies": { - "fast-equals": "^5.0.0", - "react-transition-group": "2.9.0" - }, - "peerDependencies": { - "prop-types": "^15.6.0", - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -4361,21 +4478,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-transition-group": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", - "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", - "dependencies": { - "dom-helpers": "^3.4.0", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0", - "react-dom": ">=15.0.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4385,38 +4487,6 @@ "pify": "^2.3.0" } }, - "node_modules/recharts": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.8.0.tgz", - "integrity": "sha512-nciXqQDh3aW8abhwUlA4EBOBusRHLNiKHfpRZiG/yjups1x+auHb2zWPuEcTn/IMiN47vVMMuF8Sr+vcQJtsmw==", - "dependencies": { - "classnames": "^2.2.5", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.19", - "react-is": "^16.10.2", - "react-resize-detector": "^8.0.4", - "react-smooth": "^2.0.2", - "recharts-scale": "^0.4.4", - "reduce-css-calc": "^2.1.8", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "prop-types": "^15.6.0", - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, "node_modules/recoil": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", @@ -4436,20 +4506,6 @@ } } }, - "node_modules/reduce-css-calc": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", - "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", - "dependencies": { - "css-unit-converter": "^1.1.1", - "postcss-value-parser": "^3.3.0" - } - }, - "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -4759,7 +4815,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -5177,6 +5232,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "dev": true + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -5225,6 +5286,20 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, "node_modules/use-callback-ref": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", @@ -5309,27 +5384,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "node_modules/victory-vendor": { - "version": "36.6.11", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.11.tgz", - "integrity": "sha512-nT8kCiJp8dQh8g991J/R5w5eE2KnO8EAIP0xocWlh9l2okngMWglOPoMZzJvek8Q1KUc4XE/mJxTZnvOB1sTYg==", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/ui/package.json b/ui/package.json index b32fc76..32016c4 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,36 +9,35 @@ "lint": "next lint" }, "dependencies": { - "@mantine/core": "^7.1.2", - "@mantine/form": "^7.1.2", - "@mantine/hooks": "^7.1.2", - "@mantine/modals": "^7.1.2", - "@mantine/notifications": "^7.1.2", - "axios": "^1.5.1", + "@mantine/core": "^7.2.2", + "@mantine/form": "^7.2.2", + "@mantine/hooks": "^7.2.2", + "@mantine/modals": "^7.2.2", + "@mantine/notifications": "^7.2.2", + "@pixi/react": "^7.1.1", "js-cookie": "^3.0.5", - "next": "^13.5.4", + "next": "^14.0.3", + "pixi.js": "^7.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^4.11.0", - "react-leaflet": "^4.2.1", - "recharts": "^2.8.0", + "react-icons": "^4.12.0", "recoil": "^0.7.7" }, "devDependencies": { - "@types/js-cookie": "^3.0.4", - "@types/node": "20.8.2", - "@types/react": "18.2.24", - "@types/react-dom": "18.2.8", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", + "@types/js-cookie": "^3.0.5", + "@types/node": "20.8.7", + "@types/react": "18.2.31", + "@types/react-dom": "18.2.14", + "@typescript-eslint/eslint-plugin": "^6.8.0", + "@typescript-eslint/parser": "^6.8.0", "autoprefixer": "^10.4.16", - "eslint": "8.50.0", - "eslint-config-next": "13.5.4", + "eslint": "8.52.0", + "eslint-config-next": "13.5.6", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", - "postcss-preset-mantine": "^1.8.0", + "postcss-preset-mantine": "^1.9.0", "prettier": "^3.0.3", "typescript": "5.2.2" } diff --git a/ui/src/api/auth.ts b/ui/src/api/auth.ts index 9b460b0..389cc32 100644 --- a/ui/src/api/auth.ts +++ b/ui/src/api/auth.ts @@ -1,10 +1,11 @@ +import Cookies from 'js-cookie'; import { getRequest, postRequest } from '.'; import { RegisterUser, ResponseAuth } from './auth.types'; export async function login(email: string, password: string): Promise { const response = await postRequest('auth/login', { email, password }); if (response?.status === 200) { - return response.data as ResponseAuth; + return response.json(); } else { return undefined; } @@ -24,9 +25,9 @@ export async function logout() { } export async function refresh(refresh_token_rotation?: boolean): Promise { - const response = await getRequest('auth/refresh', { params: { refresh_token_rotation } }); + const response = await getRequest('auth/refresh', { refresh_token_rotation }); if (response?.status === 200) { - return response.data as ResponseAuth; + return response.json(); } else { return undefined; } @@ -35,8 +36,37 @@ export async function refresh(refresh_token_rotation?: boolean): Promise { const response = await getRequest('auth/me'); if (response?.status === 200) { - return response.data; + return response.json(); } else { return undefined; } } + +export async function hasSession(): Promise { + const response = await getRequest('auth/check-session'); + if (response?.status === 200) { + return response?.json(); + } else { + return false; + } +} + +/** + * Refreshes the logged_in cookie every interval. By default, the interval is 14 minutes. + * @param interval + * @returns interval id + */ +export function refreshLoggedIn(interval = 840000) { + let loggedIn = Cookies.get('logged_in'); + const id = setInterval(async () => { + const cookie = Cookies.get('logged_in'); + if (cookie != loggedIn) { + loggedIn = cookie; + const response = await refresh(true); + if (!response) { + Cookies.remove('logged_in'); + } + } + }, interval); + return id; +} diff --git a/ui/src/api/auth.types.ts b/ui/src/api/auth.types.ts index 8228ac1..76ac70a 100644 --- a/ui/src/api/auth.types.ts +++ b/ui/src/api/auth.types.ts @@ -15,4 +15,5 @@ export interface User { role: string; first_name: string; last_name: string; + profile_picture?: string; } diff --git a/ui/src/app/bot/page.tsx b/ui/src/api/characters.types.ts similarity index 100% rename from ui/src/app/bot/page.tsx rename to ui/src/api/characters.types.ts diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts index a3f662e..c8984f1 100644 --- a/ui/src/api/guilds.ts +++ b/ui/src/api/guilds.ts @@ -1,14 +1,16 @@ -import { getRequest, postRequest } from '.'; +import { APIResponse, getRequest, postRequest } from '.'; import { GuildChannel, GuildInfo } from './guilds.types'; export async function getGuilds(): Promise { const response = await getRequest('guilds'); - return response?.data || { data: [] }; + const guilds: APIResponse = await response?.json(); + return guilds?.data || []; } export async function getTextChannels(guildId: number): Promise { const response = await getRequest(`guilds/${guildId}/text`); - return response?.data || { data: [] }; + const channels: APIResponse = await response?.json(); + return channels.data || []; } export async function sendMessage(guildId: number, channelId: number, message: string): Promise { @@ -17,7 +19,8 @@ export async function sendMessage(guildId: number, channelId: number, message: s export async function getVoiceChannels(guildId: number): Promise { const response = await getRequest(`guilds/${guildId}/voice`); - return response?.data || { data: [] }; + const channels: APIResponse = await response?.json(); + return channels.data || []; } export async function playTrack(guildId: number, channelId: number, track: string): Promise { @@ -46,5 +49,6 @@ export async function skipTrack(guildId: number): Promise { export async function getVolume(guildId: number): Promise { const response = await getRequest(`guilds/${guildId}/voice/volume`); - return response?.data?.volume || 0; + const volume: number = await response?.json(); + return volume || 0; } diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 5facad5..076de6b 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -1,43 +1,49 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; - const serviceHost = process.env.SERVICE_HOST || 'http://localhost'; const servicePort = process.env.SERVICE_PORT || 5000; +const baseURL = `${serviceHost}:${servicePort}`; -function createAxiosClient(): AxiosInstance { - const axiosClient = axios.create({ - baseURL: `${serviceHost}:${servicePort}` +export async function getRequest(endpoint: string, params: Record = {}): Promise { + // Remove undefined params + Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]); + const urlParams = new URLSearchParams(params); + const url = urlParams && urlParams.size > 0 ? `${baseURL}/${endpoint}?${urlParams}` : `${baseURL}/${endpoint}`; + const response = await fetch(url, { + method: 'GET', + credentials: 'include' }); - - axiosClient.interceptors.request.use( - (request) => { - request.withCredentials = true; - return request; - }, - (error) => { - console.error(error); - return Promise.reject(error); - } - ); - return axiosClient; + return response; } -const axiosClient = createAxiosClient(); - -export async function getRequest( - url: string, - config?: AxiosRequestConfig -): Promise | undefined> { - const response = await axiosClient.get(`/${url}`, config); - return response || undefined; +interface PostOptions { + headers?: Record; + type?: 'json' | 'form'; } -export async function postRequest( - url: string, - data?: any, - config?: AxiosRequestConfig -): Promise | undefined> { - const response = await axiosClient.post(`/${url}`, data, config); - return response || undefined; +export async function postRequest(endpoint: string, body?: any, options?: PostOptions): Promise { + const url = `${baseURL}/${endpoint}`; + let response; + if (body && (!options?.type || options.type === 'json')) { + response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(body) + }); + } else { + response = await fetch(url, { + method: 'POST', + credentials: 'include', + body + }); + } + return response; +} + +export interface APIResponse { + data: T; + metadata: Metadata; } export interface Metadata { diff --git a/ui/src/api/spells.ts b/ui/src/api/spells.ts index 426c5d3..e3fc0d1 100644 --- a/ui/src/api/spells.ts +++ b/ui/src/api/spells.ts @@ -20,22 +20,20 @@ interface GetSpellsParams { export async function getSpells(params?: GetSpellsParams): Promise { const response = await getRequest('dnd/spells', { - params: { - name: params?.name, - like_name: params?.like_name, - schools: params?.schools?.join(','), - levels: params?.levels?.join(','), - ritual: params?.ritual, - concentration: params?.concentration, - classes: params?.classes?.join(','), - damage_inflict: params?.damage_inflict?.join(','), - damage_resist: params?.damage_resist?.join(','), - conditions: params?.conditions?.join(','), - saving_throw: params?.saving_throw?.join(','), - attack_type: params?.attack_type?.join(','), - limit: params?.limit, - page: params?.page - } + name: params?.name, + like_name: params?.like_name, + schools: params?.schools?.join(','), + levels: params?.levels?.join(','), + ritual: params?.ritual, + concentration: params?.concentration, + classes: params?.classes?.join(','), + damage_inflict: params?.damage_inflict?.join(','), + damage_resist: params?.damage_resist?.join(','), + conditions: params?.conditions?.join(','), + saving_throw: params?.saving_throw?.join(','), + attack_type: params?.attack_type?.join(','), + limit: params?.limit, + page: params?.page }); - return response?.data || { data: [] }; + return response?.json() || { data: [] }; } diff --git a/ui/src/api/users.ts b/ui/src/api/users.ts new file mode 100644 index 0000000..e14ea66 --- /dev/null +++ b/ui/src/api/users.ts @@ -0,0 +1,24 @@ +import { getRequest, postRequest } from '.'; + +export async function getPicture(): Promise { + const response = await getRequest('users/picture'); + if (response?.status === 200) { + return response.blob(); + } else { + return undefined; + } +} + +export async function setPicture(payload: File): Promise { + const data = new FormData(); + data.append('data', payload); + // TODO: Figure out why the form data object is empty + const response = await postRequest('users/picture', data, { + type: 'form' + }); + if (response?.status === 200) { + return true; + } else { + return false; + } +} diff --git a/ui/src/app/admin/page.tsx b/ui/src/app/admin/page.tsx new file mode 100644 index 0000000..a08f5e3 --- /dev/null +++ b/ui/src/app/admin/page.tsx @@ -0,0 +1,201 @@ +'use client'; + +import { + getGuilds, + getTextChannels, + getVoiceChannels, + getVolume, + pauseTrack, + playTrack, + resumeTrack, + sendMessage, + setVolume, + skipTrack, + stopTrack +} from '@/api/guilds'; +import { GuildChannel, GuildInfo } from '@/api/guilds.types'; +import { userState } from '@/state/auth'; +import { Button, Card, Grid, Select, Slider, Tabs, TextInput, Textarea } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; +import { useRecoilValue } from 'recoil'; + +export default function Page() { + const user = useRecoilValue(userState); + const [guilds, setGuilds] = useState([]); + const [activeGuild, setActiveGuild] = useState(null); + const router = useRouter(); + + useEffect(() => { + // Check if the user is logged in and an admin, otherwise redirect to the home page + // if (!user || !user.roles.includes('admin')) { + if (!user || user.role !== 'admin') { + router.push('/'); + } else { + getGuilds().then((g) => { + setGuilds(g); + if (g.length > 0) { + setActiveGuild(g[0]); + } + }); + } + }, []); + + return ( + + + {guilds && guilds.map((guild) => ( + setActiveGuild(guild)}> + {guild.name} + + ))} + + {guilds && guilds.map((guild) => ( + +

{guild.name}

+ + + + + + + + +
+ ))} +
+ ); +} + +function TextChannelCard({ guild }: { guild: GuildInfo | null }) { + const [textChannels, setTextChannels] = useState([]); + const [activeChannel, setActiveChannel] = useState(null); + + const form = useForm({ + initialValues: { + message: '' + } + }); + + useEffect(() => { + if (guild) { + getTextChannels(guild.id).then((c) => setTextChannels(c)); + } + }, [guild]); + + return ( + + +

Text Channels

+