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-web",
"chrono",
"cron",
"dotenvy",
"once_cell",
"openssl",
@ -709,6 +710,7 @@ dependencies = [
"surrealdb",
"time",
"tokio",
"tokio-cron",
"tokio-cron-scheduler",
"toml",
"tracing",
@ -4277,6 +4279,19 @@ dependencies = [
"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]]
name = "tokio-cron-scheduler"
version = "0.10.2"

View File

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

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use crate::botv2::{
domain::concour::{
check_if_allowed::check_if_allowed,
@ -5,11 +7,13 @@ use crate::botv2::{
},
init::{Context, Error},
};
use chrono::{DateTime, Utc};
use cron::Schedule;
use poise::{
serenity_prelude::{model::colour, CreateEmbed, CreateEmbedFooter, Mentionable, RoleId},
CreateReply,
};
use tracing::{info, instrument};
use tracing::{info, instrument, warn};
/// 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()))]
@ -21,7 +25,8 @@ use tracing::{info, instrument};
)]
pub async fn period(
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> {
let guild = match ctx.guild_id() {
Some(guild) => guild,
@ -59,12 +64,11 @@ pub async fn period(
return Ok(());
}
};
let duration = match step.try_into() {
Ok(val) => time::Duration::new(val, 0),
let schedule = match Schedule::from_str(&cron_schedule) {
Ok(schedule) => schedule,
Err(_) => {
info!("Invalid Duration input");
let embed = CreateEmbed::new()
.title("Invalid duration input")
.title("Invalid CRON format")
.color(colour::Color::RED)
.footer(footer);
if let Err(why) = ctx
@ -76,7 +80,37 @@ pub async fn period(
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) => {
if concour.is_none() {
CreateEmbed::new()

View File

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

View File

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

View File

@ -13,6 +13,7 @@ pub enum StartConcourError {
DoesntExist,
KeyWordListEmpty,
FinishedKeyWordList,
RoleRecompenseEmpty,
FindError(String),
UnknownError(String),
}
@ -39,6 +40,10 @@ pub async fn start_concour(
return Err(StartConcourError::DoesntExist);
}
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() {
tracing::warn!("Keyword list is empty");
return Err(StartConcourError::KeyWordListEmpty);
@ -53,7 +58,7 @@ pub async fn start_concour(
return Err(StartConcourError::AlreadyOnGoing);
}
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
{
Ok(_) => {}

View File

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

View File

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

View File

@ -1 +0,0 @@

View File

@ -1,2 +1 @@
pub mod concour;
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::{
collections::HashMap,
fmt::{self, Display},
@ -6,10 +8,10 @@ use std::{
};
use tokio::sync::RwLock;
use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{error, info, instrument};
use tracing::{error, info, info_span, instrument, Instrument};
use uuid::Uuid;
use crate::db::concour::{Concour, ConcourStatus};
use crate::db::concour::{Concour, ConcourStatus, ConcourWinner};
#[derive(Debug)]
pub enum StopScheduleJob {
@ -23,12 +25,20 @@ impl Display for StopScheduleJob {
}
}
#[derive(Clone)]
pub struct ScheduleJob {
pub job_id: Arc<RwLock<HashMap<(u64, u64), Uuid>>>,
pub scheduler: JobScheduler,
}
impl Clone for ScheduleJob {
fn clone(&self) -> Self {
ScheduleJob {
job_id: self.job_id.clone(),
scheduler: self.scheduler.clone(),
}
}
}
impl ScheduleJob {
#[instrument(level = "info")]
pub async fn start_cron_scheduler() -> Result<Self, ()> {
@ -72,12 +82,7 @@ impl ScheduleJob {
};
for concour in concours {
match self
.add_concour_cron_job(
concour.server_id,
concour.channel_id,
"0 0 17 * * *".to_string(),
http,
)
.add_concour_cron_job(concour.server_id, concour.channel_id, concour.periode, http)
.await
{
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(
&mut self,
server_id: u64,
channel_id: u64,
cron_expression: String,
_http: &Http,
http: &Http,
) -> Result<Uuid, ()> {
let job = match Job::new_async(cron_expression.as_str(), |uuid, _l| {
Box::pin(async move {
// Send the message to announce the end of the concour
// Get concour data
// Get All message since the announcement
// filter out the bot's message
// count the number of reactions per message
// get the user comment with the highest reaction
// announce the winner
// Give the winner the role reward
// update concour with the winner and increment the index
// Announce the next concour
// Or not if there is no more keyword
info!("Cron job fired: {:?}", uuid);
})
}) {
let http = Arc::new(Http::new(http.token()));
let job = match Job::new_cron_job_async_tz(
cron_expression.as_str(),
chrono::Local,
move |uuid, _l| {
Box::pin(
{
let http = http.clone();
Box::pin(async move {
info!(id = uuid.to_string(), "Cron job fired");
// Get concour data
let concour = match Concour::find_by_server_id_channel_id(
&server_id,
&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,
Err(e) => {
error!("Error creating cron job: {:?}", e);