Compare commits

...

9 Commits

Author SHA1 Message Date
Zomatree
1a60aecdb3 chore: switch to method chaining over macro 2025-08-15 02:07:26 +01:00
Zomatree
789dedd9f1 chore: cleanup errors 2025-08-15 02:07:26 +01:00
Zomatree
d7cf809424 fix: make sentry dep optional 2025-08-15 02:07:25 +01:00
Zomatree
249a4818fc feat: error docs 2025-08-15 02:07:25 +01:00
Zomatree
46e127ccd2 chore: send rest of the errors to the catchers 2025-08-15 02:07:25 +01:00
Zomatree
cf4fe859bf feat: move all json and validate to new utils 2025-08-15 02:07:25 +01:00
Zomatree
3d6f39a0eb feat: better error handling 2025-08-15 02:00:12 +01:00
Zomatree
ed22b3a5ce chore: switch Reference to using an actual reference 2025-08-12 03:44:29 +01:00
Zomatree
65fbd36624 fix: allow for disabling default features 2025-08-12 02:35:09 +01:00
158 changed files with 1096 additions and 739 deletions

4
Cargo.lock generated
View File

@@ -6291,7 +6291,6 @@ dependencies = [
"bson",
"deadqueue",
"decancer",
"fcm_v1",
"futures",
"indexmap 1.9.3",
"isahc",
@@ -6311,7 +6310,6 @@ dependencies = [
"revolt-permissions",
"revolt-presence",
"revolt-result",
"revolt_a2",
"revolt_okapi",
"revolt_optional_struct",
"revolt_rocket_okapi",
@@ -6323,7 +6321,6 @@ dependencies = [
"unicode-segmentation",
"url-escape",
"validator 0.16.1",
"web-push",
]
[[package]]
@@ -6514,6 +6511,7 @@ dependencies = [
"revolt_rocket_okapi",
"rocket",
"schemars",
"sentry",
"serde",
"serde_json",
"utoipa",

View File

@@ -26,3 +26,7 @@ disallowed-methods = [
# Prefer to use Object::delete(&self)
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
]
disallowed-types = [
"rocket::serde::json::Json",
]

View File

@@ -37,7 +37,7 @@ async-std = { version = "1.8.0", features = [
# core
authifier = { version = "1.0.15" }
revolt-result = { path = "../core/result" }
revolt-result = { path = "../core/result", features = ["sentry"] }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }

View File

@@ -1,7 +1,7 @@
use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender;
use revolt_database::events::client::ReadyPayloadFields;
use revolt_result::{create_error, Result};
use revolt_result::{create_error, Result, ToRevoltError};
use serde::{Deserialize, Serialize};
/// Enumeration of supported protocol formats
@@ -38,16 +38,22 @@ impl ProtocolConfiguration {
match self.format {
ProtocolFormat::Json => {
if let Message::Text(text) = msg {
serde_json::from_str(text).map_err(|_| create_error!(InternalError))
// Log the error in-case we make a breaking change to the payload
serde_json::from_str(text)
.capture_error()
.map_err(|_| create_error!(UnprocessableEntity))
} else {
Err(create_error!(InternalError))
Err(create_error!(UnprocessableEntity))
}
}
ProtocolFormat::Msgpack => {
if let Message::Binary(buf) = msg {
rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
rmp_serde::from_slice(buf)
.capture_error()
.map_err(|_| create_error!(UnprocessableEntity))
} else {
Err(create_error!(InternalError))
Err(create_error!(UnprocessableEntity))
}
}
}

View File

@@ -14,7 +14,6 @@ use futures::{
FutureExt, SinkExt, StreamExt, TryStreamExt,
};
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_config::report_internal_error;
use revolt_database::{
events::{client::EventV1, server::ClientMessage},
iso8601_timestamp::Timestamp,
@@ -27,7 +26,7 @@ use async_std::{
sync::{Mutex, RwLock},
task::spawn,
};
use revolt_result::create_error;
use revolt_result::{create_error, ToRevoltError};
use sentry::Level;
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
@@ -110,21 +109,21 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated.
if report_internal_error!(write.send(config.encode(&EventV1::Authenticated)).await).is_err() {
if write.send(config.encode(&EventV1::Authenticated)).await.to_internal_error().is_err() {
return;
}
// Download required data to local cache and send Ready payload.
let ready_payload = match report_internal_error!(
state
let ready_payload = match state
.generate_ready_payload(db, config.get_ready_payload_fields())
.await
) {
.to_internal_error()
{
Ok(ready_payload) => ready_payload,
Err(_) => return,
};
if report_internal_error!(write.send(config.encode(&ready_payload)).await).is_err() {
if write.send(config.encode(&ready_payload)).await.to_internal_error().is_err() {
return;
}
@@ -219,14 +218,15 @@ async fn listener(
write: &Mutex<WsWriter>,
) {
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
let subscriber = match report_internal_error!(
fred::types::Builder::from_config(redis_config).build_subscriber_client()
) {
let subscriber = match fred::types::Builder::from_config(redis_config)
.build_subscriber_client()
.to_internal_error()
{
Ok(subscriber) => subscriber,
Err(_) => return,
};
if report_internal_error!(subscriber.init().await).is_err() {
if subscriber.init().await.to_internal_error().is_err() {
return;
}
@@ -249,13 +249,13 @@ async fn listener(
// Check for state changes for subscriptions.
match state.apply_state().await {
SubscriptionStateChange::Reset => {
if report_internal_error!(subscriber.unsubscribe_all().await).is_err() {
if subscriber.unsubscribe_all().await.to_internal_error().is_err() {
break 'out;
}
let subscribed = state.subscribed.read().await;
for id in subscribed.iter() {
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
if subscriber.subscribe(id).await.to_internal_error().is_err() {
break 'out;
}
}
@@ -268,7 +268,7 @@ async fn listener(
#[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}");
if report_internal_error!(subscriber.unsubscribe(id).await).is_err() {
if subscriber.unsubscribe(id).await.to_internal_error().is_err() {
break 'out;
}
}
@@ -277,7 +277,7 @@ async fn listener(
#[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
if subscriber.subscribe(id).await.to_internal_error().is_err() {
break 'out;
}
}
@@ -302,7 +302,7 @@ async fn listener(
_ = t2 => {},
message = t1 => {
// Handle incoming events.
let message = match report_internal_error!(message) {
let message = match message.to_internal_error() {
Ok(message) => message,
Err(_) => break 'out
};
@@ -311,15 +311,15 @@ async fn listener(
PayloadType::Json => message
.value
.as_str()
.and_then(|s| report_internal_error!(serde_json::from_str::<EventV1>(s.as_ref())).ok()),
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).to_internal_error().ok()),
PayloadType::Msgpack => message
.value
.as_bytes()
.and_then(|b| report_internal_error!(rmp_serde::from_slice::<EventV1>(b)).ok()),
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).to_internal_error().ok()),
PayloadType::Bincode => message
.value
.as_bytes()
.and_then(|b| report_internal_error!(bincode::deserialize::<EventV1>(b)).ok()),
.and_then(|b| bincode::deserialize::<EventV1>(b).to_internal_error().ok()),
};
let Some(mut event) = event else {
@@ -379,7 +379,7 @@ async fn listener(
}
}
report_internal_error!(subscriber.quit().await).ok();
subscriber.quit().await.to_internal_error().ok();
}
#[allow(clippy::too_many_arguments)]

View File

@@ -11,8 +11,9 @@ description = "Revolt Backend: Configuration"
[features]
anyhow = ["dep:sentry-anyhow"]
report-macros = ["revolt-result"]
sentry = ["dep:sentry"]
test = ["async-std"]
default = ["test", "anyhow"]
default = ["test", "sentry"]
[dependencies]
# Utility
@@ -32,7 +33,7 @@ log = "0.4.14"
pretty_env_logger = "0.4.0"
# Sentry
sentry = "0.31.5"
sentry = { version = "0.31.5", optional = true }
sentry-anyhow = { version = "0.38.1", optional = true }
# Core

View File

@@ -6,10 +6,12 @@ use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
#[cfg(feature = "sentry")]
pub use sentry::{capture_error, capture_message, Level};
#[cfg(feature = "anyhow")]
pub use sentry_anyhow::capture_anyhow;
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! report_error {
( $expr: expr, $error: ident $( $tt:tt )? ) => {
@@ -24,7 +26,7 @@ macro_rules! report_error {
};
}
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! capture_internal_error {
( $expr: expr ) => {
@@ -35,7 +37,7 @@ macro_rules! capture_internal_error {
};
}
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! report_internal_error {
( $expr: expr ) => {
@@ -419,6 +421,7 @@ pub async fn config() -> Settings {
}
/// Configure logging and common Rust variables
#[cfg(feature = "sentry")]
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
@@ -444,6 +447,7 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
}
}
#[cfg(feature = "sentry")]
#[macro_export]
macro_rules! configure {
($application: ident) => {

View File

@@ -10,12 +10,12 @@ description = "Revolt Backend: Database Implementation"
[features]
# Databases
mongodb = ["dep:mongodb", "bson"]
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
# ... Other
tasks = ["isahc", "linkify", "url-escape"]
async-std-runtime = ["async-std"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
axum-impl = ["axum"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
@@ -91,13 +91,8 @@ rocket = { version = "0.5.1", default-features = false, features = [
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
# Notifications
fcm_v1 = "0.3.0"
web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.15", features = ["rocket_impl"] }
authifier = { version = "1.0.15" }
# RabbitMQ
amqprs = { version = "1.7.0" }

View File

@@ -1,3 +1,4 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -13,6 +14,7 @@ use authifier::Authifier;
use rand::Rng;
use revolt_config::config;
#[cfg(feature = "mongodb")]
pub use self::mongodb::*;
pub use self::reference::*;
@@ -25,8 +27,10 @@ pub enum DatabaseInfo {
/// Use the mock database
Reference,
/// Connect to MongoDB
#[cfg(feature = "mongodb")]
MongoDb { uri: String, database_name: String },
/// Use existing MongoDB connection
#[cfg(feature = "mongodb")]
MongoDbFromClient(::mongodb::Client, String),
}
@@ -36,6 +40,7 @@ pub enum Database {
/// Mock database
Reference(ReferenceDb),
/// MongoDB database
#[cfg(feature = "mongodb")]
MongoDb(MongoDb),
}
@@ -45,7 +50,7 @@ impl DatabaseInfo {
pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
Ok(match self {
match self {
DatabaseInfo::Auto => {
if std::env::var("TEST_DB").is_ok() {
DatabaseInfo::Test(format!(
@@ -53,16 +58,20 @@ impl DatabaseInfo {
rand::thread_rng().gen_range(1_000_000..10_000_000)
))
.connect()
.await?
.await
} else if !config.database.mongodb.is_empty() {
DatabaseInfo::MongoDb {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name: "revolt".to_string(),
}
.connect()
.await?
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string())
} else {
DatabaseInfo::Reference.connect().await?
DatabaseInfo::Reference.connect().await
}
}
DatabaseInfo::Test(database_name) => {
@@ -70,30 +79,36 @@ impl DatabaseInfo {
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
.as_str()
{
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
"REFERENCE" => DatabaseInfo::Reference.connect().await,
"MONGODB" => {
DatabaseInfo::MongoDb {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name,
}
.connect()
.await?
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string())
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
}
}
DatabaseInfo::Reference => Database::Reference(Default::default()),
DatabaseInfo::Reference => Ok(Database::Reference(Default::default())),
#[cfg(feature = "mongodb")]
DatabaseInfo::MongoDb { uri, database_name } => {
let client = ::mongodb::Client::with_uri_str(uri)
.await
.map_err(|_| "Failed to init db connection.".to_string())?;
Database::MongoDb(MongoDb(client, database_name))
Ok(Database::MongoDb(MongoDb(client, database_name)))
}
#[cfg(feature = "mongodb")]
DatabaseInfo::MongoDbFromClient(client, database_name) => {
Database::MongoDb(MongoDb(client, database_name))
Ok(Database::MongoDb(MongoDb(client, database_name)))
}
})
}
}
}
@@ -219,12 +234,16 @@ impl Database {
Authifier {
database: match self {
Database::Reference(_) => Default::default(),
#[cfg(feature = "mongodb")]
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: auth_config,
#[cfg(feature = "tasks")]
event_channel: Some(crate::tasks::authifier_relay::sender()),
#[cfg(not(feature = "tasks"))]
event_channel: None,
}
}
}

View File

@@ -10,7 +10,6 @@ use serde::de::DeserializeOwned;
use serde::Serialize;
database_derived!(
#[cfg(feature = "mongodb")]
/// MongoDB implementation
pub struct MongoDb(pub ::mongodb::Client, pub String);
);

View File

@@ -25,6 +25,9 @@ pub use mongodb;
#[macro_use]
extern crate bson;
#[cfg(not(feature = "async-std-runtime"))]
compile_error!("async-std-runtime feature must be enabled.");
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! query {
@@ -103,6 +106,7 @@ pub mod util;
pub use models::*;
pub mod events;
#[cfg(feature = "tasks")]
pub mod tasks;
mod amqp;

View File

@@ -1,3 +1,4 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{Bot, FieldsBot, PartialBot};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Invite;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::ChannelUnread;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -8,10 +8,13 @@ use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::{
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
events::client::EventV1, Database, File, PartialServer,
Server, SystemMessage, User, AMQP,
};
#[cfg(feature = "mongodb")]
use crate::IntoDocumentPath;
auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel {
@@ -646,10 +649,11 @@ impl Channel {
.private(user.to_string())
.await;
#[cfg(feature = "tasks")]
crate::tasks::ack::queue_ack(
self.id().to_string(),
user.to_string(),
AckEvent::AckMessage {
crate::tasks::ack::AckEvent::AckMessage {
id: message.to_string(),
},
)
@@ -766,6 +770,7 @@ impl Channel {
}
}
#[cfg(feature = "mongodb")]
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {

View File

@@ -1,5 +1,7 @@
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Emoji;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::FileHash;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -4,6 +4,7 @@ use crate::File;
use super::FileUsedFor;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,7 +1,7 @@
use bson::to_document;
use bson::Document;
use revolt_config::report_internal_error;
use revolt_result::Result;
use revolt_result::ToRevoltError;
use crate::File;
use crate::FileUsedFor;
@@ -106,7 +106,7 @@ impl AbstractAttachments for MongoDb {
},
doc! {
"$set": {
"used_for": report_internal_error!(to_document(&used_for))?,
"used_for": to_document(&used_for).to_internal_error()?,
"uploader_id": uploader_id
}
},

View File

@@ -10,11 +10,9 @@ use revolt_models::v0::{
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
use revolt_result::{ErrorType, Result};
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::{
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
permissions::DatabasePermissionQuery,
@@ -22,6 +20,9 @@ use crate::{
Channel, Database, Emoji, File, User, AMQP,
};
#[cfg(feature = "tasks")]
use crate::tasks::{self, ack::AckEvent};
auto_derived_partial!(
/// Message
pub struct Message {
@@ -487,31 +488,28 @@ impl Message {
| Channel::VoiceChannel { ref server, .. } => {
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
if let Ok(valid_members) = valid_members {
let valid_mentions = HashSet::<&String, RandomState>::from_iter(
valid_members.iter().map(|m| &m.id.user),
);
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await?;
user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
let valid_mentions = HashSet::<&String, RandomState>::from_iter(
valid_members.iter().map(|m| &m.id.user),
);
if !user_mentions.is_empty() {
// if there are still mentions, drill down to a channel-level
let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server)
.await
.channel(&channel)
.members(&valid_members)
.members_can_see_channel()
.await;
user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
user_mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
}
} else {
revolt_config::capture_error(&valid_members.unwrap_err());
return Err(create_error!(InternalError));
if !user_mentions.is_empty() {
// if there are still mentions, drill down to a channel-level
let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server)
.await
.channel(&channel)
.members(&valid_members)
.members_can_see_channel()
.await;
user_mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
}
}
Channel::SavedMessages { .. } => {
user_mentions.clear();
@@ -616,9 +614,11 @@ impl Message {
.await;
// Update last_message_id
#[cfg(feature = "tasks")]
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
// Add mentions for affected users
#[cfg(feature = "tasks")]
if !mentions_elsewhere {
if let Some(mentions) = &self.mentions {
tasks::ack::queue_message(
@@ -637,6 +637,7 @@ impl Message {
}
// Generate embeds
#[cfg(feature = "tasks")]
if generate_embeds {
if let Some(content) = &self.content {
tasks::process_embeds::queue(
@@ -673,10 +674,12 @@ impl Message {
)
.await?;
if !self.has_suppressed_notifications()
&& (self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications
#[cfg(feature = "tasks")]
tasks::ack::queue_message(
self.channel.to_string(),
AckEvent::ProcessMessage {
@@ -710,12 +713,6 @@ impl Message {
/// Create text embed from sendable embed
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
embed.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let media = if let Some(id) = embed.media {
Some(File::use_attachment(db, &id, &self.id, &self.author).await?)
} else {

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -38,7 +38,10 @@ pub use servers::*;
pub use user_settings::*;
pub use users::*;
use crate::{Database, MongoDb, ReferenceDb};
use crate::{Database, ReferenceDb};
#[cfg(feature = "mongodb")]
use crate::MongoDb;
pub trait AbstractDatabase:
Sync
@@ -66,6 +69,8 @@ pub trait AbstractDatabase:
}
impl AbstractDatabase for ReferenceDb {}
#[cfg(feature = "mongodb")]
impl AbstractDatabase for MongoDb {}
impl std::ops::Deref for Database {
@@ -74,6 +79,7 @@ impl std::ops::Deref for Database {
fn deref(&self) -> &Self::Target {
match &self {
Database::Reference(dummy) => dummy,
#[cfg(feature = "mongodb")]
Database::MongoDb(mongo) => mongo,
}
}

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::PolicyChange;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,6 +1,8 @@
use std::time::Duration;
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Report;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Snapshot;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{MemberCompositeKey, ServerBan};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,16 +1,20 @@
use ::mongodb::SessionCursor;
#[cfg(feature = "mongodb")]
use ::mongodb::{ClientSession, SessionCursor};
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
MongoDb {
session: ::mongodb::ClientSession,
session: ClientSession,
cursor: Option<SessionCursor<Member>>,
},
@@ -22,7 +26,7 @@ pub enum ChunkedServerMembersGenerator {
impl ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
pub fn new_mongo(session: ClientSession, cursor: SessionCursor<Member>) -> Self {
ChunkedServerMembersGenerator::MongoDb {
session,
cursor: Some(cursor),

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::UserSettings;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -705,6 +705,8 @@ impl User {
duration_days: Option<usize>,
reason: Option<Vec<String>>,
) -> Result<()> {
// TODO: authifier Error should implement Error
let authifier = db.clone().to_authifier().await;
let mut account = authifier
.database

View File

@@ -4,6 +4,7 @@ use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -38,6 +38,7 @@ impl<'r> FromRequest<'r> for User {
if let Some(user) = user {
Outcome::Success(user.clone())
} else {
request.local_cache(|| Some(create_error!(InvalidSession)));
Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession))
}
}

View File

@@ -113,6 +113,8 @@ impl<'r> FromRequest<'r> for IdempotencyKey {
let idempotency = IdempotencyKey { key };
let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&idempotency.key).is_some() {
request.local_cache(|| Some(create_error!(DuplicateNonce)));
return Outcome::Error((Status::Conflict, create_error!(DuplicateNonce)));
}

View File

@@ -14,41 +14,40 @@ use crate::{
};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
pub struct Reference {
pub struct Reference<'a> {
/// Id of object
pub id: String,
pub id: &'a str,
}
impl Reference {
impl<'a> Reference<'a> {
/// Create a Ref from an unchecked string
pub fn from_unchecked(id: String) -> Reference {
pub fn from_unchecked(id: &'a str) -> Reference<'a> {
Reference { id }
}
/// Fetch ban from Ref
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
db.fetch_ban(server, &self.id).await
db.fetch_ban(server, self.id).await
}
/// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await
db.fetch_bot(self.id).await
}
/// Fetch emoji from Ref
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
db.fetch_emoji(&self.id).await
db.fetch_emoji(self.id).await
}
/// Fetch channel from Ref
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
db.fetch_channel(&self.id).await
db.fetch_channel(self.id).await
}
/// Fetch invite from Ref or create invite to server if discoverable
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
if ulid::Ulid::from_str(&self.id).is_ok() {
if ulid::Ulid::from_str(self.id).is_ok() {
let server = self.as_server(db).await?;
if !server.discoverable {
return Err(create_error!(NotFound));
@@ -65,18 +64,18 @@ impl Reference {
.ok_or(create_error!(NotFound))?,
})
} else {
db.fetch_invite(&self.id).await
db.fetch_invite(self.id).await
}
}
/// Fetch message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(&self.id).await
db.fetch_message(self.id).await
}
/// Fetch message from Ref and validate channel
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
let msg = db.fetch_message(&self.id).await?;
let msg = db.fetch_message(self.id).await?;
if msg.channel != channel {
return Err(create_error!(NotFound));
}
@@ -86,36 +85,36 @@ impl Reference {
/// Fetch member from Ref
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
db.fetch_member(server, &self.id).await
db.fetch_member(server, self.id).await
}
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await
db.fetch_server(self.id).await
}
/// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> {
db.fetch_user(&self.id).await
db.fetch_user(self.id).await
}
/// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await
db.fetch_webhook(self.id).await
}
}
#[cfg(feature = "rocket-impl")]
impl<'r> FromParam<'r> for Reference {
impl<'r> FromParam<'r> for Reference<'r> {
type Error = &'r str;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
Ok(Reference::from_unchecked(param.into()))
Ok(Reference::from_unchecked(param))
}
}
#[cfg(feature = "rocket-impl")]
impl JsonSchema for Reference {
impl<'a> JsonSchema for Reference<'a> {
fn schema_name() -> String {
"Id".to_string()
}

View File

@@ -14,7 +14,7 @@ imagesize = "0.13.0"
tempfile = "3.12.0"
base64 = "0.22.1"
aes-gcm = "0.10.3"
aes-gcm = { version = "0.10.3", features = ["std"] }
typenum = "1.17.0"
aws-config = "1.5.5"
@@ -23,7 +23,7 @@ aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.8.8", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.8", path = "../result" }
revolt-result = { version = "0.8.8", path = "../result", features = ["sentry"] }
# image processing
jxl-oxide = "0.8.1"

View File

@@ -6,7 +6,7 @@ use aes_gcm::{
};
use image::{DynamicImage, ImageBuffer};
use revolt_config::{config, report_internal_error, FilesS3};
use revolt_result::{create_error, Result};
use revolt_result::{create_error, Result, ToRevoltError};
use aws_sdk_s3::{
config::{Credentials, Region},
@@ -55,13 +55,12 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
// Send a request for the file
let mut obj =
report_internal_error!(client.get_object().bucket(bucket_id).key(path).send().await)?;
client.get_object().bucket(bucket_id).key(path).send().await.to_internal_error()?;
// Read the file from remote
let mut buf = vec![];
while let Some(bytes) = obj.body.next().await {
let data = report_internal_error!(bytes)?;
report_internal_error!(buf.write_all(&data))?;
buf.write_all(&bytes.to_internal_error()?).to_internal_error()?;
// is there a more efficient way to do this?
// we just want the Vec<u8>
}
@@ -78,7 +77,7 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
// Decrypt the file
create_cipher(&config.files.encryption_key)
.decrypt_in_place(nonce, b"", &mut buf)
.map_err(|_| create_error!(InternalError))?;
.to_internal_error()?;
Ok(buf)
}
@@ -97,18 +96,17 @@ pub async fn upload_to_s3(bucket_id: &str, path: &str, buf: &[u8]) -> Result<Str
// Encrypt the file in place
create_cipher(&config.files.encryption_key)
.encrypt_in_place(&nonce, b"", &mut buf)
.map_err(|_| create_error!(InternalError))?;
.to_internal_error()?;
// Upload the file to remote
report_internal_error!(
client
.put_object()
.bucket(bucket_id)
.key(path)
.body(buf.into())
.send()
.await
)?;
client
.put_object()
.bucket(bucket_id)
.key(path)
.body(buf.into())
.send()
.await
.to_internal_error()?;
Ok(BASE64_STANDARD.encode(nonce))
}
@@ -118,14 +116,13 @@ pub async fn delete_from_s3(bucket_id: &str, path: &str) -> Result<()> {
let config = config().await;
let client = create_client(config.files.s3);
report_internal_error!(
client
.delete_object()
.bucket(bucket_id)
.key(path)
.send()
.await
)?;
client
.delete_object()
.bucket(bucket_id)
.key(path)
.send()
.await
.to_internal_error()?;
Ok(())
}
@@ -145,8 +142,7 @@ pub fn image_size(f: &NamedTempFile) -> Option<(usize, usize)> {
pub fn image_size_vec(v: &[u8], mime: &str) -> Option<(usize, usize)> {
match mime {
"image/svg+xml" => {
let tree =
report_internal_error!(usvg::Tree::from_data(v, &Default::default())).ok()?;
let tree = usvg::Tree::from_data(v, &Default::default()).to_internal_error().ok()?;
let size = tree.size();
Some((size.width() as usize, size.height() as usize))
@@ -221,9 +217,9 @@ pub fn decode_image<R: Read + BufRead + Seek>(reader: &mut R, mime: &str) -> Res
"image/svg+xml" => {
// usvg doesn't support Read trait so copy to buffer
let mut buf = Vec::new();
report_internal_error!(reader.read_to_end(&mut buf))?;
reader.read_to_end(&mut buf).to_internal_error()?;
let tree = report_internal_error!(usvg::Tree::from_data(&buf, &Default::default()))?;
let tree = usvg::Tree::from_data(&buf, &Default::default()).to_internal_error()?;
let size = tree.size();
let mut pixmap = Pixmap::new(size.width() as u32, size.height() as u32)
.ok_or_else(|| create_error!(ImageProcessingFailed))?;
@@ -241,10 +237,11 @@ pub fn decode_image<R: Read + BufRead + Seek>(reader: &mut R, mime: &str) -> Res
))
}
// Check if we can read using image-rs crate
_ => report_internal_error!(report_internal_error!(
image::ImageReader::new(reader).with_guessed_format()
)?
.decode()),
_ => image::ImageReader::new(reader)
.with_guessed_format()
.to_internal_error()?
.decode()
.to_internal_error()
}
}

View File

@@ -7,6 +7,7 @@ mod embeds;
mod emojis;
mod files;
mod messages;
mod onboard;
mod policy_changes;
mod safety_reports;
mod server_bans;
@@ -24,6 +25,7 @@ pub use embeds::*;
pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use onboard::*;
pub use policy_changes::*;
pub use safety_reports::*;
pub use server_bans::*;

View File

@@ -0,0 +1,15 @@
auto_derived!(
/// # New User Data
#[derive(validator::Validate)]
pub struct DataOnboard {
/// New username which will be used to identify the user on the platform
#[validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")]
pub username: String,
}
/// # Onboarding Status
pub struct DataHello {
/// Whether onboarding is required
pub onboarding: bool,
}
);

View File

@@ -1,6 +1,17 @@
use iso8601_timestamp::Timestamp;
auto_derived!(
/// # Report Data
#[derive(validator::Validate)]
pub struct DataReportContent {
/// Content being reported
pub content: ReportedContent,
/// Additional report description
#[validate(length(min = 0, max = 1000))]
#[serde(default)]
pub additional_context: String,
}
/// User-generated platform moderation report
pub struct Report {
/// Unique Id

View File

@@ -1,4 +1,4 @@
use super::{Channel, File, RE_COLOUR};
use super::{Channel, File, Member, User, RE_COLOUR};
use revolt_permissions::{Override, OverrideField};
use std::collections::HashMap;
@@ -293,4 +293,22 @@ auto_derived!(
pub struct DataEditRoleRanks {
pub ranks: Vec<String>,
}
/// # Query Parameters
#[derive(FromForm)]
pub struct OptionsQueryMembers {
/// String to search for
pub query: String,
/// Discourage use of this API
pub experimental_api: bool,
}
/// # Query members by name
pub struct MemberQueryResponse {
/// List of members
pub members: Vec<Member>,
/// List of users
pub users: Vec<User>,
}
);

View File

@@ -275,6 +275,17 @@ auto_derived!(
/// Username and discriminator combo separated by #
pub username: String,
}
/// # Username Information
#[derive(Validate)]
pub struct DataChangeUsername {
/// New username
#[validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")]
pub username: String,
/// Current account password
#[validate(length(min = 8, max = 1024))]
pub password: String,
}
);
pub trait CheckRelationship {

View File

@@ -15,6 +15,7 @@ utoipa = ["dep:utoipa"]
rocket = ["dep:rocket", "dep:serde_json"]
axum = ["dep:axum", "dep:serde_json"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
sentry = ["dep:sentry"]
default = ["serde"]
@@ -34,3 +35,6 @@ revolt_okapi = { version = "0.9.1", optional = true }
# Axum
axum = { version = "0.7.5", optional = true }
# Sentry
sentry = { version = "0.31.5", optional = true }

View File

@@ -2,10 +2,9 @@ use axum::{http::StatusCode, response::IntoResponse, Json};
use crate::{Error, ErrorType};
/// HTTP response builder for Error enum
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
let status = match self.error_type {
impl Error {
pub fn axum_status(&self) -> StatusCode {
match self.error_type {
ErrorType::LabelMe => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::AlreadyOnboarded => StatusCode::FORBIDDEN,
@@ -70,11 +69,15 @@ impl IntoResponse for Error {
ErrorType::InvalidProperty => StatusCode::BAD_REQUEST,
ErrorType::InvalidSession => StatusCode::UNAUTHORIZED,
ErrorType::NotAuthenticated => StatusCode::UNAUTHORIZED,
ErrorType::Conflict => StatusCode::CONFLICT,
ErrorType::DuplicateNonce => StatusCode::CONFLICT,
ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST,
ErrorType::NotFound => StatusCode::NOT_FOUND,
ErrorType::NoEffect => StatusCode::OK,
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
ErrorType::IOError => StatusCode::BAD_REQUEST,
ErrorType::UnprocessableEntity => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::DeserializationError { .. } => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::FailedValidation { .. } => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
@@ -84,8 +87,13 @@ impl IntoResponse for Error {
ErrorType::FileTypeNotAllowed => StatusCode::BAD_REQUEST,
ErrorType::ImageProcessingFailed => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::NoEmbedData => StatusCode::BAD_REQUEST,
};
(status, Json(&self)).into_response()
}
}
}
/// HTTP response builder for Error enum
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
(self.axum_status(), Json(&self)).into_response()
}
}

View File

@@ -1,3 +1,4 @@
use std::panic::Location;
use std::fmt::Display;
#[cfg(feature = "serde")]
@@ -151,9 +152,15 @@ pub enum ErrorType {
InvalidSession,
InvalidFlagValue,
NotAuthenticated,
Conflict,
DuplicateNonce,
NotFound,
NoEffect,
IOError,
UnprocessableEntity,
DeserializationError {
error: String,
},
FailedValidation {
error: String,
},
@@ -174,7 +181,7 @@ pub enum ErrorType {
// ? Feature flag disabled in the config
FeatureDisabled {
feature: String,
},
}
}
#[macro_export]
@@ -197,6 +204,62 @@ macro_rules! create_database_error {
};
}
pub trait ToRevoltError<T>: Sized {
fn capture_error(self) -> Self;
#[track_caller]
fn to_internal_error(self) -> Result<T, Error>;
}
impl<T, E: std::error::Error> ToRevoltError<T> for Result<T, E> {
fn capture_error(self) -> Self {
#[allow(unused_variables)]
self.inspect_err(|e| {
#[cfg(feature = "sentry")]
sentry::capture_error(e);
})
}
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self
.capture_error()
.map_err(|_| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
}
})
}
}
impl<T: std::error::Error> ToRevoltError<T> for Option<T> {
fn capture_error(self) -> Self {
#[allow(unused_variables)]
self.inspect(|e| {
#[cfg(feature = "sentry")]
sentry::capture_error(e);
})
}
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self
.capture_error()
.ok_or_else(|| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
}
})
}
}
#[cfg(test)]
mod tests {
use crate::ErrorType;

View File

@@ -8,10 +8,9 @@ use rocket::{
use crate::{Error, ErrorType};
/// HTTP response builder for Error enum
impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
let status = match self.error_type {
impl Error {
pub fn rocket_status(&self) -> Status {
match self.error_type {
ErrorType::LabelMe => Status::InternalServerError,
ErrorType::AlreadyOnboarded => Status::Forbidden,
@@ -77,11 +76,15 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::InvalidProperty => Status::BadRequest,
ErrorType::InvalidSession => Status::Unauthorized,
ErrorType::NotAuthenticated => Status::Unauthorized,
ErrorType::Conflict => Status::Conflict,
ErrorType::DuplicateNonce => Status::Conflict,
ErrorType::VosoUnavailable => Status::BadRequest,
ErrorType::NotFound => Status::NotFound,
ErrorType::NoEffect => Status::Ok,
ErrorType::FailedValidation { .. } => Status::BadRequest,
ErrorType::IOError => Status::BadRequest,
ErrorType::UnprocessableEntity => Status::UnprocessableEntity,
ErrorType::DeserializationError { .. } => Status::UnprocessableEntity,
ErrorType::FailedValidation { .. } => Status::UnprocessableEntity,
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest,
@@ -90,8 +93,13 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::FileTypeNotAllowed => Status::BadRequest,
ErrorType::ImageProcessingFailed => Status::InternalServerError,
ErrorType::NoEmbedData => Status::BadRequest,
};
}
}
}
/// HTTP response builder for Error enum
impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
// Serialize the error data structure into JSON.
let string = serde_json::to_string(&self).unwrap();
@@ -99,7 +107,7 @@ impl<'r> Responder<'r, 'static> for Error {
Response::build()
.sized_body(string.len(), Cursor::new(string))
.header(ContentType::new("application", "json"))
.status(status)
.status(self.rocket_status())
.ok()
}
}

View File

@@ -1,19 +1,32 @@
use revolt_config::configure;
use revolt_database::DatabaseInfo;
use std::{future::Future, time::Duration};
use revolt_config::{configure, capture_error};
use revolt_database::{Database, DatabaseInfo};
use revolt_result::Result;
use tasks::{file_deletion, prune_dangling_files};
use tokio::try_join;
use tokio::{join, time::sleep};
pub mod tasks;
pub async fn cron_task_wrapper<Fut: Future<Output = Result<()>>>(func: fn(Database) -> Fut, db: Database) {
loop {
if let Err(error) = func(db.clone()).await {
log::error!("cron task failed unexpectidly: {error:?}\nRetrying after 60s");
capture_error(&error);
}
sleep(Duration::from_secs(60)).await;
}
}
#[tokio::main]
async fn main() -> Result<()> {
async fn main() {
configure!(crond);
let db = DatabaseInfo::Auto.connect().await.expect("database");
try_join!(
file_deletion::task(db.clone()),
prune_dangling_files::task(db)
)
.map(|_| ())
join!(
cron_task_wrapper(file_deletion::task, db.clone()),
cron_task_wrapper(prune_dangling_files::task, db.clone()),
);
}

View File

@@ -8,6 +8,7 @@ license = "AGPL-3.0-or-later"
revolt-result = { version = "0.8.8", path = "../../core/result" }
revolt-config = { version = "0.8.8", path = "../../core/config", features = [
"report-macros",
"anyhow"
] }
revolt-database = { version = "0.8.8", path = "../../core/database" }
revolt-models = { version = "0.8.8", path = "../../core/models", features = [

View File

@@ -123,24 +123,26 @@ impl AsyncConsumer for AckConsumer {
token: session.subscription.as_ref().unwrap().auth.clone(),
extras: Default::default(),
};
let raw_service_payload = serde_json::to_string(&service_payload);
if let Ok(p) = raw_service_payload {
let args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
match serde_json::to_string(&service_payload) {
Ok(p) => {
let args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
log::debug!(
"Publishing ack to apn session {}",
session.subscription.as_ref().unwrap().auth
);
log::debug!(
"Publishing ack to apn session {}",
session.subscription.as_ref().unwrap().auth
);
publish_message(self, p.into(), args).await;
} else {
log::warn!("Failed to serialize ack badge update payload!");
revolt_config::capture_error(&raw_service_payload.unwrap_err());
publish_message(self, p.into(), args).await;
},
Err(e) => {
log::warn!("Failed to serialize ack badge update payload!");
revolt_config::capture_error(&e);
}
}
}
}

View File

@@ -79,7 +79,7 @@ revolt-models = { path = "../core/models", features = [
"rocket",
] }
revolt-presence = { path = "../core/presence" }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-result = { path = "../core/result", features = ["rocket", "okapi", "sentry"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
[build-dependencies]

View File

@@ -135,6 +135,7 @@ pub async fn web() -> Rocket<Build> {
.manage(cors.clone())
.attach(util::ratelimiter::RatelimitFairing)
.attach(cors)
.register("/", util::catchers::all_catchers())
.configure(rocket::Config {
limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
address: Ipv4Addr::new(0, 0, 0, 0).into(),

View File

@@ -1,9 +1,8 @@
use crate::util::json::{Json, Validate};
use revolt_database::{Bot, Database, User};
use revolt_models::v0;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use revolt_result::Result;
use rocket::State;
use validator::Validate;
/// # Create Bot
///
@@ -13,14 +12,9 @@ use validator::Validate;
pub async fn create_bot(
db: &State<Database>,
user: User,
info: Json<v0::DataCreateBot>,
info: Validate<Json<v0::DataCreateBot>>,
) -> Result<Json<v0::BotWithUserResponse>> {
let info = info.into_inner();
info.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let info = info.into_inner().into_inner();
let (bot, user) = Bot::create(db, info.name, &user, None).await?;
Ok(Json(v0::BotWithUserResponse {

View File

@@ -11,7 +11,7 @@ use rocket_empty::EmptyResponse;
pub async fn delete_bot(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
) -> Result<EmptyResponse> {
let bot = target.as_bot(db).await?;
if bot.owner != user.id {

View File

@@ -3,8 +3,7 @@ use revolt_models::v0::{self, DataEditBot};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket::serde::json::Json;
use validator::Validate;
use crate::util::json::{Json, Validate};
/// # Edit Bot
///
@@ -14,15 +13,10 @@ use validator::Validate;
pub async fn edit_bot(
db: &State<Database>,
user: User,
target: Reference,
data: Json<DataEditBot>,
target: Reference<'_>,
data: Validate<Json<DataEditBot>>,
) -> Result<Json<v0::BotWithUserResponse>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
let mut bot = target.as_bot(db).await?;
if bot.owner != user.id {

View File

@@ -1,7 +1,8 @@
use revolt_database::{util::reference::Reference, Database, User};
use revolt_models::v0::FetchBotResponse;
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Fetch Bot
///
@@ -11,7 +12,7 @@ use rocket::{serde::json::Json, State};
pub async fn fetch_bot(
db: &State<Database>,
user: User,
bot: Reference,
bot: Reference<'_>,
) -> Result<Json<FetchBotResponse>> {
if user.bot.is_some() {
return Err(create_error!(IsBot));

View File

@@ -2,7 +2,7 @@ use futures::future::join_all;
use revolt_database::{Database, User};
use revolt_models::v0::OwnedBotsResponse;
use revolt_result::Result;
use rocket::serde::json::Json;
use crate::util::json::Json;
use rocket::State;
/// # Fetch Owned Bots

View File

@@ -2,7 +2,7 @@ use revolt_database::{util::reference::Reference, Database, User};
use revolt_models::v0::PublicBot;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use crate::util::json::Json;
use rocket::State;
/// # Fetch Public Bot
@@ -13,9 +13,9 @@ use rocket::State;
pub async fn fetch_public_bot(
db: &State<Database>,
user: Option<User>,
target: Reference,
target: Reference<'_>,
) -> Result<Json<PublicBot>> {
let bot = db.fetch_bot(&target.id).await?;
let bot = db.fetch_bot(target.id).await?;
if !bot.public && user.is_none_or(|x| x.id != bot.owner) {
return Err(create_error!(NotFound));
}

View File

@@ -8,7 +8,7 @@ use revolt_permissions::{
use revolt_result::{create_error, Result};
use rocket::State;
use rocket::serde::json::Json;
use crate::util::json::Json;
use rocket_empty::EmptyResponse;
/// # Invite Bot
@@ -20,7 +20,7 @@ pub async fn invite_bot(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
target: Reference<'_>,
dest: Json<v0::InviteBotDestination>,
) -> Result<EmptyResponse> {
if user.bot.is_some() {

View File

@@ -15,8 +15,8 @@ use rocket_empty::EmptyResponse;
pub async fn ack(
db: &State<Database>,
user: User,
target: Reference,
message: Reference,
target: Reference<'_>,
message: Reference<'_>,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(create_error!(IsBot));
@@ -29,7 +29,7 @@ pub async fn ack(
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
channel
.ack(&user.id, &message.id)
.ack(&user.id, message.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -17,7 +17,7 @@ pub async fn delete(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
target: Reference<'_>,
options: v0::OptionsChannelDelete,
) -> Result<EmptyResponse> {
let mut channel = target.as_channel(db).await?;

View File

@@ -5,8 +5,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use rocket::State;
use crate::util::json::{Json, Validate};
/// # Edit Channel
///
@@ -17,15 +17,10 @@ pub async fn edit(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
data: Json<v0::DataEditChannel>,
target: Reference<'_>,
data: Validate<Json<v0::DataEditChannel>>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
let mut channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);

View File

@@ -6,7 +6,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Fetch Channel
///
@@ -16,7 +17,7 @@ use rocket::{serde::json::Json, State};
pub async fn fetch(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
) -> Result<Json<v0::Channel>> {
let channel = target.as_channel(db).await?;

View File

@@ -17,8 +17,8 @@ pub async fn add_member(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
group_id: Reference,
member_id: Reference,
group_id: Reference<'_>,
member_id: Reference<'_>,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(create_error!(IsBot));

View File

@@ -2,9 +2,8 @@ use revolt_database::{Channel, Database, RelationshipStatus, User};
use revolt_models::v0;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use crate::util::json::{Json, Validate};
use rocket::State;
use validator::Validate;
/// # Create Group
///
@@ -14,18 +13,13 @@ use validator::Validate;
pub async fn create_group(
db: &State<Database>,
user: User,
data: Json<v0::DataCreateGroup>,
data: Validate<Json<v0::DataCreateGroup>>,
) -> Result<Json<v0::Channel>> {
if user.bot.is_some() {
return Err(create_error!(IsBot));
}
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
for target in &data.users {
match user.relationship_with(target) {

View File

@@ -14,8 +14,8 @@ pub async fn remove_member(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
member: Reference,
target: Reference<'_>,
member: Reference<'_>,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(create_error!(IsBot));

View File

@@ -6,7 +6,8 @@ use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Create Invite
///
@@ -18,7 +19,7 @@ use rocket::{serde::json::Json, State};
pub async fn create_invite(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
) -> Result<Json<v0::Invite>> {
if user.bot.is_some() {
return Err(create_error!(IsBot));

View File

@@ -5,7 +5,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Fetch Group Members
///
@@ -17,7 +18,7 @@ use rocket::{serde::json::Json, State};
pub async fn fetch_members(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
) -> Result<Json<Vec<v0::User>>> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);

View File

@@ -6,9 +6,9 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use rocket_empty::EmptyResponse;
use validator::Validate;
use crate::util::json::{Json, Validate};
/// # Bulk Delete Messages
///
@@ -22,15 +22,10 @@ use validator::Validate;
pub async fn bulk_delete_messages(
db: &State<Database>,
user: User,
target: Reference,
options: Json<v0::OptionsBulkDelete>,
target: Reference<'_>,
options: Validate<Json<v0::OptionsBulkDelete>>,
) -> Result<EmptyResponse> {
let options = options.into_inner();
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let options = options.into_inner().into_inner();
for id in &options.ids {
if ulid::Ulid::from_string(id)
@@ -51,7 +46,7 @@ pub async fn bulk_delete_messages(
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
Message::bulk_delete(db, &target.id, options.ids)
Message::bulk_delete(db, target.id, options.ids)
.await
.map(|_| EmptyResponse)
}

View File

@@ -17,8 +17,8 @@ use rocket_empty::EmptyResponse;
pub async fn clear_reactions(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
target: Reference<'_>,
msg: Reference<'_>,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);

View File

@@ -15,10 +15,10 @@ use rocket_empty::EmptyResponse;
pub async fn delete(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
target: Reference<'_>,
msg: Reference<'_>,
) -> Result<EmptyResponse> {
let message = msg.as_message_in_channel(db, &target.id).await?;
let message = msg.as_message_in_channel(db, target.id).await?;
if message.author != user.id {
let channel = target.as_channel(db).await?;

View File

@@ -7,8 +7,8 @@ use revolt_database::{
use revolt_models::v0::{self, Embed};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use rocket::State;
use crate::util::json::{Json, Validate};
/// # Edit Message
///
@@ -18,16 +18,11 @@ use validator::Validate;
pub async fn edit(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
edit: Json<v0::DataEditMessage>,
target: Reference<'_>,
msg: Reference<'_>,
edit: Validate<Json<v0::DataEditMessage>>,
) -> Result<Json<v0::Message>> {
let edit = edit.into_inner();
edit.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let edit = edit.into_inner().into_inner();
Message::validate_sum(
&edit.content,

View File

@@ -5,7 +5,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Fetch Message
///
@@ -15,8 +16,8 @@ use rocket::{serde::json::Json, State};
pub async fn fetch(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
target: Reference<'_>,
msg: Reference<'_>,
) -> Result<Json<v0::Message>> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);

View File

@@ -1,4 +1,7 @@
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, 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};
@@ -14,8 +17,8 @@ pub async fn message_pin(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
msg: Reference,
target: Reference<'_>,
msg: Reference<'_>,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
@@ -169,7 +172,7 @@ mod test {
})
.await;
let updated_message = Reference::from_unchecked(message.id)
let updated_message = Reference::from_unchecked(&message.id)
.as_message(&harness.db)
.await
.expect("Failed to find updated message");

View File

@@ -5,8 +5,8 @@ use revolt_database::{
use revolt_models::v0::{self, MessageSort};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use rocket::State;
use crate::util::json::{Json, Validate};
/// # Fetch Messages
///
@@ -16,15 +16,9 @@ use validator::Validate;
pub async fn query(
db: &State<Database>,
user: User,
target: Reference,
options: v0::OptionsQueryMessages,
target: Reference<'_>,
options: Validate<v0::OptionsQueryMessages>,
) -> Result<Json<v0::BulkMessageResponse>> {
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
if let Some(MessageSort::Relevance) = options.sort {
return Err(create_error!(InvalidOperation));
}
@@ -43,7 +37,7 @@ pub async fn query(
sort,
nearby,
include_users,
} = options;
} = options.into_inner();
Message::fetch_with_users(
db,

View File

@@ -15,9 +15,9 @@ use rocket_empty::EmptyResponse;
pub async fn react_message(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
emoji: Reference,
target: Reference<'_>,
msg: Reference<'_>,
emoji: Reference<'_>,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
@@ -30,7 +30,7 @@ pub async fn react_message(
// Add the reaction
message
.add_reaction(db, &user, &emoji.id)
.add_reaction(db, &user, emoji.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -5,8 +5,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use rocket::State;
use crate::util::json::{Json, Validate};
/// # Search for Messages
///
@@ -16,19 +16,14 @@ use validator::Validate;
pub async fn search(
db: &State<Database>,
user: User,
target: Reference,
options: Json<v0::DataMessageSearch>,
target: Reference<'_>,
options: Validate<Json<v0::DataMessageSearch>>,
) -> Result<Json<v0::BulkMessageResponse>> {
if user.bot.is_some() {
return Err(create_error!(IsBot));
}
let options = options.into_inner();
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let options = options.into_inner().into_inner();
if options.query.is_some() && options.pinned.is_some() {
return Err(create_error!(InvalidOperation))

View File

@@ -7,10 +7,9 @@ use revolt_database::{Interactions, Message, AMQP};
use revolt_models::v0;
use revolt_permissions::PermissionQuery;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use revolt_result::Result;
use crate::util::json::{Json, Validate};
use rocket::State;
use validator::Validate;
/// # Send Message
///
@@ -21,16 +20,11 @@ pub async fn message_send(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
data: Json<v0::DataMessageSend>,
target: Reference<'_>,
data: Validate<Json<v0::DataMessageSend>>,
idempotency: IdempotencyKey,
) -> Result<Json<v0::Message>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
// Ensure we have permissions to send a message
let channel = target.as_channel(db).await?;
@@ -202,7 +196,7 @@ mod test {
Member::create(&harness.db, &server, &user, Some(channels.clone()))
.await
.expect("Failed to create member");
let member = Reference::from_unchecked(user.id.clone())
let member = Reference::from_unchecked(&user.id)
.as_member(&harness.db, &server.id)
.await
.expect("Failed to get member");
@@ -242,7 +236,7 @@ mod test {
Member::create(&harness.db, &server, &second_user, Some(channels.clone()))
.await
.expect("Failed to create second member");
let mut second_member = Reference::from_unchecked(second_user.id.clone())
let mut second_member = Reference::from_unchecked(&second_user.id)
.as_member(&harness.db, &server.id)
.await
.expect("Failed to get second member");

View File

@@ -14,8 +14,8 @@ pub async fn message_unpin(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
msg: Reference,
target: Reference<'_>,
msg: Reference<'_>,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
@@ -91,7 +91,7 @@ mod test {
Member::create(&harness.db, &server, &user, Some(channels.clone()))
.await
.expect("Failed to create member");
let member = Reference::from_unchecked(user.id.clone())
let member = Reference::from_unchecked(&user.id)
.as_member(&harness.db, &server.id)
.await
.expect("Failed to get member");
@@ -174,7 +174,7 @@ mod test {
})
.await;
let updated_message = Reference::from_unchecked(message.id)
let updated_message = Reference::from_unchecked(&message.id)
.as_message(&harness.db)
.await
.expect("Failed to find updated message");

View File

@@ -18,9 +18,9 @@ use rocket_empty::EmptyResponse;
pub async fn unreact_message(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
emoji: Reference,
target: Reference<'_>,
msg: Reference<'_>,
emoji: Reference<'_>,
options: v0::OptionsUnreact,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
@@ -41,14 +41,14 @@ pub async fn unreact_message(
// Check if we should wipe all of this reaction
if remove_all {
return message
.clear_reaction(db, &emoji.id)
.clear_reaction(db, emoji.id)
.await
.map(|_| EmptyResponse);
}
// Remove the reaction
message
.remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), &emoji.id)
.remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), emoji.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -5,7 +5,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Set Role Permission
///
@@ -17,7 +18,7 @@ use rocket::{serde::json::Json, State};
pub async fn set_role_permissions(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
role_id: String,
data: Json<v0::DataSetRolePermissions>,
) -> Result<Json<v0::Channel>> {

View File

@@ -5,7 +5,8 @@ use revolt_database::{
use revolt_models::v0::{self, DataDefaultChannelPermissions};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Set Default Permission
///
@@ -17,7 +18,7 @@ use rocket::{serde::json::Json, State};
pub async fn set_default_channel_permissions(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
data: Json<v0::DataDefaultChannelPermissions>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();

View File

@@ -6,7 +6,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Join Call
///
@@ -16,7 +17,7 @@ use rocket::{serde::json::Json, State};
pub async fn call(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
) -> Result<Json<v0::LegacyCreateVoiceUserResponse>> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);

View File

@@ -7,9 +7,9 @@ use revolt_permissions::{
calculate_channel_permissions, ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS,
};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::{Json, Validate};
use ulid::Ulid;
use validator::Validate;
/// # Creates a webhook
///
@@ -19,15 +19,10 @@ use validator::Validate;
pub async fn create_webhook(
db: &State<Database>,
user: User,
target: Reference,
data: Json<v0::CreateWebhookBody>,
target: Reference<'_>,
data: Validate<Json<v0::CreateWebhookBody>>,
) -> Result<Json<v0::Webhook>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
let channel = target.as_channel(db).await?;

View File

@@ -5,7 +5,8 @@ use revolt_database::{
use revolt_models::v0::Webhook;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Gets all webhooks
///
@@ -15,7 +16,7 @@ use rocket::{serde::json::Json, State};
pub async fn fetch_webhooks(
db: &State<Database>,
user: User,
channel_id: Reference,
channel_id: Reference<'_>,
) -> Result<Json<Vec<Webhook>>> {
let channel = channel_id.as_channel(db).await?;

View File

@@ -3,9 +3,8 @@ use revolt_database::{util::permissions::DatabasePermissionQuery, Database, Emoj
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use validator::Validate;
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::{Json, Validate};
/// # Create New Emoji
///
@@ -16,16 +15,11 @@ pub async fn create_emoji(
db: &State<Database>,
user: User,
id: String,
data: Json<v0::DataCreateEmoji>,
data: Validate<Json<v0::DataCreateEmoji>>,
) -> Result<Json<v0::Emoji>> {
let config = config().await;
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
// Validate we have permission to write into parent
match &data.parent {

View File

@@ -16,7 +16,7 @@ use rocket_empty::EmptyResponse;
pub async fn delete_emoji(
db: &State<Database>,
user: User,
emoji_id: Reference,
emoji_id: Reference<'_>,
) -> Result<EmptyResponse> {
// Fetch the emoji
let emoji = emoji_id.as_emoji(db).await?;
@@ -26,7 +26,7 @@ pub async fn delete_emoji(
// Otherwise, validate we have permission to delete from parent
match &emoji.parent {
EmojiParent::Server { id } => {
let server = db.fetch_server(id).await?;
let server = db.fetch_server(id.as_str()).await?;
// Check for permission
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);

View File

@@ -2,14 +2,15 @@ use revolt_database::{util::reference::Reference, Database};
use revolt_models::v0;
use revolt_result::Result;
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Fetch Emoji
///
/// Fetch an emoji by its id.
#[openapi(tag = "Emojis")]
#[get("/emoji/<emoji_id>")]
pub async fn fetch_emoji(db: &State<Database>, emoji_id: Reference) -> Result<Json<v0::Emoji>> {
pub async fn fetch_emoji(db: &State<Database>, emoji_id: Reference<'_>) -> Result<Json<v0::Emoji>> {
emoji_id
.as_emoji(db)
.await

View File

@@ -12,7 +12,7 @@ use rocket_empty::EmptyResponse;
/// Delete an invite by its id.
#[openapi(tag = "Invites")]
#[delete("/<target>")]
pub async fn delete(db: &State<Database>, user: User, target: Reference) -> Result<EmptyResponse> {
pub async fn delete(db: &State<Database>, user: User, target: Reference<'_>) -> Result<EmptyResponse> {
let invite = target.as_invite(db).await?;
if user.id == invite.creator() {

View File

@@ -1,14 +1,15 @@
use revolt_database::{util::reference::Reference, Channel, Database, Invite};
use revolt_models::v0;
use revolt_result::Result;
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Fetch Invite
///
/// Fetch an invite by its id.
#[openapi(tag = "Invites")]
#[get("/<target>")]
pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::InviteResponse>> {
pub async fn fetch(db: &State<Database>, target: Reference<'_>) -> Result<Json<v0::InviteResponse>> {
Ok(Json(match target.as_invite(db).await? {
Invite::Server {
channel, creator, ..
@@ -34,7 +35,7 @@ pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::I
let server = db.fetch_server(&server).await?;
v0::InviteResponse::Server {
code: target.id,
code: target.id.to_string(),
member_count: db.fetch_member_count(&server.id).await? as i64,
server_id: server.id,
server_name: server.name,
@@ -64,7 +65,7 @@ pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::I
description,
..
} => v0::InviteResponse::Group {
code: target.id,
code: target.id.to_string(),
channel_id: id,
channel_name: name,
channel_description: description,

View File

@@ -1,7 +1,8 @@
use revolt_database::{util::reference::Reference, Channel, Database, Invite, Member, User, AMQP};
use revolt_models::v0::{self, InviteJoinResponse};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Join Invite
///
@@ -12,7 +13,7 @@ pub async fn join(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
target: Reference<'_>,
) -> Result<Json<v0::InviteJoinResponse>> {
if user.bot.is_some() {
return Err(create_error!(IsBot));

View File

@@ -1,27 +1,11 @@
use authifier::models::Session;
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_database::{Database, User};
use revolt_models::v0;
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate;
use rocket::State;
use crate::util::json::{Json, Validate};
/// Regex for valid usernames
///
/// Block zero width space
/// Block lookalike characters
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
/// # New User Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataOnboard {
/// New username which will be used to identify the user on the platform
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
username: String,
}
/// # Complete Onboarding
///
@@ -32,18 +16,13 @@ pub async fn complete(
db: &State<Database>,
session: Session,
user: Option<User>,
data: Json<DataOnboard>,
data: Validate<Json<v0::DataOnboard>>,
) -> Result<Json<v0::User>> {
if user.is_some() {
return Err(create_error!(AlreadyOnboarded));
}
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
Ok(Json(
User::create(db, data.username, session.user_id, None)

View File

@@ -1,23 +1,16 @@
use authifier::models::Session;
use revolt_database::User;
use revolt_models::v0;
use rocket::serde::json::Json;
use serde::Serialize;
/// # Onboarding Status
#[derive(Serialize, JsonSchema)]
pub struct DataHello {
/// Whether onboarding is required
onboarding: bool,
}
use crate::util::json::Json;
/// # Check Onboarding Status
///
/// This will tell you whether the current account requires onboarding or whether you can continue to send requests as usual. You may skip calling this if you're restoring an existing session.
#[openapi(tag = "Onboarding")]
#[get("/hello")]
pub async fn hello(_session: Session, user: Option<User>) -> Json<DataHello> {
Json(DataHello {
pub async fn hello(_session: Session, user: Option<User>) -> Json<v0::DataHello> {
Json(v0::DataHello {
onboarding: user.is_none(),
})
}

View File

@@ -3,7 +3,8 @@ use authifier::{
Authifier,
};
use revolt_result::{create_database_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
use rocket_empty::EmptyResponse;
/// # Push Subscribe

View File

@@ -1,6 +1,6 @@
use crate::util::json::Json;
use revolt_config::config;
use revolt_result::Result;
use rocket::serde::json::Json;
use serde::Serialize;
/// # hCaptcha Configuration

View File

@@ -1,22 +1,9 @@
use revolt_database::{events::client::EventV1, Database, Report, Snapshot, SnapshotContent, User};
use revolt_models::v0::{ReportStatus, ReportedContent};
use revolt_models::v0::{DataReportContent, ReportStatus, ReportedContent};
use revolt_result::{create_error, Result};
use serde::Deserialize;
use ulid::Ulid;
use validator::Validate;
use rocket::{serde::json::Json, State};
/// # Report Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataReportContent {
/// Content being reported
content: ReportedContent,
/// Additional report description
#[validate(length(min = 0, max = 1000))]
#[serde(default)]
additional_context: String,
}
use rocket::State;
use crate::util::json::{Json, Validate};
/// # Report Content
///
@@ -26,14 +13,9 @@ pub struct DataReportContent {
pub async fn report_content(
db: &State<Database>,
user: User,
data: Json<DataReportContent>,
data: Validate<Json<DataReportContent>>,
) -> Result<()> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
// Bots cannot create reports
if user.bot.is_some() {

View File

@@ -6,8 +6,8 @@ use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use rocket::State;
use crate::util::json::{Json, Validate};
/// # Ban User
///
@@ -17,16 +17,11 @@ use validator::Validate;
pub async fn ban(
db: &State<Database>,
user: User,
server: Reference,
target: Reference,
data: Json<v0::DataBanCreate>,
server: Reference<'_>,
target: Reference<'_>,
data: Validate<Json<v0::DataBanCreate>>,
) -> Result<Json<v0::ServerBan>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
let server = server.as_server(db).await?;
@@ -56,7 +51,7 @@ pub async fn ban(
.await?;
}
ServerBan::create(db, &server, &target.id, data.reason)
ServerBan::create(db, &server, target.id, data.reason)
.await
.map(Into::into)
.map(Json)

View File

@@ -6,7 +6,7 @@ use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::serde::json::Json;
use crate::util::json::Json;
use rocket::State;
/// # Fetch Bans
@@ -17,7 +17,7 @@ use rocket::State;
pub async fn list(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
) -> Result<Json<v0::BanListResult>> {
let server = target.as_server(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);

Some files were not shown because too many files have changed in this diff Show More