Compare commits
17
Commits
20250224-1
...
20250402-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a5de769d | ||
|
|
717d5e9a88 | ||
|
|
18f06467bb | ||
|
|
8d88ea9963 | ||
|
|
e3e1cab02e | ||
|
|
00bd56b687 | ||
|
|
a7727bba5c | ||
|
|
6b8005f530 | ||
|
|
714fba7ea6 | ||
|
|
ac05fb385c | ||
|
|
537c147c4b | ||
|
|
7bee609b84 | ||
|
|
859c0214ec | ||
|
|
5eea0bbc9a | ||
|
|
15dec656a8 | ||
|
|
bcf6561c0c | ||
|
|
b177a3e201 |
@@ -5,6 +5,8 @@ compose.override.yml
|
||||
target
|
||||
.data
|
||||
.env
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
.vercel
|
||||
.DS_Store
|
||||
|
||||
Generated
+15
-15
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
@@ -5523,7 +5523,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-autumn"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-macros",
|
||||
@@ -5560,7 +5560,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-std",
|
||||
@@ -5590,7 +5590,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-config"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"cached",
|
||||
@@ -5606,7 +5606,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-crond"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"revolt-config",
|
||||
@@ -5618,7 +5618,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-database"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"amqprs",
|
||||
"async-lock 2.8.0",
|
||||
@@ -5667,7 +5667,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-delta"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"amqprs",
|
||||
"async-channel 1.6.1",
|
||||
@@ -5714,7 +5714,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-files"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aws-config",
|
||||
@@ -5737,7 +5737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-january"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"axum",
|
||||
@@ -5765,7 +5765,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-models"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"iso8601-timestamp 0.2.11",
|
||||
@@ -5783,7 +5783,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-nodejs-bindings"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"neon",
|
||||
@@ -5796,7 +5796,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-permissions"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -5811,7 +5811,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-presence"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"log",
|
||||
@@ -5822,7 +5822,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-pushd"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"amqprs",
|
||||
"async-trait",
|
||||
@@ -5846,7 +5846,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-result"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"revolt_okapi",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-nodejs-bindings"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
description = "Node.js bindings for the Revolt software"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
license = "MIT"
|
||||
@@ -20,6 +20,6 @@ serde = { version = "1", features = ["derive"] }
|
||||
|
||||
async-std = "1.12.0"
|
||||
|
||||
revolt-config = { version = "0.8.3", path = "../../core/config" }
|
||||
revolt-result = { version = "0.8.3", path = "../../core/result" }
|
||||
revolt-database = { version = "0.8.3", path = "../../core/database" }
|
||||
revolt-config = { version = "0.8.4", path = "../../core/config" }
|
||||
revolt-result = { version = "0.8.4", path = "../../core/result" }
|
||||
revolt-database = { version = "0.8.4", path = "../../core/database" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -41,7 +41,7 @@ revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database" }
|
||||
revolt-permissions = { version = "0.8.3", path = "../core/permissions" }
|
||||
revolt-permissions = { version = "0.8.4", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -34,4 +34,4 @@ pretty_env_logger = "0.4.0"
|
||||
sentry = "0.31.5"
|
||||
|
||||
# Core
|
||||
revolt-result = { version = "0.8.3", path = "../result", optional = true }
|
||||
revolt-result = { version = "0.8.4", path = "../result", optional = true }
|
||||
|
||||
@@ -7,3 +7,6 @@ host = "127.0.0.1"
|
||||
port = 5672
|
||||
username = "rabbituser"
|
||||
password = "rabbitpass"
|
||||
|
||||
[features]
|
||||
webhooks_enabled = true
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
production = false
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
@@ -171,7 +173,6 @@ default_bucket = "revolt-uploads"
|
||||
|
||||
|
||||
[features]
|
||||
# Bucket to upload to by default
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -24,15 +24,15 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.8.3", path = "../config", features = [
|
||||
revolt-config = { version = "0.8.4", path = "../config", features = [
|
||||
"report-macros",
|
||||
] }
|
||||
revolt-result = { version = "0.8.3", path = "../result" }
|
||||
revolt-models = { version = "0.8.3", path = "../models", features = [
|
||||
revolt-result = { version = "0.8.4", path = "../result" }
|
||||
revolt-models = { version = "0.8.4", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.8.3", path = "../presence" }
|
||||
revolt-permissions = { version = "0.8.3", path = "../permissions", features = [
|
||||
revolt-presence = { version = "0.8.4", path = "../presence" }
|
||||
revolt-permissions = { version = "0.8.4", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
|
||||
@@ -269,16 +269,24 @@ impl Channel {
|
||||
}));
|
||||
}
|
||||
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
|
||||
let icon = if let Some(icon_id) = data.icon {
|
||||
Some(File::use_channel_icon(db, &icon_id, &id, &owner_id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let recipients = data.users.into_iter().collect::<Vec<String>>();
|
||||
let channel = Channel::Group {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
id,
|
||||
|
||||
name: data.name,
|
||||
owner: owner_id,
|
||||
description: data.description,
|
||||
recipients: recipients.clone(),
|
||||
|
||||
icon: None,
|
||||
icon,
|
||||
last_message_id: None,
|
||||
|
||||
permissions: None,
|
||||
|
||||
@@ -8,7 +8,7 @@ use revolt_models::v0::{
|
||||
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
||||
};
|
||||
use revolt_permissions::{ChannelPermission, PermissionValue};
|
||||
use revolt_result::Result;
|
||||
use revolt_result::{ErrorType, Result};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
@@ -327,14 +327,31 @@ impl Message {
|
||||
}));
|
||||
}
|
||||
|
||||
for ReplyIntent { id, mention } in entries {
|
||||
let message = db.fetch_message(&id).await?;
|
||||
for ReplyIntent {
|
||||
id,
|
||||
mention,
|
||||
fail_if_not_exists,
|
||||
} in entries
|
||||
{
|
||||
match db.fetch_message(&id).await {
|
||||
// Referenced message exists
|
||||
Ok(message) => {
|
||||
if mention && allow_mentions {
|
||||
mentions.insert(message.author.to_owned());
|
||||
}
|
||||
|
||||
if mention && allow_mentions {
|
||||
mentions.insert(message.author.to_owned());
|
||||
replies.insert(message.id);
|
||||
}
|
||||
// If the referenced message doesn't exist and fail_if_not_exists
|
||||
// is set to false, send the message without the reply.
|
||||
Err(e) => {
|
||||
if !matches!(e.error_type, ErrorType::NotFound)
|
||||
|| fail_if_not_exists.unwrap_or(true)
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replies.insert(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-files"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -20,10 +20,10 @@ typenum = "1.17.0"
|
||||
aws-config = "1.5.5"
|
||||
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
|
||||
|
||||
revolt-config = { version = "0.8.3", path = "../config", features = [
|
||||
revolt-config = { version = "0.8.4", path = "../config", features = [
|
||||
"report-macros",
|
||||
] }
|
||||
revolt-result = { version = "0.8.3", path = "../result" }
|
||||
revolt-result = { version = "0.8.4", path = "../result" }
|
||||
|
||||
# image processing
|
||||
jxl-oxide = "0.8.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -20,8 +20,8 @@ default = ["serde", "partials", "rocket"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.8.3", path = "../config" }
|
||||
revolt-permissions = { version = "0.8.3", path = "../permissions" }
|
||||
revolt-config = { version = "0.8.4", path = "../config" }
|
||||
revolt-permissions = { version = "0.8.4", path = "../permissions" }
|
||||
|
||||
# Utility
|
||||
regex = "1"
|
||||
|
||||
@@ -239,6 +239,10 @@ auto_derived!(
|
||||
pub id: String,
|
||||
/// Whether this reply should mention the message's author
|
||||
pub mention: bool,
|
||||
/// Whether to error if the referenced message doesn't exist.
|
||||
/// Otherwise, send a message without this reply.
|
||||
/// Default is true.
|
||||
pub fail_if_not_exists: Option<bool>,
|
||||
}
|
||||
|
||||
/// Message to send
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-permissions"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.8.3", path = "../result" }
|
||||
revolt-result = { version = "0.8.4", path = "../result" }
|
||||
|
||||
# Utility
|
||||
auto_ops = "0.3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-presence"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-result"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-crond"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
edition = "2021"
|
||||
@@ -16,7 +16,7 @@ log = "0.4"
|
||||
tokio = { version = "1" }
|
||||
|
||||
# Core
|
||||
revolt-database = { version = "0.8.3", path = "../../core/database" }
|
||||
revolt-result = { version = "0.8.3", path = "../../core/result" }
|
||||
revolt-config = { version = "0.8.3", path = "../../core/config" }
|
||||
revolt-files = { version = "0.8.3", path = "../../core/files" }
|
||||
revolt-database = { version = "0.8.4", path = "../../core/database" }
|
||||
revolt-result = { version = "0.8.4", path = "../../core/result" }
|
||||
revolt-config = { version = "0.8.4", path = "../../core/config" }
|
||||
revolt-files = { version = "0.8.4", path = "../../core/files" }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "revolt-pushd"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
revolt-config = { version = "0.8.3", path = "../../core/config" }
|
||||
revolt-database = { version = "0.8.3", path = "../../core/database" }
|
||||
revolt-models = { version = "0.8.3", path = "../../core/models", features = [
|
||||
revolt-config = { version = "0.8.4", path = "../../core/config" }
|
||||
revolt-database = { version = "0.8.4", path = "../../core/database" }
|
||||
revolt-models = { version = "0.8.4", path = "../../core/models", features = [
|
||||
"validator",
|
||||
] }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -99,5 +99,26 @@ mod test {
|
||||
// TEST: member leaves group (no delete)
|
||||
// TEST: no effect with saved messages
|
||||
// TEST: DM set to inactive
|
||||
// TEST: server channel deleted
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn success_delete_channel() {
|
||||
let mut harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
let (_, channels) = harness.new_server(&user).await;
|
||||
let response = TestHarness::with_session(
|
||||
session,
|
||||
harness
|
||||
.client
|
||||
.delete(format!("/channels/{}", channels[0].id())),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::NoContent);
|
||||
drop(response);
|
||||
harness
|
||||
.wait_for_event(channels[0].id(), |event| match event {
|
||||
EventV1::ChannelDelete { id, .. } => id == channels[0].id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, PartialMessage, SystemMessage, User, AMQP,
|
||||
};
|
||||
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, Database, PartialMessage, SystemMessage, User, AMQP};
|
||||
use revolt_models::v0::MessageAuthor;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
@@ -22,10 +19,12 @@ pub async fn message_pin(
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
if !matches!(channel, Channel::DirectMessage { .. }) {
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
}
|
||||
|
||||
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||
|
||||
|
||||
@@ -323,4 +323,163 @@ mod test {
|
||||
"Mention was scrubbed when the user can see the channel"
|
||||
);
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn message_reply() {
|
||||
let harness = TestHarness::new().await;
|
||||
let (_, _, user) = harness.new_user().await;
|
||||
let (server, channels) = harness.new_server(&user).await;
|
||||
let channel = harness.new_channel(&server).await;
|
||||
let (_, member, message) = harness.new_message(&user, &server, channels).await;
|
||||
|
||||
// Send a message with a reply
|
||||
// Should succeed
|
||||
let message_with_reply = Message::create_from_api(
|
||||
&harness.db,
|
||||
Some(&harness.amqp),
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content: Some("Message with reply".to_string()),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: Some(vec![v0::ReplyIntent {
|
||||
id: message.id.clone(),
|
||||
mention: false,
|
||||
fail_if_not_exists: Some(true),
|
||||
}]),
|
||||
embeds: None,
|
||||
masquerade: None,
|
||||
interactions: None,
|
||||
flags: None,
|
||||
},
|
||||
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(member.clone().into()),
|
||||
user.limits().await,
|
||||
IdempotencyKey::unchecked_from_string("1".to_string()),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create message with reply");
|
||||
|
||||
assert!(
|
||||
message_with_reply.replies.is_some(),
|
||||
"Message replies is None",
|
||||
);
|
||||
|
||||
let replies = message_with_reply.replies.unwrap();
|
||||
|
||||
assert!(!replies.is_empty(), "Message replies is empty",);
|
||||
|
||||
assert_eq!(replies[0], message.id, "Message reply ID does not match",);
|
||||
|
||||
// Delete the message
|
||||
message
|
||||
.clone()
|
||||
.delete(&harness.db)
|
||||
.await
|
||||
.expect("Failed to delete message");
|
||||
|
||||
// Attempt to create messages with a reply to a deleted message
|
||||
|
||||
// fail_if_not_exists is set to false
|
||||
// Should send the message without a reply
|
||||
let message_with_missing_reply = Message::create_from_api(
|
||||
&harness.db,
|
||||
Some(&harness.amqp),
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content: Some("Message with missing reply".to_string()),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: Some(vec![v0::ReplyIntent {
|
||||
id: message.id.clone(),
|
||||
mention: false,
|
||||
fail_if_not_exists: Some(false),
|
||||
}]),
|
||||
embeds: None,
|
||||
masquerade: None,
|
||||
interactions: None,
|
||||
flags: None,
|
||||
},
|
||||
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(member.clone().into()),
|
||||
user.limits().await,
|
||||
IdempotencyKey::unchecked_from_string("3".to_string()),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create message with missing reply");
|
||||
|
||||
assert!(
|
||||
message_with_missing_reply.replies.is_none()
|
||||
|| message_with_missing_reply.replies.unwrap().is_empty(),
|
||||
"Message replies exist when they shouldn't",
|
||||
);
|
||||
|
||||
// fail_if_not_exists is set to true
|
||||
// Should fail to send the message
|
||||
Message::create_from_api(
|
||||
&harness.db,
|
||||
Some(&harness.amqp),
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content: Some("Message with missing reply".to_string()),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: Some(vec![v0::ReplyIntent {
|
||||
id: message.id.clone(),
|
||||
mention: false,
|
||||
fail_if_not_exists: Some(true),
|
||||
}]),
|
||||
embeds: None,
|
||||
masquerade: None,
|
||||
interactions: None,
|
||||
flags: None,
|
||||
},
|
||||
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(member.clone().into()),
|
||||
user.limits().await,
|
||||
IdempotencyKey::unchecked_from_string("4".to_string()),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect_err("Created message with missing reply and true fail");
|
||||
|
||||
// fail_if_not_exists is not set
|
||||
// Should fail to send the message
|
||||
Message::create_from_api(
|
||||
&harness.db,
|
||||
Some(&harness.amqp),
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content: Some("Message with missing reply".to_string()),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: Some(vec![v0::ReplyIntent {
|
||||
id: message.id.clone(),
|
||||
mention: false,
|
||||
fail_if_not_exists: None,
|
||||
}]),
|
||||
embeds: None,
|
||||
masquerade: None,
|
||||
interactions: None,
|
||||
flags: None,
|
||||
},
|
||||
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(user.clone().into(&harness.db, Some(&user)).await),
|
||||
Some(member.clone().into()),
|
||||
user.limits().await,
|
||||
IdempotencyKey::unchecked_from_string("4".to_string()),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect_err("Created message with missing reply and none fail");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, FieldsMessage, PartialMessage, SystemMessage, User, AMQP,
|
||||
};
|
||||
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, Database, FieldsMessage, PartialMessage, SystemMessage, User, AMQP};
|
||||
use revolt_models::v0::MessageAuthor;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
@@ -22,10 +19,12 @@ pub async fn message_unpin(
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
if !matches!(channel, Channel::DirectMessage { .. }) {
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
}
|
||||
|
||||
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||
|
||||
|
||||
@@ -27,11 +27,6 @@ pub async fn create_emoji(
|
||||
})
|
||||
})?;
|
||||
|
||||
// Bots cannot manage emojis
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
// Validate we have permission to write into parent
|
||||
match &data.parent {
|
||||
v0::EmojiParent::Server { id } => {
|
||||
|
||||
@@ -18,10 +18,6 @@ pub async fn delete_emoji(
|
||||
user: User,
|
||||
emoji_id: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
// Bots cannot manage emoji
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
// Fetch the emoji
|
||||
let emoji = emoji_id.as_emoji(db).await?;
|
||||
|
||||
@@ -76,3 +76,169 @@ pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::I
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{rocket, util::test::TestHarness};
|
||||
use revolt_database::{Channel, Server};
|
||||
use revolt_models::v0::{
|
||||
DataCreateGroup, DataCreateServerChannel, Invite, InviteResponse, LegacyServerChannelType,
|
||||
};
|
||||
use rocket::http::Status;
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn success_fetch_group_invite() {
|
||||
let harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
|
||||
let group = Channel::create_group(
|
||||
&harness.db,
|
||||
DataCreateGroup {
|
||||
..Default::default()
|
||||
},
|
||||
user.id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("`Channel`");
|
||||
let create_response = TestHarness::with_session(
|
||||
session,
|
||||
harness
|
||||
.client
|
||||
.post(format!("/channels/{}/invites", group.id())),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(create_response.status(), Status::Ok);
|
||||
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
|
||||
let invite_code = match invite_from_create {
|
||||
Invite::Group { code, .. } => code,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let response = harness
|
||||
.client
|
||||
.get(format!("/invites/{}", invite_code))
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
|
||||
match invite_response {
|
||||
InviteResponse::Group {
|
||||
code,
|
||||
channel_id,
|
||||
user_name,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(code, invite_code);
|
||||
assert_eq!(channel_id, group.id());
|
||||
assert_eq!(user_name, user.username);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn fail_fetch_missing_invite() {
|
||||
let harness = TestHarness::new().await;
|
||||
let response = harness
|
||||
.client
|
||||
.get(format!("/invites/{}", TestHarness::rand_string()))
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn success_fetch_text_channel_invite() {
|
||||
let harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
let (_, channels) = harness.new_server(&user).await;
|
||||
let channel = channels.first().expect("Server Channel");
|
||||
let create_response = TestHarness::with_session(
|
||||
session,
|
||||
harness
|
||||
.client
|
||||
.post(format!("/channels/{}/invites", channel.id())),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(create_response.status(), Status::Ok);
|
||||
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
|
||||
let invite_code = match invite_from_create {
|
||||
Invite::Server { code, .. } => code,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let response = harness
|
||||
.client
|
||||
.get(format!("/invites/{}", invite_code))
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
|
||||
match invite_response {
|
||||
InviteResponse::Server {
|
||||
code,
|
||||
channel_id,
|
||||
user_name,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(code, invite_code);
|
||||
assert_eq!(channel_id, channel.id());
|
||||
assert_eq!(user_name, user.username);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn success_fetch_voice_channel_invite() {
|
||||
let harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
let (server, _) = harness.new_server(&user).await;
|
||||
let server_mut: &mut Server = &mut server.clone();
|
||||
|
||||
let channel = Channel::create_server_channel(
|
||||
&harness.db,
|
||||
server_mut,
|
||||
DataCreateServerChannel {
|
||||
channel_type: LegacyServerChannelType::Voice,
|
||||
name: "Voice Channel".to_string(),
|
||||
description: None,
|
||||
nsfw: Some(false),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to make new channel");
|
||||
let create_response = TestHarness::with_session(
|
||||
session,
|
||||
harness
|
||||
.client
|
||||
.post(format!("/channels/{}/invites", channel.id())),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(create_response.status(), Status::Ok);
|
||||
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
|
||||
let invite_code = match invite_from_create {
|
||||
Invite::Server { code, .. } => code,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let response = harness
|
||||
.client
|
||||
.get(format!("/invites/{}", invite_code))
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
|
||||
match invite_response {
|
||||
InviteResponse::Server {
|
||||
code,
|
||||
channel_id,
|
||||
user_name,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(code, invite_code);
|
||||
assert_eq!(channel_id, channel.id());
|
||||
assert_eq!(user_name, user.username);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ use authifier::{
|
||||
use futures::StreamExt;
|
||||
use rand::Rng;
|
||||
use redis_kiss::redis::aio::PubSub;
|
||||
use revolt_database::{events::client::EventV1, Database, User, AMQP};
|
||||
use revolt_database::util::idempotency::IdempotencyKey;
|
||||
use revolt_database::{
|
||||
events::client::EventV1, Channel, Database, Member, Message, Server, User, AMQP,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use rocket::local::asynchronous::Client;
|
||||
use rocket::http::Header;
|
||||
use rocket::local::asynchronous::{Client, LocalRequest, LocalResponse};
|
||||
|
||||
pub struct TestHarness {
|
||||
pub client: Client,
|
||||
@@ -104,6 +108,83 @@ impl TestHarness {
|
||||
(account, session, user)
|
||||
}
|
||||
|
||||
pub async fn new_server(&self, user: &User) -> (Server, Vec<Channel>) {
|
||||
Server::create(
|
||||
&self.db,
|
||||
v0::DataCreateServer {
|
||||
name: "Test Server".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
user,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create test server")
|
||||
}
|
||||
|
||||
pub async fn new_channel(&self, server: &Server) -> Channel {
|
||||
Channel::create_server_channel(
|
||||
&self.db,
|
||||
&mut server.clone(),
|
||||
v0::DataCreateServerChannel {
|
||||
channel_type: v0::LegacyServerChannelType::Text,
|
||||
name: "Test Channel".to_string(),
|
||||
description: None,
|
||||
nsfw: Some(false),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to make test channel")
|
||||
}
|
||||
|
||||
pub async fn new_message(
|
||||
&self,
|
||||
user: &User,
|
||||
server: &Server,
|
||||
channels: Vec<Channel>,
|
||||
) -> (Channel, Member, Message) {
|
||||
let (member, channels) = Member::create(&self.db, server, user, Some(channels))
|
||||
.await
|
||||
.expect("Failed to create member");
|
||||
let channel = &channels[0];
|
||||
let message = Message::create_from_api(
|
||||
&self.db,
|
||||
None,
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content: Some("Test message".to_string()),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: None,
|
||||
embeds: None,
|
||||
masquerade: None,
|
||||
interactions: None,
|
||||
flags: None,
|
||||
},
|
||||
v0::MessageAuthor::User(&user.clone().into(&self.db, Some(user)).await),
|
||||
Some(user.clone().into(&self.db, Some(user)).await),
|
||||
Some(member.clone().into()),
|
||||
user.limits().await,
|
||||
IdempotencyKey::unchecked_from_string("0".to_string()),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create message");
|
||||
(channel.clone(), member, message)
|
||||
}
|
||||
|
||||
pub async fn with_session<'c>(
|
||||
session: Session,
|
||||
request: LocalRequest<'c>,
|
||||
) -> LocalResponse<'c> {
|
||||
request
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.dispatch()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn wait_for_event<F>(&mut self, topic: &str, predicate: F) -> EventV1
|
||||
where
|
||||
F: Fn(&EventV1) -> bool,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-autumn"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -43,12 +43,12 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Core crates
|
||||
revolt-files = { version = "0.8.3", path = "../../core/files" }
|
||||
revolt-config = { version = "0.8.3", path = "../../core/config" }
|
||||
revolt-database = { version = "0.8.3", path = "../../core/database", features = [
|
||||
revolt-files = { version = "0.8.4", path = "../../core/files" }
|
||||
revolt-config = { version = "0.8.4", path = "../../core/config" }
|
||||
revolt-database = { version = "0.8.4", path = "../../core/database", features = [
|
||||
"axum-impl",
|
||||
] }
|
||||
revolt-result = { version = "0.8.3", path = "../../core/result", features = [
|
||||
revolt-result = { version = "0.8.4", path = "../../core/result", features = [
|
||||
"utoipa",
|
||||
"axum",
|
||||
] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-january"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -32,13 +32,13 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Core crates
|
||||
revolt-config = { version = "0.8.3", path = "../../core/config" }
|
||||
revolt-models = { version = "0.8.3", path = "../../core/models" }
|
||||
revolt-result = { version = "0.8.3", path = "../../core/result", features = [
|
||||
revolt-config = { version = "0.8.4", path = "../../core/config" }
|
||||
revolt-models = { version = "0.8.4", path = "../../core/models" }
|
||||
revolt-result = { version = "0.8.4", path = "../../core/result", features = [
|
||||
"utoipa",
|
||||
"axum",
|
||||
] }
|
||||
revolt-files = { version = "0.8.3", path = "../../core/files" }
|
||||
revolt-files = { version = "0.8.4", path = "../../core/files" }
|
||||
|
||||
# Axum / web server
|
||||
axum = { version = "0.7.5" }
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# a test script that generates a ton of users for debugging use
|
||||
# note that you'll need to comment out the ratelimiter in delta/src/main.rs
|
||||
# and keep the number relatively low or requests will time out (the beefier the machine the more you can handle).
|
||||
# this script assumes mailhog is running, and uses that to automate "emails".
|
||||
|
||||
# In the real world, antispam will catch this and nuke you to hell and back.
|
||||
# But it works fine in a dev env!
|
||||
|
||||
# requires httpx
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
|
||||
import httpx
|
||||
|
||||
API_URL: str = os.getenv("API_URL") # type: ignore
|
||||
MAILHOG_API: str = os.getenv("MAILHOG_API") # type: ignore
|
||||
COUNT = int(os.getenv("COUNT")) # type: ignore # cbf to deal with type checking
|
||||
INVITE: str = os.getenv("INVITE") # type: ignore
|
||||
|
||||
assert API_URL and MAILHOG_API and COUNT and INVITE
|
||||
|
||||
API_URL = API_URL.strip("/")
|
||||
MAILHOG_API = MAILHOG_API.strip("/")
|
||||
|
||||
async def filter_hog(client: httpx.AsyncClient, email: str) -> str:
|
||||
"""
|
||||
returns the token provided by the mail server.
|
||||
This script assumes the use of mailhog.
|
||||
"""
|
||||
|
||||
resp = await client.get(MAILHOG_API + "/api/v2/search", params={"kind": "to", "query": email}, follow_redirects=True, timeout=60)
|
||||
if resp.status_code != 200:
|
||||
raise Exception(resp.status_code, resp.content)
|
||||
|
||||
data = resp.json()
|
||||
if not data["items"]:
|
||||
raise Exception("No message found")
|
||||
|
||||
message_id = data["items"][0]["ID"]
|
||||
body = data["items"][0]["Content"]["Body"].replace("\r", "")
|
||||
token = re.search("/login/verify(=\n/|/\n=|/=\n)(?P<token>[^\n]+)", body, re.MULTILINE)
|
||||
if not token:
|
||||
raise Exception("No token found")
|
||||
|
||||
ret = token.group("token")
|
||||
|
||||
await client.delete(MAILHOG_API + f"/api/v1/messages/{message_id}", timeout=60)
|
||||
return ret
|
||||
|
||||
|
||||
async def task() -> None:
|
||||
_id = str(uuid.uuid4())[:4]
|
||||
email = f"{_id}@example.com"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(API_URL + "/auth/account/create", json={"email": email, "password": _id*3, "invite": INVITE}, timeout=60)
|
||||
if resp.status_code != 204:
|
||||
raise Exception(resp.status_code, resp.content)
|
||||
|
||||
token = await filter_hog(client, email)
|
||||
|
||||
resp = await client.post(API_URL + f"/auth/account/verify/{token}", timeout=60)
|
||||
if resp.status_code != 200:
|
||||
raise Exception("verify", resp.status_code, resp.content)
|
||||
|
||||
ticket = resp.json()["ticket"]
|
||||
userid = ticket["_id"]
|
||||
|
||||
resp = await client.post(API_URL + "/auth/session/login", json={"email": email, "password": _id*3, "friendly_name": "Not A Client"}, timeout=60)
|
||||
if resp.status_code != 200:
|
||||
raise Exception("session", resp.status_code, resp.content)
|
||||
|
||||
session = resp.json()
|
||||
token = session["token"]
|
||||
|
||||
resp = await client.post(API_URL + "/onboard/complete", json={"username": _id}, headers={"x-session-token": token}, timeout=60) # complete onboarding to allow creating a session
|
||||
if resp.status_code != 200:
|
||||
raise Exception("onboard", resp.status_code, resp.content)
|
||||
|
||||
resp = await client.post(API_URL + f"/invites/{INVITE}", headers={"x-session-token": token}, timeout=60)
|
||||
if resp.status_code != 200:
|
||||
raise Exception("invite", resp.status_code, resp.content)
|
||||
|
||||
print(f"Created account and session for {email} with ID: {userid}")
|
||||
return userid
|
||||
|
||||
async def main():
|
||||
tasks = [asyncio.create_task(task()) for _ in range(COUNT)]
|
||||
print(await asyncio.gather(*tasks))
|
||||
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user