mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-16 07:18:54 +00:00
Merge branch 'master' into codeforces
This commit is contained in:
commit
3998617f97
8 changed files with 444 additions and 127 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1777,6 +1777,7 @@ name = "youmubot-prelude"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"youmubot-db 0.1.0",
|
||||
|
|
121
youmubot-cf/src/live.rs
Normal file
121
youmubot-cf/src/live.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
use codeforces::{Problem, ProblemResult, ProblemResultType, RanklistRow};
|
||||
use serenity::{
|
||||
framework::standard::CommandResult,
|
||||
model::{
|
||||
guild::Member,
|
||||
id::{ChannelId, GuildId},
|
||||
},
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
/// Watch and commentate a contest.
|
||||
///
|
||||
/// Does the thing on a channel, block until the contest ends.
|
||||
pub fn watch_contest(
|
||||
ctx: &mut Context,
|
||||
guild: GuildId,
|
||||
channel: ChannelId,
|
||||
contest_id: u64,
|
||||
) -> CommandResult {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn scan_changes(
|
||||
http: <HTTPClient as TypeMapKey>::Value,
|
||||
members: &[(Member, &str)],
|
||||
) -> Vec<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn translate_change(
|
||||
handle: &str,
|
||||
row: &RanklistRow,
|
||||
member: &Member,
|
||||
problem: &Problem,
|
||||
res: &ProblemResult,
|
||||
change: Change,
|
||||
) -> String {
|
||||
let mut m = MessageBuilder::new();
|
||||
m.push_bold_safe(handle)
|
||||
.push(" (")
|
||||
.push_safe(member.distinct())
|
||||
.push(")");
|
||||
|
||||
use Change::*;
|
||||
match change {
|
||||
PretestsPassed => m
|
||||
.push(" passed the pretest on problem ")
|
||||
.push_bold_safe(format!("{} - {}", problem.index, problem.name))
|
||||
.push(", scoring ")
|
||||
.push_bold(format!("{:.0}", res.points))
|
||||
.push(" points, now at ")
|
||||
.push_bold(format!("#{}", row.rank))
|
||||
.push("! 🍷"),
|
||||
Attempted => m
|
||||
.push(format!(
|
||||
" made another attempt (total **{}**) on problem ",
|
||||
res.rejected_attempt_count
|
||||
))
|
||||
.push_bold_safe(format!("{} - {}", problem.index, problem.name))
|
||||
.push(", but did not get the right answer. Keep going! 🏃♂️"),
|
||||
Hacked => m
|
||||
.push(" got **hacked** on problem ")
|
||||
.push_bold_safe(format!("{} - {}", problem.index, problem.name))
|
||||
.push(", now at rank ")
|
||||
.push_bold(format!("#{}", row.rank))
|
||||
.push("... Find your bug!🐛"),
|
||||
Accepted => m
|
||||
.push(" got **Accepted** on the **final tests** on problem ")
|
||||
.push_bold_safe(format!("{} - {}", problem.index, problem.name))
|
||||
.push(", permanently scoring ")
|
||||
.push_bold(format!("{:.0}", res.points))
|
||||
.push(" points, now at ")
|
||||
.push_bold(format!("#{}", row.rank))
|
||||
.push(" 🎉"),
|
||||
TestFailed => m
|
||||
.push(" **failed** on the **final tests** on problem ")
|
||||
.push_bold_safe(format!("{} - {}", problem.index, problem.name))
|
||||
.push(", now at ")
|
||||
.push_bold(format!("#{}", row.rank))
|
||||
.push(" 😭"),
|
||||
};
|
||||
m.build()
|
||||
}
|
||||
|
||||
enum Change {
|
||||
PretestsPassed,
|
||||
Attempted,
|
||||
Hacked,
|
||||
Accepted,
|
||||
TestFailed,
|
||||
}
|
||||
|
||||
fn analyze_change(old: &ProblemResult, new: &ProblemResult) -> Option<Change> {
|
||||
use Change::*;
|
||||
if old.points == new.points {
|
||||
if new.rejected_attempt_count > old.rejected_attempt_count {
|
||||
return Some(Attempted);
|
||||
}
|
||||
if old.result_type != new.result_type {
|
||||
return Some(Accepted);
|
||||
}
|
||||
None
|
||||
} else {
|
||||
if new.points == 0.0 {
|
||||
if new.result_type == ProblemResultType::Preliminary {
|
||||
Some(Hacked)
|
||||
} else {
|
||||
Some(TestFailed)
|
||||
}
|
||||
} else if new.points > old.points {
|
||||
if new.result_type == ProblemResultType::Preliminary {
|
||||
Some(PretestsPassed)
|
||||
} else {
|
||||
Some(Accepted)
|
||||
}
|
||||
} else {
|
||||
Some(PretestsPassed)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,87 +5,79 @@ use crate::{
|
|||
request::{BeatmapRequestKind, UserID},
|
||||
Client as Osu,
|
||||
};
|
||||
use announcer::MemberToChannels;
|
||||
use rayon::prelude::*;
|
||||
use serenity::{
|
||||
framework::standard::{CommandError as Error, CommandResult},
|
||||
http::Http,
|
||||
model::id::{ChannelId, UserId},
|
||||
http::CacheHttp,
|
||||
CacheAndHttp,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
/// osu! announcer's unique announcer key.
|
||||
pub const ANNOUNCER_KEY: &'static str = "osu";
|
||||
|
||||
/// Announce osu! top scores.
|
||||
pub struct OsuAnnouncer;
|
||||
|
||||
impl Announcer for OsuAnnouncer {
|
||||
fn announcer_key() -> &'static str {
|
||||
"osu"
|
||||
}
|
||||
fn send_messages(
|
||||
c: &Http,
|
||||
d: AppData,
|
||||
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
|
||||
) -> CommandResult {
|
||||
let osu = d.get_cloned::<OsuClient>();
|
||||
// For each user...
|
||||
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||
'user_loop: for (user_id, osu_user) in data.iter_mut() {
|
||||
let mut pp_values = vec![]; // Store the pp values here...
|
||||
for mode in &[Mode::Std, Mode::Taiko, Mode::Mania, Mode::Catch] {
|
||||
let scores = OsuAnnouncer::scan_user(&osu, osu_user, *mode)?;
|
||||
if scores.is_empty() && !osu_user.pp.is_empty() {
|
||||
// Nothing to update: no new scores and pp is there.
|
||||
pp_values.push(osu_user.pp[*mode as usize]);
|
||||
continue;
|
||||
}
|
||||
let user = match osu.user(UserID::ID(osu_user.id), |f| f.mode(*mode)) {
|
||||
Ok(Some(u)) => u,
|
||||
_ => continue 'user_loop,
|
||||
};
|
||||
pp_values.push(user.pp);
|
||||
scores
|
||||
.into_par_iter()
|
||||
.filter_map(|(rank, score)| {
|
||||
let beatmap = osu
|
||||
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| f)
|
||||
.map(|v| BeatmapWithMode(v.into_iter().next().unwrap(), *mode));
|
||||
let channels = channels(*user_id);
|
||||
match beatmap {
|
||||
Ok(v) => Some((rank, score, v, channels)),
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.for_each(|(rank, score, beatmap, channels)| {
|
||||
for channel in channels {
|
||||
if let Err(e) = channel.send_message(c, |c| {
|
||||
c.content(format!("New top record from {}!", user_id.mention()))
|
||||
.embed(|e| score_embed(&score, &beatmap, &user, Some(rank), e))
|
||||
}) {
|
||||
dbg!(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) -> CommandResult {
|
||||
let osu = d.get_cloned::<OsuClient>();
|
||||
// For each user...
|
||||
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||
'user_loop: for (user_id, osu_user) in data.iter_mut() {
|
||||
let mut pp_values = vec![]; // Store the pp values here...
|
||||
for mode in &[Mode::Std, Mode::Taiko, Mode::Mania, Mode::Catch] {
|
||||
let scores = scan_user(&osu, osu_user, *mode)?;
|
||||
if scores.is_empty() && !osu_user.pp.is_empty() {
|
||||
// Nothing to update: no new scores and pp is there.
|
||||
pp_values.push(osu_user.pp[*mode as usize]);
|
||||
continue;
|
||||
}
|
||||
osu_user.last_update = chrono::Utc::now();
|
||||
osu_user.pp = pp_values;
|
||||
let user = match osu.user(UserID::ID(osu_user.id), |f| f.mode(*mode)) {
|
||||
Ok(Some(u)) => u,
|
||||
_ => continue 'user_loop,
|
||||
};
|
||||
pp_values.push(user.pp);
|
||||
scores
|
||||
.into_par_iter()
|
||||
.filter_map(|(rank, score)| {
|
||||
let beatmap = osu
|
||||
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| f)
|
||||
.map(|v| BeatmapWithMode(v.into_iter().next().unwrap(), *mode));
|
||||
let channels = channels.channels_of(c.clone(), *user_id);
|
||||
match beatmap {
|
||||
Ok(v) => Some((rank, score, v, channels)),
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.for_each(|(rank, score, beatmap, channels)| {
|
||||
for channel in channels {
|
||||
if let Err(e) = channel.send_message(c.http(), |c| {
|
||||
c.content(format!("New top record from {}!", user_id.mention()))
|
||||
.embed(|e| score_embed(&score, &beatmap, &user, Some(rank), e))
|
||||
}) {
|
||||
dbg!(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Update users
|
||||
*OsuSavedUsers::open(&*d.read()).borrow_mut()? = data;
|
||||
Ok(())
|
||||
osu_user.last_update = chrono::Utc::now();
|
||||
osu_user.pp = pp_values;
|
||||
}
|
||||
// Update users
|
||||
*OsuSavedUsers::open(&*d.read()).borrow_mut()? = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl OsuAnnouncer {
|
||||
fn scan_user(osu: &Osu, u: &OsuUser, mode: Mode) -> Result<Vec<(u8, Score)>, Error> {
|
||||
let scores = osu.user_best(UserID::ID(u.id), |f| f.mode(mode).limit(25))?;
|
||||
let scores = scores
|
||||
.into_iter()
|
||||
.filter(|s: &Score| s.date >= u.last_update)
|
||||
.enumerate()
|
||||
.map(|(i, v)| ((i + 1) as u8, v))
|
||||
.collect();
|
||||
Ok(scores)
|
||||
}
|
||||
fn scan_user(osu: &Osu, u: &OsuUser, mode: Mode) -> Result<Vec<(u8, Score)>, Error> {
|
||||
let scores = osu.user_best(UserID::ID(u.id), |f| f.mode(mode).limit(25))?;
|
||||
let scores = scores
|
||||
.into_iter()
|
||||
.filter(|s: &Score| s.date >= u.last_update)
|
||||
.enumerate()
|
||||
.map(|(i, v)| ((i + 1) as u8, v))
|
||||
.collect();
|
||||
Ok(scores)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ pub(crate) mod embeds;
|
|||
mod hook;
|
||||
mod server_rank;
|
||||
|
||||
pub use announcer::OsuAnnouncer;
|
||||
use db::OsuUser;
|
||||
use db::{OsuLastBeatmap, OsuSavedUsers};
|
||||
use embeds::{beatmap_embed, score_embed, user_embed};
|
||||
|
@ -48,8 +47,8 @@ impl TypeMapKey for OsuClient {
|
|||
///
|
||||
pub fn setup(
|
||||
path: &std::path::Path,
|
||||
client: &serenity::client::Client,
|
||||
data: &mut ShareMap,
|
||||
announcers: &mut AnnouncerHandler,
|
||||
) -> CommandResult {
|
||||
// Databases
|
||||
OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.yaml"))?;
|
||||
|
@ -63,7 +62,7 @@ pub fn setup(
|
|||
));
|
||||
|
||||
// Announcer
|
||||
OsuAnnouncer::scan(&client, std::time::Duration::from_secs(300));
|
||||
announcers.add(announcer::ANNOUNCER_KEY, announcer::updates);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -11,3 +11,4 @@ serenity = "0.8"
|
|||
youmubot-db = { path = "../youmubot-db" }
|
||||
crossbeam-channel = "0.4"
|
||||
reqwest = "0.10"
|
||||
rayon = "1"
|
||||
|
|
|
@ -1,80 +1,252 @@
|
|||
use crate::AppData;
|
||||
use crate::{AppData, GetCloned};
|
||||
use rayon::prelude::*;
|
||||
use serenity::{
|
||||
framework::standard::{CommandError as Error, CommandResult},
|
||||
http::{CacheHttp, Http},
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
Args, CommandError as Error, CommandResult,
|
||||
},
|
||||
http::CacheHttp,
|
||||
model::{
|
||||
channel::Message,
|
||||
id::{ChannelId, GuildId, UserId},
|
||||
},
|
||||
prelude::*,
|
||||
utils::MessageBuilder,
|
||||
CacheAndHttp,
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
thread::{spawn, JoinHandle},
|
||||
};
|
||||
use youmubot_db::DB;
|
||||
|
||||
/// A list of assigned channels for an announcer.
|
||||
pub(crate) type AnnouncerChannels = DB<HashMap<String, HashMap<GuildId, ChannelId>>>;
|
||||
|
||||
pub trait Announcer {
|
||||
fn announcer_key() -> &'static str;
|
||||
fn send_messages(
|
||||
c: &Http,
|
||||
/// The Announcer trait.
|
||||
///
|
||||
/// Every announcer needs to implement a method to look for updates.
|
||||
/// This method is called "updates", which takes:
|
||||
/// - A CacheHttp implementation, for interaction with Discord itself.
|
||||
/// - An AppData, which can be used for interacting with internal databases.
|
||||
/// - A function "channels", which takes an UserId and returns the list of ChannelIds, which any update related to that user should be
|
||||
/// sent to.
|
||||
pub trait Announcer: Send {
|
||||
/// Look for updates and send them to respective channels.
|
||||
///
|
||||
/// Errors returned from this function gets ignored and logged down.
|
||||
fn updates(
|
||||
&mut self,
|
||||
c: Arc<CacheAndHttp>,
|
||||
d: AppData,
|
||||
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
|
||||
channels: MemberToChannels,
|
||||
) -> 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(())
|
||||
impl<T> Announcer for T
|
||||
where
|
||||
T: FnMut(Arc<CacheAndHttp>, AppData, MemberToChannels) -> CommandResult + Send,
|
||||
{
|
||||
fn updates(
|
||||
&mut self,
|
||||
c: Arc<CacheAndHttp>,
|
||||
d: AppData,
|
||||
channels: MemberToChannels,
|
||||
) -> CommandResult {
|
||||
self(c, d, channels)
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple struct that allows looking up the relevant channels to an user.
|
||||
pub struct MemberToChannels(Vec<(GuildId, ChannelId)>);
|
||||
|
||||
impl MemberToChannels {
|
||||
/// Gets the channel list of an user related to that channel.
|
||||
pub fn channels_of(
|
||||
&self,
|
||||
http: impl CacheHttp + Clone + Sync,
|
||||
u: impl Into<UserId>,
|
||||
) -> Vec<ChannelId> {
|
||||
let u = u.into();
|
||||
self.0
|
||||
.par_iter()
|
||||
.filter_map(|(guild, channel)| {
|
||||
guild.member(http.clone(), u).ok().map(|_| channel.clone())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
/// The announcer handler.
|
||||
///
|
||||
/// This struct manages the list of all Announcers, firing them in a certain interval.
|
||||
pub struct AnnouncerHandler {
|
||||
cache_http: Arc<CacheAndHttp>,
|
||||
data: AppData,
|
||||
announcers: HashMap<&'static str, Box<dyn Announcer>>,
|
||||
}
|
||||
|
||||
// Querying for the AnnouncerHandler in the internal data returns a vec of keys.
|
||||
impl TypeMapKey for AnnouncerHandler {
|
||||
type Value = Vec<&'static str>;
|
||||
}
|
||||
|
||||
/// Announcer-managing related.
|
||||
impl AnnouncerHandler {
|
||||
/// Create a new instance of the handler.
|
||||
pub fn new(client: &serenity::Client) -> Self {
|
||||
Self {
|
||||
cache_http: client.cache_and_http.clone(),
|
||||
data: client.data.clone(),
|
||||
announcers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_guilds(d: AppData) -> Result<Vec<(GuildId, ChannelId)>, Error> {
|
||||
/// Insert a new announcer into the handler.
|
||||
///
|
||||
/// The handler must take an unique key. If a duplicate is found, this method panics.
|
||||
pub fn add(&mut self, key: &'static str, announcer: impl Announcer + 'static) -> &mut Self {
|
||||
if let Some(_) = self.announcers.insert(key, Box::new(announcer)) {
|
||||
panic!(
|
||||
"Announcer keys must be unique: another announcer with key `{}` was found",
|
||||
key
|
||||
)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execution-related.
|
||||
impl AnnouncerHandler {
|
||||
/// Collect the list of guilds and their respective channels, by the key of the announcer.
|
||||
fn get_guilds(&self, key: &'static str) -> Result<Vec<(GuildId, ChannelId)>, Error> {
|
||||
let d = &self.data;
|
||||
let data = AnnouncerChannels::open(&*d.read())
|
||||
.borrow()?
|
||||
.get(Self::announcer_key())
|
||||
.get(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
|
||||
})?;
|
||||
/// Run the announcing sequence on a certain announcer.
|
||||
fn announce(&mut self, key: &'static str) -> CommandResult {
|
||||
let guilds: Vec<_> = self.get_guilds(key)?;
|
||||
let channels = MemberToChannels(guilds);
|
||||
let cache_http = self.cache_http.clone();
|
||||
let data = self.data.clone();
|
||||
let announcer = self
|
||||
.announcers
|
||||
.get_mut(&key)
|
||||
.expect("Key is from announcers");
|
||||
announcer.updates(cache_http, data, channels)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scan(client: &serenity::Client, cooldown: std::time::Duration) -> JoinHandle<()> {
|
||||
let c = client.cache_and_http.clone();
|
||||
let data = client.data.clone();
|
||||
/// Start the AnnouncerHandler, moving it into another thread.
|
||||
///
|
||||
/// It will run all the announcers in sequence every *cooldown* seconds.
|
||||
pub fn scan(mut self, cooldown: std::time::Duration) -> JoinHandle<()> {
|
||||
// First we store all the keys inside the database.
|
||||
let keys = self.announcers.keys().cloned().collect::<Vec<_>>();
|
||||
self.data.write().insert::<Self>(keys.clone());
|
||||
spawn(move || loop {
|
||||
if let Err(e) = Self::announce(c.http(), data.clone()) {
|
||||
dbg!(e);
|
||||
for key in &keys {
|
||||
if let Err(e) = self.announce(key) {
|
||||
dbg!(e);
|
||||
}
|
||||
}
|
||||
std::thread::sleep(cooldown);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[command("register")]
|
||||
#[description = "Register the current channel with an announcer"]
|
||||
#[usage = "[announcer key]"]
|
||||
#[required_permissions(MANAGE_CHANNELS)]
|
||||
#[only_in(guilds)]
|
||||
#[num_args(1)]
|
||||
pub fn register_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult {
|
||||
let key = args.single::<String>()?;
|
||||
let keys = ctx.data.get_cloned::<AnnouncerHandler>();
|
||||
if !keys.contains(&key.as_str()) {
|
||||
m.reply(
|
||||
&ctx,
|
||||
format!(
|
||||
"Key not found. Available announcer keys are: `{}`",
|
||||
keys.join(", ")
|
||||
),
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
let guild = m.guild(&ctx).expect("Guild-only command");
|
||||
let guild = guild.read();
|
||||
let channel = m.channel_id.to_channel(&ctx)?;
|
||||
AnnouncerChannels::open(&*ctx.data.read())
|
||||
.borrow_mut()?
|
||||
.entry(key.clone())
|
||||
.or_default()
|
||||
.insert(guild.id, m.channel_id);
|
||||
m.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
.push("Announcer ")
|
||||
.push_mono_safe(key)
|
||||
.push(" has been activated for server ")
|
||||
.push_bold_safe(&guild.name)
|
||||
.push(" on channel ")
|
||||
.push_bold_safe(channel)
|
||||
.build(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command("remove")]
|
||||
#[description = "Remove an announcer from the server"]
|
||||
#[usage = "[announcer key]"]
|
||||
#[required_permissions(MANAGE_CHANNELS)]
|
||||
#[only_in(guilds)]
|
||||
#[num_args(1)]
|
||||
pub fn remove_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult {
|
||||
let key = args.single::<String>()?;
|
||||
let keys = ctx.data.get_cloned::<AnnouncerHandler>();
|
||||
if !keys.contains(&key.as_str()) {
|
||||
m.reply(
|
||||
&ctx,
|
||||
format!(
|
||||
"Key not found. Available announcer keys are: `{}`",
|
||||
keys.join(", ")
|
||||
),
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
let guild = m.guild(&ctx).expect("Guild-only command");
|
||||
let guild = guild.read();
|
||||
AnnouncerChannels::open(&*ctx.data.read())
|
||||
.borrow_mut()?
|
||||
.entry(key.clone())
|
||||
.and_modify(|m| {
|
||||
m.remove(&guild.id);
|
||||
});
|
||||
m.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
.push("Announcer ")
|
||||
.push_mono_safe(key)
|
||||
.push(" has been de-activated for server ")
|
||||
.push_bold_safe(&guild.name)
|
||||
.build(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[group("announcer")]
|
||||
#[prefix("announcer")]
|
||||
#[only_in(guilds)]
|
||||
#[required_permissions(MANAGE_CHANNELS)]
|
||||
#[description = "Manage the announcers in the server."]
|
||||
#[commands(remove_announcer, register_announcer)]
|
||||
pub struct AnnouncerCommands;
|
||||
|
|
|
@ -7,7 +7,7 @@ pub mod pagination;
|
|||
pub mod reaction_watch;
|
||||
pub mod setup;
|
||||
|
||||
pub use announcer::Announcer;
|
||||
pub use announcer::{Announcer, AnnouncerHandler};
|
||||
pub use args::{Duration, UsernameArg};
|
||||
pub use pagination::Pagination;
|
||||
pub use reaction_watch::{ReactionHandler, ReactionWatcher};
|
||||
|
@ -50,3 +50,28 @@ impl GetCloned for AppData {
|
|||
self.read().get::<T>().cloned().expect("Should be there")
|
||||
}
|
||||
}
|
||||
|
||||
pub mod prelude_commands {
|
||||
use crate::announcer::ANNOUNCERCOMMANDS_GROUP;
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
CommandResult,
|
||||
},
|
||||
model::channel::Message,
|
||||
prelude::Context,
|
||||
};
|
||||
|
||||
#[group("Prelude")]
|
||||
#[description = "All the commands that makes the base of Youmu"]
|
||||
#[commands(ping)]
|
||||
#[sub_groups(AnnouncerCommands)]
|
||||
pub struct Prelude;
|
||||
|
||||
#[command]
|
||||
#[description = "pong!"]
|
||||
fn ping(ctx: &mut Context, m: &Message) -> CommandResult {
|
||||
m.reply(&ctx, "Pong!")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,9 @@ fn main() {
|
|||
// Set up base framework
|
||||
let mut fw = setup_framework(&client);
|
||||
|
||||
// Set up announcer handler
|
||||
let mut announcers = AnnouncerHandler::new(&client);
|
||||
|
||||
// Setup each package starting from the prelude.
|
||||
{
|
||||
let mut data = client.data.write();
|
||||
|
@ -80,7 +83,8 @@ fn main() {
|
|||
youmubot_core::setup(&db_path, &client, &mut data).expect("Setup db should succeed");
|
||||
// osu!
|
||||
#[cfg(feature = "osu")]
|
||||
youmubot_osu::discord::setup(&db_path, &client, &mut data).expect("osu! is initialized");
|
||||
youmubot_osu::discord::setup(&db_path, &mut data, &mut announcers)
|
||||
.expect("osu! is initialized");
|
||||
// codeforces
|
||||
#[cfg(feature = "codeforces")]
|
||||
youmubot_cf::setup(&db_path, &mut data);
|
||||
|
@ -94,6 +98,7 @@ fn main() {
|
|||
println!("codeforces enabled.");
|
||||
|
||||
client.with_framework(fw);
|
||||
announcers.scan(std::time::Duration::from_secs(300));
|
||||
|
||||
println!("Starting...");
|
||||
if let Err(v) = client.start() {
|
||||
|
@ -167,7 +172,8 @@ fn setup_framework(client: &Client) -> StandardFramework {
|
|||
.bucket("images", |c| c.time_span(60).limit(2))
|
||||
.bucket("community", |c| {
|
||||
c.delay(30).time_span(30).limit(1)
|
||||
});
|
||||
})
|
||||
.group(&prelude_commands::PRELUDE_GROUP);
|
||||
// groups here
|
||||
#[cfg(feature = "core")]
|
||||
let fw = fw
|
||||
|
|
Loading…
Add table
Reference in a new issue