feat: Mise en place de cronjob fonctionne avec une logique concour qui fonctionne

This commit is contained in:
Max batleforc 2024-06-27 18:04:06 +02:00
parent 9c21b0dfdf
commit 0e4a8ae580
No known key found for this signature in database
GPG Key ID: 25D243AB4B6AC9E7
11 changed files with 376 additions and 51 deletions

15
Cargo.lock generated
View File

@ -690,6 +690,7 @@ dependencies = [
"actix-cors", "actix-cors",
"actix-web", "actix-web",
"chrono", "chrono",
"cron",
"dotenvy", "dotenvy",
"once_cell", "once_cell",
"openssl", "openssl",
@ -709,6 +710,7 @@ dependencies = [
"surrealdb", "surrealdb",
"time", "time",
"tokio", "tokio",
"tokio-cron",
"tokio-cron-scheduler", "tokio-cron-scheduler",
"toml", "toml",
"tracing", "tracing",
@ -4277,6 +4279,19 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "tokio-cron"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b29e8f172aedef409e518b9b88ea7aeb47fd6248cf7673f0a8991c0913601a55"
dependencies = [
"chrono",
"cron",
"futures",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "tokio-cron-scheduler" name = "tokio-cron-scheduler"
version = "0.10.2" version = "0.10.2"

View File

@ -48,6 +48,8 @@ tokio-cron-scheduler = { version = "0.10", features = [
"signal", "signal",
] } ] }
serde_with = "3.8.1" serde_with = "3.8.1"
tokio-cron = "0.1.3"
cron = "0.12.1"
[[bin]] [[bin]]

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use crate::botv2::{ use crate::botv2::{
domain::concour::{ domain::concour::{
check_if_allowed::check_if_allowed, check_if_allowed::check_if_allowed,
@ -5,11 +7,13 @@ use crate::botv2::{
}, },
init::{Context, Error}, init::{Context, Error},
}; };
use chrono::{DateTime, Utc};
use cron::Schedule;
use poise::{ use poise::{
serenity_prelude::{model::colour, CreateEmbed, CreateEmbedFooter, Mentionable, RoleId}, serenity_prelude::{model::colour, CreateEmbed, CreateEmbedFooter, Mentionable, RoleId},
CreateReply, CreateReply,
}; };
use tracing::{info, instrument}; use tracing::{info, instrument, warn};
/// Update the step duration of a concour (only for admin) /// Update the step duration of a concour (only for admin)
#[instrument(skip(ctx), level = "info", fields(channel = ctx.channel_id().get(), guild = ?ctx.guild_id().unwrap().get()))] #[instrument(skip(ctx), level = "info", fields(channel = ctx.channel_id().get(), guild = ?ctx.guild_id().unwrap().get()))]
@ -21,7 +25,8 @@ use tracing::{info, instrument};
)] )]
pub async fn period( pub async fn period(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Dureer d'une étape du concours"] step: u64, #[description = "Dureer d'une étape du concours au format CRON (min: 6H d'interval)"]
cron_schedule: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let guild = match ctx.guild_id() { let guild = match ctx.guild_id() {
Some(guild) => guild, Some(guild) => guild,
@ -59,12 +64,11 @@ pub async fn period(
return Ok(()); return Ok(());
} }
}; };
let duration = match step.try_into() { let schedule = match Schedule::from_str(&cron_schedule) {
Ok(val) => time::Duration::new(val, 0), Ok(schedule) => schedule,
Err(_) => { Err(_) => {
info!("Invalid Duration input");
let embed = CreateEmbed::new() let embed = CreateEmbed::new()
.title("Invalid duration input") .title("Invalid CRON format")
.color(colour::Color::RED) .color(colour::Color::RED)
.footer(footer); .footer(footer);
if let Err(why) = ctx if let Err(why) = ctx
@ -76,7 +80,37 @@ pub async fn period(
return Ok(()); return Ok(());
} }
}; };
let concour = match set_periode(guild.get(), ctx.channel_id().get(), duration).await { let next_to_come = schedule.upcoming(Utc).take(10);
// Check if the next_to_come has at least 6 hours of interval
let mut last: Option<DateTime<Utc>> = None;
for next in next_to_come {
if let Some(last) = last {
if next.timestamp() - last.timestamp() < 6 * 60 * 60 {
let embed = CreateEmbed::new()
.title("Interval between steps is too short")
.description("Please provide a CRON schedule with at least 6 hours of interval")
.color(colour::Color::RED)
.footer(footer);
if let Err(why) = ctx
.send(CreateReply::default().embed(embed).ephemeral(true))
.await
{
tracing::error!("Error sending message: {:?}", why);
}
warn!(
cron_schedule = cron_schedule,
"Interval between steps is too short"
);
return Ok(());
}
}
last = Some(next);
}
info!(
cron_schedule = cron_schedule.as_str(),
"Setting concour periode"
);
let concour = match set_periode(guild.get(), ctx.channel_id().get(), cron_schedule).await {
Ok(concour) => { Ok(concour) => {
if concour.is_none() { if concour.is_none() {
CreateEmbed::new() CreateEmbed::new()

View File

@ -57,7 +57,6 @@ pub async fn start(ctx: Context<'_>) -> Result<(), Error> {
return Ok(()); return Ok(());
} }
}; };
let (concour, success) = match start_concour( let (concour, success) = match start_concour(
guild.get(), guild.get(),
ctx.channel_id().get(), ctx.channel_id().get(),
@ -85,11 +84,11 @@ pub async fn start(ctx: Context<'_>) -> Result<(), Error> {
concour.index_keyword + 1 concour.index_keyword + 1
)) ))
.description(concour.description) .description(concour.description)
.field("Mot du jours ", keyword.to_string(), false) .field("Word of the day ", keyword.to_string(), false)
.field("Good luck !", "", false) .field("Good luck !", "", false)
.field( .field(
"Vous avez jusqu'a demain 17h", "Please see when the concour end in the concour get command",
"HARD CODED FOR THE MOMENT", "",
false, false,
) )
.color(colour::Color::DARK_GREEN); .color(colour::Color::DARK_GREEN);
@ -110,6 +109,9 @@ pub async fn start(ctx: Context<'_>) -> Result<(), Error> {
StartConcourError::FinishedKeyWordList => CreateEmbed::new() StartConcourError::FinishedKeyWordList => CreateEmbed::new()
.title("Finished keyword list, add new one") .title("Finished keyword list, add new one")
.color(colour::Color::RED), .color(colour::Color::RED),
StartConcourError::RoleRecompenseEmpty => CreateEmbed::new()
.title("Role recompense not defined")
.color(colour::Color::RED),
_ => CreateEmbed::new() _ => CreateEmbed::new()
.title("Error while creating concour") .title("Error while creating concour")
.field("Please contact your administrator", "", false) .field("Please contact your administrator", "", false)

View File

@ -14,7 +14,7 @@ pub enum SetPeriodeConcourError {
pub async fn set_periode( pub async fn set_periode(
server_id: u64, server_id: u64,
channel_id: u64, channel_id: u64,
periode: time::Duration, periode: String,
) -> Result<Option<Concour>, SetPeriodeConcourError> { ) -> Result<Option<Concour>, SetPeriodeConcourError> {
let concour = match Concour::find_by_server_id_channel_id(&server_id, &channel_id).await { let concour = match Concour::find_by_server_id_channel_id(&server_id, &channel_id).await {
Ok(list_concour) => list_concour, Ok(list_concour) => list_concour,

View File

@ -13,6 +13,7 @@ pub enum StartConcourError {
DoesntExist, DoesntExist,
KeyWordListEmpty, KeyWordListEmpty,
FinishedKeyWordList, FinishedKeyWordList,
RoleRecompenseEmpty,
FindError(String), FindError(String),
UnknownError(String), UnknownError(String),
} }
@ -39,6 +40,10 @@ pub async fn start_concour(
return Err(StartConcourError::DoesntExist); return Err(StartConcourError::DoesntExist);
} }
let mut concour = concour.unwrap(); let mut concour = concour.unwrap();
if concour.role_recompense == 0 {
tracing::warn!("Role recompense is empty");
return Err(StartConcourError::RoleRecompenseEmpty);
}
if concour.keywords.is_empty() { if concour.keywords.is_empty() {
tracing::warn!("Keyword list is empty"); tracing::warn!("Keyword list is empty");
return Err(StartConcourError::KeyWordListEmpty); return Err(StartConcourError::KeyWordListEmpty);
@ -53,7 +58,7 @@ pub async fn start_concour(
return Err(StartConcourError::AlreadyOnGoing); return Err(StartConcourError::AlreadyOnGoing);
} }
match cron_scheduler match cron_scheduler
.add_concour_cron_job(server_id, channel_id, "*/1 * * * * *".to_string(), http) .add_concour_cron_job(server_id, channel_id, concour.periode.clone(), http)
.await .await
{ {
Ok(_) => {} Ok(_) => {}

View File

@ -4,8 +4,9 @@ use poise::serenity_prelude as serenity;
use rand::Rng; use rand::Rng;
use serenity::all::{CreateAttachment, CreateMessage}; use serenity::all::{CreateAttachment, CreateMessage};
use tokio::fs::File; use tokio::fs::File;
use tracing::info; use tracing::{info, instrument};
#[instrument(skip(ctx, _framework, data), err, level = "trace")]
pub async fn event_handler( pub async fn event_handler(
ctx: &serenity::Context, ctx: &serenity::Context,
event: &serenity::FullEvent, event: &serenity::FullEvent,

View File

@ -1,6 +1,5 @@
use super::init::DB; use super::init::DB;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt; use std::fmt;
use surrealdb::opt::Resource; use surrealdb::opt::Resource;
use surrealdb::sql::Thing; use surrealdb::sql::Thing;
@ -34,6 +33,7 @@ pub struct ConcourRecord {
pub struct ConcourWinner { pub struct ConcourWinner {
pub user_id: u64, pub user_id: u64,
pub date: chrono::DateTime<chrono::Utc>, pub date: chrono::DateTime<chrono::Utc>,
pub keyword: String,
} }
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
@ -43,13 +43,13 @@ pub struct Concour {
pub title: String, pub title: String,
pub description: String, pub description: String,
pub start_date: chrono::DateTime<chrono::Utc>, pub start_date: chrono::DateTime<chrono::Utc>,
pub periode: time::Duration, pub periode: String,
pub role_recompense: u64, pub role_recompense: u64,
pub keywords: Vec<String>, pub keywords: Vec<String>,
pub banner: Option<String>, pub banner: Option<String>,
pub index_keyword: u64, pub index_keyword: u64,
pub status: ConcourStatus, pub status: ConcourStatus,
pub winner: HashMap<String, ConcourWinner>, pub winner: Vec<ConcourWinner>,
pub last_message_id: Option<u64>, pub last_message_id: Option<u64>,
} }
@ -61,13 +61,13 @@ impl Default for Concour {
title: String::new(), title: String::new(),
description: String::new(), description: String::new(),
start_date: chrono::Utc::now(), start_date: chrono::Utc::now(),
periode: time::Duration::days(1), periode: "0 0 17 * * * *".to_string(),
role_recompense: 0, role_recompense: 0,
keywords: Vec::new(), keywords: Vec::new(),
banner: None, banner: None,
index_keyword: 0, index_keyword: 0,
status: ConcourStatus::Created, status: ConcourStatus::Created,
winner: HashMap::new(), winner: Vec::new(),
last_message_id: None, last_message_id: None,
} }
} }
@ -81,13 +81,13 @@ impl Concour {
title: String::new(), title: String::new(),
description: String::new(), description: String::new(),
start_date: chrono::Utc::now(), start_date: chrono::Utc::now(),
periode: time::Duration::days(1), periode: "0 0 17 * * * *".to_string(),
role_recompense: 0, role_recompense: 0,
keywords: Vec::new(), keywords: Vec::new(),
banner: None, banner: None,
index_keyword: 0, index_keyword: 0,
status: ConcourStatus::Created, status: ConcourStatus::Created,
winner: HashMap::new(), winner: Vec::new(),
last_message_id: None, last_message_id: None,
}) })
} }

View File

@ -1 +0,0 @@

View File

@ -1,2 +1 @@
pub mod concour;
pub mod schedule_job; pub mod schedule_job;

View File

@ -1,4 +1,6 @@
use poise::serenity_prelude::Http; use poise::serenity_prelude::{
Color, CreateEmbed, CreateMessage, Http, Mentionable, MessagePagination,
};
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt::{self, Display}, fmt::{self, Display},
@ -6,10 +8,10 @@ use std::{
}; };
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio_cron_scheduler::{Job, JobScheduler}; use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{error, info, instrument}; use tracing::{error, info, info_span, instrument, Instrument};
use uuid::Uuid; use uuid::Uuid;
use crate::db::concour::{Concour, ConcourStatus}; use crate::db::concour::{Concour, ConcourStatus, ConcourWinner};
#[derive(Debug)] #[derive(Debug)]
pub enum StopScheduleJob { pub enum StopScheduleJob {
@ -23,12 +25,20 @@ impl Display for StopScheduleJob {
} }
} }
#[derive(Clone)]
pub struct ScheduleJob { pub struct ScheduleJob {
pub job_id: Arc<RwLock<HashMap<(u64, u64), Uuid>>>, pub job_id: Arc<RwLock<HashMap<(u64, u64), Uuid>>>,
pub scheduler: JobScheduler, pub scheduler: JobScheduler,
} }
impl Clone for ScheduleJob {
fn clone(&self) -> Self {
ScheduleJob {
job_id: self.job_id.clone(),
scheduler: self.scheduler.clone(),
}
}
}
impl ScheduleJob { impl ScheduleJob {
#[instrument(level = "info")] #[instrument(level = "info")]
pub async fn start_cron_scheduler() -> Result<Self, ()> { pub async fn start_cron_scheduler() -> Result<Self, ()> {
@ -72,12 +82,7 @@ impl ScheduleJob {
}; };
for concour in concours { for concour in concours {
match self match self
.add_concour_cron_job( .add_concour_cron_job(concour.server_id, concour.channel_id, concour.periode, http)
concour.server_id,
concour.channel_id,
"0 0 17 * * *".to_string(),
http,
)
.await .await
{ {
Ok(_) => { Ok(_) => {
@ -124,30 +129,293 @@ impl ScheduleJob {
} }
} }
#[instrument(skip(self, _http), level = "info")] #[instrument(skip(self, http), level = "info")]
pub async fn add_concour_cron_job( pub async fn add_concour_cron_job(
&mut self, &mut self,
server_id: u64, server_id: u64,
channel_id: u64, channel_id: u64,
cron_expression: String, cron_expression: String,
_http: &Http, http: &Http,
) -> Result<Uuid, ()> { ) -> Result<Uuid, ()> {
let job = match Job::new_async(cron_expression.as_str(), |uuid, _l| { let http = Arc::new(Http::new(http.token()));
Box::pin(async move { let job = match Job::new_cron_job_async_tz(
// Send the message to announce the end of the concour cron_expression.as_str(),
// Get concour data chrono::Local,
// Get All message since the announcement move |uuid, _l| {
// filter out the bot's message Box::pin(
// count the number of reactions per message {
// get the user comment with the highest reaction let http = http.clone();
// announce the winner Box::pin(async move {
// Give the winner the role reward info!(id = uuid.to_string(), "Cron job fired");
// update concour with the winner and increment the index // Get concour data
// Announce the next concour let concour = match Concour::find_by_server_id_channel_id(
// Or not if there is no more keyword &server_id,
info!("Cron job fired: {:?}", uuid); &channel_id,
}) )
}) { .await
{
Ok(concour) => concour,
Err(e) => {
error!("Error getting concour: {:?}", e);
// Disable the concour ?
return;
}
};
if concour.is_none() {
error!("Concour not found");
return;
}
let mut concour = concour.unwrap();
// Send the message to announce the end of the concour
let current_keyword = concour
.keywords
.get(concour.index_keyword as usize)
.unwrap();
let embed = CreateEmbed::default()
.title(format!(
"Concour has ended Day {} has ended",
concour.index_keyword + 1
))
.description("Processing the results...")
.field("Title", concour.title.clone(), false)
.field("Description", concour.description.clone(), false)
.field("Word of the day", current_keyword, false)
.color(Color::DARK_GREEN);
let reply = CreateMessage::default().embed(embed);
let last_id =
match http.send_message(channel_id.into(), vec![], &reply).await {
Ok(message) => message.id,
Err(e) => {
error!("Error sending message: {:?}", e);
return;
}
};
// Check if the concour is still enabledl
if concour.status != ConcourStatus::OnGoing
|| concour.last_message_id.is_none()
{
info!("Concour is not enabled");
return;
}
// Get All message since the announcement
let message_pagination =
MessagePagination::After(concour.last_message_id.unwrap().into());
let mut messages = match http
.get_messages(channel_id.into(), Some(message_pagination), None)
.await
{
Ok(messages) => messages,
Err(e) => {
error!("Error getting messages: {:?}", e);
return;
}
};
if messages.is_empty() {
error!("No message found");
let embed: CreateEmbed = CreateEmbed::default()
.title("An error has occured while fetching the messages")
.color(Color::DARK_RED);
let reply = CreateMessage::default().embed(embed);
if let Err(err) =
http.send_message(channel_id.into(), vec![], &reply).await
{
error!("Error sending message: {:?}", err);
}
return;
}
if messages.last().unwrap().id != last_id {
info!("Fetching more messages because last one is not the last id");
loop {
let message_pagination = MessagePagination::After(
messages.last().unwrap().id.into(),
);
let mut new_messages = match http
.get_messages(
channel_id.into(),
Some(message_pagination),
None,
)
.await
{
Ok(messages) => messages,
Err(e) => {
error!("Error getting messages: {:?}", e);
return;
}
};
if new_messages.is_empty() {
info!("No more messages found");
break;
}
messages.append(&mut new_messages);
if messages.last().unwrap().id == last_id {
info!("Last message found");
break;
}
}
}
// filter out the bot's message
messages.retain(|message| !message.author.bot);
info!(
nbr_message = messages.len(),
"{} messages found",
messages.len()
);
// count the number of reactions per message
let mut max_reaction = 0;
let mut max_winner = None;
messages.into_iter().for_each(|msg| {
// test
let count = msg
.reactions
.into_iter()
.fold(0, |acc, reaction| acc + reaction.count);
if count > max_reaction {
max_reaction = count;
max_winner = Some(msg.author);
}
});
// announce the winner
let winner = match max_winner {
Some(winner) => winner,
None => {
let embed = CreateEmbed::default()
.title("No winner found, What happened ?")
.color(Color::DARK_RED);
let reply = CreateMessage::default().embed(embed);
if let Err(err) =
http.send_message(channel_id.into(), vec![], &reply).await
{
error!("Error sending message: {:?}", err);
}
error!("No winner found");
return;
}
};
let embed = CreateEmbed::default()
.title("Winner")
.description(format!("The winner is {}", winner.mention()))
.color(Color::DARK_GREEN);
let reply = CreateMessage::default().embed(embed);
if let Err(err) =
http.send_message(channel_id.into(), vec![], &reply).await
{
error!("Error sending message: {:?}", err);
}
let (add, previous) = match concour.winner.last() {
Some(previous_winner) => (
previous_winner.user_id != winner.id.get(),
previous_winner.user_id,
),
None => (true, 0),
};
// Remove the role from the previous winner
// Give the winner the role reward
if add {
if previous != 0 {
match http
.remove_member_role(
server_id.into(),
previous.into(),
concour.role_recompense.into(),
None,
)
.await
{
Ok(_) => {
info!("Role removed from the previous winner");
}
Err(e) => {
error!(
"Error removing role from the previous winner: {:?}",
e
);
}
}
}
match http
.add_member_role(
server_id.into(),
winner.id,
concour.role_recompense.into(),
None,
)
.await
{
Ok(_) => {
info!("Role added to the winner");
}
Err(e) => {
error!("Error adding role to the winner: {:?}", e);
}
}
}
// update concour with the winner and increment the index
concour.winner.push(ConcourWinner {
user_id: winner.id.get(),
date: chrono::Utc::now(),
keyword: current_keyword.to_string(),
});
concour.index_keyword += 1;
if concour.index_keyword as usize >= concour.keywords.len() {
concour.status = ConcourStatus::Finished;
let embed = CreateEmbed::default()
.title("Concour has ended")
.description("The concour has ended, no more keyword")
.color(Color::DARK_GREEN);
let reply = CreateMessage::default().embed(embed);
if let Err(err) =
http.send_message(channel_id.into(), vec![], &reply).await
{
error!("Error sending message: {:?}", err);
}
info!("Concour has ended, no more keyword");
return;
}
let next_keyword = concour
.keywords
.get(concour.index_keyword as usize)
.unwrap();
let output = CreateEmbed::new()
.title(format!(
"Concour: {} Jour : {}",
concour.title.clone(),
concour.index_keyword + 1
))
.description(concour.description.clone())
.field("Word of the day ", next_keyword.to_string(), false)
.field("Good luck !", "", false)
.field(
"Please see when the concour end in the concour get command",
"",
false,
)
.color(Color::DARK_GREEN);
let reply = CreateMessage::default().embed(output);
let last_id =
match http.send_message(channel_id.into(), vec![], &reply).await {
Ok(message) => message.id,
Err(e) => {
error!("Error sending message: {:?}", e);
return;
}
};
concour.last_message_id = Some(last_id.get());
match concour.update().await {
Ok(_) => {
info!("Concour updated");
}
Err(e) => {
error!("Error updating concour: {:?}", e);
}
}
})
}
.instrument(info_span!("ConcourJob", id = uuid.to_string())),
)
},
) {
Ok(job) => job, Ok(job) => job,
Err(e) => { Err(e) => {
error!("Error creating cron job: {:?}", e); error!("Error creating cron job: {:?}", e);