noise-server/src/graphql.rs

799 lines
25 KiB
Rust

use std::{
convert::{
TryFrom,
TryInto,
},
collections::BTreeSet,
fmt::Display,
str::FromStr,
time::{
SystemTime,
SystemTimeError,
UNIX_EPOCH,
},
};
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<T>(
#[cfg(feature = "sqlite")]
row: &SqliteRow,
index: &str
) -> SqlxResult<Vec<T>>
where
T: FromStr,
<T as FromStr>::Err: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Ok(from_sql_formatted_array(row.try_get(index)?, index)?.collect())
}
#[inline]
fn into_column_decode_err<E>(index: &str) -> impl FnOnce(E) -> SqlxError + '_
where
E: Into<Box<dyn std::error::Error + Send + Sync>>
{
move |err: E| SqlxError::ColumnDecode {
index: index.to_owned(),
source: err.into(),
}
}
fn from_sql_formatted_array<T>(raw: String, index: &str) -> SqlxResult<impl Iterator<Item = T>>
where
T: FromStr,
<T as FromStr>::Err: Into<Box<dyn std::error::Error + Send + Sync>>,
{
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<I, T>(values: I) -> String
where
I: IntoIterator<Item=T>,
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<impl Iterator<Item = T>>
where
T: FromStr,
<T as FromStr>::Err: Into<Box<dyn std::error::Error + Send + Sync>>,
{
from_sql_formatted_array(row.try_get(index)?, index)
}
#[inline]
fn id_to_uuid(id: &ID) -> Result<Uuid, uuid::Error> {
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
}
async fn user_authentication(db: &SqlitePool, user: &uuid::adapter::Simple, password_hash: &str) -> FieldResult<()> {
if sqlx::query(format!(
r#"SELECT users.id FROM users, security_preferences WHERE users.id = "{}" AND password_hash = "{}""#,
user,
password_hash,
).as_str()).fetch_optional(db).await?.is_some() {
Ok(())
} else {
Err("authentication failed".into())
}
}
async fn user_exists(db: &SqlitePool, user: &uuid::adapter::Simple) -> FieldResult<()> {
if sqlx::query(
format!(r#"SELECT id FROM users WHERE id = "{}""#, user).as_str()
).fetch_optional(db).await?.is_some() {
Ok(())
} else {
Err(format!(r#"user "{}" does not exist on this server"#, user).into())
}
}
async fn chat_exists(db: &SqlitePool, chat: &uuid::adapter::Simple) -> FieldResult<()> {
if sqlx::query(
format!(r#"SELECT id FROM chat_index WHERE id = "{}""#, chat).as_str()
).fetch_optional(db).await?.is_some() {
Ok(())
} else {
Err(format!(r#"chat "{}" does not exist on this server"#, chat).into())
}
}
#[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<String>,
pub activated: bool,
pub created: DateTime<Utc>,
pub last_online: Option<DateTime<Utc>>,
}
#[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<Utc> {
&self.created
}
pub const fn last_online(&self) -> Option<&DateTime<Utc>> {
self.last_online.as_ref()
}
}
impl TryFrom<SqliteRow> for User {
type Error = FieldError;
fn try_from(row: SqliteRow) -> FieldResult<Self> {
let id = ID::new(row.try_get::<String, _>("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<String>,
pub(crate) password_hash: String,
}
impl From<NewUser> 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<NewUser> 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<String>,
discovery_server_limit: Vec<Url>,
last_seen: RestrictionPolicy,
last_seen_user_limit: Vec<String>,
last_seen_server_limit: Vec<Url>,
last_seen_course: bool,
info: RestrictionPolicy,
info_user_limit: Vec<String>,
info_server_limit: Vec<Url>,
}
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<Self> {
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> {
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<ID>,
password_hash: String,
}
impl SecurityPreferences {
fn from_row_cfg(
#[cfg(feature = "sqlite")]
row: &SqliteRow,
) -> SqlxResult<Self> {
Ok(SecurityPreferences {
account_tokens: into_toml_iter_from_row::<String>(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> {
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<Url>,
}
#[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, Debug, PartialEq, Eq, GraphQLObject, Type)]
pub struct Chat {
id: ID,
users: Vec<ID>,
}
#[derive(Clone, Debug, PartialEq, Eq, GraphQLObject, Type)]
pub struct GroupChat {
id: ID,
title: String,
description: Option<String>,
users: Vec<ID>,
}
#[derive(Clone, Debug, PartialEq, Eq, GraphQLInputObject, Type)]
pub struct MessageInput {
msg_type: MsgType,
content: Option<String>,
}
impl MessageInput {
pub fn try_into_message(self, user: &uuid::adapter::Simple) -> Result<Message, SystemTimeError> {
let sender: ID = user.to_string().into();
Ok(Message {
id: Uuid::new_v4().to_simple().to_string().into(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_millis(),
sender: sender.clone(),
msg_type: self.msg_type,
content: self.content,
hide_for: None,
seen_by: vec![sender],
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Type)]
pub struct Message {
id: ID,
timestamp: u128,
sender: ID,
msg_type: MsgType,
content: Option<String>,
hide_for: Option<Vec<ID>>,
seen_by: Vec<ID>,
}
#[graphql_object]
impl Message {
fn id(&self) -> &ID {
&self.id
}
fn timestamp(&self) -> String {
self.timestamp.to_string()
}
fn sender(&self) -> &ID {
&self.sender
}
fn msg_type(&self) -> MsgType {
self.msg_type
}
fn content(&self) -> Option<&str> {
self.content.as_ref().map(|s| &**s)
}
fn hide_for(&self) -> Option<&[ID]> {
self.hide_for.as_ref().map(|v| &v[..])
}
fn seen_by(&self) -> &[ID] {
&self.seen_by
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, GraphQLEnum, Type)]
#[repr(u8)]
#[non_exhaustive]
pub enum MsgType {
Text = 0,
}
#[derive(Clone, Copy, Debug)]
pub struct Query;
#[graphql_object(context = Context)]
impl Query {
async fn getUserID(context: &Context, username: String) -> FieldResult<ID> {
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::<String, _>("id")
.map(ID::new)
.map_err(FieldError::from)
}
async fn isUsernameUsed(context: &Context, username: String) -> FieldResult<bool> {
if !is_valid_user_name(&username) {
return Err(format!("{:?} is not a valid username", username).into());
}
Ok(sqlx::query(format!(
r#"SELECT user_name FROM users WHERE user_name="{}""#,
username,
).as_str()).fetch_optional(&context.db).await?.is_some())
}
async fn users(context: &Context) -> FieldResult<Vec<User>> {
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<UserPreferences> {
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,
},
})
}
async fn chats(context: &Context, user: ID, password_hash: String) -> FieldResult<Vec<Chat>> {
let user = id_to_uuid(&user)?.to_simple();
user_authentication(&context.db, &user, &password_hash).await?;
let mut chats = Vec::new();
for chat in sqlx::query(format!(
r#"SELECT id, users FROM chat_index WHERE is_group_chat = 0 AND users LIKE "%{}%""#,
user,
).as_str()).fetch_all(&context.db).await? {
chats.push(Chat {
id: ID::from(chat.try_get::<String, _>("id")?),
users: from_sql_formatted_array(chat.try_get("users")?, "users")?.map(|u: String| u.into()).collect(),
});
}
Ok(chats)
}
async fn group_chats(context: &Context, user: ID, password_hash: String) -> FieldResult<Vec<GroupChat>> {
let user = id_to_uuid(&user)?.to_simple();
user_authentication(&context.db, &user, &password_hash).await?;
let mut chats = Vec::new();
for chat in sqlx::query(format!(
r#"SELECT id, title, description, users FROM chat_index WHERE is_group_chat = 1 AND users LIKE "%{}%""#,
user,
).as_str()).fetch_all(&context.db).await? {
chats.push(GroupChat {
id: ID::from(chat.try_get::<String, _>("id")?),
title: chat.try_get("title")?,
description: chat.try_get("description")?,
users: from_sql_formatted_array(chat.try_get("users")?, "users")?.map(|u: String| u.into()).collect(),
});
}
Ok(chats)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Mutation;
#[graphql_object(context = Context)]
impl Mutation {
async fn newUser(context: &Context, new_user: NewUser) -> FieldResult<ID> {
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<ID> {
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
user_authentication(&context.db, &user, &password_hash).await?;
// 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
user_exists(&context.db, &chat_partner).await?;
// 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())
}
async fn newGroupChat(context: &Context, user: ID, password_hash: String, title: String, description: Option<String>, with: Vec<ID>) -> FieldResult<ID> {
let user = id_to_uuid(&user)?.to_simple();
user_authentication(&context.db, &user, &password_hash).await?;
let mut chat_partners = BTreeSet::new();
for u in with {
let u = id_to_uuid(&u)?.to_simple();
user_exists(&context.db, &u).await?;
chat_partners.insert(u);
}
chat_partners.insert(user);
let group_chat_uuid = Uuid::new_v4();
sqlx::query(format!(
r#"INSERT INTO chat_index VALUES ("{}", 1, "{}", {}, "{}")"#,
group_chat_uuid.to_simple(),
title,
description.map(|d| format!(r#""{}""#, d)).unwrap_or("NULL".to_string()),
format_array_for_sql(chat_partners)
).as_str()).execute(&context.db).await?;
crate::sqlite::create_chat(&context.db, &group_chat_uuid).await?;
Ok(group_chat_uuid.to_simple().to_string().into())
}
async fn sendMessage(context: &Context, user: ID, password_hash: String, chat: ID, msg: MessageInput) -> FieldResult<ID> {
let user = id_to_uuid(&user)?.to_simple();
user_authentication(&context.db, &user, &password_hash).await?;
let msg: Message = msg.try_into_message(&user)?;
let msg_id = id_to_uuid(&msg.id)?.to_simple();
let chat = id_to_uuid(&chat)?.to_simple();
chat_exists(&context.db, &chat).await?;
if msg.timestamp > i64::MAX as u128 {
eprintln!("WARNING: Timestamp value is greater than sqlite can handle");
}
sqlx::query(format!(
r#"INSERT INTO msgdata_{} VALUES ("{}", {}, "{}", {}, {}, {}, "{}")"#,
chat,
&msg_id,
msg.timestamp,
user,
msg.msg_type as u8,
msg.content
.map(|c| format!(r#""{}""#, c))
.unwrap_or("NULL".to_string()),
msg.hide_for
.map(format_array_for_sql)
.map(|a| format!(r#""{}""#, a))
.unwrap_or("NULL".to_string()),
format_array_for_sql(msg.seen_by),
).as_str()).execute(&context.db).await?;
Ok(msg_id.to_string().into())
}
}
pub type Schema<'root_node> = RootNode<'root_node, Query, Mutation, EmptySubscription<Context>>;
pub fn schema<'root_node>() -> Schema<'root_node> {
Schema::new(Query, Mutation, EmptySubscription::new())
}