You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit 'd51a1c47c70d44bfcc1af6fe58f244170513470c' into feature-clean
This commit is contained in:
2
packages/app-lib/.cargo/config.toml
Normal file
2
packages/app-lib/.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[env]
|
||||
SQLX_OFFLINE = "true"
|
||||
@@ -29,6 +29,7 @@ regex = "1.5"
|
||||
sys-info = "0.9.0"
|
||||
sysinfo = "0.30.8"
|
||||
thiserror = "1.0"
|
||||
either = "1.13"
|
||||
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] }
|
||||
@@ -62,6 +63,8 @@ base64 = "0.22.0"
|
||||
|
||||
sqlx = { version = "0.8.2", features = [ "runtime-tokio", "sqlite", "macros" ] }
|
||||
|
||||
ariadne = { path = "../ariadne" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.52.0"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::state::{FriendsSocket, UserFriend, UserStatus};
|
||||
use crate::state::{FriendsSocket, UserFriend};
|
||||
use ariadne::users::UserStatus;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn friends() -> crate::Result<Vec<UserFriend>> {
|
||||
|
||||
@@ -20,8 +20,9 @@ pub mod data {
|
||||
Hooks, JavaVersion, LinkedData, MemorySettings, ModLoader,
|
||||
ModrinthCredentials, Organization, ProcessMetadata, ProfileFile,
|
||||
Project, ProjectType, SearchResult, SearchResults, Settings,
|
||||
TeamMember, Theme, User, UserFriend, UserStatus, Version, WindowSize,
|
||||
TeamMember, Theme, User, UserFriend, Version, WindowSize,
|
||||
};
|
||||
pub use ariadne::users::UserStatus;
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
|
||||
@@ -13,6 +13,11 @@ pub enum ErrorKind {
|
||||
#[error("Serialization error (JSON): {0}")]
|
||||
JSONError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Serialization error (websocket): {0}")]
|
||||
WebsocketSerializationError(
|
||||
#[from] ariadne::networking::serialization::SerializationError,
|
||||
),
|
||||
|
||||
#[error("Error parsing UUID: {0}")]
|
||||
UUIDError(#[from] uuid::Error),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Theseus state management system
|
||||
use crate::state::UserStatus;
|
||||
use ariadne::users::{UserId, UserStatus};
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
@@ -262,8 +262,8 @@ pub enum EventError {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "event")]
|
||||
pub enum FriendPayload {
|
||||
FriendRequest { from: String },
|
||||
UserOffline { id: String },
|
||||
FriendRequest { from: UserId },
|
||||
UserOffline { id: UserId },
|
||||
StatusUpdate { user_status: UserStatus },
|
||||
StatusSync,
|
||||
}
|
||||
|
||||
@@ -2,28 +2,42 @@ use crate::config::{MODRINTH_API_URL_V3, MODRINTH_SOCKET_URL};
|
||||
use crate::data::ModrinthCredentials;
|
||||
use crate::event::emit::emit_friend;
|
||||
use crate::event::FriendPayload;
|
||||
use crate::state::{ProcessManager, Profile};
|
||||
use crate::state::tunnel::InternalTunnelSocket;
|
||||
use crate::state::{ProcessManager, Profile, TunnelSocket};
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json, FetchSemaphore};
|
||||
use ariadne::networking::message::{
|
||||
ClientToServerMessage, ServerToClientMessage,
|
||||
};
|
||||
use ariadne::users::{UserId, UserStatus};
|
||||
use async_tungstenite::tokio::{connect_async, ConnectStream};
|
||||
use async_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use async_tungstenite::tungstenite::Message;
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use either::Either;
|
||||
use futures::stream::SplitSink;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::tcp::OwnedReadHalf;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
type WriteSocket =
|
||||
pub(super) type WriteSocket =
|
||||
Arc<RwLock<Option<SplitSink<WebSocketStream<ConnectStream>, Message>>>>;
|
||||
pub(super) type TunnelSockets = Arc<DashMap<Uuid, Arc<InternalTunnelSocket>>>;
|
||||
|
||||
pub struct FriendsSocket {
|
||||
write: WriteSocket,
|
||||
user_statuses: Arc<DashMap<String, UserStatus>>,
|
||||
user_statuses: Arc<DashMap<UserId, UserStatus>>,
|
||||
tunnel_sockets: TunnelSockets,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
@@ -34,28 +48,6 @@ pub struct UserFriend {
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ClientToServerMessage {
|
||||
StatusUpdate { profile_name: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ServerToClientMessage {
|
||||
StatusUpdate { status: UserStatus },
|
||||
UserOffline { id: String },
|
||||
FriendStatuses { statuses: Vec<UserStatus> },
|
||||
FriendRequest { from: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserStatus {
|
||||
pub user_id: String,
|
||||
pub profile_name: Option<String>,
|
||||
pub last_update: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Default for FriendsSocket {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
@@ -67,6 +59,7 @@ impl FriendsSocket {
|
||||
Self {
|
||||
write: Arc::new(RwLock::new(None)),
|
||||
user_statuses: Arc::new(DashMap::new()),
|
||||
tunnel_sockets: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +113,7 @@ impl FriendsSocket {
|
||||
|
||||
let write_handle = self.write.clone();
|
||||
let statuses = self.user_statuses.clone();
|
||||
let sockets = self.tunnel_sockets.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut read_stream = read;
|
||||
@@ -128,18 +122,14 @@ impl FriendsSocket {
|
||||
Ok(msg) => {
|
||||
let server_message = match msg {
|
||||
Message::Text(text) => {
|
||||
serde_json::from_str::<
|
||||
ServerToClientMessage,
|
||||
>(
|
||||
&text
|
||||
ServerToClientMessage::deserialize(
|
||||
Either::Left(&text),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
Message::Binary(bytes) => {
|
||||
serde_json::from_slice::<
|
||||
ServerToClientMessage,
|
||||
>(
|
||||
&bytes
|
||||
ServerToClientMessage::deserialize(
|
||||
Either::Right(&bytes),
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
@@ -165,7 +155,7 @@ impl FriendsSocket {
|
||||
{
|
||||
match server_message {
|
||||
ServerToClientMessage::StatusUpdate { status } => {
|
||||
statuses.insert(status.user_id.clone(), status.clone());
|
||||
statuses.insert(status.user_id, status.clone());
|
||||
let _ = emit_friend(FriendPayload::StatusUpdate { user_status: status }).await;
|
||||
},
|
||||
ServerToClientMessage::UserOffline { id } => {
|
||||
@@ -175,13 +165,41 @@ impl FriendsSocket {
|
||||
ServerToClientMessage::FriendStatuses { statuses: new_statuses } => {
|
||||
statuses.clear();
|
||||
new_statuses.into_iter().for_each(|status| {
|
||||
statuses.insert(status.user_id.clone(), status);
|
||||
statuses.insert(status.user_id, status);
|
||||
});
|
||||
let _ = emit_friend(FriendPayload::StatusSync).await;
|
||||
}
|
||||
ServerToClientMessage::FriendRequest { from } => {
|
||||
let _ = emit_friend(FriendPayload::FriendRequest { from }).await;
|
||||
}
|
||||
ServerToClientMessage::FriendRequestRejected { .. } => todo!(),
|
||||
|
||||
ServerToClientMessage::FriendSocketListening { .. } => {}, // TODO
|
||||
ServerToClientMessage::FriendSocketStoppedListening { .. } => {}, // TODO
|
||||
|
||||
ServerToClientMessage::SocketConnected { to_socket, new_socket } => {
|
||||
if let Some(connected_to) = sockets.get(&to_socket) {
|
||||
if let InternalTunnelSocket::Listening(local_addr) = *connected_to.value().clone() {
|
||||
if let Ok(new_stream) = TcpStream::connect(local_addr).await {
|
||||
let (read, write) = new_stream.into_split();
|
||||
sockets.insert(new_socket, Arc::new(InternalTunnelSocket::Connected(Mutex::new(write))));
|
||||
Self::socket_read_loop(write_handle.clone(), read, new_socket);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = Self::send_message(&write_handle, ClientToServerMessage::SocketClose { socket: new_socket }).await;
|
||||
},
|
||||
ServerToClientMessage::SocketClosed { socket } => {
|
||||
sockets.remove_if(&socket, |_, x| matches!(*x.clone(), InternalTunnelSocket::Connected(_)));
|
||||
},
|
||||
ServerToClientMessage::SocketData { socket, data } => {
|
||||
if let Some(mut socket) = sockets.get_mut(&socket) {
|
||||
if let InternalTunnelSocket::Connected(ref stream) = *socket.value_mut().clone() {
|
||||
let _ = stream.lock().await.write_all(&data).await;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,10 +235,7 @@ impl FriendsSocket {
|
||||
let mut last_ping = Utc::now();
|
||||
|
||||
loop {
|
||||
let connected = {
|
||||
let read = state.friends_socket.write.read().await;
|
||||
read.is_some()
|
||||
};
|
||||
let connected = state.friends_socket.is_connected().await;
|
||||
|
||||
if !connected
|
||||
&& Utc::now().signed_duration_since(last_connection)
|
||||
@@ -269,16 +284,11 @@ impl FriendsSocket {
|
||||
&self,
|
||||
profile_name: Option<String>,
|
||||
) -> crate::Result<()> {
|
||||
let mut write_lock = self.write.write().await;
|
||||
if let Some(ref mut write_half) = *write_lock {
|
||||
write_half
|
||||
.send(Message::Text(serde_json::to_string(
|
||||
&ClientToServerMessage::StatusUpdate { profile_name },
|
||||
)?))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Self::send_message(
|
||||
&self.write,
|
||||
ClientToServerMessage::StatusUpdate { profile_name },
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
@@ -346,4 +356,81 @@ impl FriendsSocket {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn open_port(&self, port: u16) -> crate::Result<TunnelSocket> {
|
||||
let socket_id = Uuid::new_v4();
|
||||
let socket = self.tunnel_sockets.entry(socket_id).insert(Arc::new(
|
||||
InternalTunnelSocket::Listening(SocketAddr::new(
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
port,
|
||||
)),
|
||||
));
|
||||
Self::send_message(
|
||||
&self.write,
|
||||
ClientToServerMessage::SocketListen { socket: socket_id },
|
||||
)
|
||||
.await?;
|
||||
self.create_tunnel_socket(socket_id, socket)
|
||||
}
|
||||
|
||||
pub async fn is_connected(&self) -> bool {
|
||||
self.write.read().await.is_some()
|
||||
}
|
||||
|
||||
fn create_tunnel_socket(
|
||||
&self,
|
||||
socket_id: Uuid,
|
||||
socket: impl Deref<Target = Arc<InternalTunnelSocket>>,
|
||||
) -> crate::Result<TunnelSocket> {
|
||||
Ok(TunnelSocket {
|
||||
socket_id,
|
||||
write: self.write.clone(),
|
||||
sockets: self.tunnel_sockets.clone(),
|
||||
internal: socket.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn socket_read_loop(
|
||||
write: WriteSocket,
|
||||
mut read_half: OwnedReadHalf,
|
||||
socket_id: Uuid,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let mut read_buffer = [0u8; 8192];
|
||||
loop {
|
||||
match read_half.read(&mut read_buffer).await {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => {
|
||||
let _ = Self::send_message(
|
||||
&write,
|
||||
ClientToServerMessage::SocketSend {
|
||||
socket: socket_id,
|
||||
data: read_buffer[..n].to_vec(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(write))]
|
||||
pub(super) async fn send_message(
|
||||
write: &WriteSocket,
|
||||
message: ClientToServerMessage,
|
||||
) -> crate::Result<()> {
|
||||
let serialized = match message.serialize()? {
|
||||
Either::Left(text) => Message::text(text),
|
||||
Either::Right(bytes) => Message::binary(bytes),
|
||||
};
|
||||
|
||||
let mut write_lock = write.write().await;
|
||||
if let Some(ref mut write_half) = *write_lock {
|
||||
write_half.send(serialized).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ pub use self::cache::*;
|
||||
mod friends;
|
||||
pub use self::friends::*;
|
||||
|
||||
mod tunnel;
|
||||
pub use self::tunnel::*;
|
||||
|
||||
pub mod db;
|
||||
pub mod fs_watcher;
|
||||
mod mr_auth;
|
||||
|
||||
61
packages/app-lib/src/state/tunnel.rs
Normal file
61
packages/app-lib/src/state/tunnel.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use crate::state::friends::{TunnelSockets, WriteSocket};
|
||||
use crate::state::FriendsSocket;
|
||||
use ariadne::networking::message::ClientToServerMessage;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::tcp::OwnedWriteHalf;
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(super) enum InternalTunnelSocket {
|
||||
Listening(SocketAddr),
|
||||
Connected(Mutex<OwnedWriteHalf>),
|
||||
}
|
||||
|
||||
pub struct TunnelSocket {
|
||||
pub(super) socket_id: Uuid,
|
||||
pub(super) write: WriteSocket,
|
||||
pub(super) sockets: TunnelSockets,
|
||||
pub(super) internal: Arc<InternalTunnelSocket>,
|
||||
}
|
||||
|
||||
impl TunnelSocket {
|
||||
pub fn socket_id(&self) -> Uuid {
|
||||
self.socket_id
|
||||
}
|
||||
|
||||
pub async fn shutdown(self) -> crate::Result<()> {
|
||||
if self.sockets.remove(&self.socket_id).is_some() {
|
||||
FriendsSocket::send_message(
|
||||
&self.write,
|
||||
ClientToServerMessage::SocketClose {
|
||||
socket: self.socket_id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if let InternalTunnelSocket::Connected(ref stream) =
|
||||
*self.internal.clone()
|
||||
{
|
||||
stream.lock().await.shutdown().await?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TunnelSocket {
|
||||
fn drop(&mut self) {
|
||||
if self.sockets.remove(&self.socket_id).is_some() {
|
||||
let write = self.write.clone();
|
||||
let socket_id = self.socket_id;
|
||||
tokio::spawn(async move {
|
||||
let _ = FriendsSocket::send_message(
|
||||
&write,
|
||||
ClientToServerMessage::SocketClose { socket: socket_id },
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Functions for fetching infromation from the Internet
|
||||
use super::io::{self, IOError};
|
||||
use crate::config::{MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
||||
use crate::event::emit::emit_loading;
|
||||
use crate::event::LoadingBarId;
|
||||
use bytes::Bytes;
|
||||
@@ -11,8 +13,6 @@ use std::time::{self};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
use super::io::{self, IOError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IoSemaphore(pub Semaphore);
|
||||
#[derive(Debug)]
|
||||
@@ -87,7 +87,8 @@ pub async fn fetch_advanced(
|
||||
.map(|x| &*x.0.to_lowercase() == "authorization")
|
||||
.unwrap_or(false)
|
||||
&& (url.starts_with("https://cdn.modrinth.com")
|
||||
|| url.starts_with("https://api.modrinth.com"))
|
||||
|| url.starts_with(MODRINTH_API_URL)
|
||||
|| url.starts_with(MODRINTH_API_URL_V3))
|
||||
{
|
||||
crate::state::ModrinthCredentials::get_active(exec).await?
|
||||
} else {
|
||||
|
||||
15
packages/ariadne/Cargo.toml
Normal file
15
packages/ariadne/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "ariadne"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
uuid = { version = "1.2.2", features = ["v4", "fast-rng", "serde"] }
|
||||
serde_bytes = "0.11"
|
||||
rand = "0.8.5"
|
||||
either = "1.13"
|
||||
chrono = { version = "0.4.26", features = ["serde"] }
|
||||
serde_cbor = "0.11"
|
||||
216
packages/ariadne/src/ids.rs
Normal file
216
packages/ariadne/src/ids.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
pub use super::users::UserId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
/// long when encoded as base62.
|
||||
///
|
||||
/// Uses `rand`'s thread rng on every call.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if `n` is 0 or greater than 11, since a `u64`
|
||||
/// can only represent up to 11 character base62 strings
|
||||
#[inline]
|
||||
pub fn random_base62(n: usize) -> u64 {
|
||||
random_base62_rng(&mut rand::thread_rng(), n)
|
||||
}
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
/// long when encoded as base62, using the given rng.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if `n` is 0 or greater than 11, since a `u64`
|
||||
/// can only represent up to 11 character base62 strings
|
||||
pub fn random_base62_rng<R: rand::RngCore>(rng: &mut R, n: usize) -> u64 {
|
||||
random_base62_rng_range(rng, n, n)
|
||||
}
|
||||
|
||||
pub fn random_base62_rng_range<R: rand::RngCore>(
|
||||
rng: &mut R,
|
||||
n_min: usize,
|
||||
n_max: usize,
|
||||
) -> u64 {
|
||||
use rand::Rng;
|
||||
assert!(n_min > 0 && n_max <= 11 && n_min <= n_max);
|
||||
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
|
||||
// which is n characters long when encoded
|
||||
rng.gen_range(MULTIPLES[n_min - 1]..MULTIPLES[n_max])
|
||||
}
|
||||
|
||||
const MULTIPLES: [u64; 12] = [
|
||||
1,
|
||||
62,
|
||||
62 * 62,
|
||||
62 * 62 * 62,
|
||||
62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||
u64::MAX,
|
||||
];
|
||||
|
||||
/// An ID encoded as base62 for use in the API.
|
||||
///
|
||||
/// All ids should be random and encode to 8-10 character base62 strings,
|
||||
/// to avoid enumeration and other attacks.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Base62Id(pub u64);
|
||||
|
||||
/// An error decoding a number from base62.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecodingError {
|
||||
/// Encountered a non-base62 character in a base62 string
|
||||
#[error("Invalid character {0:?} in base62 encoding")]
|
||||
InvalidBase62(char),
|
||||
/// Encountered integer overflow when decoding a base62 id.
|
||||
#[error("Base62 decoding overflowed")]
|
||||
Overflow,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! from_base62id {
|
||||
($($struct:ty, $con:expr;)+) => {
|
||||
$(
|
||||
impl From<Base62Id> for $struct {
|
||||
fn from(id: Base62Id) -> $struct {
|
||||
$con(id.0)
|
||||
}
|
||||
}
|
||||
impl From<$struct> for Base62Id {
|
||||
fn from(id: $struct) -> Base62Id {
|
||||
Base62Id(id.0)
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_base62_display {
|
||||
($struct:ty) => {
|
||||
impl std::fmt::Display for $struct {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&$crate::ids::base62_impl::to_base62(self.0))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_base62_display!(Base62Id);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! base62_id_impl {
|
||||
($struct:ty, $cons:expr) => {
|
||||
$crate::ids::from_base62id!($struct, $cons;);
|
||||
$crate::ids::impl_base62_display!($struct);
|
||||
}
|
||||
}
|
||||
base62_id_impl!(UserId, UserId);
|
||||
|
||||
pub use {base62_id_impl, from_base62id, impl_base62_display};
|
||||
|
||||
pub mod base62_impl {
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
use serde::ser::Serializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Base62Id, DecodingError};
|
||||
|
||||
impl<'de> Deserialize<'de> for Base62Id {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct Base62Visitor;
|
||||
|
||||
impl Visitor<'_> for Base62Visitor {
|
||||
type Value = Base62Id;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
formatter.write_str("a base62 string id")
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(Base62Id(v))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, string: &str) -> Result<Base62Id, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
parse_base62(string).map(Base62Id).map_err(E::custom)
|
||||
}
|
||||
}
|
||||
|
||||
if deserializer.is_human_readable() {
|
||||
deserializer.deserialize_str(Base62Visitor)
|
||||
} else {
|
||||
deserializer.deserialize_u64(Base62Visitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Base62Id {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if serializer.is_human_readable() {
|
||||
serializer.serialize_str(&to_base62(self.0))
|
||||
} else {
|
||||
serializer.serialize_u64(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BASE62_CHARS: [u8; 62] =
|
||||
*b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
pub fn to_base62(mut num: u64) -> String {
|
||||
let length = (num as f64).log(62.0).ceil() as usize;
|
||||
let mut output = String::with_capacity(length);
|
||||
|
||||
while num > 0 {
|
||||
// Could be done more efficiently, but requires byte
|
||||
// manipulation of strings & Vec<u8> -> String conversion
|
||||
output.insert(0, BASE62_CHARS[(num % 62) as usize] as char);
|
||||
num /= 62;
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub fn parse_base62(string: &str) -> Result<u64, DecodingError> {
|
||||
let mut num: u64 = 0;
|
||||
for c in string.chars() {
|
||||
let next_digit;
|
||||
if c.is_ascii_digit() {
|
||||
next_digit = (c as u8 - b'0') as u64;
|
||||
} else if c.is_ascii_uppercase() {
|
||||
next_digit = 10 + (c as u8 - b'A') as u64;
|
||||
} else if c.is_ascii_lowercase() {
|
||||
next_digit = 36 + (c as u8 - b'a') as u64;
|
||||
} else {
|
||||
return Err(DecodingError::InvalidBase62(c));
|
||||
}
|
||||
|
||||
// We don't want this panicking or wrapping on integer overflow
|
||||
if let Some(n) =
|
||||
num.checked_mul(62).and_then(|n| n.checked_add(next_digit))
|
||||
{
|
||||
num = n;
|
||||
} else {
|
||||
return Err(DecodingError::Overflow);
|
||||
}
|
||||
}
|
||||
Ok(num)
|
||||
}
|
||||
}
|
||||
3
packages/ariadne/src/lib.rs
Normal file
3
packages/ariadne/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod ids;
|
||||
pub mod networking;
|
||||
pub mod users;
|
||||
3
packages/ariadne/src/mod.rs
Normal file
3
packages/ariadne/src/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod ids;
|
||||
pub mod networking;
|
||||
pub mod users;
|
||||
65
packages/ariadne/src/networking/message.rs
Normal file
65
packages/ariadne/src/networking/message.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::ids::UserId;
|
||||
use crate::users::UserStatus;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ClientToServerMessage {
|
||||
StatusUpdate {
|
||||
profile_name: Option<String>,
|
||||
},
|
||||
|
||||
SocketListen {
|
||||
socket: Uuid,
|
||||
},
|
||||
SocketClose {
|
||||
socket: Uuid,
|
||||
},
|
||||
SocketSend {
|
||||
socket: Uuid,
|
||||
#[serde(with = "serde_bytes")]
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ServerToClientMessage {
|
||||
StatusUpdate {
|
||||
status: UserStatus,
|
||||
},
|
||||
UserOffline {
|
||||
id: UserId,
|
||||
},
|
||||
FriendStatuses {
|
||||
statuses: Vec<UserStatus>,
|
||||
},
|
||||
FriendRequest {
|
||||
from: UserId,
|
||||
},
|
||||
FriendRequestRejected {
|
||||
from: UserId,
|
||||
},
|
||||
|
||||
FriendSocketListening {
|
||||
user: UserId,
|
||||
socket: Uuid,
|
||||
},
|
||||
FriendSocketStoppedListening {
|
||||
user: UserId,
|
||||
},
|
||||
|
||||
SocketConnected {
|
||||
to_socket: Uuid,
|
||||
new_socket: Uuid,
|
||||
},
|
||||
SocketClosed {
|
||||
socket: Uuid,
|
||||
},
|
||||
SocketData {
|
||||
socket: Uuid,
|
||||
#[serde(with = "serde_bytes")]
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
2
packages/ariadne/src/networking/mod.rs
Normal file
2
packages/ariadne/src/networking/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod message;
|
||||
pub mod serialization;
|
||||
56
packages/ariadne/src/networking/serialization.rs
Normal file
56
packages/ariadne/src/networking/serialization.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use super::message::{ClientToServerMessage, ServerToClientMessage};
|
||||
use either::Either;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SerializationError {
|
||||
#[error("Failed to (de)serialize message: {0}")]
|
||||
SerializationFailed(#[from] serde_json::Error),
|
||||
|
||||
#[error("Failed to (de)serialize binary message: {0}")]
|
||||
BinarySerializationFailed(#[from] serde_cbor::Error),
|
||||
}
|
||||
|
||||
macro_rules! message_serialization {
|
||||
($message_enum:ty $(,$binary_pattern:pat_param)* $(,)?) => {
|
||||
impl $message_enum {
|
||||
pub fn is_binary(&self) -> bool {
|
||||
match self {
|
||||
$(
|
||||
$binary_pattern => true,
|
||||
)*
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize(
|
||||
&self,
|
||||
) -> Result<Either<String, Vec<u8>>, SerializationError> {
|
||||
Ok(match self {
|
||||
$(
|
||||
$binary_pattern => Either::Right(serde_cbor::to_vec(self)?),
|
||||
)*
|
||||
_ => Either::Left(serde_json::to_string(self)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize(
|
||||
msg: Either<&str, &[u8]>,
|
||||
) -> Result<Self, SerializationError> {
|
||||
Ok(match msg {
|
||||
Either::Left(text) => serde_json::from_str(&text)?,
|
||||
Either::Right(bytes) => serde_cbor::from_slice(&bytes)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
message_serialization!(
|
||||
ClientToServerMessage,
|
||||
ClientToServerMessage::SocketSend { .. },
|
||||
);
|
||||
message_serialization!(
|
||||
ServerToClientMessage,
|
||||
ServerToClientMessage::SocketData { .. },
|
||||
);
|
||||
15
packages/ariadne/src/users.rs
Normal file
15
packages/ariadne/src/users.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use super::ids::Base62Id;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct UserId(pub u64);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserStatus {
|
||||
pub user_id: UserId,
|
||||
pub profile_name: Option<String>,
|
||||
pub last_update: DateTime<Utc>,
|
||||
}
|
||||
@@ -37,7 +37,7 @@ async function copyText() {
|
||||
margin: 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--color-button-bg);
|
||||
width: min-content;
|
||||
width: fit-content;
|
||||
border-radius: 10px;
|
||||
user-select: text;
|
||||
transition:
|
||||
@@ -50,12 +50,6 @@ async function copyText() {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
span {
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
57
packages/ui/src/components/base/Timeline.vue
Normal file
57
packages/ui/src/components/base/Timeline.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
fadeOutStart?: boolean
|
||||
fadeOutEnd?: boolean
|
||||
}>(),
|
||||
{
|
||||
fadeOutStart: false,
|
||||
fadeOutEnd: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative flex flex-col gap-4 pb-6 isolate">
|
||||
<div class="absolute flex h-full w-4 justify-center">
|
||||
<div
|
||||
class="timeline-indicator"
|
||||
:class="{ 'fade-out-start': fadeOutStart, 'fade-out-end': fadeOutEnd }"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.timeline-indicator {
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--timeline-line-color, var(--color-raised-bg)) 66%,
|
||||
rgba(255, 255, 255, 0) 0%
|
||||
);
|
||||
background-size: 100% 30px;
|
||||
background-repeat: repeat-y;
|
||||
margin-top: 1rem;
|
||||
|
||||
height: calc(100% - 1rem);
|
||||
width: 4px;
|
||||
z-index: -1;
|
||||
|
||||
&.fade-out-start {
|
||||
mask-image: linear-gradient(to top, black calc(100% - 15rem), transparent 100%);
|
||||
}
|
||||
|
||||
&.fade-out-end {
|
||||
mask-image: linear-gradient(to bottom, black calc(100% - 15rem), transparent 100%);
|
||||
}
|
||||
|
||||
&.fade-out-start.fade-out-end {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 8rem,
|
||||
black calc(100% - 8rem),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -35,6 +35,7 @@ export { default as Slider } from './base/Slider.vue'
|
||||
export { default as StatItem } from './base/StatItem.vue'
|
||||
export { default as TagItem } from './base/TagItem.vue'
|
||||
export { default as TeleportDropdownMenu } from './base/TeleportDropdownMenu.vue'
|
||||
export { default as Timeline } from './base/Timeline.vue'
|
||||
export { default as Toggle } from './base/Toggle.vue'
|
||||
|
||||
// Branding
|
||||
|
||||
@@ -67,7 +67,7 @@ function show() {
|
||||
}
|
||||
|
||||
function hide() {
|
||||
props.onHide()
|
||||
props.onHide?.()
|
||||
actuallyShown.value = false
|
||||
setTimeout(() => {
|
||||
shown.value = false
|
||||
|
||||
@@ -90,7 +90,7 @@ function addBodyPadding() {
|
||||
}
|
||||
|
||||
function show(event?: MouseEvent) {
|
||||
props.onShow()
|
||||
props.onShow?.()
|
||||
open.value = true
|
||||
|
||||
addBodyPadding()
|
||||
@@ -109,7 +109,7 @@ function show(event?: MouseEvent) {
|
||||
}
|
||||
|
||||
function hide() {
|
||||
props.onHide()
|
||||
props.onHide?.()
|
||||
visible.value = false
|
||||
document.body.style.overflow = ''
|
||||
document.body.style.paddingRight = ''
|
||||
|
||||
@@ -10,6 +10,35 @@ export type VersionEntry = {
|
||||
}
|
||||
|
||||
const VERSIONS: VersionEntry[] = [
|
||||
{
|
||||
date: `2025-03-05T17:40:00-08:00`,
|
||||
product: 'web',
|
||||
body: `### Improvements
|
||||
- Fixed moderation-end pages failing under edge cases.`,
|
||||
},
|
||||
{
|
||||
date: `2025-03-05T12:40:00-08:00`,
|
||||
product: 'web',
|
||||
body: `### Improvements
|
||||
- Fixed various errors with modals for some users.
|
||||
- Fixed hold R button not working on some systems.`,
|
||||
},
|
||||
{
|
||||
date: `2025-03-03T22:30:00-08:00`,
|
||||
product: 'web',
|
||||
body: `### Added
|
||||
- Hold R for a random project :D
|
||||
|
||||
### Improvements
|
||||
- Improved admin navigation and admin panels.`,
|
||||
},
|
||||
{
|
||||
date: `2025-03-02T18:45:00-08:00`,
|
||||
product: 'web',
|
||||
body: `### Improvements
|
||||
- Added option to copy version IDs from the version list for project members and developer mode.
|
||||
- Fixed the staff moderation checklist going off the screen.`,
|
||||
},
|
||||
{
|
||||
date: `2025-02-25T10:20:00-08:00`,
|
||||
product: 'servers',
|
||||
|
||||
Reference in New Issue
Block a user