Skip to content

Commit 9c8e2e4

Browse files
feat: implement thread events, webhooks, voice management, and entity selects
- Inbound thread events: ThreadCreated/Updated/Deleted/MemberAdd/MemberRemove published via 5 new bridge methods + 4 serenity EventHandler impls - Outbound commands: CreateWebhookCommand, ExecuteWebhookCommand, DeleteWebhookCommand, VoiceMoveCommand, VoiceDisconnectCommand - ActionRowComponent: UserSelect, RoleSelect, ChannelSelect variants with full serenity CreateSelectMenuKind wiring in build_action_rows - 10 new NATS subjects (bot::thread_* + agent::webhook_* / agent::voice_*) - 280 tests passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c6e49cf commit 9c8e2e4

7 files changed

Lines changed: 611 additions & 31 deletions

File tree

rsworkspace/crates/discord-bot/src/bridge.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,117 @@ impl DiscordBridge {
977977
debug!("Published bot_ready to {}", subject);
978978
Ok(())
979979
}
980+
981+
pub async fn publish_thread_created(&self, thread: &GuildChannel) -> Result<()> {
982+
let meta = EventMetadata::new(
983+
format!("dc-guild-{}", thread.guild_id.get()),
984+
self.next_sequence(),
985+
);
986+
let tm = thread.thread_metadata.as_ref();
987+
let ev = ThreadCreatedEvent {
988+
metadata: meta,
989+
thread_id: thread.id.get(),
990+
guild_id: thread.guild_id.get(),
991+
parent_id: thread.parent_id.map(|id| id.get()).unwrap_or(0),
992+
name: thread.name.clone(),
993+
archived: tm.map(|m| m.archived).unwrap_or(false),
994+
locked: tm.map(|m| m.locked).unwrap_or(false),
995+
auto_archive_duration: tm
996+
.map(|m| u16::from(m.auto_archive_duration) as u64)
997+
.unwrap_or(0),
998+
};
999+
let subject = subjects::bot::thread_created(self.prefix());
1000+
self.publisher.publish(&subject, &ev).await?;
1001+
debug!("Published thread_created to {}", subject);
1002+
Ok(())
1003+
}
1004+
1005+
pub async fn publish_thread_updated(&self, thread: &GuildChannel) -> Result<()> {
1006+
let meta = EventMetadata::new(
1007+
format!("dc-guild-{}", thread.guild_id.get()),
1008+
self.next_sequence(),
1009+
);
1010+
let tm = thread.thread_metadata.as_ref();
1011+
let ev = ThreadUpdatedEvent {
1012+
metadata: meta,
1013+
thread_id: thread.id.get(),
1014+
guild_id: thread.guild_id.get(),
1015+
parent_id: thread.parent_id.map(|id| id.get()).unwrap_or(0),
1016+
name: thread.name.clone(),
1017+
archived: tm.map(|m| m.archived).unwrap_or(false),
1018+
locked: tm.map(|m| m.locked).unwrap_or(false),
1019+
};
1020+
let subject = subjects::bot::thread_updated(self.prefix());
1021+
self.publisher.publish(&subject, &ev).await?;
1022+
debug!("Published thread_updated to {}", subject);
1023+
Ok(())
1024+
}
1025+
1026+
pub async fn publish_thread_deleted(
1027+
&self,
1028+
thread_id: u64,
1029+
guild_id: u64,
1030+
parent_id: Option<u64>,
1031+
) -> Result<()> {
1032+
let meta = EventMetadata::new(
1033+
format!("dc-guild-{}", guild_id),
1034+
self.next_sequence(),
1035+
);
1036+
let ev = ThreadDeletedEvent {
1037+
metadata: meta,
1038+
thread_id,
1039+
guild_id,
1040+
parent_id,
1041+
};
1042+
let subject = subjects::bot::thread_deleted(self.prefix());
1043+
self.publisher.publish(&subject, &ev).await?;
1044+
debug!("Published thread_deleted to {}", subject);
1045+
Ok(())
1046+
}
1047+
1048+
pub async fn publish_thread_member_add(
1049+
&self,
1050+
thread_id: u64,
1051+
guild_id: u64,
1052+
user_ids: Vec<u64>,
1053+
) -> Result<()> {
1054+
let meta = EventMetadata::new(
1055+
format!("dc-guild-{}", guild_id),
1056+
self.next_sequence(),
1057+
);
1058+
let ev = ThreadMemberAddEvent {
1059+
metadata: meta,
1060+
thread_id,
1061+
guild_id,
1062+
user_ids,
1063+
};
1064+
let subject = subjects::bot::thread_member_add(self.prefix());
1065+
self.publisher.publish(&subject, &ev).await?;
1066+
debug!("Published thread_member_add to {}", subject);
1067+
Ok(())
1068+
}
1069+
1070+
pub async fn publish_thread_member_remove(
1071+
&self,
1072+
thread_id: u64,
1073+
guild_id: u64,
1074+
user_ids: Vec<u64>,
1075+
) -> Result<()> {
1076+
let meta = EventMetadata::new(
1077+
format!("dc-guild-{}", guild_id),
1078+
self.next_sequence(),
1079+
);
1080+
let ev = ThreadMemberRemoveEvent {
1081+
metadata: meta,
1082+
thread_id,
1083+
guild_id,
1084+
user_ids,
1085+
};
1086+
let subject = subjects::bot::thread_member_remove(self.prefix());
1087+
self.publisher.publish(&subject, &ev).await?;
1088+
debug!("Published thread_member_remove to {}", subject);
1089+
Ok(())
1090+
}
9801091
}
9811092

