Compare commits

..
29 changed files with 577 additions and 46 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM --platform="${BUILDPLATFORM}" rustlang/rust:nightly-slim
FROM --platform="${BUILDPLATFORM}" rust:slim
USER 0:0
WORKDIR /home/rust/src
+6 -1
View File
@@ -2,6 +2,11 @@ disallowed-methods = [
# Shouldn't need to access these directly
"revolt_database::models::bots::model::Bot::remove_field",
# Prefer to use Object::delete()
# Prefer to use Object::update()
"revolt_database::models::bots::ops::AbstractBots::update_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
# Prefer to use Object::delete()
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
]
@@ -2,12 +2,13 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{Bot, File, Member, MemberCompositeKey, Server, User, UserSettings};
use crate::{AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings};
database_derived!(
/// Reference implementation
#[derive(Default)]
pub struct ReferenceDb {
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,
@@ -64,6 +64,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create safety_snapshots collection.");
db.create_collection("safety_strikes", None)
.await
.expect("Failed to create safety_strikes collection.");
db.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
@@ -16,7 +16,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 21;
pub const LATEST_REVISION: i32 = 22;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -696,26 +696,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 19 {
info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable.");
// TODO: make these fail once production is migrated
if db
.db()
db.db()
.create_collection("safety_reports", None)
.await
.is_err()
{
info!("Failed to create safety_reports collection but this is expected in production.");
}
.unwrap();
if db
.db()
db.db()
.create_collection("safety_snapshots", None)
.await
.is_err()
{
info!(
"Failed to create safety_snapshots collection but this is expected in production."
);
}
.unwrap();
db.col::<Document>("safety_reports")
.update_many(
@@ -753,6 +742,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to create safety snapshot index.");
}
if revision <= 21 {
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
db.db()
.create_collection("safety_strikes", None)
.await
.unwrap();
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.
+11 -2
View File
@@ -91,7 +91,7 @@ impl Bot {
#[cfg(test)]
mod tests {
use crate::{Bot, FieldsBot, PartialBot};
use crate::{Bot, FieldsBot, PartialBot, User};
#[async_std::test]
async fn crud() {
@@ -100,6 +100,14 @@ mod tests {
let user_id = "user";
let token = "my_token";
let user = User {
id: bot_id.to_string(),
username: "Bot Name".to_string(),
..Default::default()
};
db.insert_user(&user).await.unwrap();
let bot = Bot {
id: bot_id.to_string(),
owner: user_id.to_string(),
@@ -139,7 +147,8 @@ mod tests {
bot.delete(&db).await.unwrap();
assert!(db.fetch_bot(bot_id).await.is_err());
assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap())
assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap());
assert_eq!(db.fetch_user(bot_id).await.unwrap().flags, Some(2))
});
}
}
+3
View File
@@ -1,6 +1,7 @@
mod admin_migrations;
mod bots;
mod files;
mod safety_strikes;
mod server_members;
mod servers;
mod user_settings;
@@ -9,6 +10,7 @@ mod users;
pub use admin_migrations::*;
pub use bots::*;
pub use files::*;
pub use safety_strikes::*;
pub use server_members::*;
pub use servers::*;
pub use user_settings::*;
@@ -22,6 +24,7 @@ pub trait AbstractDatabase:
+ admin_migrations::AbstractMigrations
+ bots::AbstractBots
+ files::AbstractAttachments
+ safety_strikes::AbstractAccountStrikes
+ server_members::AbstractServerMembers
+ servers::AbstractServers
+ user_settings::AbstractUserSettings
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,105 @@
use revolt_result::Result;
use crate::Database;
auto_derived_partial!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[serde(rename = "_id")]
pub id: String,
/// User Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
},
"PartialAccountStrike"
);
#[allow(clippy::disallowed_methods)]
impl AccountStrike {
/// Update this strike
pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> {
db.update_account_strike(&self.id, &partial).await?;
self.apply_options(partial);
Ok(())
}
/// Delete this strike
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_account_strike(&self.id).await
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{AccountStrike, PartialAccountStrike};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let user_id = "user";
let strike_a = "a";
let strike_b = "b";
let strike = AccountStrike {
id: strike_a.to_string(),
user_id: user_id.to_string(),
reason: "reason 1".to_string(),
};
db.insert_account_strike(&strike).await.unwrap();
let mut updated_strike = strike.clone();
updated_strike
.update(
&db,
PartialAccountStrike {
reason: Some("new reason".to_string()),
..Default::default()
},
)
.await
.unwrap();
db.insert_account_strike(&AccountStrike {
id: strike_b.to_string(),
user_id: user_id.to_string(),
reason: "reason 2".to_string(),
})
.await
.unwrap();
let strikes = db.fetch_account_strikes_by_user(user_id).await.unwrap();
let ids = strikes
.iter()
.cloned()
.map(|strike| strike.id)
.collect::<HashSet<String>>();
assert!(ids.contains(strike_a));
assert!(ids.contains(strike_b));
let fetched_strike = strikes
.into_iter()
.find(|strike| strike.id == strike_a)
.unwrap();
assert_eq!(fetched_strike, updated_strike);
assert_ne!(fetched_strike, strike);
strike.delete(&db).await.unwrap();
assert_eq!(
1,
db.fetch_account_strikes_by_user(user_id)
.await
.unwrap()
.len()
)
});
}
}
@@ -0,0 +1,24 @@
use revolt_result::Result;
use crate::{AccountStrike, PartialAccountStrike};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAccountStrikes: Sync + Send {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()>;
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike>;
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>>;
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()>;
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()>;
}
@@ -0,0 +1,55 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::MongoDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
static COL: &str = "bots";
#[async_trait]
impl AbstractAccountStrikes for MongoDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
query!(self, insert_one, COL, &strike).map(|_| ())
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
Ok(self
.col::<AccountStrike>(COL)
.find(
doc! {
"user_id": user_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
@@ -0,0 +1,60 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
#[async_trait]
impl AbstractAccountStrikes for ReferenceDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.contains_key(&strike.id) {
Err(create_database_error!("insert", "strike"))
} else {
strikes.insert(strike.id.to_string(), strike.clone());
Ok(())
}
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
let strikes = self.account_strikes.lock().await;
strikes
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
let strikes = self.account_strikes.lock().await;
Ok(strikes
.values()
.filter(|strike| strike.user_id == user_id)
.cloned()
.collect())
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if let Some(strike) = strikes.get_mut(id) {
strike.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -111,6 +111,7 @@ auto_derived!(
impl User {
/// Check whether a username is already in use by another user
#[allow(dead_code)]
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
match db.fetch_user_by_username(username).await {
Ok(_) => Ok(true),
@@ -8,6 +8,7 @@ use crate::{Database, User};
/// Permissions calculator
pub struct PermissionCalculator<'a> {
#[allow(dead_code)]
database: &'a Database,
perspective: &'a User,
@@ -0,0 +1,30 @@
auto_derived!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[serde(rename = "_id")]
pub id: String,
/// User Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// # Strike Data
pub struct DataEditAccountStrike {
/// New attached reason
pub reason: String,
}
);
#[cfg(feature = "from_database")]
impl From<revolt_database::AccountStrike> for AccountStrike {
fn from(value: revolt_database::AccountStrike) -> Self {
AccountStrike {
id: value.id,
user_id: value.user_id,
reason: value.reason,
}
}
}
+2
View File
@@ -1,7 +1,9 @@
mod account_strikes;
mod bots;
mod files;
mod users;
pub use account_strikes::*;
pub use bots::*;
pub use files::*;
pub use users::*;
@@ -0,0 +1,23 @@
use revolt_database::Database;
use revolt_quark::models::User;
use revolt_quark::{Error, Result};
use rocket::State;
/// # Delete Strike
///
/// Delete a strike by its ID
#[openapi(tag = "User Safety")]
#[delete("/strikes/<strike_id>")]
pub async fn delete_strike(db: &State<Database>, user: User, strike_id: String) -> Result<()> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
let strike = db
.fetch_account_strike(&strike_id)
.await
.map_err(Error::from_core)?;
strike.delete(db).await.map_err(Error::from_core)
}
@@ -0,0 +1,39 @@
use revolt_database::{Database, PartialAccountStrike};
use revolt_models::v0::DataEditAccountStrike;
use revolt_quark::models::User;
use revolt_quark::{Error, Result};
use rocket::serde::json::Json;
use rocket::State;
/// # Edit Strike
///
/// Edit a strike by its ID
#[openapi(tag = "User Safety")]
#[post("/strikes/<strike_id>", data = "<data>")]
pub async fn edit_strike(
db: &State<Database>,
user: User,
strike_id: String,
data: Json<DataEditAccountStrike>,
) -> Result<()> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
let mut strike = db
.fetch_account_strike(&strike_id)
.await
.map_err(Error::from_core)?;
strike
.update(
db,
PartialAccountStrike {
reason: Some(data.0.reason),
..Default::default()
},
)
.await
.map_err(Error::from_core)
}
@@ -1,17 +1,61 @@
use revolt_quark::models::report::{ReportStatus, ReportStatusString, ReportedContent};
use revolt_quark::models::{Report, User};
use revolt_quark::{Db, Error, Result};
use rocket::serde::json::Json;
use serde::Deserialize;
/// # Query Parameters
#[derive(Deserialize, JsonSchema, FromForm)]
pub struct OptionsFetchReports {
/// Find reports against messages, servers, or users
content_id: Option<String>,
/// Find reports created by user
author_id: Option<String>,
/// Report status to include in search
status: Option<ReportStatusString>,
}
/// # Fetch Reports
///
/// Fetch all available reports
#[openapi(tag = "User Safety")]
#[get("/reports")]
pub async fn fetch_reports(db: &Db, user: User) -> Result<Json<Vec<Report>>> {
#[get("/reports?<options..>")]
pub async fn fetch_reports(
db: &Db,
user: User,
options: OptionsFetchReports,
) -> Result<Json<Vec<Report>>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
db.fetch_reports().await.map(Json)
let mut reports = db.fetch_reports().await?;
if let Some(content_id) = options.content_id {
reports.retain(|report| match &report.content {
ReportedContent::Message { id, .. }
| ReportedContent::Server { id, .. }
| ReportedContent::User { id, .. } => id == &content_id,
});
}
if let Some(author_id) = options.author_id {
reports.retain(|report| report.author_id == author_id);
}
if let Some(status) = options.status {
reports.retain(|report| {
matches!(
(&status, &report.status),
(ReportStatusString::Created, ReportStatus::Created { .. })
| (ReportStatusString::Rejected, ReportStatus::Rejected { .. })
| (ReportStatusString::Resolved, ReportStatus::Resolved { .. })
)
});
}
Ok(Json(reports))
}
@@ -0,0 +1,28 @@
use revolt_database::Database;
use revolt_models::v0::AccountStrike;
use revolt_quark::models::User;
use revolt_quark::{Error, Result};
use rocket::serde::json::Json;
use rocket::State;
/// # Fetch Strikes
///
/// Fetch strikes for a user by their ID
#[openapi(tag = "User Safety")]
#[get("/strikes/<user_id>")]
pub async fn fetch_strikes(
db: &State<Database>,
user: User,
user_id: String,
) -> Result<Json<Vec<AccountStrike>>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
db.fetch_account_strikes_by_user(&user_id)
.await
.map(|v| v.into_iter().map(|e| e.into()).collect())
.map(Json)
.map_err(Error::from_core)
}
+9 -1
View File
@@ -8,6 +8,10 @@ mod report_content;
mod fetch_snapshots;
mod delete_strike;
mod edit_strike;
mod fetch_strikes;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
// Reports
@@ -16,6 +20,10 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
fetch_reports::fetch_reports,
report_content::report_content,
// Snapshots
fetch_snapshots::fetch_snapshots
fetch_snapshots::fetch_snapshots,
// Strikes
fetch_strikes::fetch_strikes,
edit_strike::edit_strike,
delete_strike::delete_strike
]
}
@@ -57,7 +57,7 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
return Err(Error::CannotReportYourself);
}
let (snapshot, files) = SnapshotContent::generate_from_server(db, server)?;
let (snapshot, files) = SnapshotContent::generate_from_server(server)?;
(vec![snapshot], files)
}
ReportedContent::User { id, message_id, .. } => {
@@ -75,7 +75,7 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
None
};
let (snapshot, files) = SnapshotContent::generate_from_user(db, reported_user)?;
let (snapshot, files) = SnapshotContent::generate_from_user(reported_user)?;
if let Some(message) = message {
let (message_snapshot, message_files) =
@@ -1,17 +1,65 @@
use revolt_quark::{
models::{Server, User},
models::{Channel, Server, User},
perms, Db, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
/// # Query Parameters
#[derive(Deserialize, JsonSchema, FromForm)]
pub struct OptionsFetchServer {
/// Whether to include channels
include_channels: Option<bool>,
}
/// # Fetch server route response
#[derive(Serialize, JsonSchema)]
#[serde(untagged)]
pub enum FetchServerResponse {
JustServer(Server),
ServerWithChannels {
#[serde(flatten)]
server: Server,
channels: Vec<Channel>,
},
}
/// # Fetch Server
///
/// Fetch a server by its id.
#[openapi(tag = "Server Information")]
#[get("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Server>> {
#[get("/<target>?<options..>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
options: OptionsFetchServer,
) -> Result<Json<FetchServerResponse>> {
let server = target.as_server(db).await?;
perms(&user).server(&server).calc(db).await?;
let mut perms = perms(&user).server(&server);
perms.calc(db).await?;
Ok(Json(server))
if let Some(true) = options.include_channels {
let all_channels = db.fetch_channels(&server.channels).await?;
let mut visible_channels = vec![];
for channel in all_channels {
if perms
.clone()
.channel(&channel)
.calc(db)
.await?
.can_view_channel()
{
visible_channels.push(channel);
}
}
Ok(Json(FetchServerResponse::ServerWithChannels {
server,
channels: visible_channels,
}))
} else {
Ok(Json(FetchServerResponse::JustServer(server)))
}
}
+10 -4
View File
@@ -66,14 +66,20 @@ pub async fn req(
// If we want to edit a different user than self, ensure we have
// permissions and subsequently replace the user in question
if target.id != "@me" {
if !user.privileged {
if target.id != "@me" && target.id != user.id {
let target_user = target.as_user(db).await?;
let is_bot_owner = target_user
.bot
.map(|bot| bot.owner == user.id)
.unwrap_or_default();
if !is_bot_owner && !user.privileged {
return Err(Error::NotPrivileged);
}
}
user = target.as_user(db).await?;
// Otherwise, filter out invalid edit fields
} else if data.badges.is_some() || data.flags.is_some() {
if !user.privileged && (data.badges.is_some() || data.flags.is_some()) {
return Err(Error::NotPrivileged);
}
+22 -1
View File
@@ -1,9 +1,30 @@
use crate::{models::report::PartialReport, models::Report, Database, Result};
use iso8601_timestamp::Timestamp;
use crate::{
models::report::PartialReport,
models::{report::ReportStatus, Report},
Database, Result,
};
impl Report {
/// Update report data
pub async fn update(&mut self, db: &Database, partial: PartialReport) -> Result<()> {
self.apply_options(partial.clone());
match &mut self.status {
ReportStatus::Created {} => {}
ReportStatus::Rejected { closed_at, .. } => {
if closed_at.is_none() {
closed_at.replace(Timestamp::now_utc());
}
}
ReportStatus::Resolved { closed_at } => {
if closed_at.is_none() {
closed_at.replace(Timestamp::now_utc());
}
}
}
db.update_report(&self.id, &partial).await
}
}
@@ -61,10 +61,7 @@ impl SnapshotContent {
))
}
pub fn generate_from_server(
db: &Database,
server: Server,
) -> Result<(SnapshotContent, Vec<String>)> {
pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec<String>)> {
// Collect server's icon and banner
let files = [&server.icon, &server.banner]
.iter()
@@ -74,7 +71,7 @@ impl SnapshotContent {
Ok((SnapshotContent::Server(server), files))
}
pub fn generate_from_user(db: &Database, user: User) -> Result<(SnapshotContent, Vec<String>)> {
pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> {
// Collect user's avatar and profile background
let files = [
user.avatar.as_ref(),
+14
View File
@@ -1,4 +1,5 @@
use iso8601_timestamp::Timestamp;
use rocket::FromFormField;
use serde::{Deserialize, Serialize};
/// Reason for reporting content (message or server)
@@ -119,6 +120,19 @@ pub enum ReportStatus {
Resolved { closed_at: Option<Timestamp> },
}
/// Just the status of the report
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, FromFormField)]
pub enum ReportStatusString {
/// Report is waiting for triage / action
Created,
/// Report was rejected
Rejected,
/// Report was actioned and resolved
Resolved,
}
/// User-generated platform moderation report.
#[derive(Serialize, Deserialize, JsonSchema, Debug, OptionalStruct, Clone)]
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
+2 -2
View File
@@ -2,8 +2,8 @@
use crate::Database;
use deadqueue::limited::Queue;
use std::{collections::HashMap, time::Duration};
use once_cell::sync::Lazy;
use std::{collections::HashMap, time::Duration};
use super::DelayedTask;
@@ -95,7 +95,7 @@ pub async fn worker(db: Database) {
}) = Q.try_pop()
{
let key = (user, channel);
if let Some(mut task) = tasks.get_mut(&key) {
if let Some(task) = tasks.get_mut(&key) {
task.delay();
match &mut event {
+2 -2
View File
@@ -2,8 +2,8 @@
use crate::{models::channel::PartialChannel, Database};
use deadqueue::limited::Queue;
use std::{collections::HashMap, time::Duration};
use once_cell::sync::Lazy;
use std::{collections::HashMap, time::Duration};
use super::DelayedTask;
@@ -73,7 +73,7 @@ pub async fn worker(db: Database) {
// Queue incoming tasks.
while let Some(Data { channel, id, is_dm }) = Q.try_pop() {
if let Some(mut task) = tasks.get_mut(&channel) {
if let Some(task) = tasks.get_mut(&channel) {
task.data.id = id;
task.delay();
} else {