General cleanup after the first game
+ Randomized codename order obscuring players more + Added better error handling code + Support hour + minute time increments + Refactored layout to be more clear + clippy + fmt
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use serenity::framework::standard::macros::{command, group, help, hook};
|
||||
use serenity::framework::standard::{
|
||||
help_commands, Args, CommandGroup, CommandResult, HelpOptions,
|
||||
};
|
||||
use serenity::framework::StandardFramework;
|
||||
use serenity::model::guild::Member;
|
||||
use serenity::model::id::ChannelId;
|
||||
use serenity::model::prelude::{Message, UserId};
|
||||
use serenity::prelude::Context;
|
||||
use serenity::utils::MessageBuilder;
|
||||
|
||||
use crate::discord::helper::{
|
||||
add_user_to_game, build_system_message, parse_duration_arg, send_msg_to_player_channels,
|
||||
};
|
||||
use crate::error::{Result, WoxlfError};
|
||||
use crate::game::global_data::GlobalData;
|
||||
use crate::game::MessageSource;
|
||||
use crate::game::Phase;
|
||||
|
||||
#[group]
|
||||
#[commands(start, say, end, broadcast, next_phase, terminate, add_time)]
|
||||
struct Host;
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[allowed_roles("wolfx host")]
|
||||
async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
msg.channel_id.say(&ctx.http, "Starting game").await?;
|
||||
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let duration = parse_duration_arg(&mut args).await?;
|
||||
|
||||
global_data.start_game(Phase::Night, duration.into());
|
||||
|
||||
let players: Result<Vec<&Member>> = args
|
||||
.iter::<String>()
|
||||
.flatten()
|
||||
.map(|discord_id| {
|
||||
let discord_id = match discord_id.parse::<u64>() {
|
||||
Ok(discord_id) => discord_id,
|
||||
Err(_) => {
|
||||
return Err(WoxlfError::DiscordIdParseError(discord_id));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(discord_user) = guild.members.get(&UserId::from(discord_id)) {
|
||||
Ok(discord_user)
|
||||
} else {
|
||||
Err(WoxlfError::DiscordIdParseError(discord_id.to_string()))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut players = match players {
|
||||
Ok(players) => players,
|
||||
Err(e) => {
|
||||
let err_msg = match e {
|
||||
WoxlfError::DiscordIdParseError(e) => {
|
||||
format!("Error parsing '{}' as a discord user id.", e)
|
||||
}
|
||||
_ => "Internal bot error".to_string(),
|
||||
};
|
||||
|
||||
msg.reply(&ctx.http, err_msg).await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
players.shuffle(&mut thread_rng());
|
||||
|
||||
for player in players {
|
||||
add_user_to_game(ctx, &guild, &mut global_data, player).await?;
|
||||
}
|
||||
|
||||
global_data.save_game_state().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[allowed_roles("wolfx host")]
|
||||
async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
for player_data in &global_data.game_state_mut()?.player_data {
|
||||
let channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(player_data.channel))
|
||||
.unwrap();
|
||||
|
||||
channel.delete(&ctx.http).await?;
|
||||
}
|
||||
|
||||
global_data.clear_game_state()?;
|
||||
|
||||
msg.reply(&ctx.http, "Game ended!").await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[allowed_roles("wolfx host")]
|
||||
async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let msg = format!("**wOxlf **> {}", args.rest());
|
||||
|
||||
send_msg_to_player_channels(
|
||||
ctx,
|
||||
&guild,
|
||||
&mut global_data,
|
||||
MessageSource::Host,
|
||||
&msg,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[allowed_roles("wolfx host")]
|
||||
async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let msg = build_system_message(args.rest());
|
||||
|
||||
send_msg_to_player_channels(
|
||||
ctx,
|
||||
&guild,
|
||||
&mut global_data,
|
||||
MessageSource::Host,
|
||||
&msg,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[allowed_roles("wolfx host")]
|
||||
async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let duration = parse_duration_arg(&mut args).await?;
|
||||
|
||||
global_data.game_state_mut()?.next_phase(duration.into());
|
||||
|
||||
let broadcast = MessageBuilder::new()
|
||||
.push_line(args.rest())
|
||||
.push_line("")
|
||||
.push(global_data.print_game_status())
|
||||
.build();
|
||||
|
||||
let broadcast = build_system_message(&broadcast);
|
||||
|
||||
send_msg_to_player_channels(
|
||||
ctx,
|
||||
&guild,
|
||||
&mut global_data,
|
||||
MessageSource::Host,
|
||||
&broadcast,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if global_data.game_state_mut()?.current_phase == Phase::Day {
|
||||
let vote_channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(global_data.cfg.vote_channel))
|
||||
.unwrap();
|
||||
vote_channel
|
||||
.send_message(&ctx.http, |m| {
|
||||
m.content(format!(
|
||||
"**DAY {} VOTES:**",
|
||||
global_data.game_state_mut().unwrap().phase_number
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
msg.reply(
|
||||
&ctx.http,
|
||||
format!(
|
||||
"Phase has been cycled to {}.",
|
||||
&global_data.game_state_mut()?.current_phase
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
global_data.save_game_state()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[allowed_roles("wolfx host")]
|
||||
async fn terminate(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let target = args.rest().to_lowercase();
|
||||
let index = global_data
|
||||
.game_state_mut()?
|
||||
.player_data
|
||||
.iter()
|
||||
.position(|p| p.codename.to_lowercase() == target);
|
||||
|
||||
if let Some(index) = index {
|
||||
let player = global_data.game_state_mut()?.player_data.remove(index);
|
||||
|
||||
let player_channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(player.channel))
|
||||
.unwrap();
|
||||
|
||||
player_channel.delete(&ctx.http).await.unwrap();
|
||||
|
||||
msg.reply(
|
||||
&ctx.http,
|
||||
format!("{} has been terminated.", player.codename),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
msg.reply(&ctx.http, "No subject found with that codename.")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
global_data.save_game_state().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[allowed_roles("wolfx host")]
|
||||
async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let duration = parse_duration_arg(&mut args).await?;
|
||||
|
||||
global_data
|
||||
.game_state_mut()?
|
||||
.add_time_to_phase(duration.into());
|
||||
|
||||
let broadcast = MessageBuilder::new()
|
||||
.push_line("EXPERIMENT PHASE HAS BEEN EXTENDED!!!")
|
||||
.push_line("")
|
||||
.push(global_data.print_game_status())
|
||||
.build();
|
||||
|
||||
let broadcast = build_system_message(&broadcast);
|
||||
|
||||
send_msg_to_player_channels(
|
||||
ctx,
|
||||
&guild,
|
||||
&mut global_data,
|
||||
MessageSource::Host,
|
||||
&broadcast,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
msg.reply(&ctx.http, "Phase has been updated")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
global_data.save_game_state().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[group]
|
||||
#[commands(vote, status, players)]
|
||||
struct Player;
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[description = "Vote another subject for termination. $vote <code_name>"]
|
||||
async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
if global_data.game_state_mut()?.current_phase != Phase::Day {
|
||||
msg.reply(
|
||||
&ctx.http,
|
||||
"You can only select subject for termination during the day!",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if global_data
|
||||
.game_state_mut()?
|
||||
.get_player_from_channel(msg.channel_id.0)
|
||||
.is_some()
|
||||
{
|
||||
let target_player = global_data
|
||||
.game_state_mut()?
|
||||
.get_player_by_codename(args.rest());
|
||||
|
||||
if let Some(target_player) = target_player {
|
||||
let vote_channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(global_data.cfg.vote_channel))
|
||||
.unwrap();
|
||||
|
||||
let player_data = global_data
|
||||
.game_state_mut()?
|
||||
.get_player_from_channel_mut(msg.channel_id.0)
|
||||
.unwrap();
|
||||
|
||||
player_data.cast_vote(target_player.discord_id);
|
||||
|
||||
vote_channel
|
||||
.send_message(&ctx.http, |m| {
|
||||
m.content(format!(
|
||||
"{} has selected {} for termination",
|
||||
&player_data.codename, target_player.codename
|
||||
))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
msg.reply(&ctx.http, "Subject not found!").await.unwrap();
|
||||
}
|
||||
} else {
|
||||
msg.reply(
|
||||
&ctx.http,
|
||||
"This command needs to be run in a game channel, goober",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
global_data.save_game_state().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[description = "Get the game status. $status"]
|
||||
async fn status(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let mut msg_builder = MessageBuilder::new();
|
||||
|
||||
msg_builder.push(global_data.print_game_status());
|
||||
|
||||
if global_data.game_state_mut()?.current_phase == Phase::Day {
|
||||
msg_builder.push_line("");
|
||||
|
||||
let vote_tallies = global_data.game_state_mut()?.get_vote_tallies();
|
||||
|
||||
if vote_tallies.is_empty() {
|
||||
msg_builder.push_line("NO TERMINATION VOTES HAVE BEEN CAST");
|
||||
} else {
|
||||
msg_builder.push_line("TERMINATION VOTE TALLIES:");
|
||||
for (player, tally) in global_data.game_state_mut()?.get_vote_tallies() {
|
||||
msg_builder.push_line(format!("{}: {}", player, tally));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg.reply(&ctx.http, msg_builder.build()).await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[description = "Get the other players in the game. $status"]
|
||||
async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let global_data = data.get::<GlobalData>().unwrap();
|
||||
|
||||
let global_data = global_data.lock().await;
|
||||
|
||||
let mut msg_builder = MessageBuilder::new();
|
||||
|
||||
msg_builder.push_line("Test Subjects:");
|
||||
|
||||
for player in &global_data.game_state()?.player_data {
|
||||
msg_builder.push("* ").push(&player.codename);
|
||||
|
||||
if msg.channel_id.0 == global_data.cfg.host_channel {
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
let member = guild.members.get(&UserId::from(player.discord_id)).unwrap();
|
||||
msg_builder.push_line(format!(" ({})", member.display_name()));
|
||||
} else {
|
||||
msg_builder.push_line("");
|
||||
}
|
||||
}
|
||||
|
||||
msg.reply(&ctx.http, msg_builder.build()).await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[help]
|
||||
#[individual_command_tip = "If you want more information about a specific command, just pass the command as argument."]
|
||||
#[command_not_found_text = "Could not find: `{}`."]
|
||||
#[max_levenshtein_distance(3)]
|
||||
#[indention_prefix = "+"]
|
||||
#[lacking_role = "Strike"]
|
||||
#[wrong_channel = "Strike"]
|
||||
async fn help(
|
||||
context: &Context,
|
||||
msg: &Message,
|
||||
args: Args,
|
||||
help_options: &'static HelpOptions,
|
||||
groups: &[&'static CommandGroup],
|
||||
owners: HashSet<UserId>,
|
||||
) -> CommandResult {
|
||||
let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[hook]
|
||||
async fn handle_errors(
|
||||
ctx: &Context,
|
||||
msg: &Message,
|
||||
command_name: &str,
|
||||
command_result: CommandResult,
|
||||
) {
|
||||
match command_result {
|
||||
Ok(()) => println!("Successfully processed command '{}'", command_name),
|
||||
Err(err) => {
|
||||
let reply_msg = format!(
|
||||
"Command '{}' returned an error. {}",
|
||||
command_name,
|
||||
err.to_string()
|
||||
);
|
||||
println!("{}", reply_msg);
|
||||
msg.reply(&ctx.http, reply_msg).await.unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn command_framework() -> StandardFramework {
|
||||
StandardFramework::new()
|
||||
.configure(|c| c.prefix('$'))
|
||||
.group(&HOST_GROUP)
|
||||
.group(&PLAYER_GROUP)
|
||||
.help(&HELP)
|
||||
.after(handle_errors)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use serenity::client::Context;
|
||||
use serenity::framework::standard::Args;
|
||||
use serenity::http::AttachmentType;
|
||||
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
|
||||
use serenity::model::guild::{Guild, Member};
|
||||
use serenity::model::id::{ChannelId, UserId};
|
||||
use serenity::model::Permissions;
|
||||
use serenity::utils::MessageBuilder;
|
||||
|
||||
use crate::error::WoxlfError;
|
||||
use crate::game::game_state::PhaseDuration;
|
||||
use crate::game::global_data::GlobalData;
|
||||
use crate::game::player_data::PlayerData;
|
||||
use crate::game::MessageSource;
|
||||
use crate::{error, game};
|
||||
|
||||
pub async fn send_msg_to_player_channels(
|
||||
ctx: &Context,
|
||||
guild: &Guild,
|
||||
global_data: &mut GlobalData,
|
||||
msg_source: MessageSource,
|
||||
msg: &str,
|
||||
attachment: Option<Vec<AttachmentType<'_>>>,
|
||||
pin: bool,
|
||||
) -> error::Result<()> {
|
||||
for player_data in &global_data.game_state_mut()?.player_data {
|
||||
if let MessageSource::Player(channel_id) = msg_source {
|
||||
if channel_id == player_data.channel {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(player_data.channel))
|
||||
.unwrap();
|
||||
|
||||
let msg = channel
|
||||
.send_message(&ctx.http, |m| {
|
||||
m.content(&msg);
|
||||
|
||||
if let Some(attachment) = attachment.clone() {
|
||||
m.add_files(attachment);
|
||||
}
|
||||
|
||||
m
|
||||
})
|
||||
.await?;
|
||||
|
||||
if pin {
|
||||
// pin system messages
|
||||
msg.pin(&ctx.http).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let host_channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(global_data.cfg.host_channel))
|
||||
.unwrap();
|
||||
|
||||
let source = match msg_source {
|
||||
MessageSource::Player(channel_id) => {
|
||||
let discord_id = global_data
|
||||
.game_state_mut()?
|
||||
.get_player_from_channel(channel_id)
|
||||
.unwrap()
|
||||
.discord_id;
|
||||
let name = guild
|
||||
.members
|
||||
.get(&UserId::from(discord_id))
|
||||
.unwrap()
|
||||
.display_name();
|
||||
|
||||
name.to_string()
|
||||
}
|
||||
MessageSource::Host => "Host".to_string(),
|
||||
MessageSource::Automated => "Automated".to_string(),
|
||||
};
|
||||
|
||||
host_channel
|
||||
.send_message(&ctx.http, |m| {
|
||||
m.content(format!("({}): {}", source, msg));
|
||||
if let Some(attachment) = attachment {
|
||||
m.add_files(attachment);
|
||||
}
|
||||
|
||||
m
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_user_to_game(
|
||||
ctx: &Context,
|
||||
guild: &Guild,
|
||||
global_data: &mut GlobalData,
|
||||
discord_user: &Member,
|
||||
) -> error::Result<()> {
|
||||
let mut codename = game::generate_codename(&global_data.cfg);
|
||||
|
||||
while global_data.game_state_mut()?.codename_exists(&codename) {
|
||||
codename = game::generate_codename(&global_data.cfg);
|
||||
}
|
||||
|
||||
let channel = guild
|
||||
.create_channel(&ctx.http, |c| {
|
||||
c.category(&ChannelId::from(global_data.cfg.category))
|
||||
.name(format!("{}'s Channel", discord_user.display_name()))
|
||||
})
|
||||
.await?;
|
||||
|
||||
let allow =
|
||||
Permissions::SEND_MESSAGES | Permissions::READ_MESSAGE_HISTORY | Permissions::READ_MESSAGES;
|
||||
|
||||
let overwrite = PermissionOverwrite {
|
||||
allow,
|
||||
deny: Default::default(),
|
||||
kind: PermissionOverwriteType::Member(discord_user.user.id),
|
||||
};
|
||||
|
||||
channel.create_permission(&ctx.http, &overwrite).await?;
|
||||
|
||||
let msg = channel.send_message(&ctx.http, |m| {
|
||||
m.content(MessageBuilder::new()
|
||||
.push("Welcome ")
|
||||
.mention(discord_user)
|
||||
.push_line(" to your WOxlf Terminal. You may use this terminal to communicate to other subjects.")
|
||||
.push_line("You will also use this terminal for choosing one of your fellow subjects for termination.")
|
||||
.push_line("Happy testing :)")
|
||||
.push_line("")
|
||||
.push_line("Do $help to see all commands.")
|
||||
.push_line("")
|
||||
.push("SUBJECT CODENAME: ")
|
||||
.push_line(&codename)
|
||||
.push(global_data.print_game_status())
|
||||
)
|
||||
}).await?;
|
||||
|
||||
channel.pin(&ctx.http, msg.id).await?;
|
||||
|
||||
let player_data = PlayerData {
|
||||
channel: channel.id.0,
|
||||
discord_id: discord_user.user.id.0,
|
||||
vote_target: None,
|
||||
codename,
|
||||
};
|
||||
|
||||
global_data.game_state_mut()?.player_data.push(player_data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_system_message(msg: &str) -> String {
|
||||
MessageBuilder::new()
|
||||
.push_bold_line("\\*\\*IMPORTANT wOxlf SYSTEM MESSAGE\\*\\*")
|
||||
.push_line("")
|
||||
.push_line(msg)
|
||||
.push_line("")
|
||||
.push_bold_line("\\*\\*END OF SYSTEM MESSAGE\\*\\*")
|
||||
.build()
|
||||
}
|
||||
|
||||
pub async fn parse_duration_arg(args: &mut Args) -> error::Result<PhaseDuration> {
|
||||
match args.single::<PhaseDuration>() {
|
||||
Ok(d) => Ok(d),
|
||||
Err(_) => Err(WoxlfError::DurationParseError),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod commands;
|
||||
pub mod helper;
|
||||
Reference in New Issue
Block a user