mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-05-24 09:10:49 +00:00
Move the prelude into a seperate package
This commit is contained in:
parent
d5f7a17a2c
commit
03be1a4acc
20 changed files with 88 additions and 44 deletions
12
youmubot-prelude/Cargo.toml
Normal file
12
youmubot-prelude/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "youmubot-prelude"
|
||||
version = "0.1.0"
|
||||
authors = ["Natsu Kagami <natsukagami@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serenity = "0.8"
|
||||
youmubot-db = { path = "../youmubot-db" }
|
||||
reqwest = "0.10"
|
80
youmubot-prelude/src/announcer.rs
Normal file
80
youmubot-prelude/src/announcer.rs
Normal 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);
|
||||
})
|
||||
}
|
||||
}
|
171
youmubot-prelude/src/args.rs
Normal file
171
youmubot-prelude/src/args.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
55
youmubot-prelude/src/lib.rs
Normal file
55
youmubot-prelude/src/lib.rs
Normal 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")
|
||||
}
|
||||
}
|
14
youmubot-prelude/src/setup.rs
Normal file
14
youmubot-prelude/src/setup.rs
Normal 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");
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue