aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/handlers/mod.rs226
-rw-r--r--src/handlers/presence_setter.rs17
-rw-r--r--src/handlers/x500_mapper.rs22
-rw-r--r--src/main.rs67
4 files changed, 319 insertions, 13 deletions
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()