This commit is contained in:
Mateusz Gruszczyński committed 2026-11-01 00:25:29 +01:00
1 parent 99a5700caa
commit b752f52fd8
22 files changed
+313 -138

No files matched your search

+12 -12
View File
@@ -36,15 +36,19 @@ pub async fn create_resource_access_token(
state
.check_rate_limit(client_limit_key, 50, window)
.await
.map_err(|seconds| ApiError::rate_limited(&format!(
"Too many password attempts. Try again in {seconds} seconds."
)))?;
.map_err(|seconds| {
ApiError::rate_limited(&format!(
"Too many password attempts. Try again in {seconds} seconds."
))
})?;
state
.check_rate_limit(limit_key.clone(), 10, window)
.await
.map_err(|seconds| ApiError::rate_limited(&format!(
"Too many password attempts. Try again in {seconds} seconds."
)))?;
.map_err(|seconds| {
ApiError::rate_limited(&format!(
"Too many password attempts. Try again in {seconds} seconds."
))
})?;
match kind {
"workspace" => {
let workspace = db::find_workspace(&state.db, slug)
@@ -81,12 +85,8 @@ pub async fn create_resource_access_token(
.execute(state.db.pool())
.await?;
state.clear_rate_limit(&limit_key).await;
let cookie = crate::security::resource_cookie(
kind,
slug,
&token,
state.anonymous_access_token_ttl_days,
);
let cookie =
crate::security::resource_cookie(kind, slug, &token, state.anonymous_access_token_ttl_days);
let mut response = Json(AccessTokenResponse {
granted: true,
expires_at,
+1
View File
@@ -13,6 +13,7 @@ use axum::{
response::{IntoResponse, Response},
};
#[derive(Debug)]
pub struct ApiError {
status: StatusCode,
message: String,
+4 -4
View File
@@ -485,7 +485,7 @@ mod tests {
.await
.unwrap();
add_favorite(
let _ = add_favorite(
State(state.clone()),
headers.clone(),
Json(FavoriteTarget {
@@ -580,7 +580,7 @@ mod tests {
.await
.unwrap();
add_favorite(
let _ = add_favorite(
State(state.clone()),
headers.clone(),
Json(FavoriteTarget {
@@ -677,7 +677,7 @@ mod tests {
HeaderValue::from_str(&format!("Bearer {share_token}")).unwrap(),
);
add_favorite(
let _ = add_favorite(
State(state.clone()),
headers.clone(),
Json(FavoriteTarget {
@@ -755,7 +755,7 @@ mod tests {
HeaderValue::from_str(&format!("Bearer {access_token}")).unwrap(),
);
add_favorite(
let _ = add_favorite(
State(state.clone()),
headers.clone(),
Json(FavoriteTarget {
+61 -49
View File
@@ -101,10 +101,8 @@ pub async fn upload_pad_file(
key = crate::storage::object_key("pads", pad.id, &file_token, &stored);
}
let stored_url = crate::file_urls::stored_file_path(&file_token, &stored);
let public_url = crate::file_urls::public_file_url(
state.files_public_url.as_deref(),
&stored_url,
);
let public_url =
crate::file_urls::public_file_url(state.files_public_url.as_deref(), &stored_url);
let mime = mime_guess::from_path(&stored)
.first_or_octet_stream()
.to_string();
@@ -123,7 +121,9 @@ pub async fn upload_pad_file(
bytes.len() as i64,
)
.await?;
Ok(Json(serde_json::json!({"name": stored, "url": public_url, "mime_type": mime})))
Ok(Json(
serde_json::json!({"name": stored, "url": public_url, "mime_type": mime}),
))
}
pub(super) fn content_references_file(content: &str, filename: &str, url: &str) -> bool {
@@ -134,9 +134,7 @@ pub(super) fn content_references_file(content: &str, filename: &str, url: &str)
let mut remaining = content;
while let Some(index) = remaining.find(marker) {
let after = &remaining[index + marker.len()..];
let end = after
.find(|character| character == ',' || character == ']')
.unwrap_or(after.len());
let end = after.find([',', ']']).unwrap_or(after.len());
if after[..end].trim() == filename {
return true;
}
@@ -202,10 +200,7 @@ pub async fn pad_files(
};
}
file.created_at = db::normalize_timestamp(&file.created_at);
file.url = crate::file_urls::public_file_url(
state.files_public_url.as_deref(),
&file.url,
);
file.url = crate::file_urls::public_file_url(state.files_public_url.as_deref(), &file.url);
}
Ok(Json(files))
}
@@ -225,14 +220,10 @@ pub async fn delete_pad_file(
&headers,
)
.await?;
let account_owner = crate::auth::is_resource_owner(
&state,
"pad",
&pad.slug,
bearer_token(&headers),
)
.await
.unwrap_or(false);
let account_owner =
crate::auth::is_resource_owner(&state, "pad", &pad.slug, bearer_token(&headers))
.await
.unwrap_or(false);
let guest_owner = pad_creator_is_requester(&headers, &pad);
let password_write_access =
has_password_write_access(&state, &headers, "pad", &pad.slug).await?;
@@ -296,7 +287,12 @@ pub async fn upload_note_file(
&workspace_slug,
&note_slug,
password.as_deref(),
resource_request_token(&headers, "workspace", &workspace_slug, access_token.as_deref()),
resource_request_token(
&headers,
"workspace",
&workspace_slug,
access_token.as_deref(),
),
bearer_token(&headers),
&headers,
)
@@ -312,7 +308,12 @@ pub async fn upload_note_file(
&headers,
"workspace",
&workspace_slug,
resource_request_token(&headers, "workspace", &workspace_slug, access_token.as_deref()),
resource_request_token(
&headers,
"workspace",
&workspace_slug,
access_token.as_deref(),
),
bearer_token(&headers),
)
.await?
@@ -342,10 +343,8 @@ pub async fn upload_note_file(
key = crate::storage::object_key("notes", note.id, &file_token, &stored);
}
let stored_url = crate::file_urls::stored_file_path(&file_token, &stored);
let public_url = crate::file_urls::public_file_url(
state.files_public_url.as_deref(),
&stored_url,
);
let public_url =
crate::file_urls::public_file_url(state.files_public_url.as_deref(), &stored_url);
let mime = mime_guess::from_path(&stored)
.first_or_octet_stream()
.to_string();
@@ -364,7 +363,9 @@ pub async fn upload_note_file(
bytes.len() as i64,
)
.await?;
Ok(Json(serde_json::json!({"name": stored, "url": public_url, "mime_type": mime})))
Ok(Json(
serde_json::json!({"name": stored, "url": public_url, "mime_type": mime}),
))
}
pub async fn delete_note(
@@ -378,7 +379,12 @@ pub async fn delete_note(
&workspace_slug,
&note_slug,
payload.password.as_deref(),
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
resource_request_token(
&headers,
"workspace",
&workspace_slug,
payload.access_token.as_deref(),
),
bearer_token(&headers),
&headers,
)
@@ -394,7 +400,12 @@ pub async fn delete_note(
&headers,
"workspace",
&workspace_slug,
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
resource_request_token(
&headers,
"workspace",
&workspace_slug,
payload.access_token.as_deref(),
),
bearer_token(&headers),
)
.await?
@@ -415,12 +426,8 @@ pub async fn delete_note(
.await
.map_err(|_| ApiError::internal("Failed to delete note files"))?;
}
db::delete_resource_editor_state(
&state.db,
"note",
&format!("{workspace_slug}/{note_slug}"),
)
.await?;
db::delete_resource_editor_state(&state.db, "note", &format!("{workspace_slug}/{note_slug}"))
.await?;
db::delete_note(&state.db, note.id).await?;
Ok(Json(serde_json::json!({"ok": true, "archived": false})))
}
@@ -436,7 +443,12 @@ pub async fn note_files(
&workspace_slug,
&note_slug,
payload.password.as_deref(),
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
resource_request_token(
&headers,
"workspace",
&workspace_slug,
payload.access_token.as_deref(),
),
bearer_token(&headers),
&headers,
)
@@ -459,10 +471,7 @@ pub async fn note_files(
};
}
file.created_at = db::normalize_timestamp(&file.created_at);
file.url = crate::file_urls::public_file_url(
state.files_public_url.as_deref(),
&file.url,
);
file.url = crate::file_urls::public_file_url(state.files_public_url.as_deref(), &file.url);
}
Ok(Json(files))
}
@@ -478,7 +487,12 @@ pub async fn delete_note_file(
&workspace_slug,
&note_slug,
payload.password.as_deref(),
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
resource_request_token(
&headers,
"workspace",
&workspace_slug,
payload.access_token.as_deref(),
),
bearer_token(&headers),
&headers,
)
@@ -627,10 +641,9 @@ async fn serve_token_file(
HeaderValue::from_str(&format!("{disposition}; filename=\"{safe}\""))
.expect("sanitized attachment filename"),
);
response.headers_mut().insert(
header::ACCEPT_RANGES,
HeaderValue::from_static("bytes"),
);
response
.headers_mut()
.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
response.headers_mut().insert(
header::CONTENT_LENGTH,
HeaderValue::from_str(&body_len.to_string()).expect("valid content length"),
@@ -669,10 +682,9 @@ fn range_not_satisfiable(total_len: usize) -> Response {
header::CONTENT_RANGE,
HeaderValue::from_str(&format!("bytes */{total_len}")).expect("valid content range"),
);
response.headers_mut().insert(
header::ACCEPT_RANGES,
HeaderValue::from_static("bytes"),
);
response
.headers_mut()
.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
response
}
+3 -2
View File
@@ -583,6 +583,7 @@ async fn user_editor_preferences(
))
}
#[allow(clippy::too_many_arguments)]
async fn save_editor_settings(
state: &SharedState,
headers: &HeaderMap,
@@ -1043,7 +1044,7 @@ pub async fn open_workspace(
let page = query.page.max(1);
let per_page = normalize_list_per_page(query.per_page);
let total = notes.len();
let total_pages = ((total + per_page - 1) / per_page).max(1);
let total_pages = total.div_ceil(per_page).max(1);
let page = page.min(total_pages);
let start = (page - 1) * per_page;
let notes = notes.into_iter().skip(start).take(per_page).collect();
@@ -1142,7 +1143,7 @@ pub async fn create_note(
title,
payload.protect,
created_by.as_deref(),
created_by_guest_id.as_deref(),
created_by_guest_id,
)
.await?;
if let Some(content) = initial_content {
+1
View File
@@ -118,6 +118,7 @@ async fn share_session_redirect(
response
}
#[allow(clippy::too_many_arguments)]
fn render_editor_page(
state: &SharedState,
entrypoint: &str,
+1 -1
View File
@@ -233,7 +233,7 @@ fn escape_filter(value: &str) -> String {
b')' => result.push_str("\\29"),
b'\\' => result.push_str("\\5c"),
0 => result.push_str("\\00"),
value if value < 0x20 || value >= 0x7f => result.push_str(&format!("\\{value:02x}")),
value if !(0x20..0x7f).contains(&value) => result.push_str(&format!("\\{value:02x}")),
value => result.push(value as char),
}
}
+13 -15
View File
@@ -480,7 +480,7 @@ pub async fn register(
.ok_or_else(|| AuthError::internal("Failed to create the account."))?;
let mut confirmation_token = None;
if state.smtp.is_some() {
if let Some(smtp) = &state.smtp {
let token = random_confirmation_token();
if state.account_confirmation_required {
let expires = (Utc::now() + Duration::hours(24)).to_rfc3339();
@@ -506,9 +506,7 @@ pub async fn register(
tx.commit().await.map_err(AuthError::database)?;
confirmation_token = Some(token.as_str());
}
if let Err(error) =
send_registration_email(state.smtp.as_ref().unwrap(), &user, confirmation_token).await
{
if let Err(error) = send_registration_email(smtp, &user, confirmation_token).await {
if state.account_confirmation_required {
if let Err(delete_error) =
sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_USER))
@@ -638,16 +636,16 @@ pub async fn resend_confirmation(
.fetch_optional(state.db.pool())
.await
.map_err(AuthError::database)?;
if let Some(value) = last_created {
if let Ok(created) = chrono::DateTime::parse_from_rfc3339(&value) {
let available = created.with_timezone(&Utc) + Duration::minutes(10);
if available > Utc::now() {
let seconds = (available - Utc::now()).num_seconds().max(1);
return Err(AuthError::bad_request(&format!(
"A new confirmation e-mail can be sent in {} minute(s).",
(seconds + 59) / 60
)));
}
if let Some(value) = last_created
&& let Ok(created) = chrono::DateTime::parse_from_rfc3339(&value)
{
let available = created.with_timezone(&Utc) + Duration::minutes(10);
if available > Utc::now() {
let seconds = (available - Utc::now()).num_seconds().max(1);
return Err(AuthError::bad_request(&format!(
"A new confirmation e-mail can be sent in {} minute(s).",
(seconds + 59) / 60
)));
}
}
sqlx::query(queries::get(
@@ -1345,7 +1343,7 @@ pub async fn resources(
let page = query.page.max(1);
let per_page = normalized_per_page(query.per_page);
let total = items.len();
let total_pages = ((total + per_page - 1) / per_page).max(1);
let total_pages = total.div_ceil(per_page).max(1);
let page = page.min(total_pages);
let start = (page - 1) * per_page;
let items = items
+4 -5
View File
@@ -219,11 +219,10 @@ impl Config {
if self.static_dir.trim().is_empty() || self.files_dir.trim().is_empty() {
return Err("STATIC_DIR and FILES_DIR cannot be empty".into());
}
if let Some(smtp) = &self.smtp {
if !(smtp.public_url.starts_with("http://") || smtp.public_url.starts_with("https://"))
{
return Err("PUBLIC_URL must start with http:// or https://".into());
}
if let Some(smtp) = &self.smtp
&& !(smtp.public_url.starts_with("http://") || smtp.public_url.starts_with("https://"))
{
return Err("PUBLIC_URL must start with http:// or https://".into());
}
Ok(())
}
+13 -17
View File
@@ -317,14 +317,11 @@ pub async fn note_slug_exists_any(
workspace_id: i64,
slug: &str,
) -> Result<bool, sqlx::Error> {
let count: i64 = sqlx::query_scalar(queries::get(
pool.kind(),
queries::NOTE_SLUG_EXISTS_ANY,
))
.bind(workspace_id)
.bind(slug)
.fetch_one(pool.pool())
.await?;
let count: i64 = sqlx::query_scalar(queries::get(pool.kind(), queries::NOTE_SLUG_EXISTS_ANY))
.bind(workspace_id)
.bind(slug)
.fetch_one(pool.pool())
.await?;
Ok(count > 0)
}
@@ -507,6 +504,7 @@ pub async fn save_revision(
Ok((revision_id, updated_at))
}
#[allow(clippy::too_many_arguments)]
pub async fn save_collaborative_revision(
pool: &Database,
note_id: i64,
@@ -680,16 +678,14 @@ pub async fn create_pad(
.await
}
pub async fn rename_pad(
pool: &Database,
pad_id: i64,
title: &str,
) -> Result<(), sqlx::Error> {
pub async fn rename_pad(pool: &Database, pad_id: i64, title: &str) -> Result<(), sqlx::Error> {
let query = match pool.kind() {
DatabaseKind::Postgres =>
"UPDATE pads SET title = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2",
DatabaseKind::Sqlite | DatabaseKind::MySql =>
"UPDATE pads SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
DatabaseKind::Postgres => {
"UPDATE pads SET title = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2"
}
DatabaseKind::Sqlite | DatabaseKind::MySql => {
"UPDATE pads SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
}
};
sqlx::query(query)
.bind(title)
+4 -4
View File
@@ -447,10 +447,10 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for PostgresPublishedPageRow {
pub async fn pad_public_page_disabled(pool: &Database, pad_id: i64) -> Result<bool, sqlx::Error> {
let query = queries::get(pool.kind(), queries::PAD_PUBLIC_PAGE_DISABLED);
if pool.kind() == DatabaseKind::Postgres {
return Ok(sqlx::query_scalar::<_, bool>(query)
return sqlx::query_scalar::<_, bool>(query)
.bind(pad_id)
.fetch_one(pool.pool())
.await?);
.await;
}
Ok(sqlx::query_scalar::<_, i64>(query)
.bind(pad_id)
@@ -462,10 +462,10 @@ pub async fn pad_public_page_disabled(pool: &Database, pad_id: i64) -> Result<bo
pub async fn note_public_page_disabled(pool: &Database, note_id: i64) -> Result<bool, sqlx::Error> {
let query = queries::get(pool.kind(), queries::NOTE_PUBLIC_PAGE_DISABLED);
if pool.kind() == DatabaseKind::Postgres {
return Ok(sqlx::query_scalar::<_, bool>(query)
return sqlx::query_scalar::<_, bool>(query)
.bind(note_id)
.fetch_one(pool.pool())
.await?);
.await;
}
Ok(sqlx::query_scalar::<_, i64>(query)
.bind(note_id)
+1 -4
View File
@@ -53,10 +53,7 @@ pub fn canonical_file_path(value: &str) -> Option<String> {
let path_start = after_scheme.find('/')?;
&after_scheme[path_start..]
};
let path = path
.split(|character| matches!(character, '?' | '#'))
.next()
.unwrap_or(path);
let path = path.split(['?', '#']).next().unwrap_or(path);
if !path.starts_with(FILE_ROUTE_PREFIX) {
return None;
}
+2 -3
View File
@@ -86,10 +86,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.database_url
.strip_prefix("sqlite://")
.and_then(|v| v.split('?').next())
&& let Some(parent) = std::path::Path::new(path).parent()
{
if let Some(parent) = std::path::Path::new(path).parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::create_dir_all(parent)?;
}
info!("connecting to database");
let db = Database::connect(&config.database_url, config.database_max_connections).await?;
+4 -1
View File
@@ -203,7 +203,10 @@ pub fn websocket_origin_allowed(headers: &HeaderMap) -> bool {
fn resource_cookie_name(kind: &str, slug: &str) -> String {
let digest = Sha256::digest(format!("{kind}:{slug}").as_bytes());
format!("{RESOURCE_ACCESS_COOKIE_PREFIX}{}", hex::encode(&digest[..12]))
format!(
"{RESOURCE_ACCESS_COOKIE_PREFIX}{}",
hex::encode(&digest[..12])
)
}
fn valid_resource_access_cookie_name(name: &str) -> bool {
+6 -10
View File
@@ -129,6 +129,7 @@ pub struct AppState {
}
impl AppState {
#[allow(clippy::too_many_arguments)]
pub fn new(
db: Database,
asset_version: String,
@@ -183,14 +184,13 @@ impl AppState {
let mut limits = self.rate_limits.lock().await;
if !limits.contains_key(&key) && limits.len() >= MAX_RATE_LIMIT_ENTRIES {
limits.retain(|_, entry| now.duration_since(entry.started_at) < entry.window);
if limits.len() >= MAX_RATE_LIMIT_ENTRIES {
if let Some(oldest_key) = limits
if limits.len() >= MAX_RATE_LIMIT_ENTRIES
&& let Some(oldest_key) = limits
.iter()
.min_by_key(|(_, entry)| entry.started_at)
.map(|(key, _)| key.clone())
{
limits.remove(&oldest_key);
}
{
limits.remove(&oldest_key);
}
}
let entry = limits.entry(key).or_insert(RateLimitEntry {
@@ -283,11 +283,7 @@ impl AppState {
pub async fn pad_channel(&self, slug: &str) -> broadcast::Sender<RoomEvent> {
self.channel_for_key(Self::pad_room_key(slug)).await
}
pub async fn notify_pad_password_required(
&self,
slug: &str,
except_client_id: Option<String>,
) {
pub async fn notify_pad_password_required(&self, slug: &str, except_client_id: Option<String>) {
let key = Self::pad_room_key(slug);
let sender = self.channels.read().await.get(&key).cloned();
if let Some(sender) = sender {
+12 -3
View File
@@ -72,9 +72,18 @@ fn video_alias_is_still_attached() {
#[test]
fn byte_ranges_support_video_seeking() {
assert_eq!(parse_byte_range("bytes=0-99", 1_000), Ok(Some((0, 100))));
assert_eq!(parse_byte_range("bytes=500-", 1_000), Ok(Some((500, 1_000))));
assert_eq!(parse_byte_range("bytes=-100", 1_000), Ok(Some((900, 1_000))));
assert_eq!(parse_byte_range("bytes=900-2000", 1_000), Ok(Some((900, 1_000))));
assert_eq!(
parse_byte_range("bytes=500-", 1_000),
Ok(Some((500, 1_000)))
);
assert_eq!(
parse_byte_range("bytes=-100", 1_000),
Ok(Some((900, 1_000)))
);
assert_eq!(
parse_byte_range("bytes=900-2000", 1_000),
Ok(Some((900, 1_000)))
);
assert_eq!(parse_byte_range("bytes=1000-", 1_000), Err(()));
assert_eq!(parse_byte_range("bytes=0-1,4-5", 1_000), Err(()));
}
+3 -3
View File
@@ -14,8 +14,8 @@ fn boolean_projections_are_normalized_for_sqlx_any() {
// MySQL BOOLEAN is TINYINT(1), which sqlx::Any 0.8 cannot map directly.
for (query, expected_casts) in [
(Query::RESOURCE_EDITOR_SETTINGS_SELECT, 1),
(Query::EDITOR_PREFERENCES_SELECT_PAD, 5),
(Query::EDITOR_PREFERENCES_SELECT_NOTE, 5),
(Query::EDITOR_PREFERENCES_SELECT_PAD, 6),
(Query::EDITOR_PREFERENCES_SELECT_NOTE, 6),
(Query::AUTH_USER_BY_EXTERNAL_ID, 1),
(Query::AUTH_USER_BY_SESSION, 1),
(Query::AUTH_USER_BY_NICKNAME, 1),
@@ -27,7 +27,7 @@ fn boolean_projections_are_normalized_for_sqlx_any() {
(Query::USER_FAVORITE_NOTE_LIST, 2),
(Query::PAD_PUBLIC_PAGE_DISABLED, 1),
(Query::NOTE_PUBLIC_PAGE_DISABLED, 1),
(Query::Q001, 1),
(Query::Q001, 2),
(Query::Q003, 1),
(Query::Q004, 1),
(Query::Q011, 1),
+6 -5
View File
@@ -84,7 +84,8 @@ fn rejects_missing_websocket_origin() {
#[test]
fn secure_cookies_are_not_script_readable() {
let value = session_cookie("abc123", 7).to_str().unwrap();
let cookie = session_cookie("abc123", 7);
let value = cookie.to_str().unwrap();
assert!(value.contains("HttpOnly"));
assert!(value.contains("Secure"));
assert!(value.contains("SameSite=Lax"));
@@ -93,9 +94,8 @@ fn secure_cookies_are_not_script_readable() {
#[test]
fn share_sessions_use_separate_scoped_opaque_cookies() {
let value = share_session_cookie("workspace", "private-space", "opaque", 600)
.to_str()
.unwrap();
let cookie = share_session_cookie("workspace", "private-space", "opaque", 600);
let value = cookie.to_str().unwrap();
assert!(value.starts_with("__Host-rustpad_share_"));
assert!(value.contains("=opaque;"));
assert!(value.contains("Max-Age=600"));
@@ -166,7 +166,8 @@ fn csrf_requires_matching_cookie_and_header() {
#[test]
fn csrf_cookie_is_strict_and_script_readable() {
let token = "a".repeat(CSRF_TOKEN_BYTES * 2);
let value = csrf_cookie(&token).to_str().unwrap();
let cookie = csrf_cookie(&token);
let value = cookie.to_str().unwrap();
assert!(value.contains("Secure"));
assert!(value.contains("SameSite=Strict"));
assert!(!value.contains("HttpOnly"));
+1
View File
@@ -594,6 +594,7 @@ pub async fn upgrade(
})
}
#[allow(clippy::too_many_arguments)]
async fn handle_socket(
mut socket: WebSocket,
state: SharedState,
+2
View File
@@ -77,6 +77,8 @@ pub async fn upgrade_pad(
)
})
}
#[allow(clippy::too_many_arguments)]
async fn handle_pad_socket(
mut socket: WebSocket,
state: SharedState,