Faculty Bot runs several automated background tasks that handle scheduled posting, feed monitoring, and dynamic channel management. These tasks run continuously in separate tokio tasks and operate independently of user commands.
The meal plan task checks the time every configured interval and posts when conditions are met:
pub async fn post_mensaplan(ctx: serenity::Context, data: Data) -> Result<(), Error> { loop { let now = chrono::Local::now(); let weekday = now.weekday(); let hour = now.hour(); // Check if it's time to post if weekday == task_conf.post_on_day && hour == task_conf.post_at.hour() { let mensa_plan = crate::utils::fetch_mensaplan(&task_conf.mealplan_settings.url) .await .unwrap(); let today = now.date_naive().format("%Y-%m-%d").to_string(); // Check if already posted today let mensaplan_posted = sqlx::query_as::<sqlx::Postgres, structs::Mensaplan>( "SELECT * FROM mensaplan WHERE date = $1", ) .bind(&today) .fetch_optional(&data.db) .await? .map(|row| row.posted) .unwrap_or(false); if !mensaplan_posted { // Post meal plan... } } // Sleep until next check tokio::time::sleep(tokio::time::Duration::from_secs( data.config.mealplan.check * 60, )) .await; }}
Every N minutes, check if current day/hour matches configured schedule
2
Database Check
Query database to see if meal plan was already posted today
let mensaplan_posted = sqlx::query_as::<sqlx::Postgres, structs::Mensaplan>( "SELECT * FROM mensaplan WHERE date = $1",).bind(&today).fetch_optional(&data.db).await?.map(|row| row.posted).unwrap_or(false);
3
Fetch Image
Download meal plan image from configured URL
4
Post Message
Send message with image and notification button to configured channel
Users can click a button to receive the meal plan notification role:
if let serenity::Interaction::MessageComponent(button) = interaction { match button.data.custom_id.as_str() { "mensaplan_notify_button" => { let role = bot_data.config.roles.mealplannotify; member .clone() .add_role(&ctx, role) .await?; button .create_interaction_response(&ctx, |f| { f.kind(serenity::InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|f| { f.flags(serenity::MessageFlags::EPHEMERAL) .content("You will now be notified when the mensaplan is updated!") }) }) .await?; } }}
The RSS task continuously monitors configured feeds for new posts:
pub async fn post_rss(ctx: serenity::Context, data: Data) -> Result<(), Error> { let conf = TaskConfigRss { map: data.config.rss_settings.rss_feed_data, clean_regex: regex::Regex::new(r"\\n(if wk med|all)").unwrap(), timeout_hrs: data.config.rss_settings.rss_check_interval_hours, }; loop { for (channel_id, feed_url) in conf.map.iter() { let channel = fetch_feed(feed_url).await.unwrap(); let items = channel.items(); for item in items { let title = item.title().unwrap(); let link = item.link().unwrap(); let description = item.description().unwrap(); let date = chrono::DateTime::parse_from_rfc2822(item.pub_date().unwrap()).unwrap(); // Check if already posted let sql_res = sqlx::query_as::<sqlx::Postgres, structs::Rss>( "SELECT * FROM posted_rss WHERE rss_title = $1 AND channel_id = $2", ) .bind(title) .bind(channel_id.0 as i64) .fetch_optional(&db) .await?; if sql_res.is_none() { // Post new item post_item(&ctx, &db, channel_id, &conf, title, link, description, &date).await?; } } } // Sleep before next check tokio::time::sleep(tokio::time::Duration::from_secs(conf.timeout_hrs * 60 * 60)).await; }}
The system can detect when RSS items are updated and post a follow-up message:
if let Some(exists) = sql_res { let msg = channel_id .message(&ctx, exists.message_id as u64) .await?; let embed = msg.embeds.first().unwrap(); let this_date = embed.timestamp.as_ref().unwrap() .parse::<chrono::DateTime<chrono::Local>>()?; let item_date = date.with_timezone(&chrono::Local); // Compare dates and post update if newer if this_date < item_date { update_posts(&ctx, &db, channel_id, &conf, title, link, description, &item_date, &msg).await?; }}
When a temporary channel becomes empty, it’s automatically deleted:
if let Some(old_chan) = old { let channel = old_chan .channel_id .unwrap_or_default() .to_channel(&ctx) .await?; if let serenity::Channel::Guild(channel) = channel { // Don't delete the create channel if channel.name() == data.config.channels.create_channel { return Ok(()); } // Only delete tracked temp channels if !created_channels .iter() .any(|c| c.channel_id == channel.id.0 as i64) { return Ok(()); } // Delete if empty if channel .members(&ctx) .await? .is_empty() { channel.delete(&ctx).await?; // Remove from database sqlx::query("DELETE FROM voice_channels WHERE channel_id = $1") .bind(channel.id.0 as i64) .execute(&mut data.db.acquire().await?) .await?; } }}