1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Add minimum chat moderation to enable bans for #22

This commit is contained in:
NG (Graham)
2025-05-29 20:55:36 -04:00
parent 9d9f8a73fc
commit c441b8b5ef
15 changed files with 467 additions and 7 deletions

View File

@@ -12,6 +12,7 @@ pub mod weapon_list;
pub mod weapon_upgrade;
pub mod crf;
pub mod channel;
pub mod sanction;
pub mod error_codes;

View File

@@ -0,0 +1,72 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SanctionJson {
#[serde(rename="Type")]
pub type_: SanctionType,
#[serde(rename="Reason")]
pub reason: String,
#[serde(rename="Reporter")]
pub reporter: String,
#[serde(rename="Issued", serialize_with="serde_issued::serialize")]
pub issued: chrono::DateTime<chrono::Utc>,
}
impl SanctionJson {
pub fn as_json(&self) -> String {
serde_json::to_string(&self).unwrap()
}
}
mod serde_issued {
use chrono::{DateTime, Utc};
/*pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<DateTime<Utc>, D::Error> {
d.deserialize_str(todo!())
}*/
pub fn serialize<S: serde::Serializer>(dt: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&dt.to_rfc2822())
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
pub enum SanctionType {
Warning = 0,
Mute = 1,
Suspension = 2,
Note = 3,
Kick = 4,
}
impl SanctionType {
pub(crate) fn from_db(t: rc_database::schema::sanction::Descriptor) -> Self {
match t {
rc_database::schema::sanction::Descriptor::Warn => Self::Warning,
rc_database::schema::sanction::Descriptor::Mute => Self::Mute,
rc_database::schema::sanction::Descriptor::Ban => Self::Suspension,
rc_database::schema::sanction::Descriptor::Note => Self::Note,
rc_database::schema::sanction::Descriptor::Kick => Self::Kick,
}
}
pub(crate) fn to_db(self) -> rc_database::schema::sanction::Descriptor {
match self {
Self::Warning => rc_database::schema::sanction::Descriptor::Warn,
Self::Mute => rc_database::schema::sanction::Descriptor::Mute,
Self::Suspension => rc_database::schema::sanction::Descriptor::Ban,
Self::Note => rc_database::schema::sanction::Descriptor::Note,
Self::Kick => rc_database::schema::sanction::Descriptor::Kick,
}
}
pub(crate) fn from_persist(t: crate::persist::user::SanctionType) -> Self {
match t {
crate::persist::user::SanctionType::Warn => Self::Warning,
crate::persist::user::SanctionType::Mute => Self::Mute,
crate::persist::user::SanctionType::Ban => Self::Suspension,
crate::persist::user::SanctionType::Note => Self::Note,
crate::persist::user::SanctionType::Kick => Self::Kick,
}
}
}

View File

@@ -198,6 +198,33 @@ impl UserData {
Ok(())
}
}
fn check_perms_to_exec(&self, ty: &super::SanctionType) -> Result<(), i16> {
match ty {
super::SanctionType::Warn
| super::SanctionType::Mute
| super::SanctionType::Note => if self.has_any_elevated_perms() {
Ok(())
} else {
Err(crate::data::error_codes::ChatErrorCodes::ModeratorsOnly as i16)
},
super::SanctionType::Ban
| super::SanctionType::Kick => if self.has_admin_or_better_perms() {
Ok(())
} else {
Err(crate::data::error_codes::ChatErrorCodes::AdminsOnly as i16)
},
}
}
#[inline]
fn has_any_elevated_perms(&self) -> bool {
self.perms.moderator | self.perms.administrator | self.perms.developer
}
fn has_admin_or_better_perms(&self) -> bool {
self.perms.administrator | self.perms.developer
}
}
const INVALID_ROBOT_ERR: i16 = crate::data::error_codes::WebServicesError::InvalidRobot as i16; // 140
@@ -716,4 +743,83 @@ impl super::ChatUser for UserData {
}
Ok(())
}
/*async fn has_pending_sanctions(&self) -> Result<bool, i16> {
let count = self.db.count_sanctions_to_ack_by_user_id_and_descriptor(self.account.id, rc_database::schema::sanction::Descriptor::Warn).await.map_err(|e| {
log::error!("Failed to count pending sanctions for user_id {}: {}", self.account.id, e);
crate::data::error_codes::ChatErrorCodes::UnexpectedError as i16
})?;
Ok(count != 0)
}*/
async fn get_sanctions(&self, username: String) -> Result<polariton::operation::Typed<()>, i16> {
let user_opt = self.db.user_by_display_name(username.clone()).await.map_err(|e| {
log::error!("Failed to retrieve user by username {} for user_id {}: {}", username, self.account.id, e);
crate::data::error_codes::ChatErrorCodes::UnexpectedError as i16
})?;
if let Some(user) = user_opt {
let sanctions = self.db.sanctions_by_user_id(user.id).await.map_err(|e| {
log::error!("Failed to retrieve sanctions by username {} for user_id {}: {}", username, self.account.id, e);
crate::data::error_codes::ChatErrorCodes::UnexpectedError as i16
})?;
Ok(polariton::operation::Typed::Arr(polariton::operation::Arr {
ty: polariton::serdes::TypePrefix::Str,
items: sanctions.into_iter().map(|x| {
let data = crate::data::sanction::SanctionJson {
type_: crate::data::sanction::SanctionType::from_db(x.descriptor),
reason: x.reason,
reporter: x.issuer_name,
issued: chrono::DateTime::from_timestamp(x.creation_time, 0).unwrap(),
};
polariton::operation::Typed::Str(data.as_json().into())
}).collect(),
}))
} else {
Err(crate::data::error_codes::ChatErrorCodes::DoesNotExist as i16)
}
}
async fn set_sanction(&self, sanction: super::SetSanction) -> Result<(), i16> {
self.check_perms_to_exec(&sanction.type_)?;
let user_opt = self.db.user_by_display_name(sanction.username.clone()).await.map_err(|e| {
log::error!("Failed to retrieve user by username {} for user_id {}: {}", sanction.username, self.account.id, e);
crate::data::error_codes::ChatErrorCodes::UnexpectedError as i16
})?;
if let Some(user) = user_opt {
if sanction.is_adding {
let now = chrono::Utc::now().timestamp();
let sanction_ty = crate::data::sanction::SanctionType::from_persist(sanction.type_).to_db();
let to_add = rc_database::schema::sanction::ActiveModel {
user_id: rc_database::sea_orm::ActiveValue::Set(user.id),
creation_time: rc_database::sea_orm::ActiveValue::Set(now),
issuer_id: rc_database::sea_orm::ActiveValue::Set(self.account.id),
issuer_name: rc_database::sea_orm::ActiveValue::Set(self.account.display_name.clone()),
descriptor: rc_database::sea_orm::ActiveValue::Set(sanction_ty.clone()),
reason: rc_database::sea_orm::ActiveValue::Set(sanction.reason),
duration: rc_database::sea_orm::ActiveValue::Set(if sanction.duration <= 0 { None } else { Some(sanction.duration as i64) }),
..Default::default()
};
if matches!(sanction_ty, rc_database::schema::sanction::Descriptor::Ban) {
self.db.update_perms_by_user_id(rc_database::schema::permissions::ActiveModel {
banned: rc_database::sea_orm::ActiveValue::Set(true),
..Default::default()
}, user.id).await.map_err(|e| {
log::error!("Failed to update permissions (to ban) for user_id {} by user_id {}: {}", user.id, self.account.id, e);
crate::data::error_codes::ChatErrorCodes::UnexpectedError as i16
})?;
}
self.db.insert_sanction(to_add).await.map_err(|e| {
log::error!("Failed to insert sanction for user_id {} by user_id {}: {}", user.id, self.account.id, e);
crate::data::error_codes::ChatErrorCodes::UnexpectedError as i16
})?;
Ok(())
} else {
// FIXME
log::error!("Modifying sanctions is not currently supported");
Err(crate::data::error_codes::ChatErrorCodes::UnexpectedError as i16)
}
} else {
Err(crate::data::error_codes::ChatErrorCodes::DoesNotExist as i16)
}
}
}

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::UnlockedParts;
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType};
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -190,5 +190,36 @@ pub trait ChatUser {
async fn subscribed_channels_strings(&self) -> Result<Vec<String>, i16>;
async fn add_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Result<polariton::operation::Typed<()>, i16>;
async fn remove_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Result<(), i16>;
//async fn has_pending_sanctions(&self) -> Result<bool, i16>;
async fn get_sanctions(&self, username: String) -> Result<polariton::operation::Typed<()>, i16>;
async fn set_sanction(&self, sanction: SetSanction) -> Result<(), i16>;
}
pub struct SetSanction {
pub is_adding: bool, // if false, it's modifying
pub type_: SanctionType,
pub duration: i32,
pub reason: String,
pub username: String,
}
pub enum SanctionType {
Warn = 0,
Mute = 1,
Ban = 2,
Note = 3,
Kick = 4,
}
impl SanctionType {
pub fn from_i32(i: i32) -> Result<Self, i16> {
match i {
0 => Ok(Self::Warn),
1 => Ok(Self::Mute),
2 => Ok(Self::Ban),
3 => Ok(Self::Note),
4 => Ok(Self::Kick),
_ => Err(crate::data::error_codes::ChatErrorCodes::UnexpectedError as i16),
}
}
}