feat: add load question
This commit is contained in:
parent
ace28c402f
commit
38ea1f0632
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -460,6 +460,7 @@ dependencies = [
|
||||
"clickhouse",
|
||||
"clickhouse_pool",
|
||||
"config",
|
||||
"croner",
|
||||
"database",
|
||||
"poise",
|
||||
"serde",
|
||||
|
@ -25,6 +25,7 @@ serde_json = '1.0'
|
||||
clickhouse = { version = '0.13', features = ['native-tls', 'uuid', 'chrono'] }
|
||||
uuid = { version = '1.16', features = ['serde', 'v4'] }
|
||||
chrono = { version = '0.4.41', features = ['serde'] }
|
||||
croner = { version = "*" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
@ -23,12 +23,12 @@
|
||||
- [x] [ADMIN] trivial config : Modification des paramétre de base d'un event
|
||||
- [ ] [ADMIN] trivial add-question : Ajout de question + réponse ou X réponse
|
||||
- [ ] [ADMIN] trivial del-question : Suppression de question
|
||||
- [ ] [ADMIN] trivial load-question : Charge un fichier/lien de question
|
||||
- [x] [ADMIN] trivial load-question : Charge un fichier/lien de question
|
||||
- [ ] trivial scoreboard : Scoreboard
|
||||
- [ ] trivial score : Score d'une personne par défaut le sien
|
||||
- [ ] trivial top : Top X par défaut 3
|
||||
- [ ] [ADMIN] trivial balance : Modifie le score d'une personne +/-/=
|
||||
- [ ] [ADMIN] trivial status : Return trvial status or change trivial status
|
||||
- [x] [ADMIN] trivial status : Return trvial status or change trivial status
|
||||
|
||||
#### TODO
|
||||
|
||||
|
@ -13,5 +13,6 @@ clickhouse = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
config = { path = "../../libs/config" }
|
||||
database = { path = "../../libs/database" }
|
||||
croner = { workspace = true }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::helper::is_user_admin_right;
|
||||
use crate::{Context, Error};
|
||||
use clickhouse_pool::traits::Model;
|
||||
use croner::Cron;
|
||||
use database::trivial::{Trivial, TrivialRewardKind};
|
||||
use poise::serenity_prelude::{Channel, Role};
|
||||
use tracing::{debug, info, instrument};
|
||||
@ -26,6 +27,10 @@ pub async fn config(
|
||||
#[description = "Reward amount for the trivia game"] reward_amount: Option<u64>,
|
||||
#[description = "Whether to send an ephemeral message when the answer is taken into account"]
|
||||
taken_into_account: Option<bool>,
|
||||
#[description = "When to ask the question, in cron format (e.g. '0 0 * * *' for daily)"]
|
||||
cron: Option<String>,
|
||||
#[description = "When to answer the question, in cron format (e.g. '0 0 * * *' for daily)"]
|
||||
cron_answer: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let guild_id = match ctx.guild_id() {
|
||||
Some(id) => id.get(),
|
||||
@ -43,7 +48,7 @@ pub async fn config(
|
||||
}
|
||||
};
|
||||
let manager = ctx.data().datalake_config.clone();
|
||||
let seach_query = Trivial::build_select_query(
|
||||
let search_query = Trivial::build_select_query(
|
||||
Some(&format!(
|
||||
"channel_id = {} and guild_id = {}",
|
||||
channel_id, guild_id
|
||||
@ -52,7 +57,7 @@ pub async fn config(
|
||||
None,
|
||||
);
|
||||
let mut trivia_exists: Trivial = match manager
|
||||
.execute_select_with_retry::<Trivial>(&seach_query)
|
||||
.execute_select_with_retry::<Trivial>(&search_query)
|
||||
.await
|
||||
{
|
||||
Ok(trivia) => {
|
||||
@ -111,6 +116,27 @@ pub async fn config(
|
||||
if let Some(taken_into_account) = taken_into_account {
|
||||
trivia_exists.taken_into_account = taken_into_account;
|
||||
}
|
||||
if let Some(cron) = cron {
|
||||
match Cron::new(&cron).parse() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
ctx.say(format!("Invalid cron format: {}", e)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
trivia_exists.question_asked = cron;
|
||||
}
|
||||
if let Some(cron_answer) = cron_answer {
|
||||
match Cron::new(&cron_answer).parse() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
ctx.say(format!("Invalid cron answer format: {}", e))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
trivia_exists.answer_given = cron_answer;
|
||||
}
|
||||
trivia_exists.updated_at = chrono::Utc::now();
|
||||
trivia_exists.updater_id = ctx.author().id.get();
|
||||
debug!("Updated trivia game: {:?}", trivia_exists);
|
||||
|
199
libs/bot/src/trivia/load.rs
Normal file
199
libs/bot/src/trivia/load.rs
Normal file
@ -0,0 +1,199 @@
|
||||
use crate::helper::is_user_admin_right;
|
||||
use crate::{Context, Error};
|
||||
use clickhouse_pool::traits::Model;
|
||||
use database::trivial::Trivial;
|
||||
use database::trivial_question::TrivialQuestion;
|
||||
use poise::serenity_prelude;
|
||||
use poise::serenity_prelude::Channel;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
/// Load a csv file containing trivia questions and answers into the database.
|
||||
#[instrument(name = "trivia_load", skip(ctx), level = "info", fields(channel = ctx.channel_id().get(), guild = ?ctx.guild_id().unwrap().get()))]
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
guild_only,
|
||||
category = "Trivia",
|
||||
check = "is_user_admin_right"
|
||||
)]
|
||||
pub async fn load(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Fichier au format CSV contenant questions, réponses"]
|
||||
file: serenity_prelude::Attachment,
|
||||
#[description = "Channel of the trivia game, or current"] channel_id: Option<Channel>,
|
||||
#[description = "Whether or not the file has an header row"] has_header: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
let guild_id = match ctx.guild_id() {
|
||||
Some(id) => id.get(),
|
||||
None => {
|
||||
ctx.say("This command can only be used in a server.")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let channel_id = match channel_id {
|
||||
Some(c) => c.id().get(),
|
||||
None => {
|
||||
ctx.say("No channel specified, using current channel.")
|
||||
.await?;
|
||||
ctx.channel_id().get()
|
||||
}
|
||||
};
|
||||
let manager_pool = ctx.data().datalake_config.clone();
|
||||
// Check if the trivia game already exists in the channel
|
||||
let trivia = Trivial::build_select_query(
|
||||
Some(&format!(
|
||||
"channel_id = {} and guild_id = {}",
|
||||
channel_id.clone(),
|
||||
guild_id.clone()
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let trivia_exists: Vec<Trivial> = match manager_pool.execute_select_with_retry(&trivia).await {
|
||||
Ok(trivia) => trivia,
|
||||
Err(e) => {
|
||||
ctx.say(format!("Failed to fetch trivia game: {}", e))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
if trivia_exists.is_empty() {
|
||||
ctx.say(format!("No trivia game exists in this channel.",))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
let trivia = trivia_exists.first().unwrap();
|
||||
let has_header = has_header.unwrap_or(true);
|
||||
let file_content = match file.download().await {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
ctx.say(format!("Failed to download file: {}", e)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
// Assuming the file is a CSV, we can parse it here.
|
||||
let content = String::from_utf8_lossy(&file_content);
|
||||
let content_split = if has_header {
|
||||
// Skip the first line if it is a header
|
||||
let mut lines = content.lines();
|
||||
lines.next(); // Skip header
|
||||
lines.collect::<Vec<&str>>().into_iter()
|
||||
} else {
|
||||
content.lines().collect::<Vec<&str>>().into_iter()
|
||||
};
|
||||
// Split each line by comma and process
|
||||
|
||||
let parsed_content = content_split
|
||||
.map(|line| {
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
if parts.len() < 2 || parts[0].trim().is_empty() || parts[1].trim().is_empty() {
|
||||
warn!("Invalid line format: {}", line);
|
||||
return ("".to_string(), "".to_string());
|
||||
}
|
||||
let question = parts[0].trim().to_string();
|
||||
let answer = parts[1].trim().to_string();
|
||||
(question, answer)
|
||||
})
|
||||
.filter(|(q, a)| !q.is_empty() && !a.is_empty())
|
||||
.collect::<Vec<(String, String)>>();
|
||||
if parsed_content.is_empty() {
|
||||
ctx.say("No valid questions found in the file.").await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let get_trivia = TrivialQuestion::build_select_query(
|
||||
Some(&format!(
|
||||
"channel_id = {} and guild_id = {}",
|
||||
channel_id, guild_id
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let trivial_questions: Vec<TrivialQuestion> =
|
||||
match manager_pool.execute_select_with_retry(&get_trivia).await {
|
||||
Ok(questions) => questions,
|
||||
Err(e) => {
|
||||
ctx.say(format!("Failed to fetch trivia questions: {}", e))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let parsed_content = if !trivial_questions.is_empty() {
|
||||
// Check for duplicate questions
|
||||
let existing_questions: Vec<String> = trivial_questions
|
||||
.iter()
|
||||
.map(|q| q.question.clone())
|
||||
.collect();
|
||||
let no_duplicates: Vec<(String, String)> = parsed_content
|
||||
.iter()
|
||||
.filter(|(q, _)| !existing_questions.contains(q))
|
||||
.cloned()
|
||||
.collect();
|
||||
if no_duplicates.is_empty() {
|
||||
ctx.say("Questions already exist in the database, no new questions to add.")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
no_duplicates
|
||||
} else {
|
||||
// No existing questions, all are new
|
||||
parsed_content
|
||||
};
|
||||
let mut inserter = match manager_pool
|
||||
.get_insert::<TrivialQuestion>(TrivialQuestion::table_name())
|
||||
.await
|
||||
{
|
||||
Ok(inserter) => inserter,
|
||||
Err(e) => {
|
||||
ctx.say(format!(
|
||||
"Internal Server error, please contact Bot Developer: {}",
|
||||
e
|
||||
))
|
||||
.await?;
|
||||
warn!("Failed to create inserter for TrivialQuestion: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
for (question, answer) in parsed_content {
|
||||
let trivial_question = TrivialQuestion {
|
||||
trivial_id: trivia.id,
|
||||
guild_id: guild_id,
|
||||
channel_id: channel_id,
|
||||
question: question,
|
||||
answer: answer,
|
||||
creator_id: ctx.author().id.get(),
|
||||
updater_id: ctx.author().id.get(),
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(e) = inserter.write(&trivial_question).await {
|
||||
warn!("Failed to insert trivial question: {}", e);
|
||||
ctx.say(format!("Failed to insert question: {}", e)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
match inserter.end().await {
|
||||
Ok(_) => {
|
||||
info!("Successfully loaded trivia questions into the database.");
|
||||
ctx.say("Trivia questions loaded successfully.").await?;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to finalize insert operation: {}", e);
|
||||
ctx.say(format!("Failed to load trivia questions: {}", e))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let optimize = TrivialQuestion::optimize_table_sql();
|
||||
if let Err(e) = manager_pool.execute_with_retry(optimize).await {
|
||||
warn!("Failed to optimize trivial_question table: {}", e);
|
||||
ctx.say(format!("Failed to optimize trivia questions: {}", e))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
info!("Successfully optimized trivial_question table.");
|
||||
ctx.say("Trivia questions loaded and table optimized successfully.")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
pub mod config;
|
||||
pub mod create;
|
||||
pub mod list;
|
||||
pub mod load;
|
||||
pub mod status;
|
||||
|
||||
use crate::{Context, Error};
|
||||
use config::config;
|
||||
use create::create;
|
||||
use list::list;
|
||||
use load::load;
|
||||
use status::status;
|
||||
use tracing::instrument;
|
||||
|
||||
@ -16,7 +18,7 @@ use tracing::instrument;
|
||||
slash_command,
|
||||
prefix_command,
|
||||
category = "Trivia",
|
||||
subcommands("create", "list", "config", "status"),
|
||||
subcommands("create", "list", "config", "status", "load"),
|
||||
guild_only = true
|
||||
)]
|
||||
pub async fn trivia(ctx: Context<'_>) -> Result<(), Error> {
|
||||
|
@ -78,6 +78,9 @@ pub struct Trivial {
|
||||
pub taken_into_account: bool,
|
||||
pub status: TrivialStatus,
|
||||
pub sign: i8,
|
||||
|
||||
pub question_asked: String,
|
||||
pub answer_given: String,
|
||||
}
|
||||
|
||||
impl Default for Trivial {
|
||||
@ -100,6 +103,8 @@ impl Default for Trivial {
|
||||
taken_into_account: true,
|
||||
status: TrivialStatus::Init,
|
||||
sign: 1,
|
||||
question_asked: "".to_string(),
|
||||
answer_given: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,7 +159,9 @@ impl Model for Trivial {
|
||||
reward_amount UInt64,
|
||||
taken_into_account Bool,
|
||||
status Enum8('Init' = 0, 'Started' = 1, 'Finished' = 2, 'Paused' = 3),
|
||||
sign Int8
|
||||
sign Int8,
|
||||
question_asked String,
|
||||
answer_given String
|
||||
) ENGINE = CollapsingMergeTree(sign)
|
||||
PRIMARY KEY (guild_id, id)
|
||||
ORDER BY (guild_id, id)
|
||||
@ -180,6 +187,8 @@ impl Model for Trivial {
|
||||
"taken_into_account",
|
||||
"status",
|
||||
"sign",
|
||||
"question_asked",
|
||||
"answer_given",
|
||||
]
|
||||
}
|
||||
|
||||
@ -204,6 +213,8 @@ impl Model for Trivial {
|
||||
self.taken_into_account.to_string(),
|
||||
format!("'{:?}'", serde_json::to_string(&self.status)),
|
||||
self.sign.to_string(),
|
||||
format!("'{}'", self.question_asked),
|
||||
format!("'{}'", self.answer_given),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,9 @@ pub struct TrivialQuestion {
|
||||
#[serde(with = "clickhouse::serde::uuid")]
|
||||
pub trivial_id: Uuid,
|
||||
|
||||
pub guild_id: u64,
|
||||
pub channel_id: u64,
|
||||
|
||||
pub question: String,
|
||||
pub answer: String,
|
||||
|
||||
@ -23,6 +26,23 @@ pub struct TrivialQuestion {
|
||||
pub updater_id: u64,
|
||||
}
|
||||
|
||||
impl Default for TrivialQuestion {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
trivial_id: Uuid::new_v4(),
|
||||
guild_id: 0,
|
||||
channel_id: 0,
|
||||
question: String::new(),
|
||||
answer: String::new(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
creator_id: 0,
|
||||
updater_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for TrivialQuestion {
|
||||
type T = TrivialQuestion;
|
||||
|
||||
@ -42,6 +62,8 @@ impl Model for TrivialQuestion {
|
||||
CREATE TABLE IF NOT EXISTS trivial_question (
|
||||
id UUID PRIMARY KEY,
|
||||
trivial_id UUID,
|
||||
guild_id UInt64,
|
||||
channel_id UInt64,
|
||||
question String,
|
||||
answer String,
|
||||
created_at DateTime64(3),
|
||||
@ -58,6 +80,8 @@ impl Model for TrivialQuestion {
|
||||
vec![
|
||||
"id",
|
||||
"trivial_id",
|
||||
"guild_id",
|
||||
"channel_id",
|
||||
"question",
|
||||
"answer",
|
||||
"created_at",
|
||||
@ -73,6 +97,8 @@ impl Model for TrivialQuestion {
|
||||
vec![
|
||||
self.id.to_string(),
|
||||
self.trivial_id.to_string(),
|
||||
self.guild_id.to_string(),
|
||||
self.channel_id.to_string(),
|
||||
self.question.clone(),
|
||||
self.answer.clone(),
|
||||
self.created_at.to_string(),
|
||||
|
4
resources/template_question.csv
Normal file
4
resources/template_question.csv
Normal file
@ -0,0 +1,4 @@
|
||||
question;response
|
||||
De qu'elle couleur est le cheval blanc d'henry IV?;blanc
|
||||
Joseph ?;joestar
|
||||
Rick ?;morty
|
|
Loading…
Reference in New Issue
Block a user