use std::{ convert::{ TryFrom, TryInto, }, collections::BTreeSet, fmt::Display, str::FromStr, }; use juniper::{ EmptySubscription, ID, RootNode, graphql_object, GraphQLObject, GraphQLInputObject, GraphQLEnum, FieldResult, FieldError, }; use sqlx::{ types::chrono::{ DateTime, Utc, }, FromRow, Encode, Decode, Type, Row, Result as SqlxResult, Error as SqlxError, }; #[cfg(feature = "sqlite")] use sqlx::{ SqlitePool, sqlite::SqliteRow, }; use url::Url; use uuid::Uuid; #[inline] fn into_limit( #[cfg(feature = "sqlite")] row: &SqliteRow, index: &str ) -> SqlxResult> where T: FromStr, ::Err: Into>, { Ok(from_sql_formatted_array(row.try_get(index)?, index)?.collect()) } #[inline] fn into_column_decode_err(index: &str) -> impl FnOnce(E) -> SqlxError + '_ where E: Into> { move |err: E| SqlxError::ColumnDecode { index: index.to_owned(), source: err.into(), } } fn from_sql_formatted_array(raw: String, index: &str) -> SqlxResult> where T: FromStr, ::Err: Into>, { let mut raw_values: Vec<_> = raw.split(',').collect(); if raw_values[0] == raw { raw_values.clear(); } let mut values = Vec::new(); for r in raw_values.drain(..) { values.push(r.parse().map_err(into_column_decode_err(index))?); } Ok(values.into_iter()) } #[inline] fn format_array_for_sql(values: I) -> String where I: IntoIterator, T: Display, { let mut iter = values.into_iter(); let mut output = match iter.next() { Some(first) => first.to_string(), None => return String::new(), }; for v in iter { output += ","; output += &v.to_string(); } output } #[inline] fn into_toml_iter_from_row<'d, T>( #[cfg(feature = "sqlite")] row: &'d SqliteRow, index: &str ) -> SqlxResult> where T: FromStr, ::Err: Into>, { from_sql_formatted_array(row.try_get(index)?, index) } #[inline] fn id_to_uuid(id: &ID) -> Result { Uuid::from_str(&id) } fn is_valid_user_name(username: &str) -> bool { if !username.starts_with('@') { return false; } for c in username[1..].chars() { if !( c.is_alphabetic() && c.to_string() == c.to_lowercase().to_string() || c.is_ascii_digit() || "_-.".contains(c) ) { return false; } } true } #[derive(Clone, Debug)] pub struct Context { #[cfg(feature = "sqlite")] pub db: SqlitePool, } impl juniper::Context for Context {} #[derive(Clone, Debug, Decode, Type)] pub struct User { // can we change this to Uuid? pub id: ID, pub user_name: String, pub display_name: Option, pub activated: bool, pub created: DateTime, pub last_online: Option>, } #[graphql_object(context = Context)] impl User { pub const fn id(&self) -> &ID { &self.id } pub const fn user_name(&self) -> &String { &self.user_name } pub const fn display_name(&self) -> Option<&String> { self.display_name.as_ref() } pub const fn activated(&self) -> bool { self.activated } pub const fn created(&self) -> &DateTime { &self.created } pub const fn last_online(&self) -> Option<&DateTime> { self.last_online.as_ref() } } impl TryFrom for User { type Error = FieldError; fn try_from(row: SqliteRow) -> FieldResult { let id = ID::new(row.try_get::("id")?); Ok(User { id, user_name: row.try_get("user_name")?, display_name: row.try_get("display_name")?, activated: row.try_get("activated")?, created: row.try_get("created")?, last_online: row.try_get("last_online")?, }) } } #[derive(Clone, Debug, GraphQLInputObject)] pub struct NewUser { pub(crate) user_name: String, pub(crate) display_name: Option, pub(crate) password_hash: String, } impl From for User { fn from(new_user: NewUser) -> Self { User { // TODO: how do we generate uuids? id: ID::new(Uuid::new_v4().to_string()), user_name: new_user.user_name, display_name: new_user.display_name, activated: false, created: Utc::now(), last_online: None, } } } impl From for UserPreferences { fn from(new_user: NewUser) -> Self { UserPreferences { privacy_preferences: Default::default(), security_preferences: SecurityPreferences { account_tokens: Vec::new(), password_hash: new_user.password_hash, }, notification_preferences: Default::default(), external_servers_preferences: Default::default(), } } } #[derive(Clone, Debug, GraphQLObject, Decode, Encode, FromRow)] pub struct UserPreferences { privacy_preferences: PrivacyPreferences, notification_preferences: NotificationPreferences, security_preferences: SecurityPreferences, external_servers_preferences: ExternalServersPreferences, } #[derive(Clone, Debug, GraphQLObject)] pub struct PrivacyPreferences { discovery: RestrictionPolicy, discovery_user_limit: Vec, discovery_server_limit: Vec, last_seen: RestrictionPolicy, last_seen_user_limit: Vec, last_seen_server_limit: Vec, last_seen_course: bool, info: RestrictionPolicy, info_user_limit: Vec, info_server_limit: Vec, } impl Default for PrivacyPreferences { fn default() -> Self { PrivacyPreferences { discovery: Default::default(), discovery_user_limit: Default::default(), discovery_server_limit: Default::default(), last_seen: Default::default(), last_seen_user_limit: Default::default(), last_seen_server_limit: Default::default(), last_seen_course: true, info: Default::default(), info_user_limit: Default::default(), info_server_limit: Default::default(), } } } impl PrivacyPreferences { fn from_row_cfg( #[cfg(feature = "sqlite")] row: &SqliteRow, ) -> SqlxResult { Ok(PrivacyPreferences { discovery: row.try_get("discovery")?, discovery_user_limit: into_limit(row, "discovery_user_limit")?, discovery_server_limit: into_limit(row, "discovery_server_limit")?, last_seen: row.try_get("last_seen")?, last_seen_user_limit: into_limit(row, "last_seen_user_limit")?, last_seen_server_limit: into_limit(row, "last_seen_server_limit")?, last_seen_course: row.try_get("last_seen_course")?, info: row.try_get("info")?, info_user_limit: into_limit(row, "info_user_limit")?, info_server_limit: into_limit(row, "info_server_limit")?, }) } } #[cfg(feature = "sqlite")] impl FromRow<'_, SqliteRow> for PrivacyPreferences { fn from_row(row: &SqliteRow) -> SqlxResult { Self::from_row_cfg(row) } } #[derive(Clone, Copy, Debug, Default, GraphQLObject, Type, FromRow)] pub struct NotificationPreferences { lock_details: bool, do_not_disturb: bool, } #[derive(Clone, Debug, GraphQLObject, Decode, Encode)] pub struct SecurityPreferences { account_tokens: Vec, password_hash: String, } impl SecurityPreferences { fn from_row_cfg( #[cfg(feature = "sqlite")] row: &SqliteRow, ) -> SqlxResult { Ok(SecurityPreferences { account_tokens: into_toml_iter_from_row::(row, "account_tokens")? .map(ID::new) .collect(), password_hash: row.try_get("password_hash")?, }) } } #[cfg(feature = "sqlite")] impl FromRow<'_, SqliteRow> for SecurityPreferences { fn from_row(row: &SqliteRow) -> SqlxResult { Self::from_row_cfg(row) } } #[derive(Clone, Debug, Default, GraphQLObject, Decode, Encode, FromRow)] pub struct ExternalServersPreferences { privacy_preferences: PrivacyPreferences, external_servers: RestrictionPolicy, external_servers_limit: Vec, } #[derive(Clone, Copy, Debug, GraphQLEnum, Type)] #[repr(u8)] pub enum RestrictionPolicy { Everyone = 0, Excluding = 1, Including = 2, None = 3, } impl Default for RestrictionPolicy { fn default() -> Self { Self::Everyone } } #[derive(Clone, Copy, Debug)] pub struct Query; #[graphql_object(context = Context)] impl Query { async fn getUserID(context: &Context, username: String) -> FieldResult { if !is_valid_user_name(&username) { return Err(format!("{:?} is not a valid username", username).into()); } sqlx::query(format!( "SELECT id FROM users WHERE user_name={:?}", username ).as_str()).fetch_one(&context.db).await? .try_get::("id") .map(ID::new) .map_err(FieldError::from) } async fn users(context: &Context) -> FieldResult> { let rows = sqlx::query("SELECT * FROM users") .fetch_all(&context.db).await?; let mut users = Vec::with_capacity(rows.capacity()); for row in rows { users.push(row.try_into()?); } Ok(users) } async fn userPreferences(context: &Context, id: ID, password_hash: String) -> FieldResult { let uuid = id_to_uuid(&id)?.to_simple(); let security_preferences: SecurityPreferences = sqlx::query_as(format!( r#"SELECT * FROM security_preferences WHERE id = "{}""#, uuid, ).as_str()).fetch_one(&context.db).await?; if security_preferences.password_hash != password_hash { return Err("incorrect password hash".into()) } let privacy_preferences: PrivacyPreferences = sqlx::query_as(format!( r#"SELECT * FROM privacy_preferences WHERE id = "{}""#, uuid, ).as_str()).fetch_one(&context.db).await?; let notification_preferences: NotificationPreferences = sqlx::query_as(format!( r#"SELECT * FROM notification_preferences WHERE id = "{}""#, uuid, ).as_str()).fetch_one(&context.db).await?; let external_servers_preferences = sqlx::query(format!( r#"SELECT * FROM external_servers_preferences WHERE id = "{}""#, uuid, ).as_str()).fetch_one(&context.db).await?; let external_servers_privacy_preferences: PrivacyPreferences = sqlx::query_as(format!( r#"SELECT * FROM external_servers_privacy_preferences WHERE id = "{}""#, uuid, ).as_str()).fetch_one(&context.db).await?; Ok(UserPreferences { privacy_preferences, notification_preferences, security_preferences, external_servers_preferences: ExternalServersPreferences { external_servers: external_servers_preferences.try_get("external_servers")?, external_servers_limit: into_limit(&external_servers_preferences, "external_servers_limit")?, privacy_preferences: external_servers_privacy_preferences, }, }) } } #[derive(Clone, Copy, Debug)] pub struct Mutation; #[graphql_object(context = Context)] impl Mutation { async fn createUser(context: &Context, new_user: NewUser) -> FieldResult { let user: User = new_user.clone().into(); if !is_valid_user_name(&user.user_name) { return Err(format!("{:?} is not a valid username", user.user_name).into()); } let uuid = id_to_uuid(&user.id)?.to_simple(); let user_preferences: UserPreferences = new_user.into(); if sqlx::query( format!("SELECT id FROM users WHERE user_name={:?}", user.user_name).as_str() ).fetch_all(&context.db).await?.len() > 0 { return Err("username is already in use".into()); } sqlx::query(format!( r#"INSERT INTO users VALUES ("{}", {:?}, {}, {}, {}, {})"#, uuid, user.user_name, user.display_name.map(|x| format!("{:?}", x)).unwrap_or("NULL".to_string()), user.activated as u8, user.created.timestamp(), match user.last_online { None => "NULL".to_string(), Some(d) => format!("{}", d.timestamp()), }, ).as_str()).execute(&context.db).await?; let privacy_preferences = user_preferences.privacy_preferences; sqlx::query(format!( r#"INSERT INTO privacy_preferences VALUES ("{}", {}, {:?}, {:?}, {}, {:?}, {:?}, {}, {}, {:?}, {:?})"#, uuid, privacy_preferences.discovery as u8, format_array_for_sql(&privacy_preferences.discovery_user_limit), format_array_for_sql(&privacy_preferences.discovery_server_limit), privacy_preferences.last_seen as u8, format_array_for_sql(&privacy_preferences.last_seen_user_limit), format_array_for_sql(&privacy_preferences.last_seen_server_limit), privacy_preferences.last_seen_course as u8, privacy_preferences.info as u8, format_array_for_sql(&privacy_preferences.info_user_limit), format_array_for_sql(&privacy_preferences.info_server_limit), ).as_str()).execute(&context.db).await?; let notification_preferences = user_preferences.notification_preferences; sqlx::query(format!( r#"INSERT INTO notification_preferences VALUES ("{}", {}, {})"#, uuid, notification_preferences.lock_details as u8, notification_preferences.do_not_disturb as u8, ).as_str()).execute(&context.db).await?; let security_preferences = user_preferences.security_preferences; sqlx::query(format!( r#"INSERT INTO security_preferences VALUES ("{}", {:?}, {:?})"#, uuid, format_array_for_sql(&security_preferences.account_tokens), security_preferences.password_hash, ).as_str()).execute(&context.db).await?; let external_servers_preferences = user_preferences.external_servers_preferences; sqlx::query(format!( r#"INSERT INTO external_servers_preferences VALUES ("{}", {}, {:?})"#, uuid, external_servers_preferences.external_servers as u8, format_array_for_sql(&external_servers_preferences.external_servers_limit), ).as_str()).execute(&context.db).await?; let external_servers_privacy_preferences = external_servers_preferences.privacy_preferences; sqlx::query(format!( r#"INSERT INTO external_servers_privacy_preferences VALUES ("{}", {}, {:?}, {:?}, {}, {:?}, {:?}, {}, {}, {:?}, {:?})"#, uuid, external_servers_privacy_preferences.discovery as u8, format_array_for_sql(&external_servers_privacy_preferences.discovery_user_limit), format_array_for_sql(&external_servers_privacy_preferences.discovery_server_limit), external_servers_privacy_preferences.last_seen as u8, format_array_for_sql(&external_servers_privacy_preferences.last_seen_user_limit), format_array_for_sql(&external_servers_privacy_preferences.last_seen_server_limit), external_servers_privacy_preferences.last_seen_course as u8, external_servers_privacy_preferences.info as u8, format_array_for_sql(&external_servers_privacy_preferences.info_user_limit), format_array_for_sql(&external_servers_privacy_preferences.info_server_limit), ).as_str()).execute(&context.db).await?; Ok(ID::new(id_to_uuid(&user.id)?.to_simple().to_string())) } async fn newChat(context: &Context, user: ID, password_hash: String, with: ID) -> FieldResult { let user = id_to_uuid(&user)?.to_simple(); let chat_partner = id_to_uuid(&with)?.to_simple(); // Sort users let users = BTreeSet::from_iter([user, chat_partner]); // User authentication let authentication_successful = sqlx::query(format!( r#"SELECT users.id FROM users, security_preferences WHERE users.id = "{}" AND password_hash = "{}""#, user, password_hash, ).as_str()).fetch_optional(&context.db).await?.is_some(); if !authentication_successful { return Err("authentication failed".into()); } // Chat partner needs to be another user (for now) if user == chat_partner { return Err("chat partner is the same user".into()); } // Chat partner must exist let chat_partner_exists = sqlx::query( format!(r#"SELECT id FROM users WHERE id = "{}""#, chat_partner).as_str() ).fetch_optional(&context.db).await?.is_some(); if !chat_partner_exists { return Err(format!(r#"chat partner "{}" does not exist on this server"#, chat_partner).into()); } // non-group chats must be unique let chat_already_exists = sqlx::query(format!( r#"SELECT id FROM chat_index WHERE is_group_chat = 0 AND users = "{}""#, format_array_for_sql(&users), ).as_str()).fetch_optional(&context.db).await?.is_some(); if chat_already_exists { return Err("a personal chat with this chat partner already exists".into()); } // create new personal chat let chat_uuid = Uuid::new_v4(); sqlx::query(format!( r#"INSERT INTO chat_index VALUES ("{}", 0, "{}", NULL, "{1}")"#, chat_uuid.to_simple(), format_array_for_sql(users), ).as_str()).execute(&context.db).await?; crate::sqlite::create_chat(&context.db, &chat_uuid).await?; Ok(chat_uuid.to_simple().to_string().into()) } } pub type Schema<'root_node> = RootNode<'root_node, Query, Mutation, EmptySubscription>; pub fn schema<'root_node>() -> Schema<'root_node> { Schema::new(Query, Mutation, EmptySubscription::new()) }