chore(commands): reorganize command files by metadata categories

This commit is contained in:
Puechberty Arthur
2026-04-10 15:25:21 +02:00
parent 23dcc69977
commit 1b5e51c428
154 changed files with 399 additions and 399 deletions
+86
View File
@@ -0,0 +1,86 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
use crate::commands::common::{send_embed, theme_color};
use crate::db::DbPoolKey;
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
}
pub async fn handle_autobackup(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(kind_raw) = args.first() else {
return;
};
let Some(days_raw) = args.get(1) else {
return;
};
let Some(kind) = advanced_tools::backup_kind_from_input(kind_raw) else {
return;
};
let Ok(days) = days_raw.parse::<i32>() else {
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id;
let _ = sqlx::query(
r#"
INSERT INTO bot_autobackups (bot_id, guild_id, kind, interval_days, next_run_at)
VALUES ($1, $2, $3, $4, NOW() + make_interval(days => $4))
ON CONFLICT (bot_id, guild_id, kind)
DO UPDATE SET interval_days = EXCLUDED.interval_days,
next_run_at = NOW() + make_interval(days => EXCLUDED.interval_days);
"#,
)
.bind(bot_id.get() as i64)
.bind(guild_id.get() as i64)
.bind(kind)
.bind(days.max(1))
.execute(&pool)
.await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AutoBackup")
.description(format!(
"Auto-backup `{}` configuree toutes les {} jours.",
kind,
days.max(1)
))
.color(theme_color(ctx).await),
)
.await;
}
pub struct AutoBackupCommand;
pub static COMMAND_DESCRIPTOR: AutoBackupCommand = AutoBackupCommand;
impl crate::commands::command_contract::CommandSpec for AutoBackupCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "autobackup",
category: "automation",
params: "<serveur/emoji> <jours>",
description: "Definit l'intervalle en jours des backups automatiques.",
examples: &["+autobackup serveur 3", "+autobackup emoji 7"],
default_aliases: &["abkp"],
allow_in_dm: false,
default_permission: 7,
}
}
}
+134
View File
@@ -0,0 +1,134 @@
use chrono::Utc;
use serenity::builder::CreateEmbed;
use serenity::model::Colour;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{parse_channel_id, send_embed};
use crate::db;
pub async fn handle_autopublish(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
if args.is_empty() {
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let channels = db::get_autopublish_channels(&pool, bot_id, guild_id.get() as i64)
.await
.unwrap_or_default();
let description = if channels.is_empty() {
"Aucun salon d'annonces configuré.".to_string()
} else {
channels
.into_iter()
.map(|channel| format!("<#{}>", channel.channel_id))
.collect::<Vec<_>>()
.join("\n")
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Autopublish")
.description(description)
.colour(Colour::from_rgb(100, 150, 255)),
)
.await;
return;
}
let enabled = args[0].eq_ignore_ascii_case("on") || args[0].eq_ignore_ascii_case("enable");
let disabled = args[0].eq_ignore_ascii_case("off") || args[0].eq_ignore_ascii_case("disable");
if !enabled && !disabled {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Autopublish")
.description("Utilisation: +autopublish on|off [#canal]")
.color(0xED4245),
)
.await;
return;
}
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_i64 = guild_id.get() as i64;
let channel_id = args
.get(1)
.and_then(|value| parse_channel_id(value))
.unwrap_or(msg.channel_id);
let result = if enabled {
db::add_autopublish_channel(&pool, bot_id, guild_id_i64, channel_id.get() as i64).await
} else {
db::remove_autopublish_channel(&pool, bot_id, guild_id_i64, channel_id.get() as i64).await
};
if result.is_err() {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Autopublish")
.description("Impossible de mettre à jour le salon d'annonces.")
.color(0xED4245),
)
.await;
return;
}
let embed = if enabled {
CreateEmbed::new()
.title("Autopublish activé")
.description(format!("Salon: <#{}>", channel_id.get()))
.colour(Colour::from_rgb(0, 200, 120))
.timestamp(Utc::now())
} else {
CreateEmbed::new()
.title("Autopublish désactivé")
.description(format!("Salon: <#{}>", channel_id.get()))
.colour(Colour::from_rgb(255, 120, 0))
.timestamp(Utc::now())
};
send_embed(ctx, msg, embed).await;
}
pub struct AutopublishCommand;
pub static COMMAND_DESCRIPTOR: AutopublishCommand = AutopublishCommand;
impl crate::commands::command_contract::CommandSpec for AutopublishCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "autopublish",
category: "automation",
params: "on|off [#canal]",
description: "Affiche, active ou desactive la publication automatique des annonces.",
examples: &[
"+autopublish",
"+autopublish on #annonces",
"+help autopublish",
],
default_aliases: &["apb"],
allow_in_dm: false,
default_permission: 5,
}
}
}
+156
View File
@@ -0,0 +1,156 @@
use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{parse_channel_id, send_embed, theme_color};
use crate::db::DbPoolKey;
fn owned_component_id(action: &str, owner_id: UserId) -> String {
format!("{}:{}", action, owner_id.get())
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
}
pub async fn handle_autoreact(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(action) = args.first().map(|s| s.to_lowercase()) else {
let embed = CreateEmbed::new()
.title("AutoReact")
.description("Utilise les boutons pour ajouter/supprimer/lister via UI.")
.color(theme_color(ctx).await);
let components = vec![CreateActionRow::Buttons(vec![
CreateButton::new(owned_component_id("adv:autoreact:add_modal", msg.author.id))
.label("Ajouter")
.style(ButtonStyle::Success),
CreateButton::new(owned_component_id("adv:autoreact:del_modal", msg.author.id))
.label("Supprimer")
.style(ButtonStyle::Danger),
CreateButton::new(owned_component_id("adv:autoreact:list", msg.author.id))
.label("Lister")
.style(ButtonStyle::Primary),
])];
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new().embed(embed).components(components),
)
.await;
return;
};
let Some(pool) = pool(ctx).await else {
return;
};
let bot_id = ctx.cache.current_user().id;
if action == "list" {
let rows = sqlx::query_as::<_, (i64, String)>(
r#"
SELECT channel_id, emoji
FROM bot_autoreacts
WHERE bot_id = $1 AND guild_id = $2
ORDER BY channel_id ASC, emoji ASC;
"#,
)
.bind(bot_id.get() as i64)
.bind(guild_id.get() as i64)
.fetch_all(&pool)
.await
.unwrap_or_default();
let desc = if rows.is_empty() {
"Aucun autoreact configure.".to_string()
} else {
rows.into_iter()
.map(|(channel_id, emoji)| format!("- <#{}> -> {}", channel_id, emoji))
.collect::<Vec<_>>()
.join("\n")
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AutoReact")
.description(desc)
.color(theme_color(ctx).await),
)
.await;
return;
}
if args.len() < 3 {
return;
}
let Some(channel_id) = parse_channel_id(args[1]) else {
return;
};
let emoji = args[2];
if action == "add" {
let _ = sqlx::query(
r#"
INSERT INTO bot_autoreacts (bot_id, guild_id, channel_id, emoji)
VALUES ($1, $2, $3, $4)
ON CONFLICT (bot_id, guild_id, channel_id, emoji) DO NOTHING;
"#,
)
.bind(bot_id.get() as i64)
.bind(guild_id.get() as i64)
.bind(channel_id.get() as i64)
.bind(emoji)
.execute(&pool)
.await;
} else if action == "del" {
let _ = sqlx::query(
r#"
DELETE FROM bot_autoreacts
WHERE bot_id = $1 AND guild_id = $2 AND channel_id = $3 AND emoji = $4;
"#,
)
.bind(bot_id.get() as i64)
.bind(guild_id.get() as i64)
.bind(channel_id.get() as i64)
.bind(emoji)
.execute(&pool)
.await;
}
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("AutoReact")
.description("Configuration mise a jour.")
.color(theme_color(ctx).await),
)
.await;
}
pub struct AutoReactCommand;
pub static COMMAND_DESCRIPTOR: AutoReactCommand = AutoReactCommand;
impl crate::commands::command_contract::CommandSpec for AutoReactCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "autoreact",
category: "automation",
params: "<add/del> <salon> <emoji> | list",
description: "Ajoute, retire et liste les reactions automatiquement appliquees aux messages d'un salon.",
examples: &["+autoreact add #general 😀", "+autoreact list"],
default_aliases: &["ar", "reactauto"],
allow_in_dm: false,
default_permission: 6,
}
}
}
+180
View File
@@ -0,0 +1,180 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_backup(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
if args.is_empty() {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Backup")
.description("Utilisation: +backup <serveur/emoji> <nom> | list/delete/load")
.color(0xED4245),
)
.await;
return;
}
let mut action = "create";
let mut index = 0usize;
if let Some(first) = args.first() {
match first.to_lowercase().as_str() {
"list" | "ls" => {
action = "list";
index = 1;
}
"delete" | "del" | "rm" => {
action = "delete";
index = 1;
}
"load" => {
action = "load";
index = 1;
}
_ => {}
}
}
let Some(kind_raw) = args.get(index) else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Backup")
.description("Type invalide: utilise serveur ou emoji.")
.color(0xED4245),
)
.await;
return;
};
let Some(kind) = advanced_tools::backup_kind_from_input(kind_raw) else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Backup")
.description("Type invalide: utilise serveur ou emoji.")
.color(0xED4245),
)
.await;
return;
};
match action {
"list" => {
advanced_tools::backup_list(ctx, msg, guild_id, kind).await;
}
"delete" => {
let Some(name) = args
.get(index + 1)
.map(|s| s.trim())
.filter(|s| !s.is_empty())
else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Backup")
.description("Tu dois preciser un nom de backup.")
.color(0xED4245),
)
.await;
return;
};
advanced_tools::backup_delete(ctx, msg, guild_id, kind, name).await;
}
"load" => {
let Some(name) = args
.get(index + 1)
.map(|s| s.trim())
.filter(|s| !s.is_empty())
else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Backup")
.description("Tu dois preciser un nom de backup.")
.color(0xED4245),
)
.await;
return;
};
advanced_tools::backup_load(ctx, msg, guild_id, kind, name).await;
}
_ => {
let Some(name) = args
.get(index + 1)
.map(|s| s.trim())
.filter(|s| !s.is_empty())
else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Backup")
.description("Tu dois preciser un nom de backup.")
.color(0xED4245),
)
.await;
return;
};
match advanced_tools::backup_create(ctx, guild_id, kind, name).await {
Ok(()) => {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Backup")
.description(format!("Backup `{}` de type `{}` creee.", name, kind))
.color(theme_color(ctx).await),
)
.await;
}
Err(err) => {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Backup")
.description(format!("Erreur: {}", err))
.color(0xED4245),
)
.await;
}
}
}
}
}
pub struct BackupCommand;
pub static COMMAND_DESCRIPTOR: BackupCommand = BackupCommand;
impl crate::commands::command_contract::CommandSpec for BackupCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "backup",
category: "automation",
params: "<serveur/emoji> <nom> | list/delete/load",
description: "Cree, liste, supprime et recharge des backups serveur ou emojis.",
examples: &[
"+backup serveur prod_1",
"+backup list serveur",
"+backup load emoji nightly",
],
default_aliases: &["bkp"],
allow_in_dm: false,
default_permission: 7,
}
}
}
+59
View File
@@ -0,0 +1,59 @@
use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_button(ctx: &Context, msg: &Message, args: &[&str]) {
if args.len() < 2 {
return;
}
let action = args[0].to_lowercase();
let link = args[1];
if action == "add" {
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new()
.content("Bouton personnalisé")
.components(vec![CreateActionRow::Buttons(vec![
CreateButton::new_link(link).label("Ouvrir"),
])]),
)
.await;
} else if action == "del" {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Button")
.description("Suppression: supprime simplement le message contenant le bouton.")
.color(theme_color(ctx).await),
)
.await;
}
}
pub struct ButtonCommand;
pub static COMMAND_DESCRIPTOR: ButtonCommand = ButtonCommand;
impl crate::commands::command_contract::CommandSpec for ButtonCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "button",
category: "automation",
params: "<add/del> <lien>",
description: "Ajoute ou supprime un bouton de decoration personnalise sur un message du bot.",
examples: &[
"+button add https://example.com",
"+button del https://example.com",
],
default_aliases: &["btn"],
allow_in_dm: false,
default_permission: 6,
}
}
}
+105
View File
@@ -0,0 +1,105 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
fn emoji_url_from_source(msg: &Message, source: &str) -> String {
if source.starts_with("http://") || source.starts_with("https://") {
return source.to_string();
}
if source.starts_with("<:") || source.starts_with("<a:") {
let cleaned = source.trim_matches(|c| c == '<' || c == '>');
let parts = cleaned.split(':').collect::<Vec<_>>();
if parts.len() == 3 {
let animated = parts[0] == "a";
return format!(
"https://cdn.discordapp.com/emojis/{}.{}",
parts[2],
if animated { "gif" } else { "png" }
);
}
}
if let Some(att) = msg.attachments.first() {
return att.url.clone();
}
String::new()
}
pub async fn handle_create(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
if args.len() < 2 {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Create Emoji")
.description("Usage: +create <emoji/url> <nom>")
.color(0xED4245),
)
.await;
return;
}
let image_url = emoji_url_from_source(msg, args[0]);
if image_url.is_empty() {
return;
}
let response = match reqwest::get(&image_url).await {
Ok(r) => r,
Err(_) => return,
};
let bytes = match response.bytes().await {
Ok(b) => b,
Err(_) => return,
};
let data_uri = format!("data:image/png;base64,{}", {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(bytes)
});
let result = guild_id.create_emoji(&ctx.http, args[1], &data_uri).await;
let embed = if let Ok(emoji) = result {
CreateEmbed::new()
.title("Emoji")
.description(format!("Emoji cree: {}", emoji))
.color(theme_color(ctx).await)
} else {
CreateEmbed::new()
.title("Emoji")
.description("Impossible de creer l'emoji.")
.color(0xED4245)
};
send_embed(ctx, msg, embed).await;
}
pub struct CreateCommand;
pub static COMMAND_DESCRIPTOR: CreateCommand = CreateCommand;
impl crate::commands::command_contract::CommandSpec for CreateCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "create",
category: "automation",
params: "[emoji/url] [nom]",
description: "Cree un emoji custom a partir d'une image, d'un lien ou d'un emoji nitro.",
examples: &[
"+create <:blob:123456789012345678> blobcopy",
"+create https://... logo",
],
default_aliases: &["mkemoji", "ce"],
allow_in_dm: false,
default_permission: 6,
}
}
}
+36
View File
@@ -0,0 +1,36 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_newsticker(ctx: &Context, msg: &Message, args: &[&str]) {
let _ = args;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("NewSticker")
.description("Creation de sticker disponible prochainement (API sticker V2).")
.color(theme_color(ctx).await),
)
.await;
}
pub struct NewStickerCommand;
pub static COMMAND_DESCRIPTOR: NewStickerCommand = NewStickerCommand;
impl crate::commands::command_contract::CommandSpec for NewStickerCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "newsticker",
category: "automation",
params: "[nom]",
description: "Cree un nouveau sticker a partir d'un sticker ou fichier repondu.",
examples: &["+newsticker cool_pack"],
default_aliases: &["stcreate", "nst"],
allow_in_dm: false,
default_permission: 6,
}
}
}
+209
View File
@@ -0,0 +1,209 @@
use chrono::Utc;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{parse_channel_id, send_embed};
use crate::db;
fn is_image_filename(filename: &str) -> bool {
let extension = filename
.rsplit('.')
.next()
.unwrap_or("")
.to_ascii_lowercase();
matches!(
extension.as_str(),
"jpg" | "jpeg" | "png" | "gif" | "webp" | "bmp" | "heic" | "heif"
)
}
fn has_only_photo_attachments(msg: &Message) -> bool {
!msg.attachments.is_empty()
&& msg
.attachments
.iter()
.all(|attachment| is_image_filename(&attachment.filename))
}
fn is_piconly_command_message(content: &str, prefix: &str) -> bool {
if !content.starts_with(prefix) {
return false;
}
let without_prefix = content.trim_start_matches(prefix).trim();
without_prefix
.split_whitespace()
.next()
.map(|command| command.eq_ignore_ascii_case("piconly"))
.unwrap_or(false)
}
pub async fn enforce_piconly_message(
ctx: &Context,
msg: &Message,
content: &str,
prefix: &str,
) -> bool {
let Some(guild_id) = msg.guild_id else {
return false;
};
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return false;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let is_selfie_channel = db::is_piconly_channel(
&pool,
bot_id,
guild_id.get() as i64,
msg.channel_id.get() as i64,
)
.await
.unwrap_or(false);
if !is_selfie_channel || is_piconly_command_message(content, prefix) {
return false;
}
if has_only_photo_attachments(msg) {
return false;
}
let _ = msg.delete(&ctx.http).await;
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Salon selfie")
.description("Seules les photos sont autorisees dans ce salon.")
.color(0xED4245)
.timestamp(Utc::now()),
)
.await;
true
}
pub async fn handle_piconly(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_i64 = guild_id.get() as i64;
if args.is_empty() {
let channels = db::get_piconly_channels(&pool, bot_id, guild_id_i64)
.await
.unwrap_or_default();
let description = if channels.is_empty() {
"Aucun salon selfie configure.".to_string()
} else {
channels
.into_iter()
.map(|channel| format!("<#{}>", channel.channel_id))
.collect::<Vec<_>>()
.join("\n")
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("PicOnly")
.description(description)
.timestamp(Utc::now()),
)
.await;
return;
}
let adding = args[0].eq_ignore_ascii_case("add");
let deleting = args[0].eq_ignore_ascii_case("del")
|| args[0].eq_ignore_ascii_case("remove")
|| args[0].eq_ignore_ascii_case("delete");
if !adding && !deleting {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("PicOnly")
.description("Utilisation: +piconly <add/del> [#salon]")
.color(0xED4245),
)
.await;
return;
}
let channel_id = args
.get(1)
.and_then(|raw| parse_channel_id(raw))
.unwrap_or(msg.channel_id);
let result = if adding {
db::add_piconly_channel(&pool, bot_id, guild_id_i64, channel_id.get() as i64).await
} else {
db::remove_piconly_channel(&pool, bot_id, guild_id_i64, channel_id.get() as i64).await
};
if result.is_err() {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("PicOnly")
.description("Impossible de mettre a jour le salon selfie.")
.color(0xED4245),
)
.await;
return;
}
let embed = if adding {
CreateEmbed::new()
.title("Salon selfie ajoute")
.description(format!("Salon: <#{}>", channel_id.get()))
.timestamp(Utc::now())
} else {
CreateEmbed::new()
.title("Salon selfie retire")
.description(format!("Salon: <#{}>", channel_id.get()))
.timestamp(Utc::now())
};
send_embed(ctx, msg, embed).await;
}
pub struct PiconlyCommand;
pub static COMMAND_DESCRIPTOR: PiconlyCommand = PiconlyCommand;
impl crate::commands::command_contract::CommandSpec for PiconlyCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "piconly",
category: "automation",
params: "<add/del> [salon]",
description: "Definit ou supprime un salon selfie, ou les membres ne peuvent envoyer que des photos.",
examples: &["+piconly", "+piconly add #selfie", "+piconly del #selfie"],
default_aliases: &["selfieonly"],
allow_in_dm: false,
default_permission: 6,
}
}
}