Move the prelude into a seperate package

This commit is contained in:
Natsu Kagami 2020-02-05 16:21:11 -05:00
parent d5f7a17a2c
commit 03be1a4acc
20 changed files with 88 additions and 44 deletions

View file

@ -0,0 +1,80 @@
use crate::AppData;
use serenity::{
framework::standard::{CommandError as Error, CommandResult},
http::{CacheHttp, Http},
model::id::{ChannelId, GuildId, UserId},
};
use std::{
collections::{HashMap, HashSet},
thread::{spawn, JoinHandle},
};
use youmubot_db::DB;
pub(crate) type AnnouncerChannels = DB<HashMap<String, HashMap<GuildId, ChannelId>>>;
pub trait Announcer {
fn announcer_key() -> &'static str;
fn send_messages(
c: &Http,
d: AppData,
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
) -> CommandResult;
fn set_channel(d: AppData, guild: GuildId, channel: ChannelId) -> CommandResult {
AnnouncerChannels::open(&*d.read())
.borrow_mut()?
.entry(Self::announcer_key().to_owned())
.or_default()
.insert(guild, channel);
Ok(())
}
fn get_guilds(d: AppData) -> Result<Vec<(GuildId, ChannelId)>, Error> {
let data = AnnouncerChannels::open(&*d.read())
.borrow()?
.get(Self::announcer_key())
.map(|m| m.iter().map(|(a, b)| (*a, *b)).collect())
.unwrap_or_else(|| vec![]);
Ok(data)
}
fn announce(c: impl AsRef<Http>, d: AppData) -> CommandResult {
let guilds: Vec<_> = Self::get_guilds(d.clone())?;
let member_sets = {
let mut v = Vec::with_capacity(guilds.len());
for (guild, channel) in guilds.into_iter() {
let mut s = HashSet::new();
for user in guild
.members_iter(c.as_ref())
.take_while(|u| u.is_ok())
.filter_map(|u| u.ok())
{
s.insert(user.user_id());
}
v.push((s, channel))
}
v
};
Self::send_messages(c.as_ref(), d, |user_id| {
let mut v = Vec::new();
for (members, channel) in member_sets.iter() {
if members.contains(&user_id) {
v.push(*channel);
}
}
v
})?;
Ok(())
}
fn scan(client: &serenity::Client, cooldown: std::time::Duration) -> JoinHandle<()> {
let c = client.cache_and_http.clone();
let data = client.data.clone();
spawn(move || loop {
if let Err(e) = Self::announce(c.http(), data.clone()) {
dbg!(e);
}
std::thread::sleep(cooldown);
})
}
}

View file