9821093
fn convert_reaction_type(reaction: &ReactionType) -> Emoji {

rsworkspace/crates/discord-bot/src/handlers/mod.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,4 +618,107 @@ impl EventHandler for Handler {
618618
error!("failed to publish guild_member_update: {e}");
619619
}
620620
}
621+
622+
async fn thread_create(&self, ctx: Context, thread: GuildChannel) {
623+
let bridge = {
624+
let data = ctx.data.read().await;
625+
match data.get::<DiscordBridge>() {
626+
Some(b) => b.clone(),
627+
None => return,
628+
}
629+
};
630+
if !bridge.check_guild_access(thread.guild_id.get()) {
631+
return;
632+
}
633+
if let Err(e) = bridge.publish_thread_created(&thread).await {
634+
error!("Failed to publish thread_created: {}", e);
635+
}
636+
}
637+
638+
async fn thread_update(&self, ctx: Context, _old: Option<GuildChannel>, new: GuildChannel) {
639+
let bridge = {
640+
let data = ctx.data.read().await;
641+
match data.get::<DiscordBridge>() {
642+
Some(b) => b.clone(),
643+
None => return,
644+
}
645+
};
646+
if !bridge.check_guild_access(new.guild_id.get()) {
647+
return;
648+
}
649+
if let Err(e) = bridge.publish_thread_updated(&new).await {
650+
error!("Failed to publish thread_updated: {}", e);
651+
}
652+
}
653+
654+
async fn thread_delete(
655+
&self,
656+
ctx: Context,
657+
thread: serenity::model::channel::PartialGuildChannel,
658+
_full: Option<GuildChannel>,
659+
) {
660+
let bridge = {
661+
let data = ctx.data.read().await;
662+
match data.get::<DiscordBridge>() {
663+
Some(b) => b.clone(),
664+
None => return,
665+
}
666+
};
667+
let guild_id = thread.guild_id.get();
668+
if !bridge.check_guild_access(guild_id) {
669+
return;
670+
}
671+
let parent_id = Some(thread.parent_id.get());
672+
if let Err(e) = bridge
673+
.publish_thread_deleted(thread.id.get(), guild_id, parent_id)
674+
.await
675+
{
676+
error!("Failed to publish thread_deleted: {}", e);
677+
}
678+
}
679+
680+
async fn thread_members_update(
681+
&self,
682+
ctx: Context,
683+
event: serenity::model::event::ThreadMembersUpdateEvent,
684+
) {
685+
let bridge = {
686+
let data = ctx.data.read().await;
687+
match data.get::<DiscordBridge>() {
688+
Some(b) => b.clone(),
689+
None => return,
690+
}
691+
};
692+
let guild_id = event.guild_id.get();
693+
if !bridge.check_guild_access(guild_id) {
694+
return;
695+
}
696+
let thread_id = event.id.get();
697+
698+
// Publish member additions
699+
let added: Vec<u64> = event
700+
.added_members
701+
.iter()
702+
.map(|m| m.user_id.get())
703+
.collect();
704+
if !added.is_empty() {
705+
if let Err(e) = bridge
706+
.publish_thread_member_add(thread_id, guild_id, added)
707+
.await
708+
{
709+
error!("Failed to publish thread_member_add: {}", e);
710+
}
711+
}
712+
713+
// Publish member removals
714+
let removed: Vec<u64> = event.removed_member_ids.iter().map(|id| id.get()).collect();
715+
if !removed.is_empty() {
716+
if let Err(e) = bridge
717+
.publish_thread_member_remove(thread_id, guild_id, removed)
718+
.await
719+
{
720+
error!("Failed to publish thread_member_remove: {}", e);
721+
}
722+
}
723+
}
621724
}

0 commit comments

Comments
 (0)