feat: implement discover endpoints (#940)

* feat: implement discover endpoints

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
Release-As: 0.15.2
This commit is contained in:
Tom
2026-08-24 10:57:44 -07:00
committed by IAmTomahawkx
parent 8e2eacb1b0
commit 793061685d
25 changed files with 703 additions and 25 deletions

View File

@@ -3,9 +3,10 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
Account, AccountInvite, AuditLogEntry, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji,
File, FileHash, Invite, MFATicket, Member, MemberCompositeKey, Message, PolicyChange,
RatelimitEvent, Report, Server, ServerBan, Session, Snapshot, User, UserSettings, Webhook,
Account, AccountInvite, AuditLogEntry, Bot, Channel, ChannelCompositeKey, ChannelUnread,
DiscoverBan, DiscoverRequest, DiscoverRequestType, Emoji, File, FileHash, Invite, MFATicket,
Member, MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan,
Session, Snapshot, User, UserSettings, Webhook,
};
database_derived!(
@@ -19,6 +20,8 @@ database_derived!(
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
pub discover_requests: Arc<Mutex<HashMap<(DiscoverRequestType, String), DiscoverRequest>>>,
pub discover_bans: Arc<Mutex<HashMap<String, DiscoverBan>>>,
pub file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,

View File

@@ -113,6 +113,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create mfa_tickets collection.");
db.create_collection("discover_requests")
.await
.expect("Failed to create discover_requests collection");
db.run_command(doc! {
"createIndexes": "users",
"indexes": [
@@ -414,5 +418,20 @@ pub async fn create_database(db: &MongoDb) {
.await
.unwrap();
db.run_command(doc! {
"createIndexes": "discover_requests",
"indexes": [
{
"key": {
"request_type": 1,
"request_id": 1
},
"name": "request_type_id"
}
]
})
.await
.expect("Failed to create discover_requests index");
info!("Created database.");
}

View File

@@ -26,7 +26,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 52; // MUST BE +1 to last migration
pub const LATEST_REVISION: i32 = 53; // MUST BE +1 to last migration
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -1299,7 +1299,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
for session in sessions {
let timestamp = iso8601_timestamp::Timestamp::from(Ulid::from_string(&session._id).unwrap().datetime());
let timestamp = iso8601_timestamp::Timestamp::from(
Ulid::from_string(&session._id).unwrap().datetime(),
);
db.db()
.collection::<Document>("sessions")
@@ -1478,14 +1480,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 50 {
info!("Running migration [revision 50 / 13-04-2026]: Rename invites collection to account_invites");
let result = db.db()
let result = db
.db()
.client()
.database("admin")
.run_command(doc! {
"renameCollection": "revolt.invites",
"to": "revolt.account_invites",
"dropTarget": true
})
"renameCollection": "revolt.invites",
"to": "revolt.account_invites",
"dropTarget": true
})
.await;
if let Err(e) = result {
@@ -1496,7 +1499,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
}
if revision >= 51 {
if revision <= 51 {
info!("Running migration [revision 51 / 28-11-2025]: Add audit logs collection");
db.db()
@@ -1529,6 +1532,35 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to create audit_logs index");
};
if revision <= 52 {
let config = revolt_config::config().await;
if config.production {
info!("Running migration [revision 52 / 20-08-2026]: Discover endpoints");
db.db()
.create_collection("discover_requests")
.await
.expect("Failed to create discover_requests collection");
db.db()
.run_command(doc! {
"createIndexes": "discover_requests",
"indexes": [
{
"key": {
"request_type": 1,
"request_id": 1
},
"name": "request_type_id"
}
]
})
.await
.expect("Failed to create index");
} else {
info!("Skipping migration [revision 52 / 20-08-2026]: Discover endpoints");
}
}
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision)
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,36 @@
auto_derived!(
#[derive(Hash)]
pub enum DiscoverRequestType {
Bot,
Server,
}
pub enum DiscoverRequestStatus {
Pending,
UnderReview,
Denied(Option<String>), // reason
Approved(Option<String>), // reason
}
/// Discover request
pub struct DiscoverRequest {
/// The type of request.
#[serde(rename = "type")]
pub request_type: DiscoverRequestType,
/// The ID of the bot/server
pub request_id: String,
/// status of the request
pub status: DiscoverRequestStatus,
}
pub struct DiscoverBan {
/// Ban Id
#[serde(rename = "_id")]
pub id: String,
/// The type of item.
#[serde(rename = "type")]
pub item_type: DiscoverRequestType,
/// The ID of the bot/server
pub item_id: String,
}
);

View File

@@ -0,0 +1,35 @@
use revolt_result::Result;
use crate::{DiscoverRequest, DiscoverRequestType};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractDiscoverRequest: Sync + Send {
/// Insert discover request into database.
/// Update an existing one if it was previously denied
async fn insert_discover_request(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<DiscoverRequest>;
/// Fetch Discover request by their parent id
async fn fetch_discover_request_by_item_id(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<DiscoverRequest>;
/// Remove Discover request
async fn delete_discover_request(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<()>;
/// Fetch if the item is banned from being requested. If the item is Some, then the item is banned.
async fn get_discover_ban(&self, item_type: DiscoverRequestType, item: &str) -> Result<bool>;
}

View File

@@ -0,0 +1,90 @@
use revolt_result::Result;
use crate::DiscoverBan;
use crate::DiscoverRequest;
use crate::DiscoverRequestStatus;
use crate::DiscoverRequestType;
use crate::MongoDb;
use super::AbstractDiscoverRequest;
static DISCOVER_COL: &str = "discover_requests";
static DISCOVER_BANS_COL: &str = "discover_bans";
#[async_trait]
impl AbstractDiscoverRequest for MongoDb {
/// Insert request into database.
async fn insert_discover_request(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<DiscoverRequest> {
if let Ok(mut prev) = self
.fetch_discover_request_by_item_id(request_type.clone(), item)
.await
{
match prev.status {
DiscoverRequestStatus::Approved(_)
| DiscoverRequestStatus::Pending
| DiscoverRequestStatus::UnderReview => return Err(create_error!(NoEffect)),
_ => Ok(()),
}?;
self.col::<DiscoverRequest>(DISCOVER_COL).update_one(
doc! {"request_type": bson::to_bson(&request_type).expect("failed to serialize"), "request_id": item},
doc! {"$set": {"status": bson::to_bson(&DiscoverRequestStatus::Pending).expect("failed to serialize")}},
).await.map_err(|_| create_database_error!("update_one", DISCOVER_COL))?;
prev.status = DiscoverRequestStatus::Pending;
Ok(prev)
} else {
let ret = DiscoverRequest {
request_type,
request_id: item.to_string(),
status: DiscoverRequestStatus::Pending,
};
self.col::<DiscoverRequest>(DISCOVER_COL)
.insert_one(ret.clone())
.await
.map_err(|_| create_database_error!("insert_one", DISCOVER_COL))?;
Ok(ret)
}
}
/// Fetch discover by item type/id combo
async fn fetch_discover_request_by_item_id(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<DiscoverRequest> {
query!(
self,
find_one,
DISCOVER_COL,
doc! {"request_type": bson::to_bson(&request_type).expect("failed to serialize"), "request_id": item}
)?.ok_or_else(|| create_database_error!("find_one", DISCOVER_COL))
}
/// Remove discover request
async fn delete_discover_request(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<()> {
query!(
self,
delete_one,
DISCOVER_COL,
doc! {"request_type": bson::to_bson(&request_type).expect("failed to serialize"), "request_id": item}
).map(|_| ())
}
/// Fetch if the item is banned from being requested
async fn get_discover_ban(&self, item_type: DiscoverRequestType, item: &str) -> Result<bool> {
query!(
self,
find_one,
DISCOVER_BANS_COL,
doc! {"request_type": bson::to_bson(&item_type).expect("failed to serialize"), "request_id": item}
)?.ok_or_else(|| create_database_error!("find_one", DISCOVER_COL)).map(|_: DiscoverBan| true)
}
}

View File

@@ -0,0 +1,77 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{DiscoverRequest, DiscoverRequestStatus, DiscoverRequestType};
use super::AbstractDiscoverRequest;
#[async_trait]
impl AbstractDiscoverRequest for ReferenceDb {
/// Insert request into database.
async fn insert_discover_request(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<DiscoverRequest> {
let ret = DiscoverRequest {
request_type: request_type.clone(),
request_id: item.to_string(),
status: DiscoverRequestStatus::Pending,
};
let mut discover = self.discover_requests.lock().await;
if let std::collections::hash_map::Entry::Vacant(e) =
discover.entry((request_type, item.to_string()))
{
e.insert(ret.clone());
Ok(ret)
} else {
Err(create_database_error!("insert", "discover_requests"))
}
}
/// Fetch discover by item type/id combo
async fn fetch_discover_request_by_item_id(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<DiscoverRequest> {
let discover = self.discover_requests.lock().await;
discover
.iter()
.find(|(_, d)| d.request_id == item && d.request_type == request_type)
.map(|(_, d)| d.clone())
.ok_or_else(|| create_error!(NotFound))
}
/// Remove discover request
async fn delete_discover_request(
&self,
request_type: DiscoverRequestType,
item: &str,
) -> Result<()> {
let discover = self.discover_requests.lock().await;
let req = discover
.iter()
.find(|(_, d)| d.request_id == item && d.request_type == request_type)
.map(|(id, _)| id);
if let Some(req) = req {
let mut discover = self.discover_requests.lock().await;
discover.remove_entry(req);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Fetch if the item is banned from being requested
async fn get_discover_ban(&self, item_type: DiscoverRequestType, item: &str) -> Result<bool> {
let discover = self.discover_bans.lock().await;
discover
.iter()
.find(|(_, d)| d.item_id == item && d.item_type == item_type)
.map(|(_, _)| true)
.ok_or_else(|| create_error!(NotFound))
}
}

View File

@@ -1,3 +1,5 @@
mod account_invites;
mod accounts;
mod admin_migrations;
mod audit_logs;
mod bots;
@@ -5,10 +7,12 @@ mod channel_invites;
mod channel_unreads;
mod channel_webhooks;
mod channels;
mod discover_requests;
mod emojis;
mod file_hashes;
mod files;
mod messages;
mod mfa_tickets;
mod policy_changes;
mod ratelimit_events;
mod safety_reports;
@@ -16,13 +20,12 @@ mod safety_snapshots;
mod server_bans;
mod server_members;
mod servers;
mod sessions;
mod user_settings;
mod users;
mod accounts;
mod account_invites;
mod sessions;
mod mfa_tickets;
pub use account_invites::*;
pub use accounts::*;
pub use admin_migrations::*;
pub use audit_logs::*;
pub use bots::*;
@@ -30,10 +33,12 @@ pub use channel_invites::*;
pub use channel_unreads::*;
pub use channel_webhooks::*;
pub use channels::*;
pub use discover_requests::*;
pub use emojis::*;
pub use file_hashes::*;
pub use files::*;
pub use messages::*;
pub use mfa_tickets::*;
pub use policy_changes::*;
pub use ratelimit_events::*;
pub use safety_reports::*;
@@ -41,12 +46,9 @@ pub use safety_snapshots::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
pub use sessions::*;
pub use user_settings::*;
pub use users::*;
pub use accounts::*;
pub use account_invites::*;
pub use sessions::*;
pub use mfa_tickets::*;
use crate::{Database, ReferenceDb};
@@ -80,6 +82,7 @@ pub trait AbstractDatabase:
+ account_invites::AbstractAccountInvites
+ sessions::AbstractSessions
+ mfa_tickets::AbstractMFATickets
+ discover_requests::AbstractDiscoverRequest
{
}

View File

@@ -102,6 +102,46 @@ impl From<crate::ChannelCompositeKey> for ChannelCompositeKey {
}
}
impl From<crate::DiscoverBan> for DiscoverBan {
fn from(value: crate::DiscoverBan) -> Self {
DiscoverBan {
id: value.id,
item_type: value.item_type.into(),
item_id: value.item_id,
}
}
}
impl From<crate::DiscoverRequest> for DiscoverRequest {
fn from(value: crate::DiscoverRequest) -> Self {
DiscoverRequest {
request_type: value.request_type.into(),
request_id: value.request_id,
status: value.status.into(),
}
}
}
impl From<crate::DiscoverRequestType> for DiscoverRequestType {
fn from(value: crate::DiscoverRequestType) -> Self {
match value {
crate::DiscoverRequestType::Bot => DiscoverRequestType::Bot,
crate::DiscoverRequestType::Server => DiscoverRequestType::Server,
}
}
}
impl From<crate::DiscoverRequestStatus> for DiscoverRequestStatus {
fn from(value: crate::DiscoverRequestStatus) -> Self {
match value {
crate::DiscoverRequestStatus::Approved(s) => DiscoverRequestStatus::Approved(s),
crate::DiscoverRequestStatus::Denied(s) => DiscoverRequestStatus::Denied(s),
crate::DiscoverRequestStatus::Pending => DiscoverRequestStatus::Pending,
crate::DiscoverRequestStatus::UnderReview => DiscoverRequestStatus::UnderReview,
}
}
}
impl From<crate::Webhook> for Webhook {
fn from(value: crate::Webhook) -> Self {
Webhook {