@ -0,0 +1,171 @@
pub use duration::Duration;
mod duration {
use std::fmt;
use std::time::Duration as StdDuration;
use String as Error;
// Parse a single duration unit
fn parse_duration_string(s: &str) -> Result<StdDuration, Error> {
// We reject the empty case
if s == "" {
return Err(Error::from("empty strings are not valid durations"));
}
struct ParseStep {
current_value: Option<u64>,
current_duration: StdDuration,
}
s.chars()
.try_fold(
ParseStep {
current_value: None,
current_duration: StdDuration::from_secs(0),
},
|s, item| match (item, s.current_value) {
('0'..='9', v) => Ok(ParseStep {
current_value: Some(v.unwrap_or(0) * 10 + ((item as u64) - ('0' as u64))),
..s
}),
(_, None) => Err(Error::from("Not a valid duration")),
(item, Some(v)) => Ok(ParseStep {
current_value: None,
current_duration: s.current_duration
+ match item.to_ascii_lowercase() {
's' => StdDuration::from_secs(1),
'm' => StdDuration::from_secs(60),
'h' => StdDuration::from_secs(60 * 60),
'd' => StdDuration::from_secs(60 * 60 * 24),
'w' => StdDuration::from_secs(60 * 60 * 24 * 7),
_ => return Err(Error::from("Not a valid duration")),
} * (v as u32),
}),
},
)
.and_then(|v| match v.current_value {
// All values should be consumed
None => Ok(v),
_ => Err(Error::from("Not a valid duration")),
})
.map(|v| v.current_duration)
}
// Our new-orphan-type of duration.
#[derive(Copy, Clone, Debug)]
pub struct Duration(pub StdDuration);
impl std::str::FromStr for Duration {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_duration_string(s).map(|v| Duration(v))
}
}
impl From<Duration> for StdDuration {
fn from(d: Duration) -> Self {
d.0
}
}
impl Duration {
fn num_weeks(&self) -> u64 {
self.0.as_secs() / (60 * 60 * 24 * 7)
}
fn num_days(&self) -> u64 {
self.0.as_secs() / (60 * 60 * 24)
}
fn num_hours(&self) -> u64 {
self.0.as_secs() / (60 * 60)
}
fn num_minutes(&self) -> u64 {
self.0.as_secs() / 60
}
fn num_seconds(&self) -> u64 {
self.0.as_secs()
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let d = self;
// weeks
let weeks = d.num_weeks();
let days = d.num_days() - d.num_weeks() * 7;
let hours = d.num_hours() - d.num_days() * 24;
let minutes = d.num_minutes() - d.num_hours() * 60;
let seconds = d.num_seconds() - d.num_minutes() * 60;
let formats = [
(weeks, "week"),
(days, "day"),
(hours, "hour"),
(minutes, "minute"),
(seconds, "second"),
];
let count = f.precision().unwrap_or(formats.len());
let mut first = true;
for (val, counter) in formats.iter().skip_while(|(a, _)| *a == 0).take(count) {
if *val > 0 {
write!(
f,
"{}{} {}{}",
(if first { "" } else { " " }),
val,
counter,
(if *val == 1 { "" } else { "s" })
)?;
first = false;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration as StdDuration;
#[test]
fn test_parse_success() {
let tests = [
(
"2D2h1m",
StdDuration::from_secs(2 * 60 * 60 * 24 + 2 * 60 * 60 + 1 * 60),
),
(
"1W2D3h4m5s",
StdDuration::from_secs(
1 * 7 * 24 * 60 * 60 + // 1W
2 * 24 * 60 * 60 + // 2D
3 * 60 * 60 + // 3h
4 * 60 + // 4m
5, // 5s
),
),
(
"1W2D3h4m5s6W",
StdDuration::from_secs(
1 * 7 * 24 * 60 * 60 + // 1W
2 * 24 * 60 * 60 + // 2D
3 * 60 * 60 + // 3h
4 * 60 + // 4m
5 + // 5s
6 * 7 * 24 * 60 * 60,
), // 6W
),
];
for (input, output) in &tests {
assert_eq!(parse_duration_string(input).unwrap(), *output);
}
}
#[test]
fn test_parse_fail() {
let tests = ["", "1w", "-1W", "1"];
for input in &tests {
assert!(
parse_duration_string(input).is_err(),
"parsing {} succeeded",
input
);
}
}
}
}

View file

@ -0,0 +1,55 @@
pub use serenity::prelude::*;
use std::sync::Arc;
pub mod announcer;
pub mod args;
pub mod setup;
pub use announcer::Announcer;
pub use args::Duration;
/// The global app data.
pub type AppData = Arc<RwLock<ShareMap>>;
/// The HTTP client.
pub struct HTTPClient;
impl TypeMapKey for HTTPClient {
type Value = reqwest::blocking::Client;
}
/// The osu! client.
// pub(crate) struct OsuClient;
// impl TypeMapKey for OsuClient {
// type Value = OsuHttpClient;
// }
/// The TypeMap trait that allows TypeMaps to quickly get a clonable item.
pub trait GetCloned {
/// Gets an item from the store, cloned.
fn get_cloned<T>(&self) -> T::Value
where
T: TypeMapKey,
T::Value: Clone + Send + Sync;
}
impl GetCloned for ShareMap {
fn get_cloned<T>(&self) -> T::Value
where
T: TypeMapKey,
T::Value: Clone + Send + Sync,
{
self.get::<T>().cloned().expect("Should be there")
}
}
impl GetCloned for AppData {
fn get_cloned<T>(&self) -> T::Value
where
T: TypeMapKey,
T::Value: Clone + Send + Sync,
{
self.read().get::<T>().cloned().expect("Should be there")
}
}

View file

@ -0,0 +1,14 @@
use serenity::{framework::standard::StandardFramework, prelude::*};
use std::path::Path;
/// Set up the prelude libraries.
///
/// Panics on failure: Youmubot should *NOT* attempt to continue when this function fails.
pub fn setup_prelude(db_path: &Path, data: &mut ShareMap, _: &mut StandardFramework) {
// Setup the announcer DB.
crate::announcer::AnnouncerChannels::insert_into(data, db_path.join("announcers.yaml"))
.expect("Announcers DB set up");
data.insert::<crate::HTTPClient>(reqwest::blocking::Client::new())
.expect("Should be able to insert");
}