forked from jmug/stoatchat
Add group icons / profile backgrounds.
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2475,7 +2475,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt"
|
name = "revolt"
|
||||||
version = "0.4.1-alpha.2"
|
version = "0.4.1-alpha.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt"
|
name = "revolt"
|
||||||
version = "0.4.1-alpha.2"
|
version = "0.4.1-alpha.3"
|
||||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub enum Channel {
|
|||||||
DirectMessage {
|
DirectMessage {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
id: String,
|
id: String,
|
||||||
|
|
||||||
active: bool,
|
active: bool,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -38,10 +39,14 @@ pub enum Channel {
|
|||||||
id: String,
|
id: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
nonce: Option<String>,
|
nonce: Option<String>,
|
||||||
|
|
||||||
name: String,
|
name: String,
|
||||||
owner: String,
|
owner: String,
|
||||||
description: String,
|
description: String,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
icon: Option<File>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
last_message: Option<LastMessage>,
|
last_message: Option<LastMessage>,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,18 +45,20 @@ pub enum Presence {
|
|||||||
pub struct UserStatus {
|
pub struct UserStatus {
|
||||||
#[validate(length(min = 1, max = 128))]
|
#[validate(length(min = 1, max = 128))]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
text: Option<String>,
|
pub text: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
presence: Option<Presence>,
|
pub presence: Option<Presence>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Validate, Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct UserProfile {
|
pub struct UserProfile {
|
||||||
#[validate(length(min = 1, max = 2000))]
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
content: Option<String>,
|
pub content: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub background: Option<File>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When changing this struct, update notifications/payload.rs#80
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
FindOptions::builder()
|
FindOptions::builder()
|
||||||
.projection(doc! { "_id": 1, "username": 1, "badges": 1, "status": 1 })
|
.projection(doc! { "_id": 1, "username": 1, "avatar": 1, "badges": 1, "status": 1 })
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -15,14 +15,17 @@ pub struct Data {
|
|||||||
#[validate(length(min = 0, max = 1024))]
|
#[validate(length(min = 0, max = 1024))]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
#[validate(length(min = 1, max = 128))]
|
||||||
|
icon: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("/<target>", data = "<info>")]
|
#[patch("/<target>", data = "<info>")]
|
||||||
pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> {
|
pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> {
|
||||||
|
let info = info.into_inner();
|
||||||
info.validate()
|
info.validate()
|
||||||
.map_err(|error| Error::FailedValidation { error })?;
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
if info.name.is_none() && info.description.is_none() {
|
if info.name.is_none() && info.description.is_none() && info.icon.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,11 +40,34 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match &target {
|
match &target {
|
||||||
Channel::Group { id, .. } => {
|
Channel::Group { id, icon, .. } => {
|
||||||
|
let mut set = doc! {};
|
||||||
|
if let Some(name) = &info.name {
|
||||||
|
set.insert("name", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(description) = info.description {
|
||||||
|
set.insert("description", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut remove_icon = false;
|
||||||
|
if let Some(attachment_id) = info.icon {
|
||||||
|
let attachment = File::find_and_use(&attachment_id, "icons", "object", &user.id).await?;
|
||||||
|
set.insert(
|
||||||
|
"icon",
|
||||||
|
to_document(&attachment).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "to_document",
|
||||||
|
with: "attachment",
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_icon = true;
|
||||||
|
}
|
||||||
|
|
||||||
get_collection("channels")
|
get_collection("channels")
|
||||||
.update_one(
|
.update_one(
|
||||||
doc! { "_id": &id },
|
doc! { "_id": &id },
|
||||||
doc! { "$set": to_document(&info.0).map_err(|_| Error::DatabaseError { operation: "to_document", with: "data" })? },
|
doc! { "$set": &set },
|
||||||
None
|
None
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -49,18 +75,18 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> {
|
|||||||
|
|
||||||
ClientboundNotification::ChannelUpdate {
|
ClientboundNotification::ChannelUpdate {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
data: json!(info.0),
|
data: json!(set),
|
||||||
}
|
}
|
||||||
.publish(id.clone())
|
.publish(id.clone())
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
if let Some(name) = &info.name {
|
if let Some(name) = info.name {
|
||||||
Message::create(
|
Message::create(
|
||||||
"00000000000000000000000000".to_string(),
|
"00000000000000000000000000".to_string(),
|
||||||
id.clone(),
|
id.clone(),
|
||||||
Content::SystemMessage(SystemMessage::ChannelRenamed {
|
Content::SystemMessage(SystemMessage::ChannelRenamed {
|
||||||
name: name.clone(),
|
name,
|
||||||
by: user.id,
|
by: user.id,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -69,6 +95,12 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if remove_icon {
|
||||||
|
if let Some(old_icon) = icon {
|
||||||
|
old_icon.delete().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(Error::InvalidOperation),
|
_ => Err(Error::InvalidOperation),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> {
|
|||||||
.unwrap_or_else(|| "A group.".to_string()),
|
.unwrap_or_else(|| "A group.".to_string()),
|
||||||
owner: user.id,
|
owner: user.id,
|
||||||
recipients: set.into_iter().collect::<Vec<String>>(),
|
recipients: set.into_iter().collect::<Vec<String>>(),
|
||||||
|
icon: None,
|
||||||
last_message: None,
|
last_message: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use rocket_contrib::json::JsonValue;
|
|||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn root() -> JsonValue {
|
pub async fn root() -> JsonValue {
|
||||||
json!({
|
json!({
|
||||||
"revolt": "0.4.1-alpha.2",
|
"revolt": "0.4.1-alpha.3",
|
||||||
"features": {
|
"features": {
|
||||||
"registration": !*DISABLE_REGISTRATION,
|
"registration": !*DISABLE_REGISTRATION,
|
||||||
"captcha": {
|
"captcha": {
|
||||||
|
|||||||
@@ -7,34 +7,100 @@ use rocket_contrib::json::Json;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
#[derive(Validate, Serialize, Deserialize)]
|
#[derive(Validate, Serialize, Deserialize, Debug)]
|
||||||
pub struct Data {
|
pub struct UserProfileData {
|
||||||
|
#[validate(length(min = 0, max = 2000))]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[validate]
|
content: Option<String>,
|
||||||
status: Option<UserStatus>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
#[validate]
|
|
||||||
profile: Option<UserProfile>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[validate(length(min = 1, max = 128))]
|
#[validate(length(min = 1, max = 128))]
|
||||||
|
background: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum RemoveField {
|
||||||
|
ProfileContent,
|
||||||
|
ProfileBackground,
|
||||||
|
StatusText,
|
||||||
|
Avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize)]
|
||||||
|
pub struct Data {
|
||||||
|
#[validate]
|
||||||
|
status: Option<UserStatus>,
|
||||||
|
#[validate]
|
||||||
|
profile: Option<UserProfileData>,
|
||||||
|
#[validate(length(min = 1, max = 128))]
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
|
remove: Option<RemoveField>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("/<_ignore_id>", data = "<data>")]
|
#[patch("/<_ignore_id>", data = "<data>")]
|
||||||
pub async fn req(user: User, mut data: Json<Data>, _ignore_id: String) -> Result<()> {
|
pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<()> {
|
||||||
if data.0.status.is_none() && data.0.profile.is_none() && data.0.avatar.is_none() {
|
let mut data = data.into_inner();
|
||||||
|
if data.status.is_none() && data.profile.is_none() && data.avatar.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
data.validate()
|
data.validate()
|
||||||
.map_err(|error| Error::FailedValidation { error })?;
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
let mut set = to_document(&data.0).map_err(|_| Error::DatabaseError {
|
let mut unset = doc! {};
|
||||||
operation: "to_document",
|
let mut set = doc! {};
|
||||||
with: "data",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let avatar = std::mem::replace(&mut data.0.avatar, None);
|
let mut remove_background = false;
|
||||||
|
let mut remove_avatar = false;
|
||||||
|
|
||||||
|
if let Some(remove) = data.remove {
|
||||||
|
match remove {
|
||||||
|
RemoveField::ProfileContent => {
|
||||||
|
unset.insert("profile.content", 1);
|
||||||
|
},
|
||||||
|
RemoveField::ProfileBackground => {
|
||||||
|
unset.insert("profile.background", 1);
|
||||||
|
remove_background = true;
|
||||||
|
}
|
||||||
|
RemoveField::StatusText => {
|
||||||
|
unset.insert("status.text", 1);
|
||||||
|
},
|
||||||
|
RemoveField::Avatar => {
|
||||||
|
unset.insert("avatar", 1);
|
||||||
|
remove_avatar = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(status) = &data.status {
|
||||||
|
set.insert(
|
||||||
|
"status",
|
||||||
|
to_document(&status).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "to_document",
|
||||||
|
with: "status",
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(profile) = data.profile {
|
||||||
|
if let Some(content) = profile.content {
|
||||||
|
set.insert("profile.content", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(attachment_id) = profile.background {
|
||||||
|
let attachment = File::find_and_use(&attachment_id, "backgrounds", "user", &user.id).await?;
|
||||||
|
set.insert(
|
||||||
|
"profile.background",
|
||||||
|
to_document(&attachment).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "to_document",
|
||||||
|
with: "attachment",
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_background = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatar = std::mem::replace(&mut data.avatar, None);
|
||||||
let attachment = if let Some(attachment_id) = avatar {
|
let attachment = if let Some(attachment_id) = avatar {
|
||||||
let attachment = File::find_and_use(&attachment_id, "avatars", "user", &user.id).await?;
|
let attachment = File::find_and_use(&attachment_id, "avatars", "user", &user.id).await?;
|
||||||
set.insert(
|
set.insert(
|
||||||
@@ -44,6 +110,8 @@ pub async fn req(user: User, mut data: Json<Data>, _ignore_id: String) -> Result
|
|||||||
with: "attachment",
|
with: "attachment",
|
||||||
})?,
|
})?,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
remove_avatar = true;
|
||||||
Some(attachment)
|
Some(attachment)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -57,7 +125,7 @@ pub async fn req(user: User, mut data: Json<Data>, _ignore_id: String) -> Result
|
|||||||
with: "user",
|
with: "user",
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let Some(status) = data.0.status {
|
if let Some(status) = data.status {
|
||||||
ClientboundNotification::UserUpdate {
|
ClientboundNotification::UserUpdate {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
data: json!({ "status": status }),
|
data: json!({ "status": status }),
|
||||||
@@ -75,11 +143,21 @@ pub async fn req(user: User, mut data: Json<Data>, _ignore_id: String) -> Result
|
|||||||
.publish(user.id.clone())
|
.publish(user.id.clone())
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if remove_avatar {
|
||||||
if let Some(old_avatar) = user.avatar {
|
if let Some(old_avatar) = user.avatar {
|
||||||
old_avatar.delete().await?;
|
old_avatar.delete().await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if remove_background {
|
||||||
|
if let Some(profile) = user.profile {
|
||||||
|
if let Some(old_background) = profile.background {
|
||||||
|
old_background.delete().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user