Merge branch 'main' into feat/admin-api
# Conflicts: # crates/core/database/src/util/bridge/v0.rs # crates/core/result/src/axum.rs # crates/core/result/src/lib.rs # crates/core/result/src/rocket.rs
This commit is contained in:
@@ -25,6 +25,7 @@ use amqprs::{
|
||||
use async_std::channel::unbounded;
|
||||
use authifier::AuthifierEvent;
|
||||
use rocket::data::ToByteUnit;
|
||||
use revolt_database::voice::VoiceClient;
|
||||
|
||||
pub async fn web() -> Rocket<Build> {
|
||||
// Get settings
|
||||
@@ -35,6 +36,7 @@ pub async fn web() -> Rocket<Build> {
|
||||
|
||||
// Setup database
|
||||
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
|
||||
log::info!("database_here {db:?}");
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Setup Authifier event channel
|
||||
@@ -68,6 +70,15 @@ pub async fn web() -> Rocket<Build> {
|
||||
.iter()
|
||||
.map(|s| FromStr::from_str(s).unwrap())
|
||||
.collect(),
|
||||
expose_headers: [
|
||||
"X-Ratelimit-Limit",
|
||||
"X-Ratelimit-Bucket",
|
||||
"X-Ratelimit-Remaining",
|
||||
"X-Ratelimit-Reset-After",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
.to_cors()
|
||||
@@ -90,6 +101,16 @@ pub async fn web() -> Rocket<Build> {
|
||||
)
|
||||
.into();
|
||||
|
||||
let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui(
|
||||
&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig {
|
||||
url: "/0.8/openapi.json".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.into();
|
||||
|
||||
// Voice handler
|
||||
let voice_client = VoiceClient::new(config.api.livekit.nodes.clone());
|
||||
// Configure Rabbit
|
||||
let connection = Connection::open(&OpenConnectionArguments::new(
|
||||
&config.rabbit.host,
|
||||
@@ -137,6 +158,7 @@ pub async fn web() -> Rocket<Build> {
|
||||
.manage(db)
|
||||
.manage(amqp)
|
||||
.manage(cors.clone())
|
||||
.manage(voice_client)
|
||||
.manage(ratelimits)
|
||||
.attach(ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use revolt_database::{util::reference::Reference, Database, User};
|
||||
use revolt_database::{
|
||||
util::reference::Reference,
|
||||
voice::{remove_user_from_voice_channels, VoiceClient},
|
||||
Database, User,
|
||||
};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
@@ -7,18 +11,23 @@ use rocket_empty::EmptyResponse;
|
||||
///
|
||||
/// Delete a bot by its id.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[delete("/<target>")]
|
||||
#[delete("/<bot_id>")]
|
||||
pub async fn delete_bot(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
bot_id: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let bot = target.as_bot(db).await?;
|
||||
let bot = bot_id.as_bot(db).await?;
|
||||
if bot.owner != user.id {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
bot.delete(db).await.map(|_| EmptyResponse)
|
||||
bot.delete(db).await?;
|
||||
|
||||
remove_user_from_voice_channels(voice_client, &bot.id).await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -10,11 +10,11 @@ use validator::Validate;
|
||||
///
|
||||
/// Edit bot details by its id.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[patch("/<target>", data = "<data>")]
|
||||
#[patch("/<bot_id>", data = "<data>")]
|
||||
pub async fn edit_bot(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
bot_id: Reference<'_>,
|
||||
data: Json<DataEditBot>,
|
||||
) -> Result<Json<v0::BotWithUserResponse>> {
|
||||
let data = data.into_inner();
|
||||
@@ -24,7 +24,7 @@ pub async fn edit_bot(
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut bot = target.as_bot(db).await?;
|
||||
let mut bot = bot_id.as_bot(db).await?;
|
||||
if bot.owner != user.id {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
@@ -7,17 +7,17 @@ use rocket::{serde::json::Json, State};
|
||||
///
|
||||
/// Fetch details of a bot you own by its id.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[get("/<bot>")]
|
||||
#[get("/<bot_id>")]
|
||||
pub async fn fetch_bot(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
bot: Reference<'_>,
|
||||
bot_id: Reference<'_>,
|
||||
) -> Result<Json<FetchBotResponse>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let bot = bot.as_bot(db).await?;
|
||||
let bot = bot_id.as_bot(db).await?;
|
||||
if bot.owner != user.id {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
voice::{
|
||||
delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel,
|
||||
UserVoiceChannel, VoiceClient,
|
||||
},
|
||||
Channel, Database, PartialChannel, User, AMQP,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
@@ -15,6 +19,7 @@ use rocket_empty::EmptyResponse;
|
||||
#[delete("/<target>?<options..>")]
|
||||
pub async fn delete(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
@@ -26,34 +31,47 @@ pub async fn delete(
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||
|
||||
#[allow(deprecated)]
|
||||
match &channel {
|
||||
Channel::SavedMessages { .. } => Err(create_error!(NoEffect)),
|
||||
Channel::DirectMessage { .. } => channel
|
||||
.update(
|
||||
db,
|
||||
PartialChannel {
|
||||
active: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::Group { .. } => channel
|
||||
.remove_user_from_group(
|
||||
db,
|
||||
amqp,
|
||||
&user,
|
||||
None,
|
||||
options.leave_silently.unwrap_or_default(),
|
||||
)
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
||||
channel.delete(db).await.map(|_| EmptyResponse)
|
||||
Channel::SavedMessages { .. } => Err(create_error!(NoEffect))?,
|
||||
Channel::DirectMessage { .. } => {
|
||||
channel
|
||||
.update(
|
||||
db,
|
||||
PartialChannel {
|
||||
active: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
Channel::Group { .. } => {
|
||||
channel
|
||||
.remove_user_from_group(
|
||||
db,
|
||||
amqp,
|
||||
&user,
|
||||
None,
|
||||
options.leave_silently.unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user_voice_channel = UserVoiceChannel::from_channel(&channel);
|
||||
|
||||
if is_in_voice_channel(&user.id, &user_voice_channel).await? {
|
||||
remove_user_from_voice_channel(voice_client, &user_voice_channel, &user.id).await?;
|
||||
};
|
||||
}
|
||||
Channel::TextChannel { .. } => {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
||||
channel.delete(db).await?;
|
||||
|
||||
delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
voice::{delete_voice_channel, UserVoiceChannel, VoiceClient},
|
||||
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
@@ -15,6 +16,7 @@ use validator::Validate;
|
||||
#[patch("/<target>", data = "<data>")]
|
||||
pub async fn edit(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
@@ -38,6 +40,8 @@ pub async fn edit(
|
||||
&& data.icon.is_none()
|
||||
&& data.nsfw.is_none()
|
||||
&& data.owner.is_none()
|
||||
&& data.voice.is_none()
|
||||
&& data.slowmode.is_none()
|
||||
&& data.remove.is_empty()
|
||||
{
|
||||
return Ok(Json(channel.into()));
|
||||
@@ -95,22 +99,6 @@ pub async fn edit(
|
||||
icon,
|
||||
nsfw,
|
||||
..
|
||||
}
|
||||
| Channel::TextChannel {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
..
|
||||
} => {
|
||||
if data.remove.contains(&v0::FieldsChannel::Icon) {
|
||||
if let Some(icon) = &icon {
|
||||
@@ -151,73 +139,136 @@ pub async fn edit(
|
||||
}
|
||||
|
||||
// Send out mutation system messages.
|
||||
if let Channel::Group { .. } = &channel {
|
||||
if let Some(name) = &partial.name {
|
||||
SystemMessage::ChannelRenamed {
|
||||
name: name.to_string(),
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
user.as_author_for_system(),
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
if let Some(name) = &partial.name {
|
||||
SystemMessage::ChannelRenamed {
|
||||
name: name.to_string(),
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
user.as_author_for_system(),
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if partial.description.is_some() {
|
||||
SystemMessage::ChannelDescriptionChanged {
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
user.as_author_for_system(),
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
if partial.description.is_some() {
|
||||
SystemMessage::ChannelDescriptionChanged {
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
user.as_author_for_system(),
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if partial.icon.is_some() {
|
||||
SystemMessage::ChannelIconChanged {
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
user.as_author_for_system(),
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
if partial.icon.is_some() {
|
||||
SystemMessage::ChannelIconChanged {
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
user.as_author_for_system(),
|
||||
None,
|
||||
None,
|
||||
&channel,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Channel::TextChannel {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
voice,
|
||||
slowmode,
|
||||
..
|
||||
} => {
|
||||
if data.remove.contains(&v0::FieldsChannel::Icon) {
|
||||
if let Some(icon) = &icon {
|
||||
db.mark_attachment_as_deleted(&icon.id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
channel
|
||||
.update(
|
||||
db,
|
||||
partial,
|
||||
data.remove.into_iter().map(|f| f.into()).collect(),
|
||||
)
|
||||
.await?;
|
||||
for field in &data.remove {
|
||||
match field {
|
||||
v0::FieldsChannel::Description => {
|
||||
description.take();
|
||||
}
|
||||
v0::FieldsChannel::Icon => {
|
||||
icon.take();
|
||||
}
|
||||
v0::FieldsChannel::Voice => {
|
||||
voice.take();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon_id) = data.icon {
|
||||
partial.icon = Some(File::use_channel_icon(db, &icon_id, id, &user.id).await?);
|
||||
*icon = partial.icon.clone();
|
||||
}
|
||||
|
||||
if let Some(new_name) = data.name {
|
||||
*name = new_name.clone();
|
||||
partial.name = Some(new_name);
|
||||
}
|
||||
|
||||
if let Some(new_description) = data.description {
|
||||
partial.description = Some(new_description);
|
||||
*description = partial.description.clone();
|
||||
}
|
||||
|
||||
if let Some(new_nsfw) = data.nsfw {
|
||||
*nsfw = new_nsfw;
|
||||
partial.nsfw = Some(new_nsfw);
|
||||
}
|
||||
|
||||
if let Some(new_voice) = data.voice {
|
||||
*voice = Some(new_voice.clone().into());
|
||||
partial.voice = Some(new_voice.into());
|
||||
}
|
||||
|
||||
if let Some(new_slowmode) = data.slowmode {
|
||||
*slowmode = Some(new_slowmode);
|
||||
partial.slowmode = Some(new_slowmode);
|
||||
}
|
||||
}
|
||||
_ => return Err(create_error!(InvalidOperation)),
|
||||
};
|
||||
|
||||
channel
|
||||
.update(
|
||||
db,
|
||||
partial,
|
||||
data.remove.into_iter().map(|f| f.into()).collect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if channel.voice().is_none() {
|
||||
delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
|
||||
}
|
||||
|
||||
Ok(Json(channel.into()))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use revolt_database::{util::reference::Reference, Channel, Database, User, AMQP};
|
||||
use revolt_database::{
|
||||
AMQP, Channel, Database, User, util::reference::Reference, voice::{UserVoiceChannel, VoiceClient, is_in_voice_channel, remove_user_from_voice_channel}
|
||||
};
|
||||
use revolt_permissions::ChannelPermission;
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
@@ -9,46 +11,54 @@ use rocket_empty::EmptyResponse;
|
||||
///
|
||||
/// Removes a user from the group.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[delete("/<target>/recipients/<member>")]
|
||||
#[delete("/<group_id>/recipients/<member_id>")]
|
||||
pub async fn remove_member(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
member: Reference<'_>,
|
||||
group_id: Reference<'_>,
|
||||
member_id: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
let channel = group_id.as_channel(db).await?;
|
||||
|
||||
match &channel {
|
||||
Channel::Group {
|
||||
owner, recipients, ..
|
||||
} => {
|
||||
if &user.id != owner {
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: ChannelPermission::ManageChannel.to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
let member = member.as_user(db).await?;
|
||||
if user.id == member.id {
|
||||
return Err(create_error!(CannotRemoveYourself));
|
||||
}
|
||||
|
||||
if !recipients.iter().any(|x| *x == member.id) {
|
||||
return Err(create_error!(NotInGroup));
|
||||
}
|
||||
|
||||
channel
|
||||
.remove_user_from_group(db, amqp, &member, Some(&user.id), false)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
if let Channel::Group {
|
||||
owner, recipients, ..
|
||||
} = &channel
|
||||
{
|
||||
if &user.id != owner {
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: ChannelPermission::ManageChannel.to_string()
|
||||
}));
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
|
||||
let member = member_id.as_user(db).await?;
|
||||
if user.id == member.id {
|
||||
return Err(create_error!(CannotRemoveYourself));
|
||||
}
|
||||
|
||||
if !recipients.contains(&member.id) {
|
||||
return Err(create_error!(NotInGroup));
|
||||
}
|
||||
|
||||
channel
|
||||
.remove_user_from_group(db, amqp, &member, Some(&user.id), false)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
};
|
||||
|
||||
let user_voice_channel = UserVoiceChannel::from_channel(&channel);
|
||||
|
||||
if is_in_voice_channel(member_id.id, &user_voice_channel).await? {
|
||||
remove_user_from_voice_channel(voice_client, &user_voice_channel, member_id.id).await?;
|
||||
};
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
|
||||
Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
|
||||
};
|
||||
use revolt_models::v0::{self, MessageSort};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -65,12 +65,7 @@ pub async fn query(
|
||||
},
|
||||
&user,
|
||||
include_users,
|
||||
match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
channel.server(),
|
||||
)
|
||||
.await
|
||||
.map(Json)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
|
||||
Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -31,7 +31,7 @@ pub async fn search(
|
||||
})?;
|
||||
|
||||
if options.query.is_some() && options.pinned.is_some() {
|
||||
return Err(create_error!(InvalidOperation))
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
@@ -69,12 +69,7 @@ pub async fn search(
|
||||
},
|
||||
&user,
|
||||
include_users,
|
||||
match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
channel.server(),
|
||||
)
|
||||
.await
|
||||
.map(Json)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use redis_kiss::{get_connection, redis, AsyncCommands};
|
||||
use revolt_database::util::permissions::DatabasePermissionQuery;
|
||||
use revolt_database::{
|
||||
util::idempotency::IdempotencyKey, util::reference::Reference, Database, User,
|
||||
};
|
||||
use revolt_database::{Interactions, Message, AMQP};
|
||||
use revolt_database::{Channel, Interactions, Message, AMQP};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::PermissionQuery;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -57,6 +58,50 @@ pub async fn message_send(
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::UploadFiles)?;
|
||||
}
|
||||
|
||||
if !permissions.has_channel_permission(ChannelPermission::BypassSlowmode) {
|
||||
if let Channel::TextChannel {
|
||||
slowmode: Some(channel_slowmode),
|
||||
id: channel_id,
|
||||
..
|
||||
} = &channel
|
||||
{
|
||||
if *channel_slowmode > 0 {
|
||||
if let Ok(conn) = get_connection().await {
|
||||
let mut conn = conn.into_inner();
|
||||
|
||||
let slowmode_key = format!("slowmode:{}:{}", user.id, channel_id);
|
||||
|
||||
// Atomic check-and-set: only set if absent and apply expiry in one command.
|
||||
let set_result: Option<String> = conn
|
||||
.set_options(
|
||||
&slowmode_key,
|
||||
"1", // The value doesn't matter, only the key's existence
|
||||
redis::SetOptions::default()
|
||||
.conditional_set(redis::ExistenceCheck::NX)
|
||||
.with_expiration(redis::SetExpiry::EX(*channel_slowmode as usize)),
|
||||
)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
// If `set_result` is None, the `NX` condition failed because the key already exists.
|
||||
// This means the user is currently in slowmode.
|
||||
if set_result.is_none() {
|
||||
// Fetch the remaining TTL to accurately populate the retry_after field
|
||||
let ttl: i64 = conn.ttl(&slowmode_key).await.unwrap_or(0);
|
||||
|
||||
// Redis returns positive integers for valid TTLs
|
||||
if ttl > 0 {
|
||||
return Err(create_error!(InSlowmode {
|
||||
retry_after: ttl as u64
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
// If Redis connection fails, just skip the slowmode check
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure interactions information is correct
|
||||
if let Some(interactions) = &data.interactions {
|
||||
let interactions: Interactions = interactions.clone().into();
|
||||
@@ -151,28 +196,20 @@ mod test {
|
||||
name: "Hidden Channel".to_string(),
|
||||
description: None,
|
||||
nsfw: Some(false),
|
||||
voice: None,
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to make new channel");
|
||||
|
||||
let role = Role {
|
||||
name: "Show Hidden Channel".to_string(),
|
||||
permissions: OverrideField { a: 0, d: 0 },
|
||||
colour: None,
|
||||
hoist: false,
|
||||
rank: 5,
|
||||
};
|
||||
|
||||
let role_id = role
|
||||
.create(&harness.db, &server.id)
|
||||
let role = Role::create(&harness.db, &server, "Show Hidden Channel".to_string())
|
||||
.await
|
||||
.expect("Failed to create the role");
|
||||
|
||||
let mut overrides = HashMap::new();
|
||||
overrides.insert(
|
||||
role_id.clone(),
|
||||
role.id.clone(),
|
||||
OverrideField {
|
||||
a: (ChannelPermission::ViewChannel) as i64,
|
||||
d: 0,
|
||||
@@ -193,6 +230,8 @@ mod test {
|
||||
d: ChannelPermission::ViewChannel as i64,
|
||||
}),
|
||||
last_message_id: None,
|
||||
voice: None,
|
||||
slowmode: None,
|
||||
};
|
||||
locked_channel
|
||||
.update(&harness.db, partial, vec![])
|
||||
@@ -279,7 +318,7 @@ mod test {
|
||||
"Mention failed to be scrubbed when the user cannot see the channel"
|
||||
);
|
||||
|
||||
let second_member_roles = vec![role_id.clone()];
|
||||
let second_member_roles = vec![role.id.clone()];
|
||||
let partial = PartialMember {
|
||||
id: None,
|
||||
joined_at: None,
|
||||
@@ -287,6 +326,8 @@ mod test {
|
||||
avatar: None,
|
||||
timeout: None,
|
||||
roles: Some(second_member_roles),
|
||||
can_publish: None,
|
||||
can_receive: None,
|
||||
};
|
||||
second_member
|
||||
.update(&harness.db, partial, vec![])
|
||||
@@ -492,7 +533,7 @@ mod test {
|
||||
let (_, _, other_user) = harness.new_user().await;
|
||||
let (server, _) = harness.new_server(&user).await;
|
||||
let channel = harness.new_channel(&server).await;
|
||||
let (role_id, _role) = harness
|
||||
let role = harness
|
||||
.new_role(
|
||||
&server,
|
||||
1,
|
||||
@@ -514,7 +555,7 @@ mod test {
|
||||
Some(&harness.amqp),
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content: Some(format!("Mentioning @everyone and role <%{}>", &role_id)),
|
||||
content: Some(format!("Mentioning @everyone and role <%{}>", &role.id)),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: None,
|
||||
@@ -559,7 +600,7 @@ mod test {
|
||||
Some(&harness.amqp),
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content: Some(format!("Mentioning `@everyone` and role `<%{}>`", &role_id)),
|
||||
content: Some(format!("Mentioning `@everyone` and role `<%{}>`", &role.id)),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: None,
|
||||
@@ -601,7 +642,7 @@ mod test {
|
||||
"Role mentions detected when inside codeblock"
|
||||
);
|
||||
|
||||
other_member.roles.push(role_id.clone());
|
||||
other_member.roles.push(role.id.clone());
|
||||
harness
|
||||
.db
|
||||
.update_member(
|
||||
@@ -611,8 +652,10 @@ mod test {
|
||||
id: None,
|
||||
joined_at: None,
|
||||
nickname: None,
|
||||
roles: Some(vec![role_id.clone()]),
|
||||
roles: Some(vec![role.id.clone()]),
|
||||
timeout: None,
|
||||
can_publish: None,
|
||||
can_receive: None,
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
@@ -626,7 +669,7 @@ mod test {
|
||||
Some(&harness.amqp),
|
||||
channel.clone(),
|
||||
v0::DataMessageSend {
|
||||
content: Some(format!("Mentioning @everyone and role <%{}>", &role_id)),
|
||||
content: Some(format!("Mentioning @everyone and role <%{}>", &role.id)),
|
||||
nonce: None,
|
||||
attachments: None,
|
||||
replies: None,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, 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};
|
||||
|
||||
@@ -25,6 +25,7 @@ mod message_unreact;
|
||||
mod permissions_set;
|
||||
mod permissions_set_default;
|
||||
mod voice_join;
|
||||
mod voice_stop_ring;
|
||||
mod webhook_create;
|
||||
mod webhook_fetch_all;
|
||||
|
||||
@@ -49,6 +50,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
group_add_member::add_member,
|
||||
group_remove_member::remove_member,
|
||||
voice_join::call,
|
||||
voice_stop_ring::stop_ring,
|
||||
permissions_set::set_role_permissions,
|
||||
permissions_set_default::set_default_channel_permissions,
|
||||
message_react::react_message,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, User,
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, User
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override};
|
||||
@@ -11,19 +10,20 @@ use rocket::{serde::json::Json, State};
|
||||
///
|
||||
/// Sets permissions for the specified role in this channel.
|
||||
///
|
||||
/// Channel must be a `TextChannel` or `VoiceChannel`.
|
||||
/// Channel must be a `TextChannel`.
|
||||
#[openapi(tag = "Channel Permissions")]
|
||||
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
|
||||
pub async fn set_role_permissions(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
role_id: String,
|
||||
data: Json<v0::DataSetRolePermissions>,
|
||||
) -> Result<Json<v0::Channel>> {
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
let permissions = calculate_channel_permissions(&mut query).await;
|
||||
let permissions: revolt_permissions::PermissionValue = calculate_channel_permissions(&mut query).await;
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?;
|
||||
|
||||
@@ -38,11 +38,15 @@ pub async fn set_role_permissions(
|
||||
.throw_permission_override(current_value, &data.permissions)
|
||||
.await?;
|
||||
|
||||
channel
|
||||
let mut new_channel = channel.clone();
|
||||
|
||||
new_channel
|
||||
.set_role_permission(db, &role_id, data.permissions.clone().into())
|
||||
.await?;
|
||||
|
||||
Ok(Json(channel.into()))
|
||||
sync_voice_permissions(db, voice_client, &new_channel, Some(server), Some(&role_id)).await?;
|
||||
|
||||
Ok(Json(new_channel.into()))
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, PartialChannel, User,
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Channel, Database, PartialChannel, User
|
||||
};
|
||||
use revolt_models::v0::{self, DataDefaultChannelPermissions};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -11,11 +10,12 @@ use rocket::{serde::json::Json, State};
|
||||
///
|
||||
/// Sets permissions for the default role in this channel.
|
||||
///
|
||||
/// Channel must be a `Group`, `TextChannel` or `VoiceChannel`.
|
||||
/// Channel must be a `Group` or `TextChannel`.
|
||||
#[openapi(tag = "Channel Permissions")]
|
||||
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
|
||||
pub async fn set_default_channel_permissions(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::DataDefaultChannelPermissions>,
|
||||
@@ -48,10 +48,6 @@ pub async fn set_default_channel_permissions(
|
||||
Channel::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
} => {
|
||||
if let DataDefaultChannelPermissions::Field { permissions: field } = data {
|
||||
permissions
|
||||
@@ -75,5 +71,12 @@ pub async fn set_default_channel_permissions(
|
||||
_ => return Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
|
||||
let server = match channel.server() {
|
||||
Some(server_id) => Some(Reference::from_unchecked(server_id).as_server(db).await?),
|
||||
None => None
|
||||
};
|
||||
|
||||
sync_voice_permissions(db, voice_client, &channel, server.as_ref(), None).await?;
|
||||
|
||||
Ok(Json(channel.into()))
|
||||
}
|
||||
|
||||
@@ -1,105 +1,121 @@
|
||||
use revolt_config::config;
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, User,
|
||||
util::{permissions::perms, reference::Reference},
|
||||
voice::{
|
||||
delete_voice_state, get_channel_node, get_user_voice_channels, get_voice_channel_members,
|
||||
raise_if_in_voice, set_call_notification_recipients, set_channel_node, UserVoiceChannel,
|
||||
VoiceClient,
|
||||
},
|
||||
Database, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Join Call
|
||||
///
|
||||
/// Asks the voice server for a token to join the call.
|
||||
#[openapi(tag = "Voice")]
|
||||
#[post("/<target>/join_call")]
|
||||
#[post("/<target>/join_call", data = "<data>")]
|
||||
pub async fn call(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
) -> Result<Json<v0::LegacyCreateVoiceUserResponse>> {
|
||||
data: Json<v0::DataJoinCall>,
|
||||
) -> Result<Json<v0::CreateVoiceUserResponse>> {
|
||||
if !voice_client.is_enabled() {
|
||||
return Err(create_error!(LiveKitUnavailable));
|
||||
}
|
||||
|
||||
let v0::DataJoinCall {
|
||||
node,
|
||||
force_disconnect,
|
||||
recipients,
|
||||
} = data.into_inner();
|
||||
|
||||
if user.bot.is_some() && force_disconnect == Some(true) {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
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::Connect)?;
|
||||
|
||||
let Some(voice_info) = channel.voice() else {
|
||||
return Err(create_error!(NotAVoiceChannel));
|
||||
};
|
||||
|
||||
let mut permissions = perms(db, &user).channel(&channel);
|
||||
|
||||
let current_permissions = calculate_channel_permissions(&mut permissions).await;
|
||||
current_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
|
||||
|
||||
let user_voice_channel = UserVoiceChannel::from_channel(&channel);
|
||||
|
||||
if get_voice_channel_members(&user_voice_channel)
|
||||
.await?
|
||||
.zip(voice_info.max_users)
|
||||
.is_some_and(|(ms, max_users)| ms.len() >= max_users)
|
||||
&& !current_permissions.has(ChannelPermission::ManageChannel as u64)
|
||||
{
|
||||
return Err(create_error!(CannotJoinCall));
|
||||
}
|
||||
|
||||
let existing_node = get_channel_node(channel.id()).await?;
|
||||
let has_existing_node = existing_node.is_some(); // we move existing_node in the next statement so this is the quickest way to know if we need to set it.
|
||||
|
||||
let node = existing_node
|
||||
.or(node)
|
||||
.ok_or_else(|| create_error!(UnknownNode))?;
|
||||
|
||||
let config = config().await;
|
||||
if config.api.security.voso_legacy_token.is_empty() {
|
||||
return Err(create_error!(VosoUnavailable));
|
||||
}
|
||||
|
||||
match channel {
|
||||
Channel::SavedMessages { .. } | Channel::TextChannel { .. } => {
|
||||
return Err(create_error!(CannotJoinCall))
|
||||
let node_host = config
|
||||
.hosts
|
||||
.livekit
|
||||
.get(&node)
|
||||
.ok_or_else(|| create_error!(UnknownNode))?
|
||||
.clone();
|
||||
|
||||
if force_disconnect == Some(true) {
|
||||
// Finds and disconnects any existing voice connections by the user,
|
||||
// should only ever loop once but just to cover our backs.
|
||||
|
||||
for previous_channel in get_user_voice_channels(&user.id).await? {
|
||||
if let Some(node) = get_channel_node(&previous_channel.id).await? {
|
||||
// if this errors its just a mismatching state - ignore and proceed to still delete our state
|
||||
let _ = voice_client
|
||||
.remove_user(&node, &user.id, &previous_channel.id)
|
||||
.await;
|
||||
};
|
||||
|
||||
delete_voice_state(&previous_channel, &user.id).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// To join a call:
|
||||
// - Check if the room exists.
|
||||
// - If not, create it.
|
||||
let client = reqwest::Client::new();
|
||||
let result = client
|
||||
.get(format!(
|
||||
"{}/room/{}",
|
||||
config.hosts.voso_legacy,
|
||||
channel.id()
|
||||
))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
config.api.security.voso_legacy_token.clone(),
|
||||
)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Err(_) => return Err(create_error!(VosoUnavailable)),
|
||||
Ok(result) => match result.status() {
|
||||
reqwest::StatusCode::OK => (),
|
||||
reqwest::StatusCode::NOT_FOUND => {
|
||||
if (client
|
||||
.post(format!(
|
||||
"{}/room/{}",
|
||||
config.hosts.voso_legacy,
|
||||
channel.id()
|
||||
))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
config.api.security.voso_legacy_token.clone(),
|
||||
)
|
||||
.send()
|
||||
.await)
|
||||
.is_err()
|
||||
{
|
||||
return Err(create_error!(VosoUnavailable));
|
||||
}
|
||||
}
|
||||
_ => return Err(create_error!(VosoUnavailable)),
|
||||
},
|
||||
}
|
||||
|
||||
// Then create a user for the room.
|
||||
if let Ok(response) = client
|
||||
.post(format!(
|
||||
"{}/room/{}/user/{}",
|
||||
config.hosts.voso_legacy,
|
||||
channel.id(),
|
||||
user.id
|
||||
))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
config.api.security.voso_legacy_token,
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|_| create_error!(InvalidOperation))
|
||||
.map(Json)
|
||||
} else {
|
||||
Err(create_error!(VosoUnavailable))
|
||||
raise_if_in_voice(&user, &user_voice_channel).await?;
|
||||
}
|
||||
|
||||
let token = voice_client
|
||||
.create_token(&node, db, &user, current_permissions, &channel)
|
||||
.await?;
|
||||
|
||||
let room = voice_client.create_room(&node, &channel).await?;
|
||||
|
||||
if !has_existing_node {
|
||||
set_channel_node(channel.id(), &node).await?;
|
||||
}
|
||||
|
||||
log::debug!("Created room {}", room.name);
|
||||
|
||||
if let Some(recipients) = recipients {
|
||||
if room.num_participants == 0 && !recipients.is_empty() {
|
||||
set_call_notification_recipients(channel.id(), &user.id, &recipients).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(v0::CreateVoiceUserResponse {
|
||||
token,
|
||||
url: node_host.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
75
crates/delta/src/routes/channels/voice_stop_ring.rs
Normal file
75
crates/delta/src/routes/channels/voice_stop_ring.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use revolt_database::{
|
||||
util::reference::Reference,
|
||||
voice::{get_voice_state, UserVoiceChannel, VoiceClient},
|
||||
Channel, Database, User, AMQP,
|
||||
};
|
||||
use revolt_result::{create_error, Result, ToRevoltError};
|
||||
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Stop Ring
|
||||
/// Stops ringing a specific user in a dm call.
|
||||
/// You must be in the call to use this endpoint, returns NotConnected otherwise.
|
||||
/// Only valid in DM/Group channels, will return NoEffect in servers.
|
||||
/// Returns NotFound if the user is not in the dm/group channel
|
||||
#[openapi(tag = "Voice")]
|
||||
#[put("/<target>/end_ring/<target_user>")]
|
||||
pub async fn stop_ring(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
voice: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
target_user: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if !voice.is_enabled() {
|
||||
return Err(create_error!(LiveKitUnavailable));
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
if channel.server().is_some() {
|
||||
return Err(create_error!(NoEffect));
|
||||
}
|
||||
|
||||
if get_voice_state(
|
||||
&UserVoiceChannel {
|
||||
id: channel.id().to_string(),
|
||||
server_id: None,
|
||||
},
|
||||
&user.id,
|
||||
)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Err(create_error!(NotConnected));
|
||||
}
|
||||
|
||||
let members = match channel {
|
||||
Channel::DirectMessage { ref recipients, .. } | Channel::Group { ref recipients, .. } => {
|
||||
recipients
|
||||
}
|
||||
_ => return Err(create_error!(NoEffect)),
|
||||
};
|
||||
|
||||
if members.iter().any(|m| target_user.id == m) {
|
||||
if let Err(e) = amqp
|
||||
.dm_call_updated(
|
||||
&user.id,
|
||||
channel.id(),
|
||||
None,
|
||||
true,
|
||||
Some(vec![target_user.id.to_string()]),
|
||||
)
|
||||
.await
|
||||
.to_internal_error()
|
||||
{
|
||||
revolt_config::capture_internal_error!(&e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(EmptyResponse)
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,11 @@ use validator::Validate;
|
||||
///
|
||||
/// Creates a webhook which 3rd party platforms can use to send messages
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[post("/<target>/webhooks", data = "<data>")]
|
||||
#[post("/<channel_id>/webhooks", data = "<data>")]
|
||||
pub async fn create_webhook(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
channel_id: Reference<'_>,
|
||||
data: Json<v0::CreateWebhookBody>,
|
||||
) -> Result<Json<v0::Webhook>> {
|
||||
let data = data.into_inner();
|
||||
@@ -29,7 +29,7 @@ pub async fn create_webhook(
|
||||
})
|
||||
})?;
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
let channel = channel_id.as_channel(db).await?;
|
||||
|
||||
if !matches!(channel, Channel::TextChannel { .. } | Channel::Group { .. }) {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
|
||||
@@ -11,11 +11,11 @@ use rocket::{serde::json::Json, State};
|
||||
///
|
||||
/// Create an emoji by its Autumn upload id.
|
||||
#[openapi(tag = "Emojis")]
|
||||
#[put("/emoji/<id>", data = "<data>")]
|
||||
#[put("/emoji/<emoji_id>", data = "<data>")]
|
||||
pub async fn create_emoji(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
id: String,
|
||||
emoji_id: String,
|
||||
data: Json<v0::DataCreateEmoji>,
|
||||
) -> Result<Json<v0::Emoji>> {
|
||||
let config = config().await;
|
||||
@@ -50,11 +50,11 @@ pub async fn create_emoji(
|
||||
};
|
||||
|
||||
// Find the relevant attachment
|
||||
let attachment = File::use_emoji(db, &id, &id, &user.id).await?;
|
||||
let attachment = File::use_emoji(db, &emoji_id, &emoji_id, &user.id).await?;
|
||||
|
||||
// Create the emoji object
|
||||
let emoji = Emoji {
|
||||
id,
|
||||
id: emoji_id,
|
||||
parent: data.parent.into(),
|
||||
creator_id: user.id,
|
||||
name: data.name,
|
||||
|
||||
@@ -23,13 +23,6 @@ pub async fn fetch(db: &State<Database>, target: Reference<'_>) -> Result<Json<v
|
||||
name,
|
||||
description,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
..
|
||||
} => {
|
||||
let server = db.fetch_server(&server).await?;
|
||||
|
||||
@@ -202,6 +195,7 @@ mod test {
|
||||
name: "Voice Channel".to_string(),
|
||||
description: None,
|
||||
nsfw: Some(false),
|
||||
voice: None
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -207,7 +207,7 @@ fn custom_openapi_spec() -> OpenApi {
|
||||
}),
|
||||
license: Some(License {
|
||||
name: "AGPLv3".to_owned(),
|
||||
url: Some("https://github.com/revoltchat/delta/blob/master/LICENSE".to_owned()),
|
||||
url: Some("https://github.com/stoatchat/stoatchat/blob/main/crates/delta/LICENSE".to_owned()),
|
||||
..Default::default()
|
||||
}),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
|
||||
@@ -2,12 +2,14 @@ use revolt_database::{Database, User};
|
||||
use revolt_result::Result;
|
||||
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Acknowledge Policy Changes
|
||||
///
|
||||
/// Accept/acknowledge changes to platform policy.
|
||||
#[openapi(tag = "Policy")]
|
||||
#[post("/acknowledge")]
|
||||
pub async fn acknowledge_policy_changes(db: &State<Database>, user: User) -> Result<()> {
|
||||
db.acknowledge_policy_changes(&user.id).await
|
||||
pub async fn acknowledge_policy_changes(db: &State<Database>, user: User) -> Result<EmptyResponse> {
|
||||
db.acknowledge_policy_changes(&user.id).await?;
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// # hCaptcha Configuration
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
@@ -21,15 +22,22 @@ pub struct Feature {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// # Information about a livekit node
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct VoiceNode {
|
||||
pub name: String,
|
||||
pub lat: f64,
|
||||
pub lon: f64,
|
||||
pub public_url: String,
|
||||
}
|
||||
|
||||
/// # Voice Server Configuration
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct VoiceFeature {
|
||||
/// Whether voice is enabled
|
||||
pub enabled: bool,
|
||||
/// URL pointing to the voice API
|
||||
pub url: String,
|
||||
/// URL pointing to the voice WebSocket server
|
||||
pub ws: String,
|
||||
/// All livekit nodes
|
||||
pub nodes: Vec<VoiceNode>,
|
||||
}
|
||||
|
||||
/// # Feature Configuration
|
||||
@@ -46,7 +54,88 @@ pub struct RevoltFeatures {
|
||||
/// Proxy service configuration
|
||||
pub january: Feature,
|
||||
/// Voice server configuration
|
||||
pub voso: VoiceFeature,
|
||||
pub livekit: VoiceFeature,
|
||||
/// Limits
|
||||
pub limits: LimitsConfig,
|
||||
}
|
||||
|
||||
/// # Limits For Users
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct LimitsConfig {
|
||||
/// Global Limits
|
||||
pub global: GlobalLimits,
|
||||
/// New User Limits
|
||||
pub new_user: UserLimits,
|
||||
/// Default User Limits
|
||||
pub default: UserLimits,
|
||||
}
|
||||
|
||||
/// # Global limits
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct GlobalLimits {
|
||||
/// max group size
|
||||
group_size: i64,
|
||||
/// max message embeds
|
||||
message_embeds: i64,
|
||||
/// max replies
|
||||
message_replies: i64,
|
||||
/// max reactions per message
|
||||
message_reactions: i64,
|
||||
/// max server emoji
|
||||
server_emoji: i64,
|
||||
/// max server roles
|
||||
server_roles: i64,
|
||||
/// max server channels
|
||||
server_channels: i64,
|
||||
body_limit_size: i64,
|
||||
|
||||
/// restrict server creation to these users.
|
||||
/// if blank, all users can create servers
|
||||
pub restrict_server_creation: Vec<String>,
|
||||
}
|
||||
|
||||
/// # User Limits
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct UserLimits {
|
||||
/// Max Outgoing Friend Requests
|
||||
pub outgoing_friend_requests: i64,
|
||||
/// Max Owned Bots
|
||||
pub bots: i64,
|
||||
/// Max message content length
|
||||
pub message_length: i64,
|
||||
/// max message attachments
|
||||
pub message_attachments: i64,
|
||||
/// max servers
|
||||
pub servers: i64,
|
||||
/// max audio quality
|
||||
pub voice_quality: i64,
|
||||
/// video streaming enabled
|
||||
pub video: bool,
|
||||
/// max video resolution (vertical, horizontal)
|
||||
pub video_resolution: [i64; 2],
|
||||
/// min/max aspect ratios
|
||||
pub video_aspect_ratio: [f64; 2],
|
||||
pub file_upload_size_limits: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl UserLimits {
|
||||
fn from_feature_limits(fl: revolt_config::FeaturesLimits) -> UserLimits {
|
||||
UserLimits {
|
||||
outgoing_friend_requests: fl.outgoing_friend_requests as i64,
|
||||
bots: fl.bots as i64,
|
||||
message_length: fl.message_length as i64,
|
||||
message_attachments: fl.message_attachments as i64,
|
||||
servers: fl.servers as i64,
|
||||
voice_quality: fl.voice_quality as i64,
|
||||
video: fl.video,
|
||||
video_resolution: [fl.video_resolution[0] as i64, fl.video_resolution[1] as i64],
|
||||
video_aspect_ratio: [
|
||||
fl.video_aspect_ratio[0] as f64,
|
||||
fl.video_aspect_ratio[1] as f64,
|
||||
],
|
||||
file_upload_size_limits: fl.file_upload_size_limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Build Information
|
||||
@@ -94,22 +183,57 @@ pub async fn root() -> Result<Json<RevoltConfig>> {
|
||||
features: RevoltFeatures {
|
||||
captcha: CaptchaFeature {
|
||||
enabled: !config.api.security.captcha.hcaptcha_key.is_empty(),
|
||||
key: config.api.security.captcha.hcaptcha_sitekey,
|
||||
key: config.api.security.captcha.hcaptcha_sitekey.clone(),
|
||||
},
|
||||
email: !config.api.smtp.host.is_empty(),
|
||||
invite_only: config.api.registration.invite_only,
|
||||
autumn: Feature {
|
||||
enabled: !config.hosts.autumn.is_empty(),
|
||||
url: config.hosts.autumn,
|
||||
url: config.hosts.autumn.clone(),
|
||||
},
|
||||
january: Feature {
|
||||
enabled: !config.hosts.january.is_empty(),
|
||||
url: config.hosts.january,
|
||||
url: config.hosts.january.clone(),
|
||||
},
|
||||
voso: VoiceFeature {
|
||||
enabled: !config.hosts.voso_legacy.is_empty(),
|
||||
url: config.hosts.voso_legacy,
|
||||
ws: config.hosts.voso_legacy_ws,
|
||||
livekit: VoiceFeature {
|
||||
enabled: !config.hosts.livekit.is_empty(),
|
||||
nodes: config
|
||||
.api
|
||||
.livekit
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|(_, node)| !node.private)
|
||||
.map(|(name, value)| VoiceNode {
|
||||
name: name.clone(),
|
||||
lat: value.lat,
|
||||
lon: value.lon,
|
||||
public_url: config
|
||||
.hosts
|
||||
.livekit
|
||||
.get(name)
|
||||
.expect("Missing corresponding host for voice node")
|
||||
.clone(),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
limits: LimitsConfig {
|
||||
global: GlobalLimits {
|
||||
group_size: config.features.limits.global.group_size as i64,
|
||||
message_embeds: config.features.limits.global.message_embeds as i64,
|
||||
message_replies: config.features.limits.global.message_replies as i64,
|
||||
message_reactions: config.features.limits.global.message_reactions as i64,
|
||||
server_emoji: config.features.limits.global.server_emoji as i64,
|
||||
server_roles: config.features.limits.global.server_roles as i64,
|
||||
server_channels: config.features.limits.global.server_channels as i64,
|
||||
body_limit_size: config.features.limits.global.body_limit_size as i64,
|
||||
restrict_server_creation: config
|
||||
.features
|
||||
.limits
|
||||
.global
|
||||
.restrict_server_creation,
|
||||
},
|
||||
new_user: UserLimits::from_feature_limits(config.features.limits.new_user),
|
||||
default: UserLimits::from_feature_limits(config.features.limits.default),
|
||||
},
|
||||
},
|
||||
ws: config.hosts.events,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use revolt_database::{events::client::EventV1, Database, Report, Snapshot, SnapshotContent, User};
|
||||
use revolt_models::v0::{ReportStatus, ReportedContent};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket_empty::EmptyResponse;
|
||||
use serde::Deserialize;
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
@@ -27,7 +28,7 @@ pub async fn report_content(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
data: Json<DataReportContent>,
|
||||
) -> Result<()> {
|
||||
) -> Result<EmptyResponse> {
|
||||
let data = data.into_inner();
|
||||
data.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
@@ -130,5 +131,5 @@ pub async fn report_content(
|
||||
|
||||
EventV1::ReportCreate(report.into()).global().await;
|
||||
|
||||
Ok(())
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, RemovalIntention, ServerBan, User,
|
||||
voice::{
|
||||
get_user_voice_channel_in_server, remove_user_from_voice_channel, UserVoiceChannel,
|
||||
VoiceClient,
|
||||
},
|
||||
Database, Message, RemovalIntention, ServerBan, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use revolt_database::events::client::EventV1;
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
/// # Ban User
|
||||
@@ -16,6 +23,7 @@ use validator::Validate;
|
||||
#[put("/<server>/bans/<target>", data = "<data>")]
|
||||
pub async fn ban(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
server: Reference<'_>,
|
||||
target: Reference<'_>,
|
||||
@@ -54,8 +62,29 @@ pub async fn ban(
|
||||
member
|
||||
.remove(db, &server, RemovalIntention::Ban, false)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// If the member is in a voice channel while banned kick them from the voice channel
|
||||
if let Some(channel_id) = get_user_voice_channel_in_server(target.id, &server.id).await? {
|
||||
remove_user_from_voice_channel(
|
||||
voice_client,
|
||||
&UserVoiceChannel {
|
||||
id: channel_id,
|
||||
server_id: Some(server.id.clone()),
|
||||
},
|
||||
target.id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
// We do this outside the member check so we can sweep hit-and-run spammers who already left.
|
||||
if let Some(seconds) = data.delete_message_seconds {
|
||||
if seconds > 0 {
|
||||
let threshold_time = SystemTime::now() - Duration::from_secs(seconds as u64);
|
||||
|
||||
Message::bulk_delete_by_author_since(db, &server.channels, target.id, threshold_time)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
ServerBan::create(db, &server, target.id, data.reason)
|
||||
.await
|
||||
.map(Into::into)
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
events::client::EventV1,
|
||||
util::{
|
||||
permissions::{perms, DatabasePermissionQuery},
|
||||
reference::Reference,
|
||||
},
|
||||
voice::{
|
||||
get_channel_node, get_user_voice_channel_in_server, set_channel_node,
|
||||
set_user_moved_from_voice, set_user_moved_to_voice, sync_user_voice_permissions,
|
||||
UserVoiceChannel, VoiceClient,
|
||||
},
|
||||
Database, File, PartialMember, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_models::v0::{self, FieldsMember};
|
||||
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
use revolt_permissions::{calculate_channel_permissions, calculate_server_permissions, ChannelPermission, UserPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use rocket::{form::validate::Contains, serde::json::Json, State};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Edit Member
|
||||
///
|
||||
/// Edit a member by their id.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[patch("/<server>/members/<member>", data = "<data>")]
|
||||
#[patch("/<server_id>/members/<member_id>", data = "<data>")]
|
||||
pub async fn edit(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
server: Reference<'_>,
|
||||
member: Reference<'_>,
|
||||
server_id: Reference<'_>,
|
||||
member_id: Reference<'_>,
|
||||
data: Json<v0::DataMemberEdit>,
|
||||
) -> Result<Json<v0::Member>> {
|
||||
let data = data.into_inner();
|
||||
@@ -31,13 +41,20 @@ pub async fn edit(
|
||||
})?;
|
||||
|
||||
// Fetch server and member
|
||||
let mut server = server.as_server(db).await?;
|
||||
let mut member = member.as_member(db, &server.id).await?;
|
||||
let mut server = server_id.as_server(db).await?;
|
||||
let target_user = member_id.as_user(db).await?;
|
||||
let mut member = member_id.as_member(db, &server.id).await?;
|
||||
|
||||
// Fetch our currrent permissions
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
let permissions = calculate_server_permissions(&mut query).await;
|
||||
|
||||
// Fetch target permissions
|
||||
let mut target_query = DatabasePermissionQuery::new(db, &target_user)
|
||||
.server(&server)
|
||||
.member(&member);
|
||||
let target_permissions = calculate_server_permissions(&mut target_query).await;
|
||||
|
||||
// Check permissions in server
|
||||
if data.nickname.is_some() || data.remove.contains(&v0::FieldsMember::Nickname) {
|
||||
if user.id == member.id.user {
|
||||
@@ -50,8 +67,10 @@ pub async fn edit(
|
||||
if data.avatar.is_some() || data.remove.contains(&v0::FieldsMember::Avatar) {
|
||||
if user.id == member.id.user {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ChangeAvatar)?;
|
||||
} else if data.remove.contains(&v0::FieldsMember::Avatar) {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::RemoveAvatars)?;
|
||||
} else {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
return Err(create_error!(InvalidOperation))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,13 +79,68 @@ pub async fn edit(
|
||||
}
|
||||
|
||||
if data.timeout.is_some() || data.remove.contains(&v0::FieldsMember::Timeout) {
|
||||
if data.timeout.is_some() && member.id.user == user.id {
|
||||
return Err(create_error!(CannotTimeoutYourself));
|
||||
if data.timeout.is_some() {
|
||||
if member.id.user == user.id {
|
||||
return Err(create_error!(CannotTimeoutYourself));
|
||||
}
|
||||
|
||||
if target_permissions.has_channel_permission(ChannelPermission::TimeoutMembers) {
|
||||
return Err(create_error!(IsElevated));
|
||||
}
|
||||
}
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::TimeoutMembers)?;
|
||||
}
|
||||
|
||||
if data.can_publish.is_some() {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::MuteMembers)?;
|
||||
}
|
||||
|
||||
if data.can_receive.is_some() {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::DeafenMembers)?;
|
||||
}
|
||||
|
||||
if data.voice_channel.is_some() && data.remove.contains(&FieldsMember::VoiceChannel) {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
if data.voice_channel.is_some() || data.remove.contains(&FieldsMember::VoiceChannel) {
|
||||
if !voice_client.is_enabled() {
|
||||
return Err(create_error!(LiveKitUnavailable));
|
||||
};
|
||||
|
||||
if member.id.user != user.id {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::MoveMembers)?;
|
||||
}
|
||||
}
|
||||
|
||||
let new_voice_channel = if let Some(new_channel) = &data.voice_channel {
|
||||
// ensure the channel we are moving them to is in the server and is a voice channel
|
||||
|
||||
let channel = Reference::from_unchecked(new_channel)
|
||||
.as_channel(db)
|
||||
.await
|
||||
.map_err(|_| create_error!(UnknownChannel))?;
|
||||
|
||||
if channel.server().is_none_or(|v| v != member.id.server) {
|
||||
Err(create_error!(UnknownChannel))?
|
||||
}
|
||||
|
||||
let channel_permissions = calculate_channel_permissions(&mut query.clone().channel(&channel)).await;
|
||||
channel_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
|
||||
|
||||
if get_user_voice_channel_in_server(&target_user.id, &server.id)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
Err(create_error!(NotConnected))?
|
||||
};
|
||||
|
||||
Some(channel)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Resolve our ranking
|
||||
let our_ranking = query.get_member_rank().unwrap_or(i64::MIN);
|
||||
|
||||
@@ -102,12 +176,17 @@ pub async fn edit(
|
||||
roles,
|
||||
timeout,
|
||||
remove,
|
||||
can_publish,
|
||||
can_receive,
|
||||
voice_channel: _,
|
||||
} = data;
|
||||
|
||||
let mut partial = PartialMember {
|
||||
nickname,
|
||||
roles,
|
||||
timeout,
|
||||
can_publish,
|
||||
can_receive,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -124,8 +203,86 @@ pub async fn edit(
|
||||
}
|
||||
|
||||
member
|
||||
.update(db, partial, remove.into_iter().map(Into::into).collect())
|
||||
.update(db, partial, remove.clone().into_iter().map(Into::into).collect())
|
||||
.await?;
|
||||
|
||||
if let Some(new_voice_channel) = new_voice_channel {
|
||||
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
|
||||
{
|
||||
let old_node = get_channel_node(&channel).await?.unwrap();
|
||||
|
||||
let new_node = match get_channel_node(new_voice_channel.id()).await? {
|
||||
Some(node) => node,
|
||||
None => {
|
||||
set_channel_node(new_voice_channel.id(), &old_node).await?;
|
||||
old_node.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let new_user_voice_channel = UserVoiceChannel::from_channel(&new_voice_channel);
|
||||
let old_user_voice_channel = UserVoiceChannel {
|
||||
id: channel.clone(),
|
||||
server_id: new_user_voice_channel.server_id.clone(),
|
||||
};
|
||||
|
||||
set_user_moved_from_voice(&channel, &new_user_voice_channel, &target_user.id).await?;
|
||||
set_user_moved_to_voice(
|
||||
new_voice_channel.id(),
|
||||
&old_user_voice_channel,
|
||||
&target_user.id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut query = perms(db, &target_user).channel(&new_voice_channel);
|
||||
let permissions = calculate_channel_permissions(&mut query).await;
|
||||
|
||||
voice_client
|
||||
.create_room(&new_node, &new_voice_channel)
|
||||
.await?;
|
||||
let token = voice_client
|
||||
.create_token(&new_node, db, &target_user, permissions, &new_voice_channel)
|
||||
.await?;
|
||||
|
||||
voice_client
|
||||
.remove_user(&old_node, &target_user.id, &channel)
|
||||
.await?;
|
||||
|
||||
EventV1::UserMoveVoiceChannel {
|
||||
node: new_node,
|
||||
from: channel,
|
||||
to: new_voice_channel.id().to_string(),
|
||||
token,
|
||||
}
|
||||
.private(target_user.id.clone())
|
||||
.await;
|
||||
};
|
||||
} else if can_publish.is_some() || can_receive.is_some() || remove.contains(FieldsMember::CanPublish) || remove.contains(FieldsMember::CanReceive) {
|
||||
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
|
||||
{
|
||||
let node = get_channel_node(&channel).await?.unwrap();
|
||||
let channel = Reference::from_unchecked(&channel).as_channel(db).await?;
|
||||
|
||||
sync_user_voice_permissions(
|
||||
db,
|
||||
voice_client,
|
||||
&node,
|
||||
&user,
|
||||
&channel,
|
||||
Some(&server),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
};
|
||||
|
||||
if remove.contains(&FieldsMember::VoiceChannel) {
|
||||
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
|
||||
{
|
||||
let node = get_channel_node(&channel).await?.unwrap();
|
||||
|
||||
voice_client.remove_user(&node, &user.id, &channel).await?;
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Json(member.into()))
|
||||
}
|
||||
|
||||
@@ -11,21 +11,21 @@ use rocket::{serde::json::Json, State};
|
||||
///
|
||||
/// Retrieve a member.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[get("/<target>/members/<member>?<roles>")]
|
||||
#[get("/<server_id>/members/<member_id>?<roles>")]
|
||||
pub async fn fetch(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
member: Reference<'_>,
|
||||
server_id: Reference<'_>,
|
||||
member_id: Reference<'_>,
|
||||
roles: Option<bool>,
|
||||
) -> Result<Json<v0::MemberResponse>> {
|
||||
let server = target.as_server(db).await?;
|
||||
let server = server_id.as_server(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
if !query.are_we_a_member().await {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
let member = member.as_member(db, &server.id).await?;
|
||||
let member = member_id.as_member(db, &server.id).await?;
|
||||
if let Some(true) = roles {
|
||||
Ok(Json(v0::MemberResponse::MemberWithRoles {
|
||||
roles: server
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
voice::{
|
||||
get_user_voice_channel_in_server, remove_user_from_voice_channel, UserVoiceChannel,
|
||||
VoiceClient,
|
||||
},
|
||||
Database, RemovalIntention, User,
|
||||
};
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
@@ -11,20 +15,21 @@ use rocket_empty::EmptyResponse;
|
||||
///
|
||||
/// Removes a member from the server.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[delete("/<target>/members/<member>")]
|
||||
#[delete("/<server_id>/members/<member_id>")]
|
||||
pub async fn kick(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
member: Reference<'_>,
|
||||
server_id: Reference<'_>,
|
||||
member_id: Reference<'_>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let server = target.as_server(db).await?;
|
||||
let server = server_id.as_server(db).await?;
|
||||
|
||||
if member.id == user.id {
|
||||
if member_id.id == user.id {
|
||||
return Err(create_error!(CannotRemoveYourself));
|
||||
}
|
||||
|
||||
if member.id == server.owner {
|
||||
if member_id.id == server.owner {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
@@ -33,7 +38,7 @@ pub async fn kick(
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::KickMembers)?;
|
||||
|
||||
let member = member.as_member(db, &server.id).await?;
|
||||
let member = member_id.as_member(db, &server.id).await?;
|
||||
if member.get_ranking(query.server_ref().as_ref().unwrap())
|
||||
<= query.get_member_rank().unwrap_or(i64::MIN)
|
||||
{
|
||||
@@ -42,6 +47,19 @@ pub async fn kick(
|
||||
|
||||
member
|
||||
.remove(db, &server, RemovalIntention::Kick, false)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
.await?;
|
||||
|
||||
if let Some(channel_id) = get_user_voice_channel_in_server(member_id.id, &server.id).await? {
|
||||
remove_user_from_voice_channel(
|
||||
voice_client,
|
||||
&UserVoiceChannel {
|
||||
id: channel_id,
|
||||
server_id: Some(server.id.clone()),
|
||||
},
|
||||
member_id.id,
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
voice::{sync_voice_permissions, VoiceClient},
|
||||
Database, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
@@ -14,6 +15,7 @@ use rocket::{serde::json::Json, State};
|
||||
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
|
||||
pub async fn set_role_permission(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
role_id: String,
|
||||
@@ -22,30 +24,38 @@ pub async fn set_role_permission(
|
||||
let data = data.into_inner();
|
||||
|
||||
let mut server = target.as_server(db).await?;
|
||||
if let Some((current_value, rank)) = server.roles.get(&role_id).map(|x| (x.permissions, x.rank))
|
||||
{
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
let permissions = calculate_server_permissions(&mut query).await;
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?;
|
||||
let (current_value, rank) = server
|
||||
.roles
|
||||
.get(&role_id)
|
||||
.map(|x| (x.permissions, x.rank))
|
||||
.ok_or_else(|| create_error!(NotFound))?;
|
||||
|
||||
// Prevent us from editing roles above us
|
||||
if rank <= query.get_member_rank().unwrap_or(i64::MIN) {
|
||||
return Err(create_error!(NotElevated));
|
||||
}
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
let permissions = calculate_server_permissions(&mut query).await;
|
||||
|
||||
// Ensure we have access to grant these permissions forwards
|
||||
let current_value: Override = current_value.into();
|
||||
permissions
|
||||
.throw_permission_override(current_value, &data.permissions)
|
||||
.await?;
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?;
|
||||
|
||||
server
|
||||
.set_role_permission(db, &role_id, data.permissions.into())
|
||||
.await?;
|
||||
|
||||
Ok(Json(server.into()))
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
// Prevent us from editing roles above us
|
||||
if rank <= query.get_member_rank().unwrap_or(i64::MIN) {
|
||||
return Err(create_error!(NotElevated));
|
||||
}
|
||||
|
||||
// Ensure we have access to grant these permissions forwards
|
||||
let current_value: Override = current_value.into();
|
||||
permissions
|
||||
.throw_permission_override(current_value, &data.permissions)
|
||||
.await?;
|
||||
|
||||
server
|
||||
.set_role_permission(db, &role_id, data.permissions.into())
|
||||
.await?;
|
||||
|
||||
for channel_id in &server.channels {
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?;
|
||||
};
|
||||
|
||||
Ok(Json(server.into()))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, PartialServer, User,
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, PartialServer, User
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{
|
||||
@@ -16,6 +15,7 @@ use rocket::{serde::json::Json, State};
|
||||
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
|
||||
pub async fn set_default_server_permissions(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
data: Json<DataPermissionsValue>,
|
||||
@@ -50,5 +50,11 @@ pub async fn set_default_server_permissions(
|
||||
)
|
||||
.await?;
|
||||
|
||||
for channel_id in &server.channels {
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
sync_voice_permissions(db, voice_client, &channel, Some(&server), None).await?;
|
||||
};
|
||||
|
||||
Ok(Json(server.into()))
|
||||
}
|
||||
|
||||
@@ -40,17 +40,10 @@ pub async fn create(
|
||||
}));
|
||||
};
|
||||
|
||||
let role = Role {
|
||||
name: data.name,
|
||||
// Rank of the new role should be below the lowest role
|
||||
rank: server.roles.len() as i64,
|
||||
colour: None,
|
||||
hoist: false,
|
||||
permissions: Default::default(),
|
||||
};
|
||||
let role = Role::create(db, &server, data.name).await?;
|
||||
|
||||
Ok(Json(v0::NewRoleResponse {
|
||||
id: role.create(db, &server.id).await?,
|
||||
id: role.id.clone(),
|
||||
role: role.into(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
voice::{sync_voice_permissions, VoiceClient},
|
||||
Database, User,
|
||||
};
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
@@ -17,6 +18,7 @@ pub async fn delete(
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
role_id: String,
|
||||
voice_client: &State<VoiceClient>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let mut server = target.as_server(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
@@ -26,15 +28,22 @@ pub async fn delete(
|
||||
|
||||
let member_rank = query.get_member_rank().unwrap_or(i64::MIN);
|
||||
|
||||
if let Some(role) = server.roles.remove(&role_id) {
|
||||
if role.rank <= member_rank {
|
||||
return Err(create_error!(NotElevated));
|
||||
}
|
||||
let role = server
|
||||
.roles
|
||||
.remove(&role_id)
|
||||
.ok_or_else(|| create_error!(NotFound))?;
|
||||
|
||||
role.delete(db, &server.id, &role_id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
if role.rank <= member_rank {
|
||||
return Err(create_error!(NotElevated));
|
||||
}
|
||||
|
||||
role.delete(db, &server.id).await?;
|
||||
|
||||
for channel_id in &server.channels {
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?;
|
||||
}
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, PartialRole, User,
|
||||
voice::{sync_voice_permissions, VoiceClient},
|
||||
Database, PartialRole, User
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
@@ -15,6 +16,7 @@ use validator::Validate;
|
||||
#[patch("/<target>/roles/<role_id>", data = "<data>", rank = 1)]
|
||||
pub async fn edit(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
role_id: String,
|
||||
@@ -59,12 +61,17 @@ pub async fn edit(
|
||||
role.update(
|
||||
db,
|
||||
&server.id,
|
||||
&role_id,
|
||||
partial,
|
||||
remove.into_iter().map(Into::into).collect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for channel_id in &server.channels {
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?;
|
||||
};
|
||||
|
||||
Ok(Json(role.into()))
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, User,
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, User
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
@@ -14,6 +13,7 @@ use rocket::{serde::json::Json, State};
|
||||
#[patch("/<target>/roles/ranks", data = "<data>")]
|
||||
pub async fn edit_role_ranks(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::DataEditRoleRanks>,
|
||||
@@ -70,6 +70,12 @@ pub async fn edit_role_ranks(
|
||||
|
||||
server.set_role_ordering(db, new_order).await?;
|
||||
|
||||
for channel_id in &server.channels {
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
sync_voice_permissions(db, voice_client, &channel, Some(&server), None).await?;
|
||||
};
|
||||
|
||||
Ok(Json(server.into()))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use revolt_config::config;
|
||||
use revolt_database::{Database, Member, Server, User};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::{create_error, Result};
|
||||
@@ -20,6 +21,24 @@ pub async fn create_server(
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
|
||||
if !config
|
||||
.features
|
||||
.limits
|
||||
.global
|
||||
.restrict_server_creation
|
||||
.is_empty()
|
||||
&& !config
|
||||
.features
|
||||
.limits
|
||||
.global
|
||||
.restrict_server_creation
|
||||
.contains(&user.id)
|
||||
{
|
||||
return Err(create_error!(CantCreateServers));
|
||||
}
|
||||
|
||||
let data = data.into_inner();
|
||||
data.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
use revolt_database::{util::reference::Reference, Database, RemovalIntention, User};
|
||||
use revolt_database::{
|
||||
util::reference::Reference,
|
||||
voice::{
|
||||
delete_voice_channel, get_user_voice_channel_in_server, remove_user_from_voice_channel,
|
||||
UserVoiceChannel, VoiceClient,
|
||||
},
|
||||
Database, RemovalIntention, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_result::Result;
|
||||
use rocket::State;
|
||||
@@ -12,6 +19,7 @@ use rocket_empty::EmptyResponse;
|
||||
#[delete("/<target>?<options..>")]
|
||||
pub async fn delete(
|
||||
db: &State<Database>,
|
||||
voice_client: &State<VoiceClient>,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
options: v0::OptionsServerDelete,
|
||||
@@ -20,8 +28,31 @@ pub async fn delete(
|
||||
let member = db.fetch_member(target.id, &user.id).await?;
|
||||
|
||||
if server.owner == user.id {
|
||||
for channel_id in &server.channels {
|
||||
delete_voice_channel(
|
||||
voice_client,
|
||||
&UserVoiceChannel {
|
||||
id: channel_id.clone(),
|
||||
server_id: Some(server.id.clone()),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
server.delete(db).await
|
||||
} else {
|
||||
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
|
||||
remove_user_from_voice_channel(
|
||||
voice_client,
|
||||
&UserVoiceChannel {
|
||||
id: channel_id,
|
||||
server_id: Some(server.id.clone()),
|
||||
},
|
||||
&user.id,
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
|
||||
member
|
||||
.remove(
|
||||
db,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use authifier::models::{totp::Totp, Account, ValidatedTicket};
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, File, PartialServer, Server, User,
|
||||
@@ -7,7 +8,7 @@ use revolt_database::{
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use rocket::{serde::json::Json, Request, State};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Edit Server
|
||||
@@ -17,9 +18,11 @@ use validator::Validate;
|
||||
#[patch("/<target>", data = "<data>")]
|
||||
pub async fn edit(
|
||||
db: &State<Database>,
|
||||
account: Account,
|
||||
user: User,
|
||||
target: Reference<'_>,
|
||||
data: Json<v0::DataEditServer>,
|
||||
validated_ticket: Option<ValidatedTicket>,
|
||||
) -> Result<Json<v0::Server>> {
|
||||
let data = data.into_inner();
|
||||
data.validate().map_err(|error| {
|
||||
@@ -43,6 +46,7 @@ pub async fn edit(
|
||||
&& data.flags.is_none()
|
||||
&& data.analytics.is_none()
|
||||
&& data.discoverable.is_none()
|
||||
&& data.owner.is_none()
|
||||
&& data.remove.is_empty()
|
||||
{
|
||||
return Ok(Json(server.into()));
|
||||
@@ -57,6 +61,17 @@ pub async fn edit(
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?;
|
||||
}
|
||||
|
||||
// Check we are the server owner or privileged if changing sensitive fields
|
||||
if data.owner.is_some() {
|
||||
if user.id != server.owner && !user.privileged {
|
||||
return Err(create_error!(NotOwner));
|
||||
}
|
||||
|
||||
if validated_ticket.is_none() {
|
||||
return Err(create_error!(InvalidCredentials));
|
||||
}
|
||||
}
|
||||
|
||||
// Check we are privileged if changing sensitive fields
|
||||
if (data.flags.is_some() /*|| data.nsfw.is_some()*/ || data.discoverable.is_some())
|
||||
&& !user.privileged
|
||||
@@ -90,6 +105,7 @@ pub async fn edit_data(
|
||||
// nsfw,
|
||||
discoverable,
|
||||
analytics,
|
||||
owner,
|
||||
remove,
|
||||
} = data;
|
||||
|
||||
@@ -102,6 +118,7 @@ pub async fn edit_data(
|
||||
// nsfw,
|
||||
discoverable,
|
||||
analytics,
|
||||
owner: owner.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -156,6 +173,21 @@ pub async fn edit_data(
|
||||
server.banner = partial.banner.clone();
|
||||
}
|
||||
|
||||
// 5. Transfer ownership
|
||||
if let Some(owner) = owner {
|
||||
let owner_reference = Reference::from_unchecked(&owner);
|
||||
// Check if member exists
|
||||
owner_reference.as_member(db, &server.id).await?;
|
||||
let owner_user = owner_reference.as_user(db).await?;
|
||||
|
||||
if owner_user.bot.is_some() {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
server.owner = owner;
|
||||
partial.owner = Some(server.owner.clone());
|
||||
}
|
||||
|
||||
server
|
||||
.update(db, partial, remove.into_iter().map(Into::into).collect())
|
||||
.await?;
|
||||
|
||||
@@ -217,7 +217,6 @@ pub struct GithubComment {
|
||||
position: Option<u32>,
|
||||
reactions: Option<GithubReactions>,
|
||||
updated_at: Value,
|
||||
url: String,
|
||||
user: GithubUser,
|
||||
}
|
||||
|
||||
@@ -689,11 +688,23 @@ const LIGHT_ORANGE: &str = "#d9916d";
|
||||
// for future use
|
||||
// const WHITE: &str = "#c3e1c3";
|
||||
|
||||
fn shorten_text(text: &str, length: usize) -> String {
|
||||
if text.len() < length {
|
||||
text.to_string()
|
||||
fn shorten_single_line_text(text: &str, length: usize) -> String {
|
||||
if text.contains('\n') {
|
||||
let text = text.split('\n').next().unwrap();
|
||||
|
||||
format!("{}...", &text[..text.len().min(length) - 3])
|
||||
} else if text.len() > length {
|
||||
format!("{}...", &text[..length - 3])
|
||||
} else {
|
||||
format!("{}...", &text[0..length])
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn shorten_text(text: &str, length: usize) -> String {
|
||||
if text.len() >= length {
|
||||
format!("{}...", &text[..length - 3])
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,7 +712,7 @@ fn safe_from_str<T: for<'de> Deserialize<'de>>(data: &str) -> Result<T> {
|
||||
match serde_json::from_str(data) {
|
||||
Ok(output) => Ok(output),
|
||||
Err(err) => {
|
||||
log::error!("{err:?}");
|
||||
revolt_config::capture_internal_error!(err);
|
||||
Err(create_error!(InvalidOperation))
|
||||
}
|
||||
}
|
||||
@@ -753,12 +764,12 @@ pub async fn webhook_execute_github(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
webhook_id: Reference<'_>,
|
||||
token: String,
|
||||
token: &str,
|
||||
event: EventHeader<'_>,
|
||||
data: String,
|
||||
) -> Result<()> {
|
||||
let webhook = webhook_id.as_webhook(db).await?;
|
||||
webhook.assert_token(&token)?;
|
||||
webhook.assert_token(token)?;
|
||||
|
||||
let channel = db.fetch_channel(&webhook.channel_id).await?;
|
||||
let event = convert_event(&data, &event)?;
|
||||
@@ -824,7 +835,7 @@ pub async fn webhook_execute_github(
|
||||
"[`{}`]({}) {} - {}",
|
||||
&commit.id[0..=7],
|
||||
commit.url,
|
||||
shorten_text(&commit.message, 50),
|
||||
shorten_single_line_text(&commit.message, 50),
|
||||
commit.author.name
|
||||
)
|
||||
})
|
||||
@@ -897,10 +908,11 @@ pub async fn webhook_execute_github(
|
||||
url: Some(event.sender.html_url),
|
||||
title: Some(event.sender.login),
|
||||
description: Some(format!(
|
||||
"#### [{}] New discussion #{}: {}\n{}",
|
||||
"#### [[{}] New discussion #{}: {}]({})\n{}",
|
||||
event.repository.full_name,
|
||||
discussion.number,
|
||||
discussion.title,
|
||||
discussion.html_url,
|
||||
shorten_text(&discussion.body, 450)
|
||||
)),
|
||||
colour: Some(LIGHT_ORANGE.to_string()),
|
||||
@@ -911,10 +923,11 @@ pub async fn webhook_execute_github(
|
||||
url: Some(answer.comment.user.html_url),
|
||||
title: Some(answer.comment.user.login),
|
||||
description: Some(format!(
|
||||
"#### [{}] discussion #{} marked answered: {}\n{}",
|
||||
"#### [[{}] Discussion #{} marked answered: {}]({})\n{}",
|
||||
event.repository.full_name,
|
||||
discussion.number,
|
||||
discussion.title,
|
||||
answer.comment.html_url,
|
||||
shorten_text(&answer.comment.body, 450)
|
||||
)),
|
||||
colour: Some(LIGHT_ORANGE.to_string()),
|
||||
@@ -930,10 +943,11 @@ pub async fn webhook_execute_github(
|
||||
url: Some(comment.comment.user.html_url),
|
||||
title: Some(comment.comment.user.login),
|
||||
description: Some(format!(
|
||||
"[{}] New comment on discussion #{}: {}\n{}",
|
||||
"#### [[{}] New comment on discussion #{}: {}]({})\n{}",
|
||||
event.repository.full_name,
|
||||
discussion.number,
|
||||
discussion.title,
|
||||
comment.comment.html_url,
|
||||
shorten_text(&comment.comment.body, 450)
|
||||
)),
|
||||
colour: Some(LIGHT_ORANGE.to_string()),
|
||||
@@ -1002,7 +1016,7 @@ pub async fn webhook_execute_github(
|
||||
event.repository.full_name,
|
||||
issue.number,
|
||||
issue.title,
|
||||
issue.html_url,
|
||||
comment.html_url,
|
||||
shorten_text(&comment.body, 450)
|
||||
)),
|
||||
colour: Some(LIGHT_ORANGE.to_string()),
|
||||
|
||||
@@ -6,7 +6,7 @@ use futures::StreamExt;
|
||||
use rand::Rng;
|
||||
use redis_kiss::redis::aio::PubSub;
|
||||
use revolt_database::{
|
||||
events::client::EventV1, Channel, Database, Member, Message, Server, User, AMQP,
|
||||
events::client::EventV1, Channel, Database, Member, Message, PartialRole, Server, User, AMQP,
|
||||
};
|
||||
use revolt_database::{util::idempotency::IdempotencyKey, Role};
|
||||
use revolt_models::v0;
|
||||
@@ -139,21 +139,26 @@ impl TestHarness {
|
||||
server: &Server,
|
||||
rank: i64,
|
||||
overrides: Option<OverrideField>,
|
||||
) -> (String, Role) {
|
||||
let role = Role {
|
||||
name: TestHarness::rand_string(),
|
||||
permissions: overrides.unwrap_or(OverrideField { a: 0, d: 0 }),
|
||||
rank,
|
||||
colour: None,
|
||||
hoist: false,
|
||||
};
|
||||
|
||||
let id = role
|
||||
.create(&self.db, &server.id)
|
||||
) -> Role {
|
||||
let mut role = Role::create(&self.db, &server, TestHarness::rand_string())
|
||||
.await
|
||||
.expect("Failed to create test role");
|
||||
|
||||
(id, role)
|
||||
if let Some(overrides) = overrides {
|
||||
role.update(
|
||||
&self.db,
|
||||
&server.id,
|
||||
PartialRole {
|
||||
permissions: Some(overrides),
|
||||
..Default::default()
|
||||
},
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to set test role overrides");
|
||||
};
|
||||
|
||||
role
|
||||
}
|
||||
|
||||
pub async fn new_channel(&self, server: &Server) -> Channel {
|
||||
@@ -165,6 +170,7 @@ impl TestHarness {
|
||||
name: "Test Channel".to_string(),
|
||||
description: None,
|
||||
nsfw: Some(false),
|
||||
voice: None,
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user