diff options
author | Nathan Ringo <nathan@remexre.com> | 2024-01-15 23:36:04 -0600 |
---|---|---|
committer | Nathan Ringo <nathan@remexre.com> | 2024-01-15 23:36:04 -0600 |
commit | 54f497163f57dacd8d621a2a3c89e1f06ac370d0 (patch) | |
tree | bc58027e740cef37c902d1f67cd97115fd5127be | |
parent | ea90b5ce89b9babb4ebc86de523efc7fa9631281 (diff) |
Start splitting up handlers.
-rw-r--r-- | Cargo.lock | 71 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | src/handlers/mod.rs | 226 | ||||
-rw-r--r-- | src/handlers/presence_setter.rs | 17 | ||||
-rw-r--r-- | src/handlers/x500_mapper.rs | 22 | ||||
-rw-r--r-- | src/main.rs | 67 |
6 files changed, 392 insertions, 14 deletions
@@ -110,6 +110,17 @@ dependencies = [ ] [[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -235,8 +246,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.48.5", ] @@ -469,6 +482,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -492,6 +506,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -613,6 +638,15 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" @@ -759,8 +793,11 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "futures", + "log", "serde", "serenity", + "stderrlog", "tokio", "toml", ] @@ -871,7 +908,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "libc", ] @@ -1320,6 +1357,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] +name = "stderrlog" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a26bbf6de627d389164afa9783739b56746c6c72c4ed16539f4ff54170327b" +dependencies = [ + "atty", + "chrono", + "log", + "termcolor", + "thread_local", +] + +[[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1388,6 +1438,15 @@ dependencies = [ ] [[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] name = "thiserror" version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1408,6 +1467,16 @@ dependencies = [ ] [[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] name = "time" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6,7 +6,10 @@ edition = "2021" [dependencies] anyhow = { version = "1.0.79", features = ["backtrace"] } clap = { version = "4.4.17", features = ["derive"] } +futures = "0.3.30" +log = "0.4.20" serde = { version = "1.0.195", features = ["derive"] } serenity = "0.12.0" +stderrlog = "0.5.4" tokio = { version = "1.35.1", features = ["rt-multi-thread"] } toml = "0.8.8" diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..f2b8a4f --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,226 @@ +use futures::stream::{self, StreamExt}; +use serenity::{ + all::{ + ActionExecution, ApplicationId, AuditLogEntry, ChannelId, ChannelPinsUpdateEvent, + CommandPermissions, CurrentUser, Emoji, EmojiId, Guild, GuildChannel, GuildId, + GuildMemberUpdateEvent, GuildMembersChunkEvent, GuildScheduledEventUserAddEvent, + GuildScheduledEventUserRemoveEvent, Integration, IntegrationId, Interaction, + InviteCreateEvent, InviteDeleteEvent, Member, Message, MessageId, MessageUpdateEvent, + PartialGuild, PartialGuildChannel, Presence, Reaction, Ready, ResumedEvent, Role, RoleId, + Rule, ScheduledEvent, StageInstance, Sticker, StickerId, ThreadListSyncEvent, ThreadMember, + ThreadMembersUpdateEvent, TypingStartEvent, UnavailableGuild, User, VoiceServerUpdateEvent, + VoiceState, + }, + async_trait, + client::{Context, EventHandler}, + gateway::ShardStageUpdateEvent, + http::RatelimitInfo, +}; +use std::collections::HashMap; + +mod presence_setter; +mod x500_mapper; + +pub use self::{presence_setter::PresenceSetter, x500_mapper::X500Mapper}; + +/// An EventHandler that proxies events to each of its contained handlers concurrently (but not in +/// parallel). +pub struct MultiHandler(pub Vec<Box<dyn EventHandler>>); + +macro_rules! define_multihandler_impl { + ( + $(async fn $name:ident(&self, $($arg:ident : $argty:ty),* $(,)?);)* + ) => { + #[async_trait] + impl EventHandler for MultiHandler { + $(async fn $name( + &self, + $($arg: $argty),* + ) { + stream::iter(self.0.iter()) + .for_each_concurrent(None, move |handler| { + handler.$name($($arg.clone()),*) + }) + .await + })* + } + }; +} + +define_multihandler_impl! { + async fn command_permissions_update(&self, ctx: Context, permission: CommandPermissions); + async fn auto_moderation_rule_create(&self, ctx: Context, rule: Rule); + async fn auto_moderation_rule_update(&self, ctx: Context, rule: Rule); + async fn auto_moderation_rule_delete(&self, ctx: Context, rule: Rule); + async fn auto_moderation_action_execution(&self, ctx: Context, execution: ActionExecution); + async fn cache_ready(&self, ctx: Context, guilds: Vec<GuildId>); + async fn shards_ready(&self, ctx: Context, total_shards: u32); + async fn channel_create(&self, ctx: Context, channel: GuildChannel); + async fn category_create(&self, ctx: Context, category: GuildChannel); + async fn category_delete(&self, ctx: Context, category: GuildChannel); + async fn channel_delete( + &self, + ctx: Context, + channel: GuildChannel, + messages: Option<Vec<Message>>, + ); + async fn channel_pins_update(&self, ctx: Context, pin: ChannelPinsUpdateEvent); + async fn channel_update(&self, ctx: Context, old: Option<GuildChannel>, new: GuildChannel); + async fn guild_audit_log_entry_create( + &self, + ctx: Context, + entry: AuditLogEntry, + guild_id: GuildId, + ); + async fn guild_ban_addition(&self, ctx: Context, guild_id: GuildId, banned_user: User); + async fn guild_ban_removal(&self, ctx: Context, guild_id: GuildId, unbanned_user: User); + async fn guild_create(&self, ctx: Context, guild: Guild, is_new: Option<bool>); + async fn guild_delete(&self, ctx: Context, incomplete: UnavailableGuild, full: Option<Guild>); + async fn guild_emojis_update( + &self, + ctx: Context, + guild_id: GuildId, + current_state: HashMap<EmojiId, Emoji>, + ); + async fn guild_integrations_update(&self, ctx: Context, guild_id: GuildId); + async fn guild_member_addition(&self, ctx: Context, new_member: Member); + async fn guild_member_removal( + &self, + ctx: Context, + guild_id: GuildId, + user: User, + member_data_if_available: Option<Member>, + ); + async fn guild_member_update( + &self, + ctx: Context, + old_if_available: Option<Member>, + new: Option<Member>, + event: GuildMemberUpdateEvent, + ); + async fn guild_members_chunk(&self, ctx: Context, chunk: GuildMembersChunkEvent); + async fn guild_role_create(&self, ctx: Context, new: Role); + async fn guild_role_delete( + &self, + ctx: Context, + guild_id: GuildId, + removed_role_id: RoleId, + removed_role_data_if_available: Option<Role>, + ); + async fn guild_role_update( + &self, + ctx: Context, + old_data_if_available: Option<Role>, + new: Role, + ); + async fn guild_stickers_update( + &self, + ctx: Context, + guild_id: GuildId, + current_state: HashMap<StickerId, Sticker>, + ); + async fn guild_update( + &self, + ctx: Context, + old_data_if_available: Option<Guild>, + new_data: PartialGuild, + ); + async fn invite_create(&self, ctx: Context, data: InviteCreateEvent); + async fn invite_delete(&self, ctx: Context, data: InviteDeleteEvent); + async fn message(&self, ctx: Context, new_message: Message); + async fn message_delete( + &self, + ctx: Context, + channel_id: ChannelId, + deleted_message_id: MessageId, + guild_id: Option<GuildId>, + ); + async fn message_delete_bulk( + &self, + ctx: Context, + channel_id: ChannelId, + multiple_deleted_messages_ids: Vec<MessageId>, + guild_id: Option<GuildId>, + ); + async fn message_update( + &self, + ctx: Context, + old_if_available: Option<Message>, + new: Option<Message>, + event: MessageUpdateEvent, + ); + async fn reaction_add(&self, ctx: Context, add_reaction: Reaction); + async fn reaction_remove(&self, ctx: Context, removed_reaction: Reaction); + async fn reaction_remove_all( + &self, + ctx: Context, + channel_id: ChannelId, + removed_from_message_id: MessageId, + ); + async fn reaction_remove_emoji(&self, ctx: Context, removed_reactions: Reaction); + async fn presence_replace(&self, ctx: Context, presences: Vec<Presence>); + async fn presence_update(&self, ctx: Context, new_data: Presence); + async fn ready(&self, ctx: Context, data_about_bot: Ready); + async fn resume(&self, ctx: Context, event: ResumedEvent); + async fn shard_stage_update(&self, ctx: Context, event: ShardStageUpdateEvent); + async fn typing_start(&self, ctx: Context, event: TypingStartEvent); + async fn user_update(&self, ctx: Context, old_data: Option<CurrentUser>, new: CurrentUser); + async fn voice_server_update(&self, ctx: Context, event: VoiceServerUpdateEvent); + async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState); + async fn voice_channel_status_update( + &self, + ctx: Context, + old: Option<String>, + status: Option<String>, + id: ChannelId, + guild_id: GuildId, + ); + async fn webhook_update( + &self, + ctx: Context, + guild_id: GuildId, + belongs_to_channel_id: ChannelId, + ); + async fn interaction_create(&self, ctx: Context, interaction: Interaction); + async fn integration_create(&self, ctx: Context, integration: Integration); + async fn integration_update(&self, ctx: Context, integration: Integration); + async fn integration_delete( + &self, + ctx: Context, + integration_id: IntegrationId, + guild_id: GuildId, + application_id: Option<ApplicationId>, + ); + async fn stage_instance_create(&self, ctx: Context, stage_instance: StageInstance); + async fn stage_instance_update(&self, ctx: Context, stage_instance: StageInstance); + async fn stage_instance_delete(&self, ctx: Context, stage_instance: StageInstance); + async fn thread_create(&self, ctx: Context, thread: GuildChannel); + async fn thread_update(&self, ctx: Context, old: Option<GuildChannel>, new: GuildChannel); + async fn thread_delete( + &self, + ctx: Context, + thread: PartialGuildChannel, + full_thread_data: Option<GuildChannel>, + ); + async fn thread_list_sync(&self, ctx: Context, thread_list_sync: ThreadListSyncEvent); + async fn thread_member_update(&self, ctx: Context, thread_member: ThreadMember); + async fn thread_members_update( + &self, + ctx: Context, + thread_members_update: ThreadMembersUpdateEvent, + ); + async fn guild_scheduled_event_create(&self, ctx: Context, event: ScheduledEvent); + async fn guild_scheduled_event_update(&self, ctx: Context, event: ScheduledEvent); + async fn guild_scheduled_event_delete(&self, ctx: Context, event: ScheduledEvent); + async fn guild_scheduled_event_user_add( + &self, + ctx: Context, + subscribed: GuildScheduledEventUserAddEvent, + ); + async fn guild_scheduled_event_user_remove( + &self, + ctx: Context, + unsubscribed: GuildScheduledEventUserRemoveEvent, + ); + async fn ratelimit(&self, data: RatelimitInfo); +} diff --git a/src/handlers/presence_setter.rs b/src/handlers/presence_setter.rs new file mode 100644 index 0000000..4e56dfd --- /dev/null +++ b/src/handlers/presence_setter.rs @@ -0,0 +1,17 @@ +use serenity::{ + all::{ActivityData, Ready}, + async_trait, + client::{Context, EventHandler}, +}; + +/// A handler that sets a fun presence. +pub struct PresenceSetter; + +#[async_trait] +impl EventHandler for PresenceSetter { + async fn ready(&self, ctx: Context, _data_about_bot: Ready) { + ctx.set_activity(Some(ActivityData::custom( + "evaluating (λx → x x)(λx → x x)".to_string(), + ))); + } +} diff --git a/src/handlers/x500_mapper.rs b/src/handlers/x500_mapper.rs new file mode 100644 index 0000000..f477e97 --- /dev/null +++ b/src/handlers/x500_mapper.rs @@ -0,0 +1,22 @@ +use serenity::{ + all::{GuildMemberUpdateEvent, Member}, + async_trait, + client::{Context, EventHandler}, +}; + +/// A handler that notices people with an X.500 in their nicknames that matches a student's, and +/// records it in the database. +pub struct X500Mapper; + +#[async_trait] +impl EventHandler for X500Mapper { + async fn guild_member_update( + &self, + _ctx: Context, + old_if_available: Option<Member>, + new: Option<Member>, + event: GuildMemberUpdateEvent, + ) { + dbg!((old_if_available, new, event)); + } +} diff --git a/src/main.rs b/src/main.rs index 0d602af..c9dbd7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,17 @@ -use anyhow::{Context, Result}; -use clap::Parser; +mod handlers; + +use crate::handlers::*; +use anyhow::{Context as _, Result}; +use clap::{value_parser, ArgAction, Parser}; use serde::Deserialize; use serenity::{ - all::{ActivityData, ActivityType, GatewayIntents, OnlineStatus}, + all::{ActivityData, GatewayIntents, GuildMemberUpdateEvent, Member, Ready}, + async_trait, + client::{Context, EventHandler}, Client, }; use std::{fs, path::PathBuf}; +use stderrlog::StdErrLog; #[derive(Debug, Deserialize)] struct Config { @@ -14,28 +20,63 @@ struct Config { #[derive(Debug, Parser)] struct Args { + /// The path to the configuration file. config_path: PathBuf, + + /// Decreases the log level. + #[clap( + short, + long, + conflicts_with("verbose"), + action = ArgAction::Count, + value_parser = value_parser!(u8).range(..=2) + )] + quiet: u8, + + /// Increases the log level. + #[clap( + short, + long, + conflicts_with("quiet"), + action = ArgAction::Count, + value_parser = value_parser!(u8).range(..=3) + )] + verbose: u8, } #[tokio::main] async fn main() -> Result<()> { let args = Args::parse(); + + // Set up logging. + { + let mut logger = StdErrLog::new(); + match args.quiet { + 0 => logger.verbosity(1 + args.verbose as usize), + 1 => logger.verbosity(0), + 2 => logger.quiet(true), + // UNREACHABLE: A maximum of two occurrences of quiet are allowed. + _ => unreachable!(), + }; + // UNWRAP: No other logger should be set up. + logger.show_module_names(true).init().unwrap() + } + let config_str = fs::read_to_string(&args.config_path) .with_context(|| format!("failed to read {}", args.config_path.display()))?; let config: Config = toml::from_str(&config_str) .with_context(|| format!("failed to parse {}", args.config_path.display()))?; drop(config_str); - let mut client = Client::builder(&config.discord_token, GatewayIntents::default()) - .activity(ActivityData { - name: "(λx → x x)(λx → x x)".to_string(), - kind: ActivityType::Custom, - state: Some("pondering".to_string()), - url: None, - }) - .status(OnlineStatus::Online) - .await - .context("failed to create Discord client")?; + let handler = MultiHandler(vec![Box::new(PresenceSetter), Box::new(X500Mapper)]); + + let mut client = Client::builder( + &config.discord_token, + GatewayIntents::default() | GatewayIntents::GUILD_MEMBERS, + ) + .event_handler(handler) + .await + .context("failed to create Discord client")?; client .start() |