use std::{future, str::FromStr}; use poise::serenity_prelude::{CreateMessage, EditMessage, Message, futures::StreamExt}; use squad_quest::{SquadObject, quest::{Quest, QuestDifficulty}}; use toml::value::Date; use crate::{Context, Error,commands::guild}; async fn find_quest_message(ctx: Context<'_>, id: u16) -> Result, Error>{ ctx.defer().await?; let dc = ctx.data().discord.clone(); let channel = { let guard = dc.lock().expect("shouldn't be locked"); guard.quests_channel }; let messages = channel.messages_iter(ctx) .filter_map(|m| async move { if m.is_ok() { Some(m.unwrap()) } else { eprintln!("{}", m.unwrap_err()); None } }) .filter(|m| { future::ready(m.content.contains(&format!("#{id}"))) }) .collect::>().await; Ok(messages.first().cloned()) } fn make_quest_message_content(ctx: Context<'_>, quest: &Quest) -> String { let strings = &ctx.data().strings; let formatter = strings.formatter().quest(quest); formatter.fmt(&strings.quest.message_format) } #[poise::command( prefix_command, slash_command, guild_only, check = "guild", subcommands("list", "create", "update", "publish", "delete"), required_permissions = "ADMINISTRATOR", name_localized("ru", "квест"), )] pub async fn quest( _ctx: Context<'_>, ) -> Result<(), Error> { Ok(()) } /// List all quests #[poise::command( prefix_command, slash_command, guild_only, check = "guild", required_permissions = "ADMINISTRATOR", name_localized("ru", "список"), description_localized("ru", "Вывести все квесты") )] pub async fn list( ctx: Context<'_>, ) -> Result<(), Error> { let conf = &ctx.data().config; let quests = conf.load_quests(); let strings = &ctx.data().strings; let mut formatter = strings.formatter().value(quests.len()); let mut reply_string = formatter.fmt(&strings.quest.list); for quest in quests { formatter = formatter.quest(&quest); reply_string.push_str(formatter.fmt(&strings.quest.list_item).as_str()); } ctx.reply(reply_string).await?; Ok(()) } #[derive(Debug, poise::ChoiceParameter)] pub enum DifficultyWrapper { Easy, Normal, Hard, Secret, } impl From for QuestDifficulty { fn from(value: DifficultyWrapper) -> Self { match &value { DifficultyWrapper::Easy => Self::Easy, DifficultyWrapper::Normal => Self::Normal, DifficultyWrapper::Hard => Self::Hard, DifficultyWrapper::Secret => Self::Secret, } } } #[derive(serde::Deserialize)] struct DateWrapper { date: Date, } impl FromStr for DateWrapper { type Err = toml::de::Error; fn from_str(s: &str) -> Result { let toml_str = format!("date = {s}"); let wrapper: Self = toml::from_str(&toml_str)?; Ok(wrapper) } } impl From for Date { fn from(value: DateWrapper) -> Self { value.date } } /// Create quest and print its identifier #[poise::command( prefix_command, slash_command, required_permissions = "ADMINISTRATOR", guild_only, check = "guild", name_localized("ru", "создать"), description_localized("ru", "Создать квест и получить его идентификатор"), )] pub async fn create( ctx: Context<'_>, #[description = "Quest difficulty"] #[name_localized("ru", "сложность")] #[description_localized("ru", "Сложность квеста")] difficulty: DifficultyWrapper, #[description = "Reward for the quest"] #[name_localized("ru", "награда")] #[description_localized("ru", "Награда за квест")] reward: u32, #[description = "Quest name"] #[name_localized("ru", "название")] #[description_localized("ru", "Название квеста")] name: String, #[description = "Quest description"] #[name_localized("ru", "описание")] #[description_localized("ru", "Описание квеста")] description: String, #[description = "Expected answer, visible when user posts their answer for review"] #[name_localized("ru", "ответ")] #[description_localized("ru", "Ожидаемый результат, отображаемый при проверке ответа игрока")] answer: String, #[description = "Date of publication (in format of YYYY-MM-DD, e.g. 2025-12-24)"] #[name_localized("ru", "доступен")] #[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24)")] available: Option, /* #[description = "Deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"] #[name_localized("ru", "дедлайн")] #[description_localized("ru", "Дедлайн (в формате ГГГГ-ММ-ДД), напр. 2025-12-24")] deadline: Option, */ ) -> Result<(), Error> { let conf = &ctx.data().config; let mut quests = conf.load_quests(); quests.sort_by(|a,b| a.id.cmp(&b.id)); let next_id = match quests.last() { Some(quest) => quest.id + 1u16, None => 0u16 }; let available_on = match available { Some(avail) => Some(avail.into()), None => None, }; /* let deadline = match deadline { Some(dl) => Some(dl.into()), None => None, }; */ let quest = Quest { id: next_id, difficulty: difficulty.into(), reward, name, description, answer, public: false, available_on, //deadline, ..Default::default() }; let path = conf.full_quests_path(); quest.save(path)?; let strings = &ctx.data().strings; let formatter = strings.formatter().quest(&quest); let reply_string = formatter.fmt(&strings.quest.create); ctx.reply(reply_string).await?; Ok(()) } /// Update quest values by its identifier and new given values #[poise::command( prefix_command, slash_command, required_permissions = "ADMINISTRATOR", guild_only, check = "guild", name_localized("ru", "обновить"), description_localized("ru", "Обновить выбранные значения указанного квеста"), )] pub async fn update( ctx: Context<'_>, #[description = "Quest identifier"] #[name_localized("ru", "идентификатор")] #[description_localized("ru", "Идентификатор квеста")] id: u16, #[description = "Quest difficulty"] #[name_localized("ru", "сложность")] #[description_localized("ru", "Сложность квеста")] difficulty: Option, #[description = "Reward for the quest"] #[name_localized("ru", "награда")] #[description_localized("ru", "Награда за квест")] reward: Option, #[description = "Quest name"] #[name_localized("ru", "название")] #[description_localized("ru", "Название квеста")] name: Option, #[description = "Quest description"] #[name_localized("ru", "описание")] #[description_localized("ru", "Описание квеста")] description: Option, #[description = "Expected answer, visible when user posts their answer for review"] #[name_localized("ru", "ответ")] #[description_localized("ru", "Ожидаемый результат, отображаемый при проверке ответа игрока")] answer: Option, #[description = "Date of publication (in format of YYYY-MM-DD, e.g. 2025-12-24)"] #[name_localized("ru", "доступен")] #[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24")] available: Option, /* #[description = "Quest deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"] #[name_localized("ru", "дедлайн")] #[description_localized("ru", "Дедлайн (в формате ГГГГ-ММ-ДД, напр. 2025-12-24)")] deadline: Option, #[description = "Reset availability and deadline if checked"] #[description_localized("ru", "Если выбрано, сбросить доступность и дедлайн")] */ #[description = "Reset availability if checked"] #[description_localized("ru", "Если выбрано, сбросить доступность")] #[name_localized("ru", "сброс")] reset: Option, ) -> Result<(), Error> { let conf = &ctx.data().config; let quests = conf.load_quests(); let Some(quest) = quests.iter().find(|q| q.id == id) else { return Err(Error::QuestNotFound(id)); }; let difficulty = match difficulty { Some(d) => d.into(), None => quest.difficulty }; let available_on: Option; //let dead_line: Option; match reset.unwrap_or(false) { true => { available_on = None; //dead_line = None; }, false => { available_on = available.map_or_else(|| quest.available_on.clone(), |v| Some(v.into())); //dead_line = deadline.map_or_else(|| quest.deadline.clone(), |v| Some(v.into())); }, } let new_quest = Quest { id, difficulty, reward: reward.unwrap_or(quest.reward), name: name.unwrap_or(quest.name.clone()), description: description.unwrap_or(quest.description.clone()), answer: answer.unwrap_or(quest.answer.clone()), public: quest.public, available_on, //deadline: dead_line, ..Default::default() }; let strings = &ctx.data().strings; let formatter = strings.formatter().quest(&new_quest); if new_quest.public { let content = make_quest_message_content(ctx, &new_quest); let builder = EditMessage::new().content(content); let message = find_quest_message(ctx, id).await?; if let Some(mut message) = message { message.edit(ctx, builder).await?; } else { let reply_string = formatter.fmt(&strings.quest.message_not_found); ctx.reply(reply_string).await?; } } let path = conf.full_quests_path(); new_quest.save(path)?; let reply_string = formatter.fmt(&strings.quest.update); ctx.reply(reply_string).await?; Ok(()) } pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result { quest.public = true; let quests_path = ctx.data().config.full_quests_path(); quest.save(quests_path)?; let content = make_quest_message_content(ctx, &quest); let builder = CreateMessage::new() .content(content); let dc = ctx.data().discord.clone(); let channel = { let guard = dc.lock().expect("shouldn't be locked"); guard.quests_channel }; match channel.send_message(ctx, builder).await { Ok(m) => Ok(m), Err(error) => Err(Error::SerenityError(error)), } } /// Mark quest as public and send its message in quests channel #[poise::command( prefix_command, slash_command, required_permissions = "ADMINISTRATOR", guild_only, check = "guild", name_localized("ru", "опубликовать"), description_localized("ru", "Отметить квест как публичный и отправить его сообщение в канал квестов"), )] pub async fn publish( ctx: Context<'_>, #[description = "Quest identifier"] #[name_localized("ru", "идентификатор")] #[description_localized("ru", "Идентификатор квеста")] id: u16, ) -> Result<(), Error> { let mut quests = ctx.data().config.load_quests(); let Some(quest) = quests.iter_mut().find(|q| q.id == id) else { return Err(Error::QuestNotFound(id)); }; if quest.public { return Err(Error::QuestIsPublic(id)); } let message = publish_inner(ctx, quest).await?; let strings = &ctx.data().strings; let formatter = strings.formatter() .quest(&quest) .message(&message); let reply_string = formatter.fmt(&strings.quest.publish); ctx.reply(reply_string).await?; Ok(()) } /// Delete quest (and its message, if published) #[poise::command( prefix_command, slash_command, required_permissions = "ADMINISTRATOR", guild_only, check = "guild", name_localized("ru", "удалить"), description_localized("ru", "Удалить квест (и его сообщение, если он был опубликован)"), )] pub async fn delete( ctx: Context<'_>, #[description = "Quest identifier"] #[name_localized("ru", "идентификатор")] #[description_localized("ru", "Идентификатор квеста")] id: u16, ) -> Result<(), Error> { if let Some(msg) = find_quest_message(ctx, id).await? { msg.delete(ctx).await?; } let mut path = ctx.data().config.full_quests_path(); path.push(format!("{id}.toml")); Quest::delete(path)?; let mut accounts = ctx.data().config.load_accounts(); let accounts_path = ctx.data().config.full_accounts_path(); for account in accounts.iter_mut().filter(|a| a.quests_completed.contains(&id)) { let index = account.quests_completed.iter().position(|qid| *qid == id).expect("We just filtered it"); account.quests_completed.remove(index); account.save(accounts_path.clone())?; } let mock_quest = Quest { id, ..Default::default() }; let strings = &ctx.data().strings; let formatter = strings.formatter().quest(&mock_quest); let reply_string = formatter.fmt(&strings.quest.delete); ctx.reply(reply_string).await?; Ok(()) }