mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-08-31 02:22:29 +00:00
feat: bot roles with permissions upon joining a server
Signed-off-by: İspik <ispik@ispik.dev>
This commit is contained in:
@@ -18,6 +18,9 @@ auto_derived_partial!(
|
||||
/// Whether the bot is public
|
||||
/// (may be invited by anyone)
|
||||
pub public: bool,
|
||||
/// The permissions the bot will ask to have upon being invited to a server,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_permissions: Option<i64>,
|
||||
|
||||
/// Whether to enable analytics
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
@@ -58,6 +61,7 @@ impl Default for Bot {
|
||||
owner: Default::default(),
|
||||
token: Default::default(),
|
||||
public: Default::default(),
|
||||
default_permissions: Default::default(),
|
||||
analytics: Default::default(),
|
||||
discoverable: Default::default(),
|
||||
interactions_url: Default::default(),
|
||||
|
||||
@@ -89,6 +89,13 @@ auto_derived_partial!(
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<File>,
|
||||
/// Id of the bot that owns this role, if it is a managed role
|
||||
///
|
||||
/// Managed roles are created automatically (e.g. via bot invite) and
|
||||
/// should be hidden from "assign role" UI and cleaned up when the
|
||||
/// owning bot leaves the server.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub owner: Option<String>,
|
||||
},
|
||||
"PartialRole"
|
||||
);
|
||||
@@ -133,6 +140,7 @@ auto_derived!(
|
||||
pub enum FieldsRole {
|
||||
Colour,
|
||||
Icon,
|
||||
Owner,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -322,6 +330,20 @@ impl Server {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete the managed role owned by the given bot in this server, if one exists.
|
||||
/// No-op if the bot never had a managed role.
|
||||
pub async fn cleanup_managed_bot_role(&self, db: &Database, bot_id: &str) -> Result<()> {
|
||||
if let Some(role) = self
|
||||
.roles
|
||||
.values()
|
||||
.find(|role| role.owner.as_deref() == Some(bot_id))
|
||||
{
|
||||
role.delete(db, &self.id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Role {
|
||||
@@ -335,20 +357,41 @@ impl Role {
|
||||
hoist: Some(self.hoist),
|
||||
rank: Some(self.rank),
|
||||
icon: self.icon,
|
||||
owner: self.owner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a role
|
||||
pub async fn create(db: &Database, server: &Server, name: String) -> Result<Self> {
|
||||
Self::create_inner(db, server, name, None).await
|
||||
}
|
||||
|
||||
/// Create a role owned/managed by a bot
|
||||
pub async fn create_managed(
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
name: String,
|
||||
owner_bot_id: String,
|
||||
) -> Result<Self> {
|
||||
Self::create_inner(db, server, name, Some(owner_bot_id)).await
|
||||
}
|
||||
|
||||
/// Helper function to avoid code duplication between `create` and `create_managed`
|
||||
async fn create_inner(
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
name: String,
|
||||
owner: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let role = Role {
|
||||
id: Ulid::new().to_string(),
|
||||
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(),
|
||||
icon: None,
|
||||
owner,
|
||||
};
|
||||
|
||||
db.insert_role(&server.id, &role).await?;
|
||||
@@ -359,12 +402,16 @@ impl Role {
|
||||
data: role.clone().into_optional().into(),
|
||||
clear: vec![],
|
||||
}
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
|
||||
Ok(role)
|
||||
}
|
||||
|
||||
pub fn is_managed(&self) -> bool {
|
||||
self.owner.is_some()
|
||||
}
|
||||
|
||||
/// Update server data
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
@@ -399,6 +446,7 @@ impl Role {
|
||||
match field {
|
||||
FieldsRole::Colour => self.colour = None,
|
||||
FieldsRole::Icon => self.icon = None,
|
||||
FieldsRole::Owner => self.owner = None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,6 +463,7 @@ impl Role {
|
||||
hoist,
|
||||
rank,
|
||||
(FieldsRole::Icon) icon,
|
||||
(FieldsRole::Owner) owner,
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -184,6 +184,7 @@ impl IntoDocumentPath for FieldsRole {
|
||||
Some(match self {
|
||||
FieldsRole::Colour => "colour",
|
||||
FieldsRole::Icon => "icon",
|
||||
FieldsRole::Owner => "owner",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ impl From<crate::Bot> for Bot {
|
||||
owner_id: value.owner,
|
||||
token: value.token,
|
||||
public: value.public,
|
||||
default_permissions: value.default_permissions,
|
||||
analytics: value.analytics,
|
||||
discoverable: value.discoverable,
|
||||
interactions_url: value.interactions_url,
|
||||
@@ -935,6 +936,7 @@ impl From<crate::Role> for Role {
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
owner: value.owner
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -949,6 +951,7 @@ impl From<Role> for crate::Role {
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
owner: value.owner
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -963,6 +966,7 @@ impl From<crate::PartialRole> for PartialRole {
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
owner: value.owner
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -977,6 +981,7 @@ impl From<PartialRole> for crate::PartialRole {
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
owner: value.owner
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -986,6 +991,7 @@ impl From<crate::FieldsRole> for FieldsRole {
|
||||
match value {
|
||||
crate::FieldsRole::Colour => FieldsRole::Colour,
|
||||
crate::FieldsRole::Icon => FieldsRole::Icon,
|
||||
crate::FieldsRole::Owner => FieldsRole::Owner,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -995,6 +1001,7 @@ impl From<FieldsRole> for crate::FieldsRole {
|
||||
match value {
|
||||
FieldsRole::Colour => crate::FieldsRole::Colour,
|
||||
FieldsRole::Icon => crate::FieldsRole::Icon,
|
||||
FieldsRole::Owner => crate::FieldsRole::Owner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ auto_derived!(
|
||||
/// Whether the bot is public
|
||||
/// (may be invited by anyone)
|
||||
pub public: bool,
|
||||
/// The permissions the bot will ask to have upon being invited to a server,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub default_permissions: Option<i64>,
|
||||
|
||||
/// Whether to enable analytics
|
||||
#[cfg_attr(
|
||||
@@ -124,6 +127,10 @@ auto_derived!(
|
||||
pub name: Option<String>,
|
||||
/// Whether the bot can be added by anyone
|
||||
pub public: Option<bool>,
|
||||
/// The permissions the bot will ask to have upon being invited to a server,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_permissions: Option<i64>,
|
||||
|
||||
/// Whether analytics should be gathered for this bot
|
||||
///
|
||||
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
|
||||
|
||||
@@ -109,6 +109,9 @@ auto_derived_partial!(
|
||||
/// Role icon
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub icon: Option<File>,
|
||||
/// Id of the bot that owns this role, if it is a managed role
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub owner: Option<String>
|
||||
},
|
||||
"PartialRole"
|
||||
);
|
||||
@@ -127,6 +130,7 @@ auto_derived!(
|
||||
pub enum FieldsRole {
|
||||
Colour,
|
||||
Icon,
|
||||
Owner,
|
||||
}
|
||||
|
||||
/// Channel category
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use revolt_database::util::permissions::DatabasePermissionQuery;
|
||||
use revolt_database::{util::reference::Reference, Database, User};
|
||||
use revolt_database::{Member, AMQP};
|
||||
use revolt_database::{Member, PartialMember, PartialRole, Role, AMQP, AuditLogEntryAction};
|
||||
use revolt_database::voice::{sync_voice_permissions, VoiceClient};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{
|
||||
calculate_channel_permissions, calculate_server_permissions, ChannelPermission,
|
||||
calculate_channel_permissions, calculate_server_permissions, ChannelPermission, Override,
|
||||
};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
@@ -11,16 +12,17 @@ use rocket::State;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Invite Bot
|
||||
///
|
||||
/// Invite a bot to a server or group by its id.`
|
||||
use crate::util::audit_log_reason::AuditLogReason;
|
||||
|
||||
#[openapi(tag = "Bots")]
|
||||
#[post("/<target>/invite", data = "<dest>")]
|
||||
#[post("/<target>/invite?<permissions>", data = "<dest>")]
|
||||
pub async fn invite_bot(
|
||||
db: &State<Database>,
|
||||
amqp: &State<AMQP>,
|
||||
user: User,
|
||||
reason: AuditLogReason,
|
||||
target: Reference<'_>,
|
||||
permissions: Option<i64>,
|
||||
dest: Json<v0::InviteBotDestination>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
@@ -36,16 +38,61 @@ pub async fn invite_bot(
|
||||
|
||||
match dest.into_inner() {
|
||||
v0::InviteBotDestination::Server { server } => {
|
||||
let server = db.fetch_server(&server).await?;
|
||||
let mut server = db.fetch_server(&server).await?;
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
calculate_server_permissions(&mut query)
|
||||
.await
|
||||
let user_permissions = calculate_server_permissions(&mut query).await;
|
||||
user_permissions
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?;
|
||||
|
||||
Member::create(db, &server, &bot_user, None)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
let (mut member, _channels) = Member::create(db, &server, &bot_user, None).await?;
|
||||
|
||||
if let Some(permissions) = permissions {
|
||||
user_permissions
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?;
|
||||
|
||||
let requested = Override {
|
||||
allow: permissions as u64,
|
||||
deny: 0,
|
||||
};
|
||||
|
||||
user_permissions
|
||||
.throw_permission_override(Override::default(), &requested)
|
||||
.await?;
|
||||
|
||||
let mut role = Role::create_managed(db, &server, bot_user.username.clone(), bot_user.id.clone()).await?;
|
||||
|
||||
role.update(
|
||||
db,
|
||||
&server.id,
|
||||
PartialRole {
|
||||
permissions: Some(requested.into()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
AuditLogEntryAction::RoleCreate {
|
||||
role: role.id.clone(),
|
||||
name: role.name.clone(),
|
||||
}
|
||||
.insert(db, server.id.clone(), reason, user.id.clone(), None)
|
||||
.await;
|
||||
|
||||
member
|
||||
.update(
|
||||
db,
|
||||
PartialMember {
|
||||
roles: Some(vec![role.id.clone()]),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
v0::InviteBotDestination::Group { group } => {
|
||||
let mut channel = db.fetch_channel(&group).await?;
|
||||
|
||||
@@ -91,6 +91,8 @@ pub async fn ban(
|
||||
|
||||
let ban = ServerBan::create(db, &server, target.id, data.reason.clone()).await?;
|
||||
|
||||
server.cleanup_managed_bot_role(db, target.id).await?;
|
||||
|
||||
AuditLogEntryAction::BanCreate {
|
||||
user: target.id.to_string(),
|
||||
}
|
||||
|
||||
@@ -175,6 +175,10 @@ pub async fn edit(
|
||||
if role.rank <= our_ranking {
|
||||
return Err(create_error!(NotElevated));
|
||||
}
|
||||
|
||||
if role.is_managed() {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
} else {
|
||||
return Err(create_error!(InvalidRole));
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ pub async fn kick(
|
||||
.remove(db, &server, RemovalIntention::Kick, false)
|
||||
.await?;
|
||||
|
||||
server.cleanup_managed_bot_role(db, &member.id.user).await?;
|
||||
|
||||
AuditLogEntryAction::MemberKick {
|
||||
user: member.id.user.clone(),
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ pub async fn delete(
|
||||
return Err(create_error!(NotElevated));
|
||||
}
|
||||
|
||||
if role.is_managed() {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
role.delete(db, &server.id).await?;
|
||||
|
||||
AuditLogEntryAction::RoleDelete {
|
||||
|
||||
Reference in New Issue
Block a user