Compare commits

..
Author SHA1 Message Date
İspik 07f2c3d77a fix: Option::Error caused endpoints that used ValidatedTicket become inaccessible to bots
Signed-off-by: İspik <ispik@ispik.dev>
2026-06-22 21:35:42 +03:00
100 changed files with 1731 additions and 3663 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ permissions:
packages: write
concurrency:
group: docker-test-${{ github.head_ref || github.ref }}
group: ${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
+1 -1
View File
@@ -6,7 +6,7 @@ on:
pull_request:
concurrency:
group: rust-build-test-${{ github.head_ref || github.ref }}
group: ${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
+1 -1
View File
@@ -7,7 +7,7 @@ gh = "2.25.0"
rust = "1.92.0"
"cargo:cargo-nextest" = "0.9.122"
"github:git-town/git-town" = "22.7.1"
"github:git-town/git-town" = "22.4.0"
[settings]
experimental = true
Generated
+1089 -648
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -23,11 +23,11 @@ async-trait = "0.1.89"
tokio = "1.49.0"
async-channel = "2.3.1"
futures = "0.3.32"
async-std = "1.8.0"
async-tungstenite = "0.17.0"
futures-locks = "0.7.1"
async-lock = "2.8.0"
async-recursion = "1.0.4"
tokio-util = { version = "0.7.18" }
# Error Handling
anyhow = "1.0.100"
@@ -89,7 +89,7 @@ log = "0.4.29"
pretty_env_logger = "0.4.0"
# Redis
redis-kiss = { version = "0.1.4", default-features = false }
redis-kiss = "0.1.4"
fred = "8.0.1"
# Serialisation
@@ -160,8 +160,8 @@ opentelemetry-appender-tracing = "0.31.1"
lapin = "4.7.1"
# Voice
livekit-api = "=0.4.23"
livekit-protocol = "=0.7.7"
livekit-api = "0.4.4"
livekit-protocol = "0.7.4"
livekit-runtime = "0.4.0"
# Other Utilities
+3 -4
View File
@@ -14,7 +14,7 @@ sentry = { workspace = true }
lru = { workspace = true }
ulid = { workspace = true }
once_cell = { workspace = true }
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
redis-kiss = { workspace = true }
lru_time_cache = { workspace = true }
async-channel = { workspace = true }
@@ -30,9 +30,8 @@ serde = { workspace = true }
# async
futures = { workspace = true }
async-tungstenite = { workspace = true, features = ["tokio-runtime"] }
tokio = { workspace = true }
tokio-util = { workspace = true, features = ["compat"] }
async-tungstenite = { workspace = true, features = ["async-std-runtime"] }
async-std = { workspace = true }
# core
revolt-result = { workspace = true }
+1 -1
View File
@@ -2,7 +2,7 @@ use std::{
collections::{HashMap, HashSet}, num::NonZeroUsize, sync::Arc, time::Duration
};
use tokio::sync::{Mutex, RwLock};
use async_std::sync::{Mutex, RwLock};
use lru::LruCache;
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
use revolt_database::{Channel, Member, Server, User};
+3 -3
View File
@@ -1,6 +1,6 @@
use std::env;
use tokio::net::TcpListener;
use async_std::net::TcpListener;
use revolt_presence::clear_region;
#[macro_use]
@@ -12,7 +12,7 @@ pub mod events;
mod database;
mod websocket;
#[tokio::main]
#[async_std::main]
async fn main() {
// Configure requirements for Bonfire.
revolt_config::configure!(events);
@@ -33,7 +33,7 @@ async fn main() {
// Start accepting new connections and spawn a client for each connection.
while let Ok((stream, addr)) = listener.accept().await {
tokio::task::spawn(async move {
async_std::task::spawn(async move {
info!("User connected from {addr:?}");
websocket::client(database::get_db(), stream, addr).await;
info!("User disconnected from {addr:?}");
+4 -5
View File
@@ -21,12 +21,11 @@ use revolt_database::{
};
use revolt_presence::{create_session, delete_session};
use tokio::{
use async_std::{
net::TcpStream,
sync::{Mutex, RwLock},
task::spawn,
};
use tokio_util::compat::{TokioAsyncReadCompatExt, Compat};
use revolt_result::create_error;
use sentry::Level;
@@ -34,8 +33,8 @@ use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
use crate::events::state::{State, SubscriptionStateChange};
use revolt_models::v0;
type WsReader = SplitStream<WebSocketStream<Compat<TcpStream>>>;
type WsWriter = SplitSink<WebSocketStream<Compat<TcpStream>>, async_tungstenite::tungstenite::Message>;
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
/// Start a new WebSocket client worker given access to the database,
/// the relevant TCP stream and the remote address of the client.
@@ -45,7 +44,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
// e.g. wss://example.com?format=json&version=1
let (sender, receiver) = oneshot::channel();
let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
stream.compat(),
stream,
WebsocketHandshakeCallback::from(sender),
None,
)
+2 -2
View File
@@ -13,7 +13,7 @@ repository = "https://github.com/stoatchat/stoatchat"
anyhow = ["dep:sentry-anyhow"]
report-macros = ["revolt-result"]
sentry = ["dep:sentry"]
test = ["tokio"]
test = ["async-std"]
default = ["sentry"]
[dependencies]
@@ -26,7 +26,7 @@ serde = { workspace = true }
# Async
futures-locks = { workspace = true }
tokio = { workspace = true, optional = true }
async-std = { workspace = true, features = ["attributes"], optional = true }
# Logging
log = { workspace = true }
-4
View File
@@ -88,10 +88,6 @@ max_concurrent_connections = 50
# How long to ring devices for when calling in dms/groups, in seconds
call_ring_duration = 30
[api.audit_logs]
# How long audit log entries last before being removed, in seconds
expires_after = 2592000 # 30d
[api.livekit.nodes]
[api.users]
+1 -8
View File
@@ -260,12 +260,6 @@ pub struct ApiUsers {
pub min_username_length: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiAuditLogs {
/// How long audit log entries last before being removed, in seconds
pub expires_after: u64,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Api {
pub registration: ApiRegistration,
@@ -274,7 +268,6 @@ pub struct Api {
pub workers: ApiWorkers,
pub livekit: ApiLiveKit,
pub users: ApiUsers,
pub audit_logs: ApiAuditLogs,
}
#[derive(Deserialize, Debug, Clone)]
@@ -593,7 +586,7 @@ macro_rules! configure {
mod tests {
use crate::init;
#[tokio::test]
#[async_std::test]
async fn it_works() {
init().await;
}
+4 -4
View File
@@ -15,7 +15,7 @@ mongodb = ["dep:mongodb", "bson"]
# ... Other
tasks = ["isahc", "linkify", "url-escape"]
tokio-runtime = ["tokio"]
async-std-runtime = ["async-std"]
rocket-impl = [
"rocket",
"schemars",
@@ -27,7 +27,7 @@ redis-is-patched = ["revolt-presence/redis-is-patched"]
voice = ["livekit-api", "livekit-protocol", "livekit-runtime"]
# Default Features
default = ["mongodb", "tokio-runtime", "tasks"]
default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
@@ -64,7 +64,7 @@ serde = { workspace = true }
iso8601-timestamp = { workspace = true, features = ["serde", "bson"] }
# Events
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
redis-kiss = { workspace = true }
# Database
bson = { workspace = true, optional = true }
@@ -81,7 +81,7 @@ async-trait = { workspace = true }
async-recursion = { workspace = true }
# Async
tokio = { workspace = true, optional = true }
async-std = { workspace = true, features = ["attributes"], optional = true }
# Axum Impl
axum = { workspace = true, optional = true }
@@ -3,16 +3,15 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
Account, AccountInvite, AuditLogEntry, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji,
File, FileHash, Invite, MFATicket, Member, MemberCompositeKey, Message, PolicyChange,
RatelimitEvent, Report, Server, ServerBan, Session, Snapshot, User, UserSettings, Webhook,
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
User, UserSettings, Webhook, Account, AccountInvite, Session, MFATicket
};
database_derived!(
/// Reference implementation
#[derive(Default, Debug)]
pub struct ReferenceDb {
pub audit_logs: Arc<Mutex<HashMap<String, AuditLogEntry>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
+3 -74
View File
@@ -25,8 +25,8 @@ pub use mongodb;
#[macro_use]
extern crate bson;
#[cfg(not(feature = "tokio-runtime"))]
compile_error!("tokio-runtime feature must be enabled.");
#[cfg(not(feature = "async-std-runtime"))]
compile_error!("async-std-runtime feature must be enabled.");
#[macro_export]
#[cfg(debug_assertions)]
@@ -77,78 +77,6 @@ macro_rules! auto_derived_partial {
};
}
/// Internal macro for `generate_diff!`, you should not need to use this yourself.
macro_rules! generate_field_diff {
(optional, $remove:ident, $fieldsmember:path, $self:ident, $before:ident, $partial:ident, $field:ident) => {
if $partial.$field.is_some() || $remove.contains(&$fieldsmember) {
$before.$field = $self.$field.clone();
};
};
(optional, default, $remove:ident, $fieldsmember:path, $self:ident, $before:ident, $partial:ident, $field:ident) => {
if $partial.$field.is_some() || $remove.contains(&$fieldsmember) {
$before.$field = Some($self.$field.clone());
};
};
($self:ident, $before:ident, $partial:ident, $field:ident) => {
if $partial.$field.is_some() {
$before.$field = Some($self.$field.clone());
};
};
}
/// Generates a partial model containing the data which has changed in an update
///
/// ## Usage:
/// `before` is the "output" containing what the model had before being updated,
/// this will corraspond to `partial` which is what the data is being changed too.
///
/// ```rs
/// let mut before = PartialModel::default();
///
/// generate_diff!(
/// self, // database model
/// before, // mutable empty partial corrasponding to the current model
/// partial, // partial containing what is being updated
/// remove, // slice of fields being removed
/// (
/// name, // regular non-nullable non-removable field
/// (FieldsEnum::Nickname) nickname, // optional removable field
/// ((default) FieldsEnum::Roles) roles, // optional removable field with custom default
/// )
/// );
/// ```
///
/// See `Member::generate_diff` `Server::generate_diff` `Role::generate_diff` for full examples
macro_rules! generate_diff {
(
$self:ident,
$before:ident,
$partial:ident,
$remove:ident,
(
$(
$(
$(@$optional:tt)? (
$($(@$default:tt)? (default))?
$fieldsmember:path
)
)?
$field: ident
),*
$(,)?
)
) => {
$(
generate_field_diff!(
$( $($optional)? optional, $($($default)? default,)? $remove, $fieldsmember,)?
$self, $before, $partial, $field
);
)*
}
}
mod drivers;
pub use drivers::*;
@@ -187,6 +115,7 @@ pub use amqp::amqp::AMQP;
#[cfg(feature = "voice")]
pub mod voice;
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
@@ -11,7 +11,7 @@ auto_derived!(
#[cfg(test)]
mod tests {
#[tokio::test]
#[async_std::test]
async fn migrate() {
database_test!(|db| async move {
// Initialise the database
@@ -98,9 +98,6 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create pubsub collection.");
db.create_collection("audit_logs")
.await
.expect("Failed to create audit_logs collection");
db.create_collection("sessions")
.await
.expect("Failed to create sessions collection.");
@@ -278,58 +275,6 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create ratelimit_events index.");
db.run_command(doc! {
"createIndexes": "audit_logs",
"indexes": [
{
"key": {
"expires_at": 1_i32,
},
"name": "expires_at_ttl",
// We set the expire after to 0 because we store when it expires instead of when the document was inserted,
// this is because mongo cant read the timestamp from the ulid so we need to do this workaround.
// relevant docs: https://www.mongodb.com/docs/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time
"expireAfterSeconds": 0
},
{
"key": {
"server": 1_i32,
"user": 1_i32,
"action.type": 1_i32,
},
"name": "audit_log_filters",
},
]
})
.await
.expect("Failed to create audit_logs index");
db.run_command(doc! {
"createIndexes": "audit_logs",
"indexes": [
{
"key": {
"expires_at": 1_i32,
},
"name": "expires_at_ttl",
// We set the expire after to 0 because we store when it expires instead of when the document was inserted,
// this is because mongo cant read the timestamp from the ulid so we need to do this workaround.
// relevant docs: https://www.mongodb.com/docs/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time
"expireAfterSeconds": 0
},
{
"key": {
"server": 1_i32,
"user": 1_i32,
"action.type": 1_i32,
},
"name": "audit_log_filters",
},
]
})
.await
.expect("Failed to create audit_logs index");
db.run_command(doc! {
"createIndexes": "accounts",
"indexes": [
@@ -26,7 +26,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 52; // MUST BE +1 to last migration
pub const LATEST_REVISION: i32 = 51; // MUST BE +1 to last migration
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -452,7 +452,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
warn!("This is a destructive operation and will wipe existing permission data (excl. defaults for SendMessage).");
warn!("Taking a backup is advised.");
warn!("Continuing in 10 seconds...");
tokio::time::sleep(Duration::from_secs(10)).await;
async_std::task::sleep(Duration::from_secs(10)).await;
let servers = db.col::<Document>("servers");
let mut cursor = servers.find(doc! {}).await.unwrap();
@@ -1478,56 +1478,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 50 {
info!("Running migration [revision 50 / 13-04-2026]: Rename invites collection to account_invites");
let result = db.db()
db.db()
.client()
.database("admin")
.run_command(doc! {
"renameCollection": "revolt.invites",
"to": "revolt.account_invites",
"dropTarget": true
})
.await;
if let Err(e) = result {
// NamespaceNotFound (26) = source collection doesn't exist, safe to ignore
if !matches!(e.kind.as_ref(), mongodb::error::ErrorKind::Command(ce) if ce.code == 26) {
panic!("Failed to rename invites collection: {e}");
}
}
}
if revision >= 51 {
info!("Running migration [revision 51 / 28-11-2025]: Add audit logs collection");
db.db()
.create_collection("audit_logs")
.await
.expect("Failed to create audit_logs collection");
db.db()
.run_command(doc! {
"createIndexes": "audit_logs",
"indexes": [
{
"key": {
"expires_at": 1_i32,
},
"name": "expires_at_ttl",
"expireAfterSeconds": 0
},
{
"key": {
"server": 1_i32,
"user": 1_i32,
"action.type": 1_i32,
},
"name": "audit_log_filters",
},
]
"renameCollection": "revolt.invites",
"to": "revolt.account_invites",
"dropTarget": true
})
.await
.expect("Failed to create audit_logs index");
};
.unwrap();
}
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision)
@@ -1,5 +0,0 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -1,275 +0,0 @@
use std::{collections::HashSet, time::Duration};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use ulid::Ulid;
use crate::{Database, PartialChannel, PartialMember, PartialRole, PartialServer, User, PartialEmoji};
use revolt_models::v0;
use revolt_permissions::OverrideField;
use revolt_result::Result;
auto_derived!(
/// Audit log entry
pub struct AuditLogEntry {
/// Unique ID
#[serde(rename = "_id")]
pub id: String,
/// When the audit log entry gets auto-deleted
///
/// This is only stored in the database and not given to users.
pub expires_at: Timestamp,
/// The server the entry happened in
pub server: String,
/// User provided reason
pub reason: Option<String>,
/// User who ran the action
pub user: String,
/// User this action is targetting
pub target: Option<String>,
/// The action ran
pub action: AuditLogEntryAction,
}
/// Indivual audit log action
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum AuditLogEntryAction {
MessageDelete {
author: String,
channel: String,
},
MessageBulkDelete {
channel: String,
count: usize,
},
MessagePin {
message: String,
author: String,
channel: String,
},
MessageUnpin {
message: String,
author: String,
channel: String,
},
BanCreate {
user: String,
},
BanDelete {
user: String,
},
ChannelCreate {
channel: String,
name: String,
},
ChannelEdit {
channel: String,
before: PartialChannel,
after: PartialChannel,
},
ChannelRolePermissionsEdit {
channel: String,
role: String,
permissions: OverrideField,
},
ChannelDelete {
channel: String,
name: String,
},
MemberEdit {
user: String,
before: PartialMember,
after: PartialMember,
},
MemberKick {
user: String,
},
ServerEdit {
before: PartialServer,
after: PartialServer,
},
RoleEdit {
role: String,
before: PartialRole,
after: PartialRole,
},
RoleCreate {
role: String,
name: String,
},
RoleDelete {
role: String,
name: String,
},
RolesReorder {
before: Vec<String>,
after: Vec<String>,
},
InviteCreate {
invite: String,
channel: String,
},
InviteDelete {
invite: String,
channel: String,
},
WebhookCreate {
webhook: String,
name: String,
channel: String,
},
WebhookDelete {
webhook: String,
name: String,
channel: String,
},
EmojiCreate {
emoji: String,
name: String,
},
EmojiUpdate {
emoji: String,
before: PartialEmoji,
after: PartialEmoji,
},
EmojiDelete {
emoji: String,
name: String,
},
}
/// Audit Log Query
pub struct AuditLogQuery {
/// Filter by who ran the action
pub user: Option<String>,
/// Filter by who the action is targetting
pub target: Option<String>,
/// Filter by the action type
pub r#type: Option<Vec<String>>,
/// Entries before a certain entry id
pub before: Option<String>,
/// Entries after a certain entry id
pub after: Option<String>,
/// Maximum number of entries to fetch
pub limit: i64,
}
);
impl AuditLogEntryAction {
// TODO: migrate this to a rabbitmq queue to avoid spawning lots of tasks
/// Generates an `AuditLogEntry` for the current action and inserts it into the database
pub async fn insert<R: Into<Option<String>>>(
self,
db: &Database,
server: String,
reason: R,
user: String,
target: Option<String>,
) -> AuditLogEntry {
let config = config().await;
let id = Ulid::new();
let expires_at = id
.datetime()
.checked_add(Duration::from_secs(config.api.audit_logs.expires_after))
.unwrap()
.into();
let entry = AuditLogEntry {
id: id.to_string(),
expires_at,
server,
reason: reason.into(),
user,
target,
action: self,
};
// running the insert inside a task can cause race conditions in the test so for now just dont use a task for tests for now
// this will need to be redone for when we migrate to using rabbitmq here anyway.
#[cfg(not(test))]
tokio::task::spawn({
let db = db.clone();
let entry = entry.clone();
async move { revolt_config::report_internal_error!(db.insert_audit_log_entry(&entry).await) }
});
#[cfg(test)]
db.insert_audit_log_entry(&entry).await.unwrap();
entry
}
}
impl AuditLogEntry {
/// Fetches the corrasponding users and members for each audit log entry
pub async fn with_users(
db: &Database,
server_id: &str,
user: &User,
entries: &[Self],
) -> Result<(Vec<v0::User>, Vec<v0::Member>)> {
let mut user_ids = HashSet::new();
for entry in entries {
user_ids.insert(entry.user.clone());
match &entry.action {
AuditLogEntryAction::MessageDelete { author, .. } => {
user_ids.insert(author.clone());
}
AuditLogEntryAction::BanCreate { user } => {
user_ids.insert(user.clone());
}
AuditLogEntryAction::BanDelete { user } => {
user_ids.insert(user.clone());
}
AuditLogEntryAction::ChannelCreate { .. } => {}
AuditLogEntryAction::MemberEdit { user, .. } => {
user_ids.insert(user.clone());
}
AuditLogEntryAction::MemberKick { user } => {
user_ids.insert(user.clone());
}
AuditLogEntryAction::MessagePin { author, .. } => {
user_ids.insert(author.clone());
}
AuditLogEntryAction::MessageUnpin { author, .. } => {
user_ids.insert(author.clone());
}
AuditLogEntryAction::ServerEdit { .. } => {}
AuditLogEntryAction::RoleEdit { .. } => {}
AuditLogEntryAction::RoleCreate { .. } => {}
AuditLogEntryAction::RoleDelete { .. } => {}
AuditLogEntryAction::RolesReorder { .. } => {}
AuditLogEntryAction::MessageBulkDelete { .. } => {}
AuditLogEntryAction::ChannelEdit { .. } => {}
AuditLogEntryAction::ChannelRolePermissionsEdit { .. } => {}
AuditLogEntryAction::ChannelDelete { .. } => {}
AuditLogEntryAction::InviteCreate { .. } => {}
AuditLogEntryAction::InviteDelete { .. } => {}
AuditLogEntryAction::WebhookCreate { .. } => {}
AuditLogEntryAction::WebhookDelete { .. } => {}
AuditLogEntryAction::EmojiCreate { .. } => {}
AuditLogEntryAction::EmojiUpdate { .. } => {}
AuditLogEntryAction::EmojiDelete { .. } => {}
};
}
let user_ids = user_ids.into_iter().collect::<Vec<_>>();
let users = User::fetch_many_ids_as_mutuals(db, user, &user_ids).await?;
let members = db
.fetch_members(server_id, &user_ids)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok((users, members))
}
}
@@ -1,20 +0,0 @@
use revolt_result::Result;
use crate::{AuditLogEntry, AuditLogQuery};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAuditLogs: Sync + Send {
/// Inserts an entry into the server's audit log
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()>;
/// Fetches a server's audit logs using the provided query options
async fn get_server_audit_logs(
&self,
server: &str,
query: AuditLogQuery,
) -> Result<Vec<AuditLogEntry>>;
}
@@ -1,66 +0,0 @@
use mongodb::options::FindOptions;
use revolt_result::Result;
use crate::{AuditLogEntry, AuditLogQuery, MongoDb};
use super::AbstractAuditLogs;
static COL: &str = "audit_logs";
#[async_trait]
impl AbstractAuditLogs for MongoDb {
/// Inserts an entry into the server's audit log
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()> {
query!(self, insert_one, COL, entry).map(|_| ())
}
/// Fetches a server's audit logs using the provided query options
async fn get_server_audit_logs(
&self,
server: &str,
query: AuditLogQuery,
) -> Result<Vec<AuditLogEntry>> {
let mut filter = doc! {
"server": server
};
if let Some(user) = query.user {
filter.insert("user", user);
};
if let Some(target) = query.target {
filter.insert("target", target);
}
if let Some(types) = query.r#type {
filter.insert("action.type", doc! { "$in": types });
};
if let Some(doc) = match (query.before, query.after) {
(Some(before), Some(after)) => Some(doc! {
"$lt": before,
"$gt": after
}),
(Some(before), _) => Some(doc! {
"$lt": before
}),
(_, Some(after)) => Some(doc! {
"$gt": after
}),
_ => None,
} {
filter.insert("_id", doc);
};
self.find_with_options(
COL,
filter,
FindOptions::builder()
.limit(query.limit)
.sort(doc! { "_id": -1 })
.build(),
)
.await
.map_err(|_| create_database_error!("find", COL))
}
}
@@ -1,81 +0,0 @@
use revolt_result::Result;
use crate::{AuditLogEntry, AuditLogQuery, ReferenceDb};
use super::AbstractAuditLogs;
#[async_trait]
impl AbstractAuditLogs for ReferenceDb {
/// Inserts an entry into the server's audit log
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()> {
self.audit_logs
.lock()
.await
.insert(entry.id.clone(), entry.clone());
Ok(())
}
/// Fetches a server's audit logs using the provided query options
async fn get_server_audit_logs(
&self,
server: &str,
query: AuditLogQuery,
) -> Result<Vec<AuditLogEntry>> {
let lock = self.audit_logs.lock().await;
let mut logs = lock
.values()
.filter(|entry| {
if entry.server != server {
return false;
};
if let Some(user) = &query.user {
if &entry.user != user {
return false;
}
}
if query.target.is_some() && entry.target != query.target {
return false;
}
if let Some(before) = &query.before {
if &entry.id > before {
return false;
};
};
if let Some(after) = &query.after {
if &entry.id < after {
return false;
};
};
if let Some(action_types) = &query.r#type {
let entry_type = serde_json::to_value(entry.action.clone())
.unwrap()
.as_object()
.unwrap()
.get("type")
.unwrap()
.as_str()
.unwrap()
.to_string();
if !action_types.contains(&entry_type) {
return false;
}
};
true
})
.cloned()
.collect::<Vec<_>>();
logs.sort_by(|a, b| b.id.cmp(&a.id));
logs.truncate(query.limit as usize);
Ok(logs)
}
}
@@ -164,7 +164,7 @@ impl Bot {
mod tests {
use crate::{Bot, FieldsBot, PartialBot, User};
#[tokio::test]
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let owner = User::create(&db, "Owner".to_string(), None, None)
@@ -128,7 +128,7 @@ impl Webhook {
mod tests {
use crate::{FieldsWebhook, PartialWebhook, Webhook};
#[tokio::test]
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let webhook_id = "webhook";
@@ -161,7 +161,6 @@ auto_derived!(
Icon,
DefaultPermissions,
Voice,
Slowmode,
}
);
@@ -555,12 +554,6 @@ impl Channel {
}
_ => {}
},
FieldsChannel::Slowmode => match self {
Self::TextChannel { slowmode, .. } => {
slowmode.take();
}
_ => {}
}
}
}
@@ -655,122 +648,6 @@ impl Channel {
}
}
/// Generates a PartialChannel containing the data which has changed in an update
pub fn generate_diff(
&self,
partial: &PartialChannel,
remove: &[FieldsChannel],
) -> PartialChannel {
let mut before = PartialChannel::default();
match self {
Channel::SavedMessages { .. } => {}
Channel::DirectMessage {
active,
last_message_id,
..
} => {
if partial.active.is_some() {
before.active = Some(*active);
};
if partial.last_message_id.is_some() {
before.last_message_id = last_message_id.clone()
};
}
Channel::Group {
name,
owner,
description,
icon,
last_message_id,
permissions,
nsfw,
..
} => {
if partial.name.is_some() {
before.name = Some(name.clone());
};
if partial.owner.is_some() {
before.owner = Some(owner.clone());
};
if partial.description.is_some() || remove.contains(&FieldsChannel::Description) {
before.description = description.clone();
};
if partial.icon.is_some() || remove.contains(&FieldsChannel::Icon) {
before.icon = icon.clone();
};
if partial.last_message_id.is_some() {
before.last_message_id = last_message_id.clone()
};
if partial.permissions.is_some() {
before.permissions = *permissions;
};
if partial.nsfw.is_some() {
before.nsfw = Some(*nsfw);
};
}
Channel::TextChannel {
name,
description,
icon,
last_message_id,
default_permissions,
role_permissions,
nsfw,
voice,
slowmode,
..
} => {
if partial.name.is_some() {
before.name = Some(name.clone());
};
if partial.description.is_some() || remove.contains(&FieldsChannel::Description) {
before.description = description.clone();
};
if partial.icon.is_some() || remove.contains(&FieldsChannel::Icon) {
before.icon = icon.clone();
};
if partial.last_message_id.is_some() {
before.last_message_id = last_message_id.clone()
};
if partial.default_permissions.is_some()
|| remove.contains(&FieldsChannel::DefaultPermissions)
{
before.default_permissions = *default_permissions;
};
if partial.role_permissions.is_some() {
before.role_permissions = Some(role_permissions.clone());
};
if partial.nsfw.is_some() {
before.nsfw = Some(*nsfw);
};
if partial.voice.is_some() || remove.contains(&FieldsChannel::Voice) {
before.voice = voice.clone();
};
if partial.slowmode.is_some() {
before.slowmode = *slowmode;
}
}
}
before
}
/// Acknowledge a message
pub async fn ack(&self, user: &str, message: &str, amqp: &AMQP) -> Result<()> {
EventV1::ChannelAck {
@@ -900,7 +777,6 @@ impl IntoDocumentPath for FieldsChannel {
FieldsChannel::Icon => "icon",
FieldsChannel::DefaultPermissions => "default_permissions",
FieldsChannel::Voice => "voice",
FieldsChannel::Slowmode => "slowmode",
})
}
}
@@ -911,7 +787,7 @@ mod tests {
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[tokio::test]
#[async_std::test]
async fn permissions_group_channel() {
database_test!(|db| async move {
fixture!(db, "group_with_members",
@@ -937,7 +813,7 @@ mod tests {
});
}
#[tokio::test]
#[async_std::test]
async fn permissions_text_channel() {
database_test!(|db| async move {
fixture!(db, "server_with_roles",
+10 -21
View File
@@ -16,7 +16,7 @@ static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
.collect()
});
auto_derived_partial!(
auto_derived!(
/// Emoji
pub struct Emoji {
/// Unique Id
@@ -34,17 +34,20 @@ auto_derived_partial!(
/// Whether the emoji is marked as nsfw
#[serde(skip_serializing_if = "crate::if_false", default)]
pub nsfw: bool,
},
"PartialEmoji"
);
}
auto_derived!(
/// Parent Id of the emoji
#[serde(tag = "type")]
pub enum EmojiParent {
Server { id: String },
Detached,
}
/// Partial representation of an emoji
pub struct PartialEmoji {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
);
#[allow(clippy::disallowed_methods)]
@@ -69,14 +72,14 @@ impl Emoji {
}
/// Delete an emoji
pub async fn delete(&self, db: &Database) -> Result<()> {
pub async fn delete(self, db: &Database) -> Result<()> {
EventV1::EmojiDelete {
id: self.id.to_string(),
}
.p(self.parent().to_string())
.await;
db.detach_emoji(self).await
db.detach_emoji(&self).await
}
/// Update an emoji
@@ -109,18 +112,4 @@ impl Emoji {
Ok(PERMISSIBLE_EMOJIS.contains(&sanitized_emoji))
}
}
/// Generates a PartialEmoji containing the data which has changed in an update
pub fn generate_diff(&self, partial: &PartialEmoji) -> PartialEmoji {
let mut before = PartialEmoji::default();
generate_diff!(
self, before, partial, remove,
(
name,
)
);
before
}
}
@@ -998,13 +998,11 @@ impl Message {
}
/// Delete a message
pub async fn delete(&self, db: &Database) -> Result<()> {
let file_ids = self
pub async fn delete(self, db: &Database) -> Result<()> {
let file_ids: Vec<String> = self
.attachments
.iter()
.flatten()
.map(|file| file.id.clone())
.collect::<Vec<_>>();
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
.unwrap_or_default();
if !file_ids.is_empty() {
db.mark_attachments_as_deleted(&file_ids).await?;
@@ -1013,10 +1011,10 @@ impl Message {
db.delete_message(&self.id).await?;
EventV1::MessageDelete {
id: self.id.clone(),
id: self.id,
channel: self.channel.clone(),
}
.p(self.channel.clone())
.p(self.channel)
.await;
Ok(())
}
@@ -51,7 +51,7 @@ impl<'r> FromRequest<'r> for ValidatedTicket {
Outcome::Error((Status::Forbidden, create_error!(InvalidToken)))
}
} else {
Outcome::Error((Status::Forbidden, create_error!(InvalidToken)))
Outcome::Forward(Status::Unauthorized)
}
}
Outcome::Forward(f) => Outcome::Forward(f),
-3
View File
@@ -1,5 +1,4 @@
mod admin_migrations;
mod audit_logs;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -24,7 +23,6 @@ mod sessions;
mod mfa_tickets;
pub use admin_migrations::*;
pub use audit_logs::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
@@ -57,7 +55,6 @@ pub trait AbstractDatabase:
Sync
+ Send
+ admin_migrations::AbstractMigrations
+ audit_logs::AbstractAuditLogs
+ bots::AbstractBots
+ channels::AbstractChannels
+ channel_invites::AbstractChannelInvites
@@ -245,26 +245,6 @@ impl Member {
}
}
/// Generates a PartialMember containing the data which has changed in an update
pub fn generate_diff(&self, partial: &PartialMember, remove: &[FieldsMember]) -> PartialMember {
let mut before = PartialMember::default();
generate_diff!(
self, before, partial, remove,
(
(FieldsMember::Nickname) nickname,
(FieldsMember::Avatar) avatar,
(FieldsMember::Timeout) timeout,
(FieldsMember::Pronouns) pronouns,
((default) FieldsMember::Roles) roles,
((default) FieldsMember::CanPublish) can_publish,
((default) FieldsMember::CanReceive) can_receive,
)
);
before
}
/// Get this user's current ranking
pub fn get_ranking(&self, server: &Server) -> i64 {
let mut value = i64::MAX;
@@ -290,7 +270,7 @@ impl Member {
/// Remove member from server
pub async fn remove(
&self,
self,
db: &Database,
server: &Server,
intention: RemovalIntention,
@@ -317,9 +297,9 @@ impl Member {
})
{
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user.clone() },
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user.clone() },
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user.clone() },
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user },
}
.into_message(id.to_string())
// TODO: support notifications here in the future?
@@ -340,7 +320,7 @@ mod tests {
use crate::{Member, PartialMember, RemovalIntention, Server, User};
#[tokio::test]
#[async_std::test]
async fn muted_member_rejoin() {
database_test!(|db| async move {
match db {
@@ -235,31 +235,6 @@ impl Server {
}
}
/// Generates a PartialServer containing the data which has changed in an update
pub fn generate_diff(&self, partial: &PartialServer, remove: &[FieldsServer]) -> PartialServer {
let mut before = PartialServer::default();
generate_diff!(
self, before, partial, remove,
(
owner,
name,
(FieldsServer::Description) description,
(FieldsServer::Categories) categories,
(FieldsServer::SystemMessages) system_messages,
roles,
default_permissions,
(FieldsServer::Icon) icon,
(FieldsServer::Banner) banner,
nsfw,
analytics,
discoverable,
)
);
before
}
/// Ordered roles list
pub fn ordered_roles(&self) -> Vec<(String, Role)> {
let mut ordered_roles = self.roles.clone().into_iter().collect::<Vec<_>>();
@@ -402,27 +377,8 @@ impl Role {
}
}
/// Generates a PartialRole containing the data which has changed in an update
pub fn generate_diff(&self, partial: &PartialRole, remove: &[FieldsRole]) -> PartialRole {
let mut before = PartialRole::default();
generate_diff!(
self, before, partial, remove,
(
name,
permissions,
(FieldsRole::Colour) colour,
hoist,
rank,
(FieldsRole::Icon) icon,
)
);
before
}
/// Delete a role
pub async fn delete(&self, db: &Database, server_id: &str) -> Result<()> {
pub async fn delete(self, db: &Database, server_id: &str) -> Result<()> {
EventV1::ServerRoleDelete {
id: server_id.to_string(),
role_id: self.id.clone(),
@@ -464,7 +420,7 @@ mod tests {
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[tokio::test]
#[async_std::test]
async fn permissions() {
database_test!(|db| async move {
fixture!(db, "server_with_roles",
@@ -259,13 +259,6 @@ impl MongoDb {
})
.await?;
self.col::<Document>("audit_logs")
.delete_many(doc! {
"server": &server_id
})
.await
.map_err(|_| create_database_error!("delete_many", "audit_logs"))?;
Ok(())
}
}
@@ -926,7 +926,7 @@ mod tests {
assert!(User::validate_username(username_https).is_err());
}
#[tokio::test]
#[async_std::test]
async fn username_sanitisation_clean() {
let username_clean = "Test";
@@ -936,7 +936,7 @@ mod tests {
assert_eq!(username_clean, username_clean_sanitised.unwrap());
}
#[tokio::test]
#[async_std::test]
async fn username_sanitisation_homoglyphs() {
let username_homoglyphs = "𝔽𝕌Ňℕy";
@@ -947,7 +947,7 @@ mod tests {
assert_eq!("funny", username_homoglyphs_sanitised);
}
#[tokio::test]
#[async_std::test]
async fn username_sanitisation_padding() {
let username_padding = "a";
@@ -956,7 +956,7 @@ mod tests {
assert_eq!("a_", username);
}
#[tokio::test]
#[async_std::test]
async fn create_user() {
use revolt_result::Result;
+1 -1
View File
@@ -305,6 +305,6 @@ pub async fn worker(db: Database, amqp: AMQP) {
}
// Sleep for an arbitrary amount of time.
tokio::time::sleep(Duration::from_secs(1)).await;
async_std::task::sleep(Duration::from_secs(1)).await;
}
}
@@ -82,6 +82,6 @@ pub async fn worker(db: Database) {
}
// Sleep for an arbitrary amount of time.
tokio::time::sleep(Duration::from_secs(1)).await;
async_std::task::sleep(Duration::from_secs(1)).await;
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
use crate::{Database, AMQP};
use tokio::task;
use async_std::task;
use std::time::Instant;
const WORKER_COUNT: usize = 5;
@@ -7,11 +7,11 @@ use revolt_config::config;
use revolt_result::Result;
use async_lock::Semaphore;
use async_std::task::spawn;
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use revolt_models::v0::Embed;
use std::{collections::HashSet, sync::Arc};
use tokio::task::spawn;
use isahc::prelude::*;
@@ -159,7 +159,6 @@ pub async fn generate(
.await
.into_iter()
.flatten()
.flatten()
.collect::<Vec<Embed>>();
// Prevent database update when no embeds are found.
+4 -161
View File
@@ -319,7 +319,6 @@ impl From<FieldsChannel> for crate::FieldsChannel {
FieldsChannel::Icon => crate::FieldsChannel::Icon,
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
FieldsChannel::Voice => crate::FieldsChannel::Voice,
FieldsChannel::Slowmode => crate::FieldsChannel::Slowmode,
}
}
}
@@ -331,7 +330,6 @@ impl From<crate::FieldsChannel> for FieldsChannel {
crate::FieldsChannel::Icon => FieldsChannel::Icon,
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
crate::FieldsChannel::Voice => FieldsChannel::Voice,
crate::FieldsChannel::Slowmode => FieldsChannel::Slowmode,
}
}
}
@@ -1423,14 +1421,6 @@ impl From<FieldsMessage> for crate::FieldsMessage {
}
}
impl From<crate::VoiceInformation> for VoiceInformation {
fn from(value: crate::VoiceInformation) -> Self {
VoiceInformation {
max_users: value.max_users,
}
}
}
impl From<VoiceInformation> for crate::VoiceInformation {
fn from(value: VoiceInformation) -> Self {
crate::VoiceInformation {
@@ -1439,151 +1429,10 @@ impl From<VoiceInformation> for crate::VoiceInformation {
}
}
impl From<crate::AuditLogEntryAction> for AuditLogEntryAction {
fn from(value: crate::AuditLogEntryAction) -> Self {
match value {
crate::AuditLogEntryAction::MessageDelete { author, channel } => {
AuditLogEntryAction::MessageDelete { author, channel }
}
crate::AuditLogEntryAction::BanCreate { user } => {
AuditLogEntryAction::BanCreate { user }
}
crate::AuditLogEntryAction::BanDelete { user } => {
AuditLogEntryAction::BanDelete { user }
}
crate::AuditLogEntryAction::ChannelCreate { channel, name } => {
AuditLogEntryAction::ChannelCreate { channel, name }
}
crate::AuditLogEntryAction::MemberEdit {
user,
before,
after,
} => AuditLogEntryAction::MemberEdit {
user,
before: before.into(),
after: after.into(),
},
crate::AuditLogEntryAction::MemberKick { user } => {
AuditLogEntryAction::MemberKick { user }
}
crate::AuditLogEntryAction::ServerEdit { before, after } => {
AuditLogEntryAction::ServerEdit {
before: before.into(),
after: after.into(),
}
}
crate::AuditLogEntryAction::RoleEdit {
role,
before,
after,
} => AuditLogEntryAction::RoleEdit {
role,
before: before.into(),
after: after.into(),
},
crate::AuditLogEntryAction::RoleCreate { role, name } => {
AuditLogEntryAction::RoleCreate { role, name }
}
crate::AuditLogEntryAction::RoleDelete { role, name } => {
AuditLogEntryAction::RoleDelete { role, name }
}
crate::AuditLogEntryAction::RolesReorder { before, after } => {
AuditLogEntryAction::RolesReorder { before, after }
}
crate::AuditLogEntryAction::MessageBulkDelete { channel, count } => {
AuditLogEntryAction::MessageBulkDelete { channel, count }
}
crate::AuditLogEntryAction::ChannelEdit {
channel,
before,
after,
} => AuditLogEntryAction::ChannelEdit {
channel,
before: before.into(),
after: after.into(),
},
crate::AuditLogEntryAction::ChannelRolePermissionsEdit {
channel,
role,
permissions,
} => AuditLogEntryAction::ChannelRolePermissionsEdit {
channel,
role,
permissions: permissions.into(),
},
crate::AuditLogEntryAction::ChannelDelete { channel, name } => {
AuditLogEntryAction::ChannelDelete { channel, name }
}
crate::AuditLogEntryAction::InviteDelete { invite, channel } => {
AuditLogEntryAction::InviteDelete { invite, channel }
}
crate::AuditLogEntryAction::WebhookCreate {
webhook,
name,
channel,
} => AuditLogEntryAction::WebhookCreate {
webhook,
name,
channel,
},
crate::AuditLogEntryAction::WebhookDelete {
webhook,
name,
channel,
} => AuditLogEntryAction::WebhookDelete {
webhook,
name,
channel,
},
crate::AuditLogEntryAction::EmojiCreate { emoji, name } => {
AuditLogEntryAction::EmojiCreate { emoji, name }
}
crate::AuditLogEntryAction::EmojiUpdate {
emoji,
before,
after,
} => AuditLogEntryAction::EmojiUpdate {
emoji,
before: before.into(),
after: after.into(),
},
crate::AuditLogEntryAction::EmojiDelete { emoji, name } => {
AuditLogEntryAction::EmojiDelete { emoji, name }
}
crate::AuditLogEntryAction::MessagePin {
message,
author,
channel,
} => AuditLogEntryAction::MessagePin {
message,
author,
channel,
},
crate::AuditLogEntryAction::MessageUnpin {
message,
author,
channel,
} => AuditLogEntryAction::MessageUnpin {
message,
author,
channel,
},
crate::AuditLogEntryAction::InviteCreate { invite, channel } => {
AuditLogEntryAction::InviteCreate { invite, channel }
}
}
}
}
impl From<crate::AuditLogEntry> for AuditLogEntry {
fn from(value: crate::AuditLogEntry) -> Self {
AuditLogEntry {
id: value.id,
server: value.server,
reason: value.reason,
user: value.user,
target: value.target,
action: value.action.into(),
impl From<crate::VoiceInformation> for VoiceInformation {
fn from(value: crate::VoiceInformation) -> Self {
VoiceInformation {
max_users: value.max_users,
}
}
}
@@ -1676,9 +1525,3 @@ impl From<WebPushSubscription> for crate::WebPushSubscription {
}
}
}
impl From<crate::PartialEmoji> for PartialEmoji {
fn from(value: crate::PartialEmoji) -> Self {
PartialEmoji { name: value.name }
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ use revolt_result::{create_error, Result};
#[cfg(feature = "rocket-impl")]
use revolt_result::Error;
use tokio::sync::Mutex;
use async_std::sync::Mutex;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@@ -159,17 +159,4 @@ impl VoiceClient {
.await
.to_internal_error()
}
pub async fn get_room_participants(
&self,
node: &str,
channel_id: &str,
) -> Result<Vec<ParticipantInfo>> {
let room = self.get_node(node)?;
room.client
.list_participants(channel_id)
.await
.to_internal_error()
}
}
-163
View File
@@ -1,163 +0,0 @@
use crate::v0::{Member, PartialChannel, PartialEmoji, PartialMember, PartialRole, PartialServer, User};
use revolt_permissions::Override;
auto_derived!(
/// Audit log entry
pub struct AuditLogEntry {
/// Unique ID
#[serde(rename = "_id")]
pub id: String,
/// The server the entry happened in
pub server: String,
/// User provided reason
pub reason: Option<String>,
/// User who ran the action
pub user: String,
/// User this action is targetting
pub target: Option<String>,
/// The action ran
pub action: AuditLogEntryAction,
}
/// Indivual action stored on the audit log
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum AuditLogEntryAction {
MessageDelete {
author: String,
channel: String,
},
MessageBulkDelete {
channel: String,
count: usize,
},
MessagePin {
message: String,
author: String,
channel: String,
},
MessageUnpin {
message: String,
author: String,
channel: String,
},
BanCreate {
user: String,
},
BanDelete {
user: String,
},
ChannelCreate {
channel: String,
name: String,
},
ChannelEdit {
channel: String,
before: PartialChannel,
after: PartialChannel,
},
ChannelRolePermissionsEdit {
channel: String,
role: String,
permissions: Override,
},
ChannelDelete {
channel: String,
name: String,
},
MemberEdit {
user: String,
before: PartialMember,
after: PartialMember,
},
MemberKick {
user: String,
},
ServerEdit {
before: PartialServer,
after: PartialServer,
},
RoleEdit {
role: String,
before: PartialRole,
after: PartialRole,
},
RoleCreate {
role: String,
name: String,
},
RoleDelete {
role: String,
name: String,
},
RolesReorder {
before: Vec<String>,
after: Vec<String>,
},
InviteCreate {
invite: String,
channel: String,
},
InviteDelete {
invite: String,
channel: String,
},
WebhookCreate {
webhook: String,
name: String,
channel: String,
},
WebhookDelete {
webhook: String,
name: String,
channel: String,
},
EmojiCreate {
emoji: String,
name: String,
},
EmojiUpdate {
emoji: String,
before: PartialEmoji,
after: PartialEmoji,
},
EmojiDelete {
emoji: String,
name: String,
},
}
/// Audit log query filters
#[cfg_attr(feature = "validator", derive(validator::Validate))]
#[cfg_attr(feature = "rocket", derive(rocket::FromForm))]
pub struct OptionsAuditLogQuery {
/// Filter by who ran the action
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub user: Option<String>,
/// Filter by who the action is targetting
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub target: Option<String>,
/// Filter by the action type
pub r#type: Option<Vec<String>>,
/// Entries before a certain entry id
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub before: Option<String>,
/// Entries after a certain entry id
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub after: Option<String>,
/// Maximum number of entries to fetch
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
pub limit: Option<i64>,
}
/// Response containing the audit log entries and the users involved
pub struct AuditLogQueryResponse {
/// List of audit logs
pub audit_logs: Vec<AuditLogEntry>,
/// List of users
pub users: Vec<User>,
/// List of members
pub members: Vec<Member>,
}
);
-1
View File
@@ -164,7 +164,6 @@ auto_derived!(
Icon,
DefaultPermissions,
Voice,
Slowmode,
}
/// New webhook information
-2
View File
@@ -1,4 +1,3 @@
mod audit_logs;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -19,7 +18,6 @@ mod accounts;
mod mfa_tickets;
mod sessions;
pub use audit_logs::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
+1 -1
View File
@@ -18,7 +18,7 @@ try-from-primitive = ["dep:num_enum"]
[dev-dependencies]
# Async
tokio = { workspace = true }
async-std = { workspace = true, features = ["attributes"] }
[dependencies]
# Core
@@ -100,11 +100,8 @@ pub enum ChannelPermission {
/// Mention roles
MentionRoles = 1 << 38,
/// Access server audit logs
ViewAuditLogs = 1 << 40,
// * Misc. permissions
// % Bits 41 to 52: free area
// % Bits 39 to 52: free area
// % Bits 53 to 64: do not use
// * Grant all permissions
+4 -4
View File
@@ -4,7 +4,7 @@ use crate::{
DEFAULT_PERMISSION_SERVER, DEFAULT_PERMISSION_VIEW_ONLY,
};
#[tokio::test]
#[async_std::test]
async fn validate_user_permissions() {
/// Scenario in which we are friends with a user
/// and we have a DM channel open with them
@@ -102,7 +102,7 @@ async fn validate_user_permissions() {
}
}
#[tokio::test]
#[async_std::test]
async fn validate_group_permissions() {
/// Scenario in which we are in a group channel with only talking permission
struct Scenario {}
@@ -202,7 +202,7 @@ async fn validate_group_permissions() {
}
}
#[tokio::test]
#[async_std::test]
async fn validate_server_permissions() {
/// Scenario in which we are in a server channel where:
/// - the server grants reading history and sending messages by default
@@ -314,7 +314,7 @@ async fn validate_server_permissions() {
}
}
#[tokio::test]
#[async_std::test]
async fn validate_timed_out_member() {
/// Scenario in which we are in a server that we have been timed out from
struct Scenario {}
+2 -2
View File
@@ -14,7 +14,7 @@ redis-is-patched = []
[dev-dependencies]
# Async
tokio = { workspace = true }
async-std = { workspace = true, features = ["attributes"] }
# Config for loading Redis URI
revolt-config = { workspace = true }
@@ -26,4 +26,4 @@ rand = { workspace = true }
once_cell = { workspace = true }
# Redis
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
redis-kiss = { workspace = true }
+1 -1
View File
@@ -195,7 +195,7 @@ mod tests {
use crate::{clear_region, create_session, delete_session, filter_online, is_online};
use rand::Rng;
#[tokio::test]
#[async_std::test]
async fn it_works() {
revolt_config::config().await;
-1
View File
@@ -90,7 +90,6 @@ impl IntoResponse for Error {
ErrorType::UnknownNode => StatusCode::BAD_REQUEST,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
ErrorType::HeaderTooLarge => StatusCode::BAD_REQUEST,
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
-1
View File
@@ -164,7 +164,6 @@ pub enum ErrorType {
FailedValidation {
error: String,
},
HeaderTooLarge,
OperationFailed,
IncorrectData {
with: String,
-1
View File
@@ -91,7 +91,6 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotConnected => Status::BadRequest,
ErrorType::UnknownNode => Status::BadRequest,
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::HeaderTooLarge => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest,
ErrorType::FileTooSmall => Status::UnprocessableEntity,
+1 -1
View File
@@ -18,7 +18,7 @@ tokio = { workspace = true }
futures = { workspace = true }
# Redis
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
redis-kiss = { workspace = true }
# RabbitMQ
lapin = { workspace = true }
+1 -1
View File
@@ -20,7 +20,7 @@ fcm_v1 = { workspace = true }
web-push = { workspace = true }
isahc = { workspace = true, features = ["json"], optional = true }
revolt_a2 = { workspace = true, features = ["ring"] }
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
redis-kiss = { workspace = true }
tokio = { workspace = true }
async-trait = { workspace = true }
ulid = { workspace = true }
+6 -1
View File
@@ -13,7 +13,7 @@ log = { workspace = true }
sentry = { workspace = true }
lru = { workspace = true }
ulid = { workspace = true }
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
redis-kiss = { workspace = true }
chrono = { workspace = true }
# Serde
@@ -27,6 +27,11 @@ rocket_empty = { workspace = true }
# Async
futures = { workspace = true }
async-std = { workspace = true, features = [
"tokio1",
"tokio02",
"attributes",
] }
# Core
revolt-result = { workspace = true, features = ["rocket"] }
+92 -91
View File
@@ -1,15 +1,19 @@
use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver};
use livekit_protocol::TrackType;
use revolt_database::{
AMQP, Database, PartialMessage, SystemMessage, events::client::EventV1, iso8601_timestamp::{Duration, Timestamp}, util::reference::Reference, voice::{
RoomMetadata, UserVoiceChannel, VoiceClient, create_voice_state, delete_channel_voice_state, delete_voice_state, get_call_notification_recipients, get_user_moved_from_voice, get_user_moved_to_voice, get_voice_channel_members, set_channel_call_started_system_message, take_channel_call_started_system_message, update_voice_state_tracks
}
events::client::EventV1,
iso8601_timestamp::{Duration, Timestamp},
util::reference::Reference,
voice::{
create_voice_state, delete_channel_voice_state, delete_voice_state,
get_user_moved_from_voice, get_user_moved_to_voice, update_voice_state_tracks,
RoomMetadata, UserVoiceChannel, VoiceClient,
},
Database, AMQP,
};
use revolt_models::v0;
use revolt_result::{Result, ToRevoltError};
use rocket::{post, State};
use rocket_empty::EmptyResponse;
use ulid::Ulid;
use crate::guard::AuthHeader;
@@ -17,12 +21,12 @@ use crate::guard::AuthHeader;
pub async fn ingress(
db: &State<Database>,
voice_client: &State<VoiceClient>,
amqp: &State<AMQP>,
_amqp: &State<AMQP>,
node: &str,
auth_header: AuthHeader<'_>,
body: &str,
) -> Result<EmptyResponse> {
log::debug!("received event: {body}");
log::debug!("received event: {body:?}");
let config = revolt_config::config().await;
@@ -59,18 +63,16 @@ pub async fn ingress(
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let voice_channel = UserVoiceChannel {
let channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
let joined_at = Timestamp::UNIX_EPOCH
.checked_add(Duration::seconds(event.created_at))
.unwrap();
let voice_state = create_voice_state(&voice_channel, user_id, joined_at).await?;
let voice_state = create_voice_state(&channel, user_id, joined_at).await?;
// Only publish one event when a user is moved from one channel to another.
if let Some(moved_from) = get_user_moved_to_voice(channel_id, user_id).await? {
@@ -91,66 +93,63 @@ pub async fn ingress(
.await;
};
let participants = voice_client.get_room_participants(node, channel_id).await?;
// TODO: fix `num_participants` being incorrect sometimes see (#457)
// First user who joined - send call started system message.
// if event.room.as_ref().unwrap().num_participants == 1 {
// let user = Reference::from_unchecked(user_id).as_user(db).await?;
if participants.len() == 1 {
let user = Reference::from_unchecked(user_id).as_user(db).await?;
let message_id = Ulid::from_datetime(
Timestamp::UNIX_EPOCH
.checked_add(Duration::seconds(event.created_at))
.unwrap()
.into(),
)
.to_string();
// let message_id =
// Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap())
// .to_string();
let mut call_started_message = SystemMessage::CallStarted {
by: user_id.to_string(),
finished_at: None,
}
.into_message(channel_id.clone());
// let mut call_started_message = SystemMessage::CallStarted {
// by: user_id.to_string(),
// finished_at: None,
// }
// .into_message(channel.id().to_string());
call_started_message.id = message_id;
// call_started_message.id = message_id;
set_channel_call_started_system_message(channel_id, &call_started_message.id)
.await?;
// set_channel_call_started_system_message(channel.id(), &call_started_message.id)
// .await?;
call_started_message
.send(
db,
Some(amqp),
v0::MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
&channel,
false,
)
.await?;
// call_started_message
// .send(
// db,
// Some(amqp),
// v0::MessageAuthor::System {
// username: &user.username,
// avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
// },
// None,
// None,
// &channel,
// false,
// )
// .await?;
let recipients = get_call_notification_recipients(channel_id, user_id).await?;
let now = joined_at.format_short().to_string();
// let recipients = get_call_notification_recipients(&channel_id, &user_id).await?;
// let now = joined_at.format_short().to_string();
if let Err(e) = amqp
.dm_call_updated(&user.id, channel_id, Some(&now), false, recipients)
.await
{
revolt_config::capture_error(&e);
}
}
// if let Err(e) = amqp
// .dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients)
// .await
// {
// revolt_config::capture_error(&e);
// }
// }
}
// User left a channel
"participant_left" => {
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let voice_channel = UserVoiceChannel {
let channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
delete_voice_state(&voice_channel, user_id).await?;
delete_voice_state(&channel, user_id).await?;
// Dont send leave event when a user is moved
if get_user_moved_from_voice(channel_id, user_id)
@@ -165,47 +164,49 @@ pub async fn ingress(
.await;
};
// See above for why this is commented out
// // Update CallStarted system message if everyone has left with the end time
let members = get_voice_channel_members(&voice_channel).await?;
// let members = get_voice_channel_members(channel_id).await?;
if members.is_none_or(|m| m.is_empty()) {
// The channel is empty so send out an "end" message for ringing
if let Err(e) = amqp
.dm_call_updated(user_id, channel_id, None, true, None)
.await
{
revolt_config::capture_internal_error!(&e);
}
// if members.is_none_or(|m| m.is_empty()) {
// // The channel is empty so send out an "end" message for ringing
// if let Err(e) = amqp
// .dm_call_updated(user_id, channel_id, None, true, None)
// .await
// {
// revolt_config::capture_internal_error!(&e);
// }
if let Some(system_message_id) =
take_channel_call_started_system_message(channel_id).await?
{
// Could have been deleted
if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
.as_message(db)
.await
{
if let Some(SystemMessage::CallStarted { finished_at, .. }) =
&mut message.system
{
*finished_at = Some(Timestamp::now_utc());
// if let Some(system_message_id) =
// take_channel_call_started_system_message(channel_id).await?
// {
// // Could have been deleted
// if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
// .as_message(db)
// .await
// {
// if let Some(SystemMessage::CallStarted { finished_at, .. }) =
// &mut message.system
// {
// *finished_at = Some(Timestamp::now_utc());
message
.update(
db,
PartialMessage {
system: message.system.clone(),
..Default::default()
},
Vec::new(),
)
.await?;
} else {
log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
}
};
};
}
// message
// .update(
// db,
// PartialMessage {
// system: message.system.clone(),
// ..Default::default()
// },
// Vec::new(),
// )
// .await?;
// } else {
// log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
// }
// };
// };
// }
}
// Audio/video track was started/stopped/unmuted/muted
"track_published" | "track_unpublished" | "track_unmuted" | "track_muted" => {
+6 -2
View File
@@ -11,7 +11,7 @@ publish = false
[dependencies]
# Test
rand = { workspace = true }
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
redis-kiss = { workspace = true }
# Utility
lru = { workspace = true }
@@ -42,7 +42,11 @@ futures = { workspace = true }
chrono = { workspace = true }
async-channel = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
tokio = { workspace = true }
async-std = { workspace = true, features = [
"tokio1",
"tokio02",
"attributes",
] }
# internal util
lettre = { workspace = true }
@@ -2,7 +2,7 @@
//! POST /account/create
use std::time::Duration;
use tokio::time::sleep;
use async_std::task::sleep;
use revolt_config::config;
use revolt_database::{
util::{
@@ -2,7 +2,7 @@
//! POST /account/reverify
use std::time::Duration;
use tokio::time::sleep;
use async_std::task::sleep;
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use revolt_result::Result;
@@ -2,7 +2,7 @@
//! POST /account/reset_password
use std::time::Duration;
use tokio::time::sleep;
use async_std::task::sleep;
use rocket::serde::json::Json;
use rocket::State;
use rocket_empty::EmptyResponse;
@@ -4,7 +4,7 @@ use revolt_database::{
delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel,
UserVoiceChannel, VoiceClient,
},
AuditLogEntryAction, Channel, Database, PartialChannel, User, AMQP,
Channel, Database, PartialChannel, User, AMQP,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -12,8 +12,6 @@ use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Close Channel
///
/// Deletes a server channel, leaves a group or closes a group.
@@ -24,7 +22,6 @@ pub async fn delete(
voice_client: &State<VoiceClient>,
amqp: &State<AMQP>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
options: v0::OptionsChannelDelete,
) -> Result<EmptyResponse> {
@@ -66,17 +63,10 @@ pub async fn delete(
remove_user_from_voice_channel(voice_client, &user_voice_channel, &user.id).await?;
};
}
Channel::TextChannel { name, server, .. } => {
Channel::TextChannel { .. } => {
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
channel.delete(db).await?;
AuditLogEntryAction::ChannelDelete {
channel: channel.id().to_string(),
name: name.clone(),
}
.insert(db, server.clone(), reason, user.id, None)
.await;
delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
}
};
@@ -1,8 +1,7 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{delete_voice_channel, UserVoiceChannel, VoiceClient},
AuditLogEntryAction, Channel, Database, FieldsChannel, File, PartialChannel, SystemMessage,
User, AMQP,
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -10,8 +9,6 @@ use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Edit Channel
///
/// Edit a channel object by its id.
@@ -22,7 +19,6 @@ pub async fn edit(
voice_client: &State<VoiceClient>,
amqp: &State<AMQP>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
data: Json<v0::DataEditChannel>,
) -> Result<Json<v0::Channel>> {
@@ -95,8 +91,6 @@ pub async fn edit(
.ok();
}
let before_channel = channel.clone();
match &mut channel {
Channel::Group {
id,
@@ -227,9 +221,6 @@ pub async fn edit(
v0::FieldsChannel::Voice => {
voice.take();
}
v0::FieldsChannel::Slowmode => {
slowmode.take();
}
_ => {}
}
}
@@ -267,33 +258,17 @@ pub async fn edit(
_ => return Err(create_error!(InvalidOperation)),
};
let remove = data
.remove
.into_iter()
.map(|f| f.into())
.collect::<Vec<FieldsChannel>>();
let before = if before_channel.server().is_some() {
Some(before_channel.generate_diff(&partial, &remove))
} else {
None
};
channel.update(db, partial.clone(), remove).await?;
channel
.update(
db,
partial,
data.remove.into_iter().map(|f| f.into()).collect(),
)
.await?;
if channel.voice().is_none() {
delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
}
if let Some(before) = before {
AuditLogEntryAction::ChannelEdit {
channel: channel.id().to_string(),
before,
after: partial,
}
.insert(db, channel.server().unwrap().to_string(), reason, user.id, None)
.await;
};
Ok(Json(channel.into()))
}
@@ -1,7 +1,5 @@
use revolt_database::{
util::reference::Reference,
voice::{is_in_voice_channel, remove_user_from_voice_channel, UserVoiceChannel, VoiceClient},
Channel, Database, User, AMQP,
AMQP, Channel, Database, User, util::reference::Reference, voice::{UserVoiceChannel, VoiceClient, is_in_voice_channel, remove_user_from_voice_channel}
};
use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result};
@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, Invite, User,
Database, Invite, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -8,8 +8,6 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::util::audit_log_reason::AuditLogReason;
/// # Create Invite
///
/// Creates an invite to this channel.
@@ -20,7 +18,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn create_invite(
db: &State<Database>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
) -> Result<Json<v0::Invite>> {
if user.bot.is_some() {
@@ -33,16 +30,8 @@ pub async fn create_invite(
.await
.throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
let invite = Invite::create_channel_invite(db, &user, &channel).await?;
if let Some(server_id) = channel.server() {
AuditLogEntryAction::InviteCreate {
invite: invite.code().to_string(),
channel: channel.id().to_string(),
}
.insert(db, server_id.to_string(), reason, user.id, None)
.await;
}
Ok(Json(invite.into()))
Invite::create_channel_invite(db, &user, &channel)
.await
.map(|invite| invite.into())
.map(Json)
}
@@ -2,7 +2,7 @@ use std::time::Duration;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, Message, User,
Database, Message, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -11,8 +11,6 @@ use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Bulk Delete Messages
///
/// Delete multiple messages you've sent or one you have permission to delete.
@@ -25,7 +23,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn bulk_delete_messages(
db: &State<Database>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
options: Json<v0::OptionsBulkDelete>,
) -> Result<EmptyResponse> {
@@ -54,16 +51,7 @@ pub async fn bulk_delete_messages(
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
Message::bulk_delete(db, target.id, options.ids.clone()).await?;
if let Some(server) = channel.server() {
AuditLogEntryAction::MessageBulkDelete {
channel: channel.id().to_string(),
count: options.ids.len(),
}
.insert(db, server.to_string(), reason, user.id, None)
.await;
};
Ok(EmptyResponse)
Message::bulk_delete(db, target.id, options.ids)
.await
.map(|_| EmptyResponse)
}
@@ -1,14 +1,12 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, User,
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Delete Message
///
/// Delete a message you've sent or one you have permission to delete.
@@ -17,40 +15,18 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn delete(
db: &State<Database>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
msg: Reference<'_>,
) -> Result<EmptyResponse> {
let message = msg.as_message_in_channel(db, target.id).await?;
let channel = if message.author != user.id {
if message.author != user.id {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
Some(channel)
} else {
None
};
message.delete(db).await?;
if let Some(server) = channel.and_then(|c| c.server().map(|s| s.to_string())) {
AuditLogEntryAction::MessageDelete {
author: message.author.clone(),
channel: message.channel.clone(),
}
.insert(
db,
server.to_string(),
reason,
user.id.clone(),
Some(message.author),
)
.await;
};
Ok(EmptyResponse)
message.delete(db).await.map(|_| EmptyResponse)
}
@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Channel, Database, PartialMessage, SystemMessage, User, AMQP,
Channel, Database, PartialMessage, SystemMessage, User, AMQP,
};
use revolt_models::v0::MessageAuthor;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -8,8 +8,6 @@ use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Pins a message
///
/// Pins a message by its id.
@@ -19,7 +17,6 @@ pub async fn message_pin(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
msg: Reference<'_>,
) -> Result<EmptyResponse> {
@@ -68,22 +65,6 @@ pub async fn message_pin(
)
.await?;
if let Some(server_id) = channel.server() {
AuditLogEntryAction::MessagePin {
message: message.id.clone(),
author: message.author.clone(),
channel: message.channel.clone(),
}
.insert(
db,
server_id.to_string(),
reason,
user.id,
Some(message.author),
)
.await;
}
Ok(EmptyResponse)
}
@@ -1,7 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Channel, Database, FieldsMessage, PartialMessage, SystemMessage, User,
AMQP,
Channel, Database, FieldsMessage, PartialMessage, SystemMessage, User, AMQP,
};
use revolt_models::v0::MessageAuthor;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -9,8 +8,6 @@ use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Unpins a message
///
/// Unpins a message by its id.
@@ -20,7 +17,6 @@ pub async fn message_unpin(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
msg: Reference<'_>,
) -> Result<EmptyResponse> {
@@ -62,22 +58,6 @@ pub async fn message_unpin(
)
.await?;
if let Some(server_id) = channel.server() {
AuditLogEntryAction::MessageUnpin {
message: message.id.clone(),
author: message.author.clone(),
channel: message.channel.clone(),
}
.insert(
db,
server_id.to_string(),
reason,
user.id,
Some(message.author),
)
.await;
}
Ok(EmptyResponse)
}
@@ -1,15 +1,11 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{sync_voice_permissions, VoiceClient},
AuditLogEntryAction, Database, User,
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, User
};
use revolt_models::v0;
use revolt_permissions::{ChannelPermission, Override, OverrideField, PermissionQuery, calculate_channel_permissions};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override, PermissionQuery};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::util::audit_log_reason::AuditLogReason;
/// # Set Role Permission
///
/// Sets permissions for the specified role in this channel.
@@ -21,15 +17,13 @@ pub async fn set_role_permissions(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
role_id: String,
data: Json<v0::DataSetRolePermissions>,
) -> Result<Json<v0::Channel>> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions: revolt_permissions::PermissionValue =
calculate_channel_permissions(&mut query).await;
let permissions: revolt_permissions::PermissionValue = calculate_channel_permissions(&mut query).await;
query.set_server_from_channel().await;
@@ -49,23 +43,12 @@ pub async fn set_role_permissions(
.await?;
let mut new_channel = channel.clone();
let override_field: OverrideField = data.permissions.clone().into();
let server_id = server.id.clone();
new_channel
.set_role_permission(db, &role_id, data.permissions.clone().into())
.await?;
sync_voice_permissions(db, voice_client, &new_channel, Some(server), Some(&role_id))
.await?;
AuditLogEntryAction::ChannelRolePermissionsEdit {
channel: new_channel.id().to_string(),
role: role_id,
permissions: override_field,
}
.insert(db, server_id, reason, user.id, None)
.await;
sync_voice_permissions(db, voice_client, &new_channel, Some(server), Some(&role_id)).await?;
Ok(Json(new_channel.into()))
} else {
@@ -1,15 +1,11 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{sync_voice_permissions, VoiceClient},
AuditLogEntryAction, Channel, Database, PartialChannel, User,
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Channel, Database, PartialChannel, User
};
use revolt_models::v0::{self, DataDefaultChannelPermissions};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::util::audit_log_reason::AuditLogReason;
/// # Set Default Permission
///
/// Sets permissions for the default role in this channel.
@@ -21,7 +17,6 @@ pub async fn set_default_channel_permissions(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
data: Json<v0::DataDefaultChannelPermissions>,
) -> Result<Json<v0::Channel>> {
@@ -51,8 +46,6 @@ pub async fn set_default_channel_permissions(
}
}
Channel::TextChannel {
id,
server,
default_permissions,
..
} => {
@@ -61,25 +54,16 @@ pub async fn set_default_channel_permissions(
.throw_permission_override(default_permissions.map(|x| x.into()), &field)
.await?;
let partial = PartialChannel {
default_permissions: Some(field.into()),
..Default::default()
};
let id = id.clone();
let server = server.clone();
let before = channel.generate_diff(&partial, &[]);
channel.update(db, partial.clone(), vec![]).await?;
AuditLogEntryAction::ChannelEdit {
channel: id,
before,
after: partial,
}
.insert(db, server, reason, user.id, None)
.await;
channel
.update(
db,
PartialChannel {
default_permissions: Some(field.into()),
..Default::default()
},
vec![],
)
.await?;
} else {
return Err(create_error!(InvalidOperation));
}
@@ -89,7 +73,7 @@ pub async fn set_default_channel_permissions(
let server = match channel.server() {
Some(server_id) => Some(Reference::from_unchecked(server_id).as_server(db).await?),
None => None,
None => None
};
sync_voice_permissions(db, voice_client, &channel, server.as_ref(), None).await?;
@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Channel, Database, File, User, Webhook,
Channel, Database, File, User, Webhook,
};
use revolt_models::v0;
use revolt_permissions::{
@@ -11,8 +11,6 @@ use rocket::{serde::json::Json, State};
use ulid::Ulid;
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Creates a webhook
///
/// Creates a webhook which 3rd party platforms can use to send messages
@@ -21,7 +19,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn create_webhook(
db: &State<Database>,
user: User,
reason: AuditLogReason,
channel_id: Reference<'_>,
data: Json<v0::CreateWebhookBody>,
) -> Result<Json<v0::Webhook>> {
@@ -54,7 +51,7 @@ pub async fn create_webhook(
id: webhook_id,
name: data.name,
avatar,
creator_id: user.id.clone(),
creator_id: user.id,
channel_id: channel.id().to_string(),
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
token: Some(nanoid::nanoid!(64)),
@@ -62,15 +59,5 @@ pub async fn create_webhook(
webhook.create(db).await?;
if let Some(server_id) = channel.server() {
AuditLogEntryAction::WebhookCreate {
webhook: webhook.id.clone(),
name: webhook.name.clone(),
channel: webhook.channel_id.clone(),
}
.insert(db, server_id.to_string(), reason, user.id, None)
.await;
};
Ok(Json(webhook.into()))
}
@@ -1,5 +1,5 @@
use revolt_config::config;
use revolt_database::{AuditLogEntryAction, Database, Emoji, File, User, util::permissions::DatabasePermissionQuery};
use revolt_database::{util::permissions::DatabasePermissionQuery, Database, Emoji, File, User};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
@@ -7,8 +7,6 @@ use validator::Validate;
use rocket::{serde::json::Json, State};
use crate::util::audit_log_reason::AuditLogReason;
/// # Create New Emoji
///
/// Create an emoji by its Autumn upload id.
@@ -17,7 +15,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn create_emoji(
db: &State<Database>,
user: User,
reason: AuditLogReason,
emoji_id: String,
data: Json<v0::DataCreateEmoji>,
) -> Result<Json<v0::Emoji>> {
@@ -58,8 +55,8 @@ pub async fn create_emoji(
// Create the emoji object
let emoji = Emoji {
id: emoji_id,
parent: data.parent.clone().into(),
creator_id: user.id.clone(),
parent: data.parent.into(),
creator_id: user.id,
name: data.name,
animated: "image/gif" == &attachment.content_type,
nsfw: data.nsfw,
@@ -67,12 +64,5 @@ pub async fn create_emoji(
// Save emoji
emoji.create(db).await?;
if let v0::EmojiParent::Server { id: server_id } = data.parent {
AuditLogEntryAction::EmojiCreate { emoji: emoji.id.clone(), name: emoji.name.clone() }
.insert(db, server_id, reason, user.id, None)
.await;
}
Ok(Json(emoji.into()))
}
@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, EmojiParent, User,
Database, EmojiParent, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::Result;
@@ -8,8 +8,6 @@ use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Delete Emoji
///
/// Delete an emoji by its id.
@@ -18,7 +16,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn delete_emoji(
db: &State<Database>,
user: User,
reason: AuditLogReason,
emoji_id: Reference<'_>,
) -> Result<EmptyResponse> {
// Fetch the emoji
@@ -42,16 +39,5 @@ pub async fn delete_emoji(
}
// Delete the emoji
emoji.delete(db).await?;
if let EmojiParent::Server { id: server_id } = emoji.parent {
AuditLogEntryAction::EmojiDelete {
emoji: emoji.id,
name: emoji.name,
}
.insert(db, server_id, reason, user.id, Some(emoji.creator_id))
.await;
};
Ok(EmptyResponse)
emoji.delete(db).await.map(|_| EmptyResponse)
}
@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, EmojiParent, PartialEmoji, User,
Database, EmojiParent, PartialEmoji, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -8,8 +8,6 @@ use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Edit Emoji
///
/// Edit an emoji by its id.
@@ -18,7 +16,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn edit_emoji(
db: &State<Database>,
user: User,
reason: AuditLogReason,
emoji_id: Reference<'_>,
data: Json<v0::DataEditEmoji>,
) -> Result<Json<v0::Emoji>> {
@@ -47,24 +44,8 @@ pub async fn edit_emoji(
return Ok(Json(emoji.into()));
}
let partial = PartialEmoji {
name: data.name,
..Default::default()
};
let before = emoji.generate_diff(&partial);
emoji.update(db, partial.clone()).await?;
if let EmojiParent::Server { id: server_id } = emoji.parent.clone() {
AuditLogEntryAction::EmojiUpdate {
emoji: emoji.id.clone(),
before,
after: partial,
}
.insert(db, server_id, reason, user.id, Some(emoji.creator_id.clone()))
.await;
};
let partial = PartialEmoji { name: data.name };
emoji.update(db, partial).await?;
Ok(Json(emoji.into()))
}
@@ -1,56 +1,35 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, Invite, User,
Database, Invite, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Delete Invite
///
/// Delete an invite by its id.
#[openapi(tag = "Invites")]
#[delete("/<target>")]
pub async fn delete(
db: &State<Database>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
) -> Result<EmptyResponse> {
pub async fn delete(db: &State<Database>, user: User, target: Reference<'_>) -> Result<EmptyResponse> {
let invite = target.as_invite(db).await?;
if user.id == invite.creator() {
db.delete_invite(invite.code()).await?;
db.delete_invite(invite.code()).await
} else {
match invite {
Invite::Server {
code,
server,
channel,
creator,
..
} => {
Invite::Server { code, server, .. } => {
let server = db.fetch_server(&server).await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?;
db.delete_invite(&code).await?;
AuditLogEntryAction::InviteDelete {
invite: code,
channel,
}
.insert(db, server.id, reason, user.id, Some(creator))
.await;
db.delete_invite(&code).await
}
_ => unreachable!(),
}
}
Ok(EmptyResponse)
.map(|_| EmptyResponse)
}
@@ -1,227 +0,0 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntry, AuditLogQuery, Database, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Audit Log Query
///
/// Queries a server's audit logs.
#[openapi(tag = "Audit Logs")]
#[get("/<target>/audit_logs?<options..>")]
pub async fn query(
db: &State<Database>,
user: User,
target: Reference<'_>,
options: v0::OptionsAuditLogQuery,
) -> Result<Json<v0::AuditLogQueryResponse>> {
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let server = target.as_server(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ViewAuditLogs)?;
let v0::OptionsAuditLogQuery {
user: user_filter,
target,
r#type,
before,
after,
limit,
} = options;
let audit_logs = db
.get_server_audit_logs(
&server.id,
AuditLogQuery {
user: user_filter,
target,
r#type,
before,
after,
limit: limit.unwrap_or(50),
},
)
.await?;
let (users, members) = AuditLogEntry::with_users(db, &server.id, &user, &audit_logs).await?;
Ok(Json(v0::AuditLogQueryResponse {
audit_logs: audit_logs.into_iter().map(Into::into).collect(),
users,
members,
}))
}
#[cfg(test)]
mod test {
use revolt_database::{Member, Server};
use revolt_models::v0;
use rocket::http::{Header, Status};
use crate::util::test::TestHarness;
#[rocket::async_test]
async fn audit_log_query() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (server, channels) = Server::create(
&harness.db,
v0::DataCreateServer {
name: "Test Server".to_string(),
..Default::default()
},
&user,
true,
)
.await
.expect("Failed to create test server.");
Member::create(&harness.db, &server, &user, None).await.unwrap();
let channel = &channels[0];
let status = harness
.client
.patch(format!("/channels/{}", channel.id()))
.header(Header::new("X-Audit-Log-Reason", "Test Reason 1"))
.header(Header::new("x-session-token", session.token.clone()))
.json(&v0::DataEditChannel {
description: Some("General chat channel.".to_string()),
name: None,
owner: None,
icon: None,
nsfw: None,
archived: None,
voice: None,
slowmode: None,
remove: Vec::new(),
})
.dispatch()
.await
.status();
assert_eq!(status, Status::Ok);
let status = harness
.client
.patch(format!("/channels/{}", channel.id()))
.header(Header::new("X-Audit-Log-Reason", "Test Reason 2"))
.header(Header::new("x-session-token", session.token.clone()))
.json(&v0::DataEditChannel {
description: Some("New description.".to_string()),
name: None,
owner: None,
icon: None,
nsfw: None,
archived: None,
voice: None,
slowmode: None,
remove: Vec::new(),
})
.dispatch()
.await
.status();
assert_eq!(status, Status::Ok);
let status = harness
.client
.delete(format!("/channels/{}", channel.id()))
.header(Header::new("X-Audit-Log-Reason", "Test Reason 3"))
.header(Header::new("x-session-token", session.token.clone()))
.dispatch()
.await
.status();
assert_eq!(status, Status::NoContent);
let response = harness
.client
.get(format!(
"/servers/{}/audit_logs?include_users=true",
&server.id
))
.header(Header::new("x-session-token", session.token.clone()))
.dispatch()
.await
.into_json::<v0::AuditLogQueryResponse>()
.await
.expect("Failed to deserialise audit_logs response");
let v0::AuditLogQueryResponse {
audit_logs: entries,
users,
members,
} = response;
assert_eq!(entries.len(), 3);
assert_eq!(users.len(), 1);
assert_eq!(members.len(), 1);
assert_eq!(&users[0].id, &user.id);
let entry = &entries[0];
assert_eq!(entry.reason.as_deref(), Some("Test Reason 3"));
assert_eq!(&entry.server, &server.id);
assert_eq!(&entry.user, &user.id);
assert_eq!(
&entry.action,
&v0::AuditLogEntryAction::ChannelDelete {
channel: channel.id().to_string(),
name: "General".to_string()
}
);
let entry = &entries[1];
assert_eq!(entry.reason.as_deref(), Some("Test Reason 2"));
assert_eq!(&entry.server, &server.id);
assert_eq!(&entry.user, &user.id);
assert_eq!(
&entry.action,
&v0::AuditLogEntryAction::ChannelEdit {
channel: channel.id().to_string(),
before: v0::PartialChannel {
description: Some("General chat channel.".to_string()),
..Default::default()
},
after: v0::PartialChannel {
description: Some("New description.".to_string()),
..Default::default()
}
}
);
let entry = &entries[2];
assert_eq!(entry.reason.as_deref(), Some("Test Reason 1"));
assert_eq!(&entry.server, &server.id);
assert_eq!(&entry.user, &user.id);
assert_eq!(
&entry.action,
&v0::AuditLogEntryAction::ChannelEdit {
channel: channel.id().to_string(),
before: v0::PartialChannel {
description: None,
..Default::default()
},
after: v0::PartialChannel {
description: Some("General chat channel.".to_string()),
..Default::default()
}
}
);
}
}
+5 -20
View File
@@ -4,7 +4,7 @@ use revolt_database::{
get_user_voice_channel_in_server, remove_user_from_voice_channel, UserVoiceChannel,
VoiceClient,
},
AuditLogEntryAction, Database, Message, RemovalIntention, ServerBan, User,
Database, Message, RemovalIntention, ServerBan, User,
};
use revolt_models::v0;
use std::time::{Duration, SystemTime};
@@ -16,8 +16,6 @@ use rocket::{serde::json::Json, State};
use ulid::Ulid;
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Ban User
///
/// Ban a user by their id.
@@ -27,7 +25,6 @@ pub async fn ban(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
audit_log_reason: AuditLogReason,
server: Reference<'_>,
target: Reference<'_>,
data: Json<v0::DataBanCreate>,
@@ -88,20 +85,8 @@ pub async fn ban(
.await?;
}
}
let ban = ServerBan::create(db, &server, target.id, data.reason.clone()).await?;
AuditLogEntryAction::BanCreate {
user: target.id.to_string(),
}
.insert(
db,
server.id,
audit_log_reason.0.or(data.reason),
user.id,
Some(target.id.to_string()),
)
.await;
Ok(Json(ban.into()))
ServerBan::create(db, &server, target.id, data.reason)
.await
.map(Into::into)
.map(Json)
}
+2 -13
View File
@@ -1,14 +1,12 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, User,
Database, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Unban user
///
/// Remove a user's ban.
@@ -17,7 +15,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn unban(
db: &State<Database>,
user: User,
reason: AuditLogReason,
server: Reference<'_>,
target: Reference<'_>,
) -> Result<EmptyResponse> {
@@ -28,13 +25,5 @@ pub async fn unban(
.throw_if_lacking_channel_permission(ChannelPermission::BanMembers)?;
let ban = target.as_ban(db, &server.id).await?;
db.delete_ban(&ban.id).await?;
AuditLogEntryAction::BanDelete {
user: target.id.to_string(),
}
.insert(db, server.id, reason, user.id, Some(target.id.to_string()))
.await;
Ok(EmptyResponse)
db.delete_ban(&ban.id).await.map(|_| EmptyResponse)
}
@@ -1,5 +1,4 @@
use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::AuditLogEntryAction;
use revolt_database::{util::reference::Reference, Channel, Database, User};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -9,8 +8,6 @@ use rocket::serde::json::Json;
use rocket::State;
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Create Channel
///
/// Create a new Text or Voice channel.
@@ -19,7 +16,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn create_server_channel(
db: &State<Database>,
user: User,
reason: AuditLogReason,
server: Reference<'_>,
data: Json<v0::DataCreateServerChannel>,
) -> Result<Json<v0::Channel>> {
@@ -36,16 +32,8 @@ pub async fn create_server_channel(
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
let channel_name = data.name.clone();
let channel = Channel::create_server_channel(db, &mut server, data, true).await?;
AuditLogEntryAction::ChannelCreate {
channel: channel.id().to_string(),
name: channel_name,
}
.insert(db, server.id, reason, user.id, None)
.await;
Ok(Json(channel.into()))
Channel::create_server_channel(db, &mut server, data, true)
.await
.map(|channel| channel.into())
.map(Json)
}
+11 -40
View File
@@ -11,19 +11,15 @@ use revolt_database::{
set_user_moved_from_voice, set_user_moved_to_voice, sync_user_voice_permissions,
UserVoiceChannel, VoiceClient,
},
AuditLogEntryAction, Database, FieldsMember, File, PartialMember, User,
Database, File, PartialMember, User,
};
use revolt_models::v0;
use revolt_models::v0::{self, FieldsMember};
use revolt_permissions::{
calculate_channel_permissions, calculate_server_permissions, ChannelPermission, UserPermission,
};
use revolt_permissions::{calculate_channel_permissions, calculate_server_permissions, ChannelPermission, UserPermission};
use revolt_result::{create_error, Result};
use rocket::{form::validate::Contains, serde::json::Json, State};
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Edit Member
///
/// Edit a member by their id.
@@ -33,7 +29,6 @@ pub async fn edit(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
server_id: Reference<'_>,
member_id: Reference<'_>,
data: Json<v0::DataMemberEdit>,
@@ -81,7 +76,7 @@ pub async fn edit(
} else if data.remove.contains(&v0::FieldsMember::Avatar) {
permissions.throw_if_lacking_channel_permission(ChannelPermission::RemoveAvatars)?;
} else {
return Err(create_error!(InvalidOperation));
return Err(create_error!(InvalidOperation))
}
}
@@ -111,11 +106,11 @@ pub async fn edit(
permissions.throw_if_lacking_channel_permission(ChannelPermission::DeafenMembers)?;
}
if data.voice_channel.is_some() && data.remove.contains(&v0::FieldsMember::VoiceChannel) {
if data.voice_channel.is_some() && data.remove.contains(&FieldsMember::VoiceChannel) {
return Err(create_error!(InvalidOperation));
}
if data.voice_channel.is_some() || data.remove.contains(&v0::FieldsMember::VoiceChannel) {
if data.voice_channel.is_some() || data.remove.contains(&FieldsMember::VoiceChannel) {
if !voice_client.is_enabled() {
return Err(create_error!(LiveKitUnavailable));
};
@@ -137,8 +132,7 @@ pub async fn edit(
Err(create_error!(UnknownChannel))?
}
let channel_permissions =
calculate_channel_permissions(&mut query.clone().channel(&channel)).await;
let channel_permissions = calculate_channel_permissions(&mut query.clone().channel(&channel)).await;
channel_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
if get_user_voice_channel_in_server(&target_user.id, &server.id)
@@ -216,28 +210,9 @@ pub async fn edit(
partial.avatar = Some(File::use_user_avatar(db, &avatar, &user.id, &user.id).await?);
}
let remove = remove
.into_iter()
.map(Into::into)
.collect::<Vec<FieldsMember>>();
let before = member.generate_diff(&partial, &remove);
member.update(db, partial.clone(), remove.clone()).await?;
AuditLogEntryAction::MemberEdit {
user: member.id.user.clone(),
before,
after: partial,
}
.insert(
db,
server.id.clone(),
reason,
user.id.clone(),
Some(member.id.user.clone()),
)
.await;
member
.update(db, partial, remove.clone().into_iter().map(Into::into).collect())
.await?;
if let Some(new_voice_channel) = new_voice_channel {
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
@@ -289,11 +264,7 @@ pub async fn edit(
.private(target_user.id.clone())
.await;
};
} else if can_publish.is_some()
|| can_receive.is_some()
|| remove.contains(FieldsMember::CanPublish)
|| remove.contains(FieldsMember::CanReceive)
{
} else if can_publish.is_some() || can_receive.is_some() || remove.contains(FieldsMember::CanPublish) || remove.contains(FieldsMember::CanReceive) {
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
{
let node = get_channel_node(&channel).await?.unwrap();
@@ -4,15 +4,13 @@ use revolt_database::{
get_user_voice_channel_in_server, remove_user_from_voice_channel, UserVoiceChannel,
VoiceClient,
},
AuditLogEntryAction, Database, RemovalIntention, User,
Database, RemovalIntention, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Kick Member
///
/// Removes a member from the server.
@@ -22,7 +20,6 @@ pub async fn kick(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
server_id: Reference<'_>,
member_id: Reference<'_>,
) -> Result<EmptyResponse> {
@@ -52,18 +49,6 @@ pub async fn kick(
.remove(db, &server, RemovalIntention::Kick, false)
.await?;
AuditLogEntryAction::MemberKick {
user: member.id.user.clone(),
}
.insert(
db,
server.id.clone(),
reason,
user.id,
Some(member.id.user.clone()),
)
.await;
if let Some(channel_id) = get_user_voice_channel_in_server(member_id.id, &server.id).await? {
remove_user_from_voice_channel(
voice_client,
+1 -3
View File
@@ -1,7 +1,6 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod audit_log_query;
mod ban_create;
mod ban_list;
mod ban_remove;
@@ -50,7 +49,6 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
permissions_set::set_role_permission,
permissions_set_default::set_default_server_permissions,
emoji_list::list_emoji,
roles_edit_positions::edit_role_ranks,
audit_log_query::query,
roles_edit_positions::edit_role_ranks
]
}
@@ -1,17 +1,13 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{sync_voice_permissions, VoiceClient},
AuditLogEntryAction, Database, PartialRole, User,
Database, User,
};
use revolt_models::v0;
use revolt_permissions::{
calculate_server_permissions, ChannelPermission, Override, OverrideField,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission, Override};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::util::audit_log_reason::AuditLogReason;
/// # Set Role Permission
///
/// Sets permissions for the specified role in the server.
@@ -21,7 +17,6 @@ pub async fn set_role_permission(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
role_id: String,
data: Json<v0::DataSetServerRolePermission>,
@@ -47,36 +42,20 @@ pub async fn set_role_permission(
}
// Ensure we have access to grant these permissions forwards
let current_override: Override = current_value.into();
let current_value: Override = current_value.into();
permissions
.throw_permission_override(current_override, &data.permissions)
.throw_permission_override(current_value, &data.permissions)
.await?;
let override_field: OverrideField = data.permissions.into();
server
.set_role_permission(db, &role_id, override_field)
.set_role_permission(db, &role_id, data.permissions.into())
.await?;
AuditLogEntryAction::RoleEdit {
role: role_id.clone(),
before: PartialRole {
permissions: Some(current_value),
..Default::default()
},
after: PartialRole {
permissions: Some(override_field),
..Default::default()
},
}
.insert(db, server.id.clone(), reason, user.id, None)
.await;
for channel_id in &server.channels {
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?;
}
};
Ok(Json(server.into()))
}
@@ -1,7 +1,5 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{sync_voice_permissions, VoiceClient},
AuditLogEntryAction, Database, PartialServer, User,
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, PartialServer, User
};
use revolt_models::v0;
use revolt_permissions::{
@@ -10,8 +8,6 @@ use revolt_permissions::{
use revolt_result::Result;
use rocket::{serde::json::Json, State};
use crate::util::audit_log_reason::AuditLogReason;
/// # Set Default Permission
///
/// Sets permissions for the default role in this server.
@@ -21,7 +17,6 @@ pub async fn set_default_server_permissions(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
data: Json<DataPermissionsValue>,
) -> Result<Json<v0::Server>> {
@@ -44,27 +39,22 @@ pub async fn set_default_server_permissions(
)
.await?;
let partial = PartialServer {
default_permissions: Some(data.permissions as i64),
..Default::default()
};
let before = server.generate_diff(&partial, &[]);
server.update(db, partial.clone(), vec![]).await?;
AuditLogEntryAction::ServerEdit {
before,
after: partial,
}
.insert(db, server.id.clone(), reason, user.id, None)
.await;
server
.update(
db,
PartialServer {
default_permissions: Some(data.permissions as i64),
..Default::default()
},
vec![],
)
.await?;
for channel_id in &server.channels {
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
sync_voice_permissions(db, voice_client, &channel, Some(&server), None).await?;
}
};
Ok(Json(server.into()))
}
@@ -1,7 +1,7 @@
use revolt_config::config;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, Role, User,
Database, Role, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -9,8 +9,6 @@ use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Create Role
///
/// Creates a new server role.
@@ -19,7 +17,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn create(
db: &State<Database>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
data: Json<v0::DataCreateRole>,
) -> Result<Json<v0::NewRoleResponse>> {
@@ -45,13 +42,6 @@ pub async fn create(
let role = Role::create(db, &server, data.name).await?;
AuditLogEntryAction::RoleCreate {
role: role.id.clone(),
name: role.name.clone(),
}
.insert(db, server.id, reason, user.id, None)
.await;
Ok(Json(v0::NewRoleResponse {
id: role.id.clone(),
role: role.into(),
@@ -1,15 +1,13 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{sync_voice_permissions, VoiceClient},
AuditLogEntryAction, Database, User,
Database, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Delete Role
///
/// Delete a server role by its id.
@@ -18,7 +16,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn delete(
db: &State<Database>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
role_id: String,
voice_client: &State<VoiceClient>,
@@ -42,13 +39,6 @@ pub async fn delete(
role.delete(db, &server.id).await?;
AuditLogEntryAction::RoleDelete {
role: role_id.clone(),
name: role.name,
}
.insert(db, server.id.clone(), reason, user.id, None)
.await;
for channel_id in &server.channels {
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
+8 -21
View File
@@ -1,7 +1,7 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{sync_voice_permissions, VoiceClient},
AuditLogEntryAction, Database, FieldsRole, PartialRole, User, File
Database, File, PartialRole, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -9,8 +9,6 @@ use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Edit Role
///
/// Edit a role by its id.
@@ -20,7 +18,6 @@ pub async fn edit(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
role_id: String,
data: Json<v0::DataEditRole>,
@@ -74,23 +71,13 @@ pub async fn edit(
..Default::default()
};
let remove = remove
.into_iter()
.map(Into::into)
.collect::<Vec<FieldsRole>>();
let before = role.generate_diff(&partial, &remove);
role.update(db, &server.id, partial.clone(), remove)
.await?;
AuditLogEntryAction::RoleEdit {
role: role_id.clone(),
before,
after: partial,
}
.insert(db, server.id.clone(), reason, user.id, None)
.await;
role.update(
db,
&server.id,
partial,
remove.into_iter().map(Into::into).collect(),
)
.await?;
for channel_id in &server.channels {
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
@@ -1,15 +1,11 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{sync_voice_permissions, VoiceClient},
AuditLogEntryAction, Database, User,
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, User
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use crate::util::audit_log_reason::AuditLogReason;
/// # Edits server roles ranks
///
/// Edit's server role's ranks.
@@ -19,7 +15,6 @@ pub async fn edit_role_ranks(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
data: Json<v0::DataEditRoleRanks>,
) -> Result<Json<v0::Server>> {
@@ -73,20 +68,13 @@ pub async fn edit_role_ranks(
}
}
server.set_role_ordering(db, new_order.clone()).await?;
AuditLogEntryAction::RolesReorder {
before: existing_order,
after: new_order,
}
.insert(db, server.id.clone(), reason, user.id, None)
.await;
server.set_role_ordering(db, new_order).await?;
for channel_id in &server.channels {
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
sync_voice_permissions(db, voice_client, &channel, Some(&server), None).await?;
}
};
Ok(Json(server.into()))
}
+4 -19
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, FieldsServer, File, PartialServer, User, ValidatedTicket
Database, File, PartialServer, User, ValidatedTicket,
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -10,8 +10,6 @@ use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Edit Server
///
/// Edit a server by its id.
@@ -20,7 +18,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn edit(
db: &State<Database>,
user: User,
reason: AuditLogReason,
target: Reference<'_>,
data: Json<v0::DataEditServer>,
validated_ticket: Option<ValidatedTicket>,
@@ -179,21 +176,9 @@ pub async fn edit(
partial.owner = Some(server.owner.clone());
}
let remove = remove
.into_iter()
.map(Into::into)
.collect::<Vec<FieldsServer>>();
let before = server.generate_diff(&partial, &remove);
server.update(db, partial.clone(), remove).await?;
AuditLogEntryAction::ServerEdit {
before,
after: partial,
}
.insert(db, server.id.clone(), reason, user.id, None)
.await;
server
.update(db, partial, remove.into_iter().map(Into::into).collect())
.await?;
Ok(Json(server.into()))
}
+1 -1
View File
@@ -3,7 +3,7 @@
use std::ops::Add;
use std::time::Duration;
use tokio::time::sleep;
use async_std::task::sleep;
use iso8601_timestamp::Timestamp;
use revolt_database::{
util::{email::normalise_email, password::assert_safe},
@@ -1,14 +1,12 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, User,
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
use crate::util::audit_log_reason::AuditLogReason;
/// # Deletes a webhook
///
/// Deletes a webhook
@@ -17,7 +15,6 @@ use crate::util::audit_log_reason::AuditLogReason;
pub async fn webhook_delete(
db: &State<Database>,
user: User,
reason: AuditLogReason,
webhook_id: Reference<'_>,
) -> Result<EmptyResponse> {
let webhook = webhook_id.as_webhook(db).await?;
@@ -28,24 +25,5 @@ pub async fn webhook_delete(
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageWebhooks)?;
webhook.delete(db).await?;
AuditLogEntryAction::WebhookDelete {
webhook: webhook.id,
name: webhook.name,
channel: webhook.channel_id,
}
.insert(
db,
channel
.server()
.expect("Webhook created on non server channel")
.to_string(),
reason,
user.id,
Some(webhook.creator_id),
)
.await;
Ok(EmptyResponse)
webhook.delete(db).await.map(|_| EmptyResponse)
}
-62
View File
@@ -1,62 +0,0 @@
use revolt_result::{create_error, Error};
use revolt_rocket_okapi::{OpenApiError, gen::OpenApiGenerator, request::{OpenApiFromRequest, RequestHeaderInput}, revolt_okapi::openapi3::{Parameter, ParameterValue}};
use rocket::{
http::Status,
request::{FromRequest, Outcome, Request},
};
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
/// Newtype for an audit log reason.
///
/// Extracts the reason from the `X-Audit-Log-Reason` header if provided.
pub struct AuditLogReason(pub Option<String>);
#[async_trait]
impl<'r> FromRequest<'r> for AuditLogReason {
type Error = Error;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let reason = req.headers().get_one("x-audit-log-reason");
if reason.is_some_and(|str| str.len() > 512) {
return Outcome::Error((Status::BadRequest, create_error!(HeaderTooLarge)));
};
Outcome::Success(Self(reason.map(|str| str.to_string())))
}
}
impl OpenApiFromRequest<'_> for AuditLogReason {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> Result<RequestHeaderInput, OpenApiError> {
Ok(RequestHeaderInput::Parameter(Parameter {
name: "X-Audit-Log-Reason".to_string(),
description: Some("Reason for action which is stored in the audit log.".to_string()),
allow_empty_value: false,
required: false,
deprecated: false,
extensions: schemars::Map::new(),
location: "header".to_string(),
value: ParameterValue::Schema {
allow_reserved: false,
example: None,
examples: None,
explode: None,
style: None,
schema: SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
..Default::default()
},
},
}))
}
}
impl From<AuditLogReason> for Option<String> {
fn from(value: AuditLogReason) -> Self {
value.0
}
}
-1
View File
@@ -1,4 +1,3 @@
pub mod audit_log_reason;
pub mod ratelimits;
#[cfg(test)]
+2 -2
View File
@@ -5,8 +5,8 @@ FROM debian:12 AS debian
# Bundle Stage
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=builder /home/rust/src/target/release/revolt-autumn ./
COPY --from=mwader/static-ffmpeg:7.1.1 /ffmpeg /usr/local/bin/
COPY --from=mwader/static-ffmpeg:7.1.1 /ffprobe /usr/local/bin/
COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/local/bin/
COPY --from=mwader/static-ffmpeg:7.0.2 /ffprobe /usr/local/bin/
COPY --from=debian /usr/bin/uname /usr/bin/uname
EXPOSE 14704
+38 -49
View File
@@ -10,7 +10,7 @@ use tokio::sync::RwLock;
pub mod types;
const TENOR_API_BASE_URL: &str = "https://compat.gifbox.me/v2";
const TENOR_API_BASE_URL: &str = "https://tenor.googleapis.com/v2";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TenorError {
@@ -59,11 +59,7 @@ impl Tenor {
}
}
pub async fn request<T: DeserializeOwned>(
&self,
path: &str,
query: &[Option<(&str, &str)>],
) -> Result<Arc<T>, TenorError> {
pub async fn request<T: DeserializeOwned>(&self, path: &str, query: &[Option<(&str, &str)>]) -> Result<Arc<T>, TenorError> {
let response = self
.client
.get(format!("{TENOR_API_BASE_URL}{path}"))
@@ -99,27 +95,24 @@ impl Tenor {
}
}
let res = self
.coalescion
.execute(unique_key.clone(), || async move {
self.request::<types::PaginatedMediaResponse>(
"/search",
&[
Some(("key", &self.key)),
Some(("q", query)),
Some(("client_key", "Gifbox")),
Some(("media_filter", "webm,tinywebm")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
Some(("limit", &limit.to_string())),
position.is_empty().then_some(("pos", position)),
is_category.then_some(("component", "categories")),
],
)
.await
})
.await
.unwrap();
let res = self.coalescion.execute(unique_key.clone(), || async move {
self.request::<types::PaginatedMediaResponse>(
"/search",
&[
Some(("key", &self.key)),
Some(("q", query)),
Some(("client_key", "Gifbox")),
Some(("media_filter", "webm,tinywebm")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
Some(("limit", &limit.to_string())),
position.is_empty().then_some(("pos", position)),
is_category.then_some(("component", "categories"))
]
).await
})
.await
.unwrap();
if let Ok(resp) = &*res {
self.cache.write().await.insert(unique_key, resp.clone());
@@ -150,9 +143,8 @@ impl Tenor {
Some(("client_key", "Gifbox")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
],
)
.await
]
).await
})
.await
.unwrap();
@@ -181,25 +173,22 @@ impl Tenor {
}
}
let res = self
.coalescion
.execute(unique_key.clone(), || async move {
self.request::<types::PaginatedMediaResponse>(
"/featured",
&[
Some(("key", &self.key)),
Some(("client_key", "Gifbox")),
Some(("media_filter", "webm,tinywebm")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
Some(("limit", &limit.to_string())),
position.is_empty().then_some(("pos", position)),
],
)
.await
})
.await
.unwrap();
let res = self.coalescion.execute(unique_key.clone(), || async move {
self.request::<types::PaginatedMediaResponse>(
"/featured",
&[
Some(("key", &self.key)),
Some(("client_key", "Gifbox")),
Some(("media_filter", "webm,tinywebm")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
Some(("limit", &limit.to_string())),
position.is_empty().then_some(("pos", position)),
]
).await
})
.await
.unwrap();
if let Ok(resp) = &*res {
self.featured.write().await.insert(unique_key, resp.clone());
+2 -2
View File
@@ -5,8 +5,8 @@ FROM debian:12 AS debian
# Bundle Stage
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=builder /home/rust/src/target/release/revolt-january ./
COPY --from=mwader/static-ffmpeg:7.1.1 /ffmpeg /usr/local/bin/
COPY --from=mwader/static-ffmpeg:7.1.1 /ffprobe /usr/local/bin/
COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/local/bin/
COPY --from=mwader/static-ffmpeg:7.0.2 /ffprobe /usr/local/bin/
COPY --from=debian /usr/bin/uname /usr/bin/uname
EXPOSE 14705
+11 -14
View File
@@ -419,22 +419,21 @@ impl Request {
}
pub async fn url_is_blacklisted(url: &Url) -> Result<IPRequest> {
let mut resolved_address: Option<IpAddr> = None;
let resolved_address: IpAddr;
if let Some(host) = url.host() {
match host {
Host::Ipv4(ipv4) => {
resolved_address = ipv4.into();
if !IP_BLOCKLIST.is_allowed(&ipv4.to_string()) {
return Err(create_error!(InvalidOperation));
}
resolved_address = Some(ipv4.into());
}
Host::Ipv6(ipv6) => {
let string = ipv6.to_string();
if string.contains("::ffff:") || !IP_BLOCKLIST.is_allowed(&string) {
resolved_address = ipv6.into();
if !IP_BLOCKLIST.is_allowed(&ipv6.to_string()) {
return Err(create_error!(InvalidOperation));
}
resolved_address = Some(ipv6.into());
}
Host::Domain(domain) => {
let domain = domain.to_string();
@@ -450,7 +449,7 @@ impl Request {
// Second step: resolve the IP and check the blocklist
let resolver = CachedDnsResolver {};
if let Ok(resolved_ips) = resolver
if let Ok(mut resolved_ip) = resolver
.resolve(
Name::from_str(&domain)
.map_err(|_| create_error!(ProxyError))
@@ -458,14 +457,16 @@ impl Request {
)
.await
{
for resolved in resolved_ips {
resolved_address = Some(resolved.ip()); // last resolved ip will be the one we hit as a consequence of this for loop.
let resolved_string = resolved_address.unwrap().to_string();
if let Some(resolved_ip) = resolved_ip.next() {
resolved_address = resolved_ip.ip();
let resolved_string = resolved_address.to_string();
if !IP_BLOCKLIST.is_allowed(&resolved_string)
|| resolved_string.contains("::ffff:")
{
return Err(create_error!(InvalidOperation));
}
} else {
return Err(create_error!(InvalidOperation));
}
} else {
return Err(create_error!(ProxyError));
@@ -476,13 +477,9 @@ impl Request {
return Err(create_error!(ProxyError));
};
if resolved_address.is_none() {
return Err(create_error!(InvalidOperation));
}
Ok(IPRequest {
url: url.clone(),
ip: resolved_address.unwrap(),
ip: resolved_address,
blocked: false,
})
}
+23 -472
View File
@@ -9,10 +9,8 @@
"version": "0.0.0",
"dependencies": {
"@docusaurus/core": "3.9.2",
"@docusaurus/plugin-client-redirects": "^3.9.2",
"@docusaurus/preset-classic": "3.9.2",
"@mdx-js/react": "^3.0.0",
"@scalar/docusaurus": "^0.7.21",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
@@ -3547,359 +3545,6 @@
"react-dom": "*"
}
},
"node_modules/@docusaurus/plugin-client-redirects": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.10.1.tgz",
"integrity": "sha512-LHgd+YDvkhfOHMAE6XtUng3DQNzVM765RqVRrMJgHtzAvfopQhY6ieprqjxDVBdv21cLma6I0jHr+YCZH8fL9A==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.10.1",
"@docusaurus/logger": "3.10.1",
"@docusaurus/utils": "3.10.1",
"@docusaurus/utils-common": "3.10.1",
"@docusaurus/utils-validation": "3.10.1",
"eta": "^2.2.0",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"tslib": "^2.6.0"
},
"engines": {
"node": ">=20.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/babel": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.10.1.tgz",
"integrity": "sha512-DZzFO1K3v/GoEt1fx1DiYHF4en+PuhtQf1AkQJa5zu3CoeKSpr5cpQRUlz3jr0m44wyzmSXu9bVpfir+N4+8bg==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.9",
"@babel/generator": "^7.25.9",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.25.9",
"@babel/preset-env": "^7.25.9",
"@babel/preset-react": "^7.25.9",
"@babel/preset-typescript": "^7.25.9",
"@babel/runtime": "^7.25.9",
"@babel/traverse": "^7.25.9",
"@docusaurus/logger": "3.10.1",
"@docusaurus/utils": "3.10.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0"
},
"engines": {
"node": ">=20.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/bundler": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.10.1.tgz",
"integrity": "sha512-HIqQPvbqnnQRe4NsBd1774KRarjXqS6wHsWELtyuSs1gCfvixJO2jUGH/OEBtr1Gvzpw+ze5CjGMvSJ8UE1KUw==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.9",
"@docusaurus/babel": "3.10.1",
"@docusaurus/cssnano-preset": "3.10.1",
"@docusaurus/logger": "3.10.1",
"@docusaurus/types": "3.10.1",
"@docusaurus/utils": "3.10.1",
"babel-loader": "^9.2.1",
"clean-css": "^5.3.3",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.11.0",
"css-minimizer-webpack-plugin": "^5.0.1",
"cssnano": "^6.1.2",
"file-loader": "^6.2.0",
"html-minifier-terser": "^7.2.0",
"mini-css-extract-plugin": "^2.9.2",
"null-loader": "^4.0.1",
"postcss": "^8.5.4",
"postcss-loader": "^7.3.4",
"postcss-preset-env": "^10.2.1",
"terser-webpack-plugin": "^5.3.9",
"tslib": "^2.6.0",
"url-loader": "^4.1.1",
"webpack": "^5.95.0",
"webpackbar": "^7.0.0"
},
"engines": {
"node": ">=20.0"
},
"peerDependencies": {
"@docusaurus/faster": "*"
},
"peerDependenciesMeta": {
"@docusaurus/faster": {
"optional": true
}
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/core": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.10.1.tgz",
"integrity": "sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w==",
"license": "MIT",
"dependencies": {
"@docusaurus/babel": "3.10.1",
"@docusaurus/bundler": "3.10.1",
"@docusaurus/logger": "3.10.1",
"@docusaurus/mdx-loader": "3.10.1",
"@docusaurus/utils": "3.10.1",
"@docusaurus/utils-common": "3.10.1",
"@docusaurus/utils-validation": "3.10.1",
"boxen": "^6.2.1",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"cli-table3": "^0.6.3",
"combine-promises": "^1.1.0",
"commander": "^5.1.0",
"core-js": "^3.31.1",
"detect-port": "^1.5.1",
"escape-html": "^1.0.3",
"eta": "^2.2.0",
"eval": "^0.1.8",
"execa": "^5.1.1",
"fs-extra": "^11.1.1",
"html-tags": "^3.3.1",
"html-webpack-plugin": "^5.6.0",
"leven": "^3.1.0",
"lodash": "^4.17.21",
"open": "^8.4.0",
"p-map": "^4.0.0",
"prompts": "^2.4.2",
"react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0",
"react-loadable": "npm:@docusaurus/react-loadable@6.0.0",
"react-loadable-ssr-addon-v5-slorber": "^1.0.3",
"react-router": "^5.3.4",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.3.4",
"semver": "^7.5.4",
"serve-handler": "^6.1.7",
"tinypool": "^1.0.2",
"tslib": "^2.6.0",
"update-notifier": "^6.0.2",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "^5.2.2",
"webpack-merge": "^6.0.1"
},
"bin": {
"docusaurus": "bin/docusaurus.mjs"
},
"engines": {
"node": ">=20.0"
},
"peerDependencies": {
"@docusaurus/faster": "*",
"@mdx-js/react": "^3.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@docusaurus/faster": {
"optional": true
}
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/cssnano-preset": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.1.tgz",
"integrity": "sha512-eNfHGcTKCSq6xmcavAkX3RRclHaE2xRCMParlDXLdXVP01/a2e/jKXMj/0ULnLFQSNwwuI62L0Ge8J+nZsR7UQ==",
"license": "MIT",
"dependencies": {
"cssnano-preset-advanced": "^6.1.2",
"postcss": "^8.5.4",
"postcss-sort-media-queries": "^5.2.0",
"tslib": "^2.6.0"
},
"engines": {
"node": ">=20.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/logger": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.10.1.tgz",
"integrity": "sha512-oPjNFnfJsRCkePVjkGrxWGq4MvJKRQT0r9jOP0eRBTZ7Wr9FAbzdP/Gjs0I2Ss6YRkPoEgygKG112OkE6skvJw==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"tslib": "^2.6.0"
},
"engines": {
"node": ">=20.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/mdx-loader": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.10.1.tgz",
"integrity": "sha512-GRmeb/wQ+iXRrFwcHBfgQhrJxGElgCsoTWZYDhccjsZVne1p8MK/EpQVIloXttz76TCe78kKD5AEG9n1xc1oxQ==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.10.1",
"@docusaurus/utils": "3.10.1",
"@docusaurus/utils-validation": "3.10.1",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3",
"estree-util-value-to-estree": "^3.0.1",
"file-loader": "^6.2.0",
"fs-extra": "^11.1.1",
"image-size": "^2.0.2",
"mdast-util-mdx": "^3.0.0",
"mdast-util-to-string": "^4.0.0",
"rehype-raw": "^7.0.0",
"remark-directive": "^3.0.0",
"remark-emoji": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0",
"stringify-object": "^3.3.0",
"tslib": "^2.6.0",
"unified": "^11.0.3",
"unist-util-visit": "^5.0.0",
"url-loader": "^4.1.1",
"vfile": "^6.0.1",
"webpack": "^5.88.1"
},
"engines": {
"node": ">=20.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/types": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.10.1.tgz",
"integrity": "sha512-XYMK8k1szDCFMw2V+Xyen0g7Kee1sP3dtFnl7vkGkZOkeAJ/oPDQPL8iz4HBKOo/cwU8QeV6onVjMqtP+tFzsw==",
"license": "MIT",
"dependencies": {
"@mdx-js/mdx": "^3.0.0",
"@types/history": "^4.7.11",
"@types/mdast": "^4.0.2",
"@types/react": "*",
"commander": "^5.1.0",
"joi": "^17.9.2",
"react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0",
"utility-types": "^3.10.0",
"webpack": "^5.95.0",
"webpack-merge": "^5.9.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/types/node_modules/webpack-merge": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
"integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
"license": "MIT",
"dependencies": {
"clone-deep": "^4.0.1",
"flat": "^5.0.2",
"wildcard": "^2.0.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/utils": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.1.tgz",
"integrity": "sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.10.1",
"@docusaurus/types": "3.10.1",
"@docusaurus/utils-common": "3.10.1",
"escape-string-regexp": "^4.0.0",
"execa": "^5.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^11.1.1",
"github-slugger": "^1.5.0",
"globby": "^11.1.0",
"gray-matter": "^4.0.3",
"jiti": "^1.20.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"micromatch": "^4.0.5",
"p-queue": "^6.6.2",
"prompts": "^2.4.2",
"resolve-pathname": "^3.0.0",
"tslib": "^2.6.0",
"url-loader": "^4.1.1",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"engines": {
"node": ">=20.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/utils-common": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.10.1.tgz",
"integrity": "sha512-5mFSgEADtnFxFH7RLw02QA5MpU5JVUCj0MPeIvi/aF4Fi45tQRIuTwXoXDqJ+1VfQJuYJGz3SI63wmGz4HvXzA==",
"license": "MIT",
"dependencies": {
"@docusaurus/types": "3.10.1",
"tslib": "^2.6.0"
},
"engines": {
"node": ">=20.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/@docusaurus/utils-validation": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.1.tgz",
"integrity": "sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.10.1",
"@docusaurus/utils": "3.10.1",
"@docusaurus/utils-common": "3.10.1",
"fs-extra": "^11.2.0",
"joi": "^17.9.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"tslib": "^2.6.0"
},
"engines": {
"node": ">=20.0"
}
},
"node_modules/@docusaurus/plugin-client-redirects/node_modules/webpackbar": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-7.0.0.tgz",
"integrity": "sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q==",
"license": "MIT",
"dependencies": {
"ansis": "^3.2.0",
"consola": "^3.2.3",
"pretty-time": "^1.1.0",
"std-env": "^3.7.0"
},
"engines": {
"node": ">=14.21.3"
},
"peerDependencies": {
"@rspack/core": "*",
"webpack": "3 || 4 || 5"
},
"peerDependenciesMeta": {
"@rspack/core": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/@docusaurus/plugin-content-blog": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz",
@@ -4745,79 +4390,6 @@
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"license": "MIT"
},
"node_modules/@scalar/docusaurus": {
"version": "0.7.44",
"resolved": "https://registry.npmjs.org/@scalar/docusaurus/-/docusaurus-0.7.44.tgz",
"integrity": "sha512-GUop+mdVe761bcUw/PDdyhR969dF948REosp7JyiQ+6TnlaDfg/7C3lUbwV8sGa1DTNrjKeXqHBh8AOMgubIWQ==",
"license": "MIT",
"dependencies": {
"@scalar/types": "0.6.10"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@docusaurus/utils": "^3.9.2",
"react": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@scalar/helpers": {
"version": "0.2.18",
"resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.2.18.tgz",
"integrity": "sha512-w1d4tpNEVZ293oB2BAgLrS0kVPUtG3eByNmOCJA5eK9vcT4D3cmsGtWjUaaqit0BQCsBFHK51rasGvSWnApYTw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/@scalar/types": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.6.10.tgz",
"integrity": "sha512-fZkelRwcEeAhsn4c0wjYXWrzSzLaEyfxTn/eazXJ4XfCIsgJTQyK0FD8mnOBZJ2vEIbtT2E1mBKnCbDxrJIlxA==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.2.18",
"nanoid": "^5.1.6",
"type-fest": "^5.3.1",
"zod": "^4.3.5"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@scalar/types/node_modules/nanoid": {
"version": "5.1.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.15.tgz",
"integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/@scalar/types/node_modules/type-fest": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz",
"integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==",
"license": "(MIT OR CC0-1.0)",
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -6008,15 +5580,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ansis": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz",
"integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==",
"license": "ISC",
"engines": {
"node": ">=14"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -6316,9 +5879,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -12940,9 +12503,9 @@
"license": "ISC"
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -15354,24 +14917,24 @@
}
},
"node_modules/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.7"
"react": "^19.2.0"
}
},
"node_modules/react-fast-compare": {
@@ -15430,9 +14993,9 @@
}
},
"node_modules/react-loadable-ssr-addon-v5-slorber": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.3.tgz",
"integrity": "sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz",
"integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.10.3"
@@ -16292,15 +15855,15 @@
}
},
"node_modules/serve-handler": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz",
"integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==",
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
"license": "MIT",
"dependencies": {
"bytes": "3.0.0",
"content-disposition": "0.5.2",
"mime-types": "2.1.18",
"minimatch": "3.1.5",
"minimatch": "3.1.2",
"path-is-inside": "1.0.2",
"path-to-regexp": "3.3.0",
"range-parser": "1.2.0"
@@ -16997,18 +16560,6 @@
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
@@ -18427,9 +17978,9 @@
}
},
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
+2 -2
View File
@@ -22,8 +22,8 @@
"@scalar/docusaurus": "^0.7.21",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.2.7",
"react-dom": "^19.2.7"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.9.2",
+245 -265
View File
@@ -10,41 +10,41 @@ importers:
dependencies:
'@docusaurus/core':
specifier: 3.9.2
version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-client-redirects':
specifier: ^3.9.2
version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/preset-classic':
specifier: 3.9.2
version: 3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@5.6.3)
version: 3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@5.6.3)
'@mdx-js/react':
specifier: ^3.0.0
version: 3.1.1(@types/react@19.2.17)(react@19.2.7)
version: 3.1.1(@types/react@19.2.4)(react@19.2.0)
'@scalar/docusaurus':
specifier: ^0.7.21
version: 0.7.21(@docusaurus/utils@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
version: 0.7.21(@docusaurus/utils@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
clsx:
specifier: ^2.0.0
version: 2.1.1
prism-react-renderer:
specifier: ^2.3.0
version: 2.4.1(react@19.2.7)
version: 2.4.1(react@19.2.0)
react:
specifier: ^19.2.7
version: 19.2.7
specifier: ^19.0.0
version: 19.2.0
react-dom:
specifier: ^19.2.7
version: 19.2.7(react@19.2.7)
specifier: ^19.0.0
version: 19.2.0(react@19.2.0)
devDependencies:
'@docusaurus/module-type-aliases':
specifier: 3.9.2
version: 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
version: 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/tsconfig':
specifier: 3.9.2
version: 3.9.2
'@docusaurus/types':
specifier: 3.9.2
version: 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
version: 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
typescript:
specifier: ~5.6.2
version: 5.6.3
@@ -708,10 +708,6 @@ packages:
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.29.7':
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
@@ -1569,9 +1565,6 @@ packages:
'@types/react-router@5.1.20':
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
'@types/react@19.2.17':
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
'@types/react@19.2.4':
resolution: {integrity: sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==}
@@ -1613,7 +1606,6 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@vercel/oidc@3.0.3':
resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==}
@@ -2247,9 +2239,6 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
@@ -4182,10 +4171,10 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
react-dom@19.2.7:
resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==}
react-dom@19.2.0:
resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
peerDependencies:
react: ^19.2.7
react: ^19.2.0
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
@@ -4222,8 +4211,8 @@ packages:
peerDependencies:
react: '>=15'
react@19.2.7:
resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
react@19.2.0:
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
engines: {node: '>=0.10.0'}
readable-stream@2.3.8:
@@ -4853,7 +4842,6 @@ packages:
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
value-equal@1.0.1:
@@ -5040,12 +5028,12 @@ snapshots:
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@2.0.93(react@19.2.7)(zod@4.1.12)':
'@ai-sdk/react@2.0.93(react@19.2.0)(zod@4.1.12)':
dependencies:
'@ai-sdk/provider-utils': 3.0.17(zod@4.1.12)
ai: 5.0.93(zod@4.1.12)
react: 19.2.7
swr: 2.3.6(react@19.2.7)
react: 19.2.0
swr: 2.3.6(react@19.2.0)
throttleit: 2.1.0
optionalDependencies:
zod: 4.1.12
@@ -5888,8 +5876,6 @@ snapshots:
'@babel/runtime@7.28.4': {}
'@babel/runtime@7.29.7': {}
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
@@ -6206,33 +6192,33 @@ snapshots:
'@discoveryjs/json-ext@0.5.7': {}
'@docsearch/core@4.3.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docsearch/core@4.3.1(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
optionalDependencies:
'@types/react': 19.2.17
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@types/react': 19.2.4
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
'@docsearch/css@4.3.2': {}
'@docsearch/react@4.3.2(@algolia/client-search@5.43.0)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)':
'@docsearch/react@4.3.2(@algolia/client-search@5.43.0)(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)':
dependencies:
'@ai-sdk/react': 2.0.93(react@19.2.7)(zod@4.1.12)
'@ai-sdk/react': 2.0.93(react@19.2.0)(zod@4.1.12)
'@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.43.0)(algoliasearch@5.43.0)(search-insights@2.17.3)
'@docsearch/core': 4.3.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docsearch/core': 4.3.1(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docsearch/css': 4.3.2
ai: 5.0.93(zod@4.1.12)
algoliasearch: 5.43.0
marked: 16.4.2
zod: 4.1.12
optionalDependencies:
'@types/react': 19.2.17
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@types/react': 19.2.4
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
search-insights: 2.17.3
transitivePeerDependencies:
- '@algolia/client-search'
'@docusaurus/babel@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docusaurus/babel@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@babel/core': 7.28.5
'@babel/generator': 7.28.5
@@ -6245,7 +6231,7 @@ snapshots:
'@babel/runtime-corejs3': 7.28.4
'@babel/traverse': 7.28.5
'@docusaurus/logger': 3.9.2
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
babel-plugin-dynamic-import-node: 2.3.3
fs-extra: 11.3.2
tslib: 2.8.1
@@ -6258,14 +6244,14 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/bundler@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/bundler@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@babel/core': 7.28.5
'@docusaurus/babel': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/babel': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/cssnano-preset': 3.9.2
'@docusaurus/logger': 3.9.2
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.102.1)
clean-css: 5.3.3
copy-webpack-plugin: 11.0.0(webpack@5.102.1)
@@ -6299,16 +6285,16 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/babel': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/bundler': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/babel': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/bundler': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/logger': 3.9.2
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7)
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mdx-js/react': 3.1.1(@types/react@19.2.4)(react@19.2.0)
boxen: 6.2.1
chalk: 4.1.2
chokidar: 3.6.0
@@ -6329,14 +6315,14 @@ snapshots:
open: 8.4.2
p-map: 4.0.0
prompts: 2.4.2
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)'
react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)'
react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.102.1)
react-router: 5.3.4(react@19.2.7)
react-router-config: 5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7)
react-router-dom: 5.3.4(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)'
react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.0)'
react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.0))(webpack@5.102.1)
react-router: 5.3.4(react@19.2.0)
react-router-config: 5.1.1(react-router@5.3.4(react@19.2.0))(react@19.2.0)
react-router-dom: 5.3.4(react@19.2.0)
semver: 7.7.3
serve-handler: 6.1.6
tinypool: 1.1.1
@@ -6375,11 +6361,11 @@ snapshots:
chalk: 4.1.2
tslib: 2.8.1
'@docusaurus/mdx-loader@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docusaurus/mdx-loader@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@docusaurus/logger': 3.9.2
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mdx-js/mdx': 3.1.1
'@slorber/remark-comment': 1.0.0
escape-html: 1.0.3
@@ -6389,8 +6375,8 @@ snapshots:
image-size: 2.0.2
mdast-util-mdx: 3.0.0
mdast-util-to-string: 4.0.0
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
rehype-raw: 7.0.0
remark-directive: 3.0.1
remark-emoji: 4.0.1
@@ -6410,17 +6396,17 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/module-type-aliases@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docusaurus/module-type-aliases@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@types/history': 4.7.11
'@types/react': 19.2.4
'@types/react-router-config': 5.0.11
'@types/react-router-dom': 5.3.3
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)'
react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)'
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)'
react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.0)'
transitivePeerDependencies:
- '@swc/core'
- esbuild
@@ -6428,18 +6414,18 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/plugin-client-redirects@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-client-redirects@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/logger': 3.9.2
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
eta: 2.2.0
fs-extra: 11.3.2
lodash: 4.17.21
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
tslib: 2.8.1
transitivePeerDependencies:
- '@docusaurus/faster'
@@ -6459,23 +6445,23 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/logger': 3.9.2
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
cheerio: 1.0.0-rc.12
feed: 4.2.2
fs-extra: 11.3.2
lodash: 4.17.21
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
schema-dts: 1.1.5
srcset: 4.0.0
tslib: 2.8.1
@@ -6500,24 +6486,24 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/logger': 3.9.2
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@types/react-router-config': 5.0.11
combine-promises: 1.2.0
fs-extra: 11.3.2
js-yaml: 4.1.1
lodash: 4.17.21
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
schema-dts: 1.1.5
tslib: 2.8.1
utility-types: 3.11.0
@@ -6540,16 +6526,16 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
fs-extra: 11.3.2
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
tslib: 2.8.1
webpack: 5.102.1
transitivePeerDependencies:
@@ -6570,12 +6556,12 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
tslib: 2.8.1
transitivePeerDependencies:
- '@docusaurus/faster'
@@ -6597,15 +6583,15 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
fs-extra: 11.3.2
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react-json-view-lite: 2.5.0(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-json-view-lite: 2.5.0(react@19.2.0)
tslib: 2.8.1
transitivePeerDependencies:
- '@docusaurus/faster'
@@ -6625,13 +6611,13 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
tslib: 2.8.1
transitivePeerDependencies:
- '@docusaurus/faster'
@@ -6651,14 +6637,14 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@types/gtag.js': 0.0.12
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
tslib: 2.8.1
transitivePeerDependencies:
- '@docusaurus/faster'
@@ -6678,13 +6664,13 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
tslib: 2.8.1
transitivePeerDependencies:
- '@docusaurus/faster'
@@ -6704,17 +6690,17 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/logger': 3.9.2
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
fs-extra: 11.3.2
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
sitemap: 7.1.2
tslib: 2.8.1
transitivePeerDependencies:
@@ -6735,16 +6721,16 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@svgr/core': 8.1.0(typescript@5.6.3)
'@svgr/webpack': 8.1.0(typescript@5.6.3)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
tslib: 2.8.1
webpack: 5.102.1
transitivePeerDependencies:
@@ -6765,25 +6751,25 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@5.6.3)':
'@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/theme-classic': 3.9.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/theme-classic': 3.9.2(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@5.6.3)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
transitivePeerDependencies:
- '@algolia/client-search'
- '@docusaurus/faster'
@@ -6805,37 +6791,37 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/react-loadable@6.0.0(react@19.2.7)':
'@docusaurus/react-loadable@6.0.0(react@19.2.0)':
dependencies:
'@types/react': 19.2.4
react: 19.2.7
react: 19.2.0
'@docusaurus/theme-classic@3.9.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)':
'@docusaurus/theme-classic@3.9.2(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)':
dependencies:
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/logger': 3.9.2
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/theme-translations': 3.9.2
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mdx-js/react': 3.1.1(@types/react@19.2.4)(react@19.2.0)
clsx: 2.1.1
infima: 0.2.0-alpha.45
lodash: 4.17.21
nprogress: 0.2.0
postcss: 8.5.6
prism-react-renderer: 2.4.1(react@19.2.7)
prism-react-renderer: 2.4.1(react@19.2.0)
prismjs: 1.30.0
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react-router-dom: 5.3.4(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-router-dom: 5.3.4(react@19.2.0)
rtlcss: 4.3.0
tslib: 2.8.1
utility-types: 3.11.0
@@ -6857,21 +6843,21 @@ snapshots:
- utf-8-validate
- webpack-cli
'@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@types/history': 4.7.11
'@types/react': 19.2.17
'@types/react': 19.2.4
'@types/react-router-config': 5.0.11
clsx: 2.1.1
parse-numeric-range: 1.3.0
prism-react-renderer: 2.4.1(react@19.2.7)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
prism-react-renderer: 2.4.1(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
tslib: 2.8.1
utility-types: 3.11.0
transitivePeerDependencies:
@@ -6881,24 +6867,24 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@5.6.3)':
'@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@5.6.3)':
dependencies:
'@docsearch/react': 4.3.2(@algolia/client-search@5.43.0)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docsearch/react': 4.3.2(@algolia/client-search@5.43.0)(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)
'@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/logger': 3.9.2
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.6.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3)
'@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.6.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/theme-translations': 3.9.2
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-validation': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
algoliasearch: 5.43.0
algoliasearch-helper: 3.26.1(algoliasearch@5.43.0)
clsx: 2.1.1
eta: 2.2.0
fs-extra: 11.3.2
lodash: 4.17.21
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
tslib: 2.8.1
utility-types: 3.11.0
transitivePeerDependencies:
@@ -6929,7 +6915,7 @@ snapshots:
'@docusaurus/tsconfig@3.9.2': {}
'@docusaurus/types@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docusaurus/types@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@mdx-js/mdx': 3.1.1
'@types/history': 4.7.11
@@ -6937,9 +6923,9 @@ snapshots:
'@types/react': 19.2.4
commander: 5.1.0
joi: 17.13.3
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)'
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)'
utility-types: 3.11.0
webpack: 5.102.1
webpack-merge: 5.10.0
@@ -6950,9 +6936,9 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/utils-common@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docusaurus/utils-common@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
tslib: 2.8.1
transitivePeerDependencies:
- '@swc/core'
@@ -6963,11 +6949,11 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/utils-validation@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docusaurus/utils-validation@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@docusaurus/logger': 3.9.2
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
fs-extra: 11.3.2
joi: 17.13.3
js-yaml: 4.1.1
@@ -6982,11 +6968,11 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/utils@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@docusaurus/utils@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@docusaurus/logger': 3.9.2
'@docusaurus/types': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/types': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@docusaurus/utils-common': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
escape-string-regexp: 4.0.0
execa: 5.1.1
file-loader: 6.2.0(webpack@5.102.1)
@@ -7125,11 +7111,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7)':
'@mdx-js/react@3.1.1(@types/react@19.2.4)(react@19.2.0)':
dependencies:
'@types/mdx': 2.0.13
'@types/react': 19.2.17
react: 19.2.7
'@types/react': 19.2.4
react: 19.2.0
'@nodelib/fs.scandir@2.1.5':
dependencies:
@@ -7159,11 +7145,11 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@scalar/docusaurus@0.7.21(@docusaurus/utils@3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)':
'@scalar/docusaurus@0.7.21(@docusaurus/utils@3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)':
dependencies:
'@docusaurus/utils': 3.9.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.9.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@scalar/types': 0.4.0
react: 19.2.7
react: 19.2.0
'@scalar/openapi-types@0.5.1':
dependencies:
@@ -7190,13 +7176,13 @@ snapshots:
'@sindresorhus/is@5.6.0': {}
'@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
'@slorber/react-helmet-async@1.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@babel/runtime': 7.29.7
'@babel/runtime': 7.28.4
invariant: 2.2.4
prop-types: 15.8.1
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-fast-compare: 3.2.2
shallowequal: 1.1.0
@@ -7432,10 +7418,6 @@ snapshots:
'@types/history': 4.7.11
'@types/react': 19.2.4
'@types/react@19.2.17':
dependencies:
csstype: 3.2.3
'@types/react@19.2.4':
dependencies:
csstype: 3.1.3
@@ -8198,8 +8180,6 @@ snapshots:
csstype@3.1.3: {}
csstype@3.2.3: {}
debounce@1.2.1: {}
debug@2.6.9:
@@ -10413,11 +10393,11 @@ snapshots:
pretty-time@1.1.0: {}
prism-react-renderer@2.4.1(react@19.2.7):
prism-react-renderer@2.4.1(react@19.2.0):
dependencies:
'@types/prismjs': 1.26.5
clsx: 2.1.1
react: 19.2.7
react: 19.2.0
prismjs@1.30.0: {}
@@ -10481,43 +10461,43 @@ snapshots:
minimist: 1.2.8
strip-json-comments: 2.0.1
react-dom@19.2.7(react@19.2.7):
react-dom@19.2.0(react@19.2.0):
dependencies:
react: 19.2.7
react: 19.2.0
scheduler: 0.27.0
react-fast-compare@3.2.2: {}
react-is@16.13.1: {}
react-json-view-lite@2.5.0(react@19.2.7):
react-json-view-lite@2.5.0(react@19.2.0):
dependencies:
react: 19.2.7
react: 19.2.0
react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.102.1):
react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.0))(webpack@5.102.1):
dependencies:
'@babel/runtime': 7.28.4
react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)'
react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.0)'
webpack: 5.102.1
react-router-config@5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7):
react-router-config@5.1.1(react-router@5.3.4(react@19.2.0))(react@19.2.0):
dependencies:
'@babel/runtime': 7.28.4
react: 19.2.7
react-router: 5.3.4(react@19.2.7)
react: 19.2.0
react-router: 5.3.4(react@19.2.0)
react-router-dom@5.3.4(react@19.2.7):
react-router-dom@5.3.4(react@19.2.0):
dependencies:
'@babel/runtime': 7.28.4
history: 4.10.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 19.2.7
react-router: 5.3.4(react@19.2.7)
react: 19.2.0
react-router: 5.3.4(react@19.2.0)
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
react-router@5.3.4(react@19.2.7):
react-router@5.3.4(react@19.2.0):
dependencies:
'@babel/runtime': 7.28.4
history: 4.10.1
@@ -10525,12 +10505,12 @@ snapshots:
loose-envify: 1.4.0
path-to-regexp: 1.9.0
prop-types: 15.8.1
react: 19.2.7
react: 19.2.0
react-is: 16.13.1
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
react@19.2.7: {}
react@19.2.0: {}
readable-stream@2.3.8:
dependencies:
@@ -11062,11 +11042,11 @@ snapshots:
csso: 5.0.5
picocolors: 1.1.1
swr@2.3.6(react@19.2.7):
swr@2.3.6(react@19.2.0):
dependencies:
dequal: 2.0.3
react: 19.2.7
use-sync-external-store: 1.6.0(react@19.2.7)
react: 19.2.0
use-sync-external-store: 1.6.0(react@19.2.0)
tagged-tag@1.0.0: {}
@@ -11237,9 +11217,9 @@ snapshots:
optionalDependencies:
file-loader: 6.2.0(webpack@5.102.1)
use-sync-external-store@1.6.0(react@19.2.7):
use-sync-external-store@1.6.0(react@19.2.0):
dependencies:
react: 19.2.7
react: 19.2.0
util-deprecate@1.0.2: {}