add: audit log and reports table
TODO: audit log/reports UIs
This commit is contained in:
parent
9a9b72bdbb
commit
b2df2739a7
16 changed files with 387 additions and 6 deletions
61
crates/app/src/routes/api/v1/auth/ipbans.rs
Normal file
61
crates/app/src/routes/api/v1/auth/ipbans.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use crate::{
|
||||
State, get_user_from_token,
|
||||
model::{ApiReturn, Error},
|
||||
routes::api::v1::CreateIpBan,
|
||||
};
|
||||
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{auth::IpBan, permissions::FinePermission};
|
||||
|
||||
/// Create a new IP ban.
|
||||
pub async fn create_request(
|
||||
jar: CookieJar,
|
||||
Path(ip): Path<String>,
|
||||
Extension(data): Extension<State>,
|
||||
Json(req): Json<CreateIpBan>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
if !user.permissions.check(FinePermission::MANAGE_BANS) {
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
match data.create_ipban(IpBan::new(ip, user.id, req.reason)).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "IP ban deleted".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the given IP ban.
|
||||
pub async fn delete_request(
|
||||
jar: CookieJar,
|
||||
Path(id): Path<usize>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
if !user.permissions.check(FinePermission::MANAGE_BANS) {
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
match data.delete_ipban(id, user).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "IP ban deleted".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub mod images;
|
||||
pub mod ipbans;
|
||||
pub mod profile;
|
||||
pub mod social;
|
||||
|
||||
|
|
|
@ -172,6 +172,9 @@ pub fn routes() -> Router {
|
|||
"/communities/{cid}/memberships/{uid}/role",
|
||||
post(communities::communities::update_membership_role),
|
||||
)
|
||||
// ipbans
|
||||
.route("/bans/{ip}", post(auth::ipbans::create_request))
|
||||
.route("/bans/id/{id}", delete(auth::ipbans::delete_request))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -265,3 +268,8 @@ pub struct UpdateMembershipRole {
|
|||
pub struct DeleteUser {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateIpBan {
|
||||
pub reason: String,
|
||||
}
|
||||
|
|
84
crates/core/src/database/audit_log.rs
Normal file
84
crates/core/src/database/audit_log.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::{
|
||||
Error, Result, auth::User, moderation::AuditLogEntry, permissions::FinePermission,
|
||||
};
|
||||
use crate::{auto_method, execute, get, query_row};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use rusqlite::Row;
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use tokio_postgres::Row;
|
||||
|
||||
impl DataManager {
|
||||
/// Get an [`AuditLogEntry`] from an SQL row.
|
||||
pub(crate) fn get_auditlog_entry_from_row(
|
||||
#[cfg(feature = "sqlite")] x: &Row<'_>,
|
||||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> AuditLogEntry {
|
||||
AuditLogEntry {
|
||||
id: get!(x->0(isize)) as usize,
|
||||
created: get!(x->1(isize)) as usize,
|
||||
moderator: get!(x->2(isize)) as usize,
|
||||
content: get!(x->3(String)),
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_auditlog_entry_by_id(usize)@get_auditlog_entry_from_row -> "SELECT * FROM auditlog WHERE id = $1" --name="audit log entry" --returns=AuditLogEntry --cache-key-tmpl="atto.auditlog:{}");
|
||||
|
||||
/// Create a new audit log entry in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`AuditLogEntry`] object to insert
|
||||
pub async fn create_auditlog_entry(&self, data: AuditLogEntry) -> Result<()> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO auditlog VALUES ($1, $2, $3, $4)",
|
||||
&[
|
||||
&data.id.to_string().as_str(),
|
||||
&data.created.to_string().as_str(),
|
||||
&data.moderator.to_string().as_str(),
|
||||
&data.content.as_str(),
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_auditlog_entry(&self, id: usize, user: User) -> Result<()> {
|
||||
if !user.permissions.check(FinePermission::MANAGE_AUDITLOG) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM auditlog WHERE id = $1",
|
||||
&[&id.to_string()]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!("atto.auditlog:{}", id)).await;
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -22,6 +22,8 @@ impl DataManager {
|
|||
execute!(&conn, common::CREATE_TABLE_USERFOLLOWS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_USERBLOCKS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_IPBANS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_AUDITLOG).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_REPORTS).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -136,6 +138,11 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,6 +168,11 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,6 +200,12 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,6 +231,12 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{x}`", stringify!($name)),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,6 +264,12 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name), id),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,6 +299,12 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,6 +454,12 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,6 +487,12 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{x}`", stringify!($name)),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -497,6 +545,12 @@ macro_rules! auto_method {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -223,6 +223,12 @@ impl DataManager {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::MANAGE_COMMUNITIES) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_community` with x value `{id}`"),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,3 +7,5 @@ pub const CREATE_TABLE_NOTIFICATIONS: &str = include_str!("./sql/create_notifica
|
|||
pub const CREATE_TABLE_USERFOLLOWS: &str = include_str!("./sql/create_userfollows.sql");
|
||||
pub const CREATE_TABLE_USERBLOCKS: &str = include_str!("./sql/create_userblocks.sql");
|
||||
pub const CREATE_TABLE_IPBANS: &str = include_str!("./sql/create_ipbans.sql");
|
||||
pub const CREATE_TABLE_AUDITLOG: &str = include_str!("./sql/create_auditlog.sql");
|
||||
pub const CREATE_TABLE_REPORTS: &str = include_str!("./sql/create_reports.sql");
|
||||
|
|
6
crates/core/src/database/drivers/sql/create_auditlog.sql
Normal file
6
crates/core/src/database/drivers/sql/create_auditlog.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE IF NOT EXISTS auditlog (
|
||||
ip TEXT NOT NULL,
|
||||
created INTEGER NOT NULL PRIMARY KEY,
|
||||
moderator TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
)
|
8
crates/core/src/database/drivers/sql/create_reports.sql
Normal file
8
crates/core/src/database/drivers/sql/create_reports.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE IF NOT EXISTS reports (
|
||||
ip TEXT NOT NULL,
|
||||
created INTEGER NOT NULL PRIMARY KEY,
|
||||
owner TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
asset INTEGER NOT NULL,
|
||||
asset_type TEXT NOT NULL
|
||||
)
|
|
@ -1,3 +1,4 @@
|
|||
mod audit_log;
|
||||
mod auth;
|
||||
mod common;
|
||||
mod communities;
|
||||
|
@ -7,6 +8,7 @@ mod memberships;
|
|||
mod notifications;
|
||||
mod posts;
|
||||
mod reactions;
|
||||
mod reports;
|
||||
mod userblocks;
|
||||
mod userfollows;
|
||||
|
||||
|
|
|
@ -395,6 +395,12 @@ impl DataManager {
|
|||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::MANAGE_POSTS) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_post` with x value `{id}`"),
|
||||
))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
86
crates/core/src/database/reports.rs
Normal file
86
crates/core/src/database/reports.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::{Error, Result, auth::User, moderation::Report, permissions::FinePermission};
|
||||
use crate::{auto_method, execute, get, query_row};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use rusqlite::Row;
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use tokio_postgres::Row;
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`Report`] from an SQL row.
|
||||
pub(crate) fn get_report_from_row(
|
||||
#[cfg(feature = "sqlite")] x: &Row<'_>,
|
||||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> Report {
|
||||
Report {
|
||||
id: get!(x->0(isize)) as usize,
|
||||
created: get!(x->1(isize)) as usize,
|
||||
owner: get!(x->2(isize)) as usize,
|
||||
content: get!(x->3(String)),
|
||||
asset: get!(x->4(isize)) as usize,
|
||||
asset_type: serde_json::from_str(&get!(x->5(String))).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_report_by_id(usize)@get_report_from_row -> "SELECT * FROM reports WHERE id = $1" --name="report" --returns=Report --cache-key-tmpl="atto.reports:{}");
|
||||
|
||||
/// Create a new report in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`Report`] object to insert
|
||||
pub async fn create_report(&self, data: Report) -> Result<()> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO reports VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
&[
|
||||
&data.id.to_string().as_str(),
|
||||
&data.created.to_string().as_str(),
|
||||
&data.owner.to_string().as_str(),
|
||||
&data.content.as_str(),
|
||||
&data.asset.to_string().as_str(),
|
||||
&serde_json::to_string(&data.asset_type).unwrap().as_str(),
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_report(&self, id: usize, user: User) -> Result<()> {
|
||||
if !user.permissions.check(FinePermission::MANAGE_REPORTS) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM reports WHERE id = $1",
|
||||
&[&id.to_string()]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!("atto.report:{}", id)).await;
|
||||
|
||||
// return
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -70,7 +70,7 @@ impl Community {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CommunityContext {
|
||||
pub display_name: String,
|
||||
pub description: String,
|
||||
|
@ -86,7 +86,7 @@ impl Default for CommunityContext {
|
|||
}
|
||||
|
||||
/// Who can read a [`Community`].
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum CommunityReadAccess {
|
||||
/// Everybody can view the community.
|
||||
Everybody,
|
||||
|
@ -101,7 +101,7 @@ impl Default for CommunityReadAccess {
|
|||
}
|
||||
|
||||
/// Who can write to a [`Community`].
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum CommunityWriteAccess {
|
||||
/// Everybody.
|
||||
Everybody,
|
||||
|
@ -120,7 +120,7 @@ impl Default for CommunityWriteAccess {
|
|||
}
|
||||
|
||||
/// Who can join a [`Community`].
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum CommunityJoinAccess {
|
||||
/// Joins are closed. Nobody can join the community.
|
||||
Nobody,
|
||||
|
@ -136,7 +136,7 @@ impl Default for CommunityJoinAccess {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommunityMembership {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
|
@ -161,7 +161,7 @@ impl CommunityMembership {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PostContext {
|
||||
pub comments_enabled: bool,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
pub mod auth;
|
||||
pub mod communities;
|
||||
pub mod communities_permissions;
|
||||
pub mod moderation;
|
||||
pub mod permissions;
|
||||
pub mod reactions;
|
||||
|
||||
|
|
54
crates/core/src/model/moderation.rs
Normal file
54
crates/core/src/model/moderation.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp};
|
||||
|
||||
use super::reactions::AssetType;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AuditLogEntry {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
pub moderator: usize,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl AuditLogEntry {
|
||||
/// Create a new [`AuditLogEntry`].
|
||||
pub fn new(moderator: usize, content: String) -> Self {
|
||||
Self {
|
||||
id: AlmostSnowflake::new(1234567890)
|
||||
.to_string()
|
||||
.parse::<usize>()
|
||||
.unwrap(),
|
||||
created: unix_epoch_timestamp() as usize,
|
||||
moderator,
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Report {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
pub owner: usize,
|
||||
pub content: String,
|
||||
pub asset: usize,
|
||||
pub asset_type: AssetType,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
/// Create a new [`Report`].
|
||||
pub fn new(owner: usize, content: String, asset: usize, asset_type: AssetType) -> Self {
|
||||
Self {
|
||||
id: AlmostSnowflake::new(1234567890)
|
||||
.to_string()
|
||||
.parse::<usize>()
|
||||
.unwrap(),
|
||||
created: unix_epoch_timestamp() as usize,
|
||||
owner,
|
||||
content,
|
||||
asset,
|
||||
asset_type,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ bitflags! {
|
|||
const MANAGE_REACTIONS = 1 << 12;
|
||||
const MANAGE_FOLLOWS = 1 << 13;
|
||||
const MANAGE_VERIFIED = 1 << 14;
|
||||
const MANAGE_AUDITLOG = 1 << 15;
|
||||
const MANAGE_REPORTS = 1 << 16;
|
||||
|
||||
const _ = !0;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue