1use super::discord_client::DiscordClient;
2use super::discord_gateway::{DiscordGateway, GatewayEvent};
3use super::storage::{DiscordAccount, DiscordStorage, DiscordStorageConfig};
4use super::types::*;
5use async_stream::stream;
6use futures::Stream;
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::Mutex;
10
11#[derive(Clone)]
12pub struct Discord {
13 storage: Arc<DiscordStorage>,
14 active_gateways: Arc<Mutex<HashMap<String, tokio::task::JoinHandle<()>>>>,
15}
16
17impl Discord {
18 pub async fn new() -> Result<Self, String> {
19 let storage = DiscordStorage::new(DiscordStorageConfig::default()).await?;
20
21 Ok(Self {
22 storage: Arc::new(storage),
23 active_gateways: Arc::new(Mutex::new(HashMap::new())),
24 })
25 }
26
27 pub async fn with_config(config: DiscordStorageConfig) -> Result<Self, String> {
28 let storage = DiscordStorage::new(config).await?;
29
30 Ok(Self {
31 storage: Arc::new(storage),
32 active_gateways: Arc::new(Mutex::new(HashMap::new())),
33 })
34 }
35}
36
37#[plexus_macros::hub_methods(
38 namespace = "discord",
39 version = "2.0.0",
40 description = "Multi-account Discord bot integration with message sending and webhooks"
41)]
42impl Discord {
43 #[plexus_macros::hub_method(
46 description = "Register a new Discord bot account",
47 params(
48 name = "Account name (identifier for this bot)",
49 bot_token = "Discord bot token"
50 )
51 )]
52 async fn register_account(
53 &self,
54 name: String,
55 bot_token: String,
56 ) -> impl Stream<Item = RegisterAccountEvent> + Send + 'static {
57 let storage = self.storage.clone();
58
59 stream! {
60 let now = chrono::Utc::now().timestamp();
61 let account = DiscordAccount {
62 name: name.clone(),
63 bot_token,
64 created_at: now,
65 updated_at: now,
66 };
67
68 match storage.register_account(account).await {
69 Ok(_) => yield RegisterAccountEvent::Registered {
70 account_name: name,
71 },
72 Err(e) => yield RegisterAccountEvent::Error { message: e },
73 }
74 }
75 }
76
77 #[plexus_macros::hub_method(
78 streaming,
79 description = "List all registered Discord bot accounts"
80 )]
81 async fn list_accounts(&self) -> impl Stream<Item = ListAccountsEvent> + Send + 'static {
82 let storage = self.storage.clone();
83
84 stream! {
85 match storage.list_accounts().await {
86 Ok(accounts) => {
87 let total = accounts.len();
88 for account in accounts {
89 yield ListAccountsEvent::Account {
90 name: account.name,
91 created_at: account.created_at,
92 };
93 }
94 yield ListAccountsEvent::Complete { total };
95 }
96 Err(e) => {
97 tracing::error!("Failed to list accounts: {}", e);
98 yield ListAccountsEvent::Complete { total: 0 };
99 }
100 }
101 }
102 }
103
104 #[plexus_macros::hub_method(
105 description = "Remove a Discord bot account",
106 params(name = "Account name to remove")
107 )]
108 async fn remove_account(
109 &self,
110 name: String,
111 ) -> impl Stream<Item = RemoveAccountEvent> + Send + 'static {
112 let storage = self.storage.clone();
113
114 stream! {
115 match storage.remove_account(&name).await {
116 Ok(true) => yield RemoveAccountEvent::Removed { account_name: name },
117 Ok(false) => yield RemoveAccountEvent::NotFound { account_name: name },
118 Err(e) => yield RemoveAccountEvent::Error { message: e },
119 }
120 }
121 }
122
123 #[plexus_macros::hub_method(
126 description = "Send a message to a Discord channel from a registered bot account",
127 params(
128 account = "Account name to send from",
129 channel_id = "Discord channel ID",
130 content = "Message content",
131 embed = "Rich embed object (optional)"
132 )
133 )]
134 async fn send_message(
135 &self,
136 account: String,
137 channel_id: String,
138 content: String,
139 embed: Option<serde_json::Value>,
140 ) -> impl Stream<Item = SendMessageEvent> + Send + 'static {
141 let storage = self.storage.clone();
142
143 stream! {
144 let account_config = match storage.get_account(&account).await {
146 Ok(Some(acc)) => acc,
147 Ok(None) => {
148 yield SendMessageEvent::Error {
149 message: format!("Account '{}' not found", account),
150 code: Some("ACCOUNT_NOT_FOUND".to_string()),
151 };
152 return;
153 }
154 Err(e) => {
155 yield SendMessageEvent::Error {
156 message: format!("Failed to load account: {}", e),
157 code: None,
158 };
159 return;
160 }
161 };
162
163 let client = DiscordClient::new(account_config.bot_token);
165
166 match client.send_message(&channel_id, content, embed).await {
168 Ok(message_id) => {
169 yield SendMessageEvent::Sent {
170 message_id,
171 channel_id,
172 };
173 }
174 Err(e) => {
175 yield SendMessageEvent::Error {
176 message: e,
177 code: None,
178 };
179 }
180 }
181 }
182 }
183
184 #[plexus_macros::hub_method(
185 description = "Create a webhook for a Discord channel using a registered bot account",
186 params(
187 account = "Account name to use",
188 channel_id = "Discord channel ID",
189 name = "Webhook name"
190 )
191 )]
192 async fn create_webhook(
193 &self,
194 account: String,
195 channel_id: String,
196 name: String,
197 ) -> impl Stream<Item = WebhookEvent> + Send + 'static {
198 let storage = self.storage.clone();
199
200 stream! {
201 let account_config = match storage.get_account(&account).await {
203 Ok(Some(acc)) => acc,
204 Ok(None) => {
205 yield WebhookEvent::Error {
206 message: format!("Account '{}' not found", account),
207 };
208 return;
209 }
210 Err(e) => {
211 yield WebhookEvent::Error {
212 message: format!("Failed to load account: {}", e),
213 };
214 return;
215 }
216 };
217
218 let client = DiscordClient::new(account_config.bot_token);
220
221 match client.create_webhook(&channel_id, name).await {
223 Ok(webhook_url) => {
224 let webhook_id = webhook_url
226 .split('/')
227 .nth_back(1)
228 .unwrap_or("unknown")
229 .to_string();
230
231 yield WebhookEvent::Created {
232 webhook_id,
233 webhook_url,
234 };
235 }
236 Err(e) => {
237 yield WebhookEvent::Error {
238 message: e,
239 };
240 }
241 }
242 }
243 }
244
245 #[plexus_macros::hub_method(
248 streaming,
249 description = "Start listening for Discord events via Gateway for a specific bot account",
250 params(account = "Account name to start listening for")
251 )]
252 async fn start_listening(
253 &self,
254 account: String,
255 ) -> impl Stream<Item = GatewayListenerEvent> + Send + 'static {
256 let storage = self.storage.clone();
257 let active_gateways = self.active_gateways.clone();
258 let account_name = account.clone();
259
260 stream! {
261 {
263 let gateways = active_gateways.lock().await;
264 if gateways.contains_key(&account) {
265 yield GatewayListenerEvent::Error {
266 message: format!("Account '{}' is already listening", account),
267 };
268 return;
269 }
270 }
271
272 let account_config = match storage.get_account(&account).await {
274 Ok(Some(acc)) => acc,
275 Ok(None) => {
276 yield GatewayListenerEvent::Error {
277 message: format!("Account '{}' not found", account),
278 };
279 return;
280 }
281 Err(e) => {
282 yield GatewayListenerEvent::Error {
283 message: format!("Failed to load account: {}", e),
284 };
285 return;
286 }
287 };
288
289 yield GatewayListenerEvent::Starting {
290 account_name: account.clone(),
291 };
292
293 let gateway = DiscordGateway::new(account_config.bot_token);
295
296 let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
298
299 let gateway_task = {
301 let gateway = gateway;
302 tokio::spawn(async move {
303 if let Err(e) = gateway.run(event_tx).await {
304 tracing::error!("Gateway error for account '{}': {}", account_name, e);
305 }
306 })
307 };
308
309 {
311 let mut gateways = active_gateways.lock().await;
312 gateways.insert(account.clone(), gateway_task);
313 }
314
315 while let Some(event) = event_rx.recv().await {
317 match event {
318 GatewayEvent::Ready { session_id, .. } => {
319 yield GatewayListenerEvent::Connected {
320 account_name: account.clone(),
321 session_id,
322 };
323 }
324 GatewayEvent::MessageCreate(msg) => {
325 yield GatewayListenerEvent::MessageReceived {
326 message_id: msg.id,
327 channel_id: msg.channel_id,
328 guild_id: msg.guild_id,
329 author_id: msg.author.id,
330 author_username: msg.author.username,
331 content: msg.content,
332 timestamp: msg.timestamp,
333 is_bot: msg.author.bot.unwrap_or(false),
334 };
335 }
336 GatewayEvent::MessageUpdate(msg) => {
337 yield GatewayListenerEvent::MessageUpdated {
338 message_id: msg.id,
339 channel_id: msg.channel_id,
340 guild_id: msg.guild_id,
341 author_id: msg.author.id,
342 author_username: msg.author.username,
343 content: msg.content,
344 edited_timestamp: msg.edited_timestamp,
345 };
346 }
347 GatewayEvent::MessageDelete { id, channel_id, guild_id } => {
348 yield GatewayListenerEvent::MessageDeleted {
349 message_id: id,
350 channel_id,
351 guild_id,
352 };
353 }
354 GatewayEvent::GuildMemberAdd(member) => {
355 if let Some(user) = member.user {
356 yield GatewayListenerEvent::MemberJoined {
357 user_id: user.id,
358 username: user.username,
359 guild_id: member.guild_id,
360 joined_at: member.joined_at,
361 };
362 }
363 }
364 GatewayEvent::Error(e) => {
365 yield GatewayListenerEvent::Error {
366 message: e.clone(),
367 };
368 }
369 GatewayEvent::Disconnected => {
370 yield GatewayListenerEvent::Disconnected {
371 account_name: account.clone(),
372 reason: "Gateway connection closed".to_string(),
373 };
374 break;
375 }
376 }
377 }
378
379 {
381 let mut gateways = active_gateways.lock().await;
382 gateways.remove(&account);
383 }
384 }
385 }
386
387 #[plexus_macros::hub_method(
388 description = "Stop listening for Discord events for a specific bot account",
389 params(account = "Account name to stop listening for")
390 )]
391 async fn stop_listening(
392 &self,
393 account: String,
394 ) -> impl Stream<Item = StopListeningEvent> + Send + 'static {
395 let active_gateways = self.active_gateways.clone();
396
397 stream! {
398 let mut gateways = active_gateways.lock().await;
399
400 if let Some(handle) = gateways.remove(&account) {
401 handle.abort();
402 yield StopListeningEvent::Stopped {
403 account_name: account,
404 };
405 } else {
406 yield StopListeningEvent::NotListening {
407 account_name: account,
408 };
409 }
410 }
411 }
412
413 #[plexus_macros::hub_method(
414 streaming,
415 description = "List all bot accounts currently listening via Gateway"
416 )]
417 async fn list_active_listeners(
418 &self,
419 ) -> impl Stream<Item = ListActiveListenersEvent> + Send + 'static {
420 let active_gateways = self.active_gateways.clone();
421
422 stream! {
423 let gateways = active_gateways.lock().await;
424 let accounts: Vec<String> = gateways.keys().cloned().collect();
425 let total = accounts.len();
426
427 for account_name in accounts {
428 yield ListActiveListenersEvent::Listener {
429 account_name,
430 };
431 }
432
433 yield ListActiveListenersEvent::Complete { total };
434 }
435 }
436
437 #[plexus_macros::hub_method(
440 streaming,
441 description = "List all guilds the bot is in",
442 params(account = "Account name to use")
443 )]
444 async fn list_guilds(
445 &self,
446 account: String,
447 ) -> impl Stream<Item = ListGuildsEvent> + Send + 'static {
448 let storage = self.storage.clone();
449
450 stream! {
451 let account_config = match storage.get_account(&account).await {
452 Ok(Some(acc)) => acc,
453 Ok(None) => {
454 yield ListGuildsEvent::Error {
455 message: format!("Account '{}' not found", account),
456 };
457 return;
458 }
459 Err(e) => {
460 yield ListGuildsEvent::Error {
461 message: format!("Failed to load account: {}", e),
462 };
463 return;
464 }
465 };
466
467 let client = DiscordClient::new(account_config.bot_token);
468
469 match client.list_guilds().await {
470 Ok(guilds) => {
471 let total = guilds.len();
472 for guild in guilds {
473 yield ListGuildsEvent::Guild {
474 id: guild.id,
475 name: guild.name,
476 icon: guild.icon,
477 owner_id: guild.owner_id,
478 member_count: guild.member_count,
479 };
480 }
481 yield ListGuildsEvent::Complete { total };
482 }
483 Err(e) => {
484 yield ListGuildsEvent::Error { message: e };
485 }
486 }
487 }
488 }
489
490 #[plexus_macros::hub_method(
491 description = "Get detailed guild information including roles and channels",
492 params(
493 account = "Account name to use",
494 guild_id = "Guild ID"
495 )
496 )]
497 async fn get_guild(
498 &self,
499 account: String,
500 guild_id: String,
501 ) -> impl Stream<Item = GetGuildEvent> + Send + 'static {
502 let storage = self.storage.clone();
503
504 stream! {
505 let account_config = match storage.get_account(&account).await {
506 Ok(Some(acc)) => acc,
507 Ok(None) => {
508 yield GetGuildEvent::Error {
509 message: format!("Account '{}' not found", account),
510 };
511 return;
512 }
513 Err(e) => {
514 yield GetGuildEvent::Error {
515 message: format!("Failed to load account: {}", e),
516 };
517 return;
518 }
519 };
520
521 let client = DiscordClient::new(account_config.bot_token);
522
523 match client.get_guild(&guild_id).await {
524 Ok(guild) => {
525 yield GetGuildEvent::GuildInfo {
526 id: guild.id,
527 name: guild.name,
528 icon: guild.icon,
529 owner_id: guild.owner_id,
530 member_count: guild.member_count,
531 description: guild.description,
532 role_count: guild.roles.len(),
533 channel_count: guild.channels.len(),
534 };
535 }
536 Err(e) => {
537 yield GetGuildEvent::Error { message: e };
538 }
539 }
540 }
541 }
542
543 #[plexus_macros::hub_method(
544 streaming,
545 description = "List all channels in a guild",
546 params(
547 account = "Account name to use",
548 guild_id = "Guild ID"
549 )
550 )]
551 async fn list_channels(
552 &self,
553 account: String,
554 guild_id: String,
555 ) -> impl Stream<Item = ListChannelsEvent> + Send + 'static {
556 let storage = self.storage.clone();
557
558 stream! {
559 let account_config = match storage.get_account(&account).await {
560 Ok(Some(acc)) => acc,
561 Ok(None) => {
562 yield ListChannelsEvent::Error {
563 message: format!("Account '{}' not found", account),
564 };
565 return;
566 }
567 Err(e) => {
568 yield ListChannelsEvent::Error {
569 message: format!("Failed to load account: {}", e),
570 };
571 return;
572 }
573 };
574
575 let client = DiscordClient::new(account_config.bot_token);
576
577 match client.list_channels(&guild_id).await {
578 Ok(channels) => {
579 let total = channels.len();
580 for channel in channels {
581 yield ListChannelsEvent::Channel {
582 id: channel.id,
583 name: channel.name,
584 channel_type: channel.channel_type,
585 position: channel.position,
586 parent_id: channel.parent_id,
587 };
588 }
589 yield ListChannelsEvent::Complete { total };
590 }
591 Err(e) => {
592 yield ListChannelsEvent::Error { message: e };
593 }
594 }
595 }
596 }
597
598 #[plexus_macros::hub_method(
599 streaming,
600 description = "List members in a guild (paginated)",
601 params(
602 account = "Account name to use",
603 guild_id = "Guild ID",
604 limit = "Maximum number of members to return (1-1000)"
605 )
606 )]
607 async fn list_members(
608 &self,
609 account: String,
610 guild_id: String,
611 limit: i32,
612 ) -> impl Stream<Item = ListMembersEvent> + Send + 'static {
613 let storage = self.storage.clone();
614
615 stream! {
616 let account_config = match storage.get_account(&account).await {
617 Ok(Some(acc)) => acc,
618 Ok(None) => {
619 yield ListMembersEvent::Error {
620 message: format!("Account '{}' not found", account),
621 };
622 return;
623 }
624 Err(e) => {
625 yield ListMembersEvent::Error {
626 message: format!("Failed to load account: {}", e),
627 };
628 return;
629 }
630 };
631
632 let client = DiscordClient::new(account_config.bot_token);
633
634 match client.list_members(&guild_id, limit).await {
635 Ok(members) => {
636 let total = members.len();
637 for member in members {
638 if let Some(user) = member.user {
639 yield ListMembersEvent::Member {
640 user_id: user.id,
641 username: user.username,
642 discriminator: user.discriminator,
643 nick: member.nick,
644 roles: member.roles,
645 joined_at: member.joined_at,
646 };
647 }
648 }
649 yield ListMembersEvent::Complete { total };
650 }
651 Err(e) => {
652 yield ListMembersEvent::Error { message: e };
653 }
654 }
655 }
656 }
657
658 #[plexus_macros::hub_method(
659 streaming,
660 description = "List all roles in a guild",
661 params(
662 account = "Account name to use",
663 guild_id = "Guild ID"
664 )
665 )]
666 async fn list_roles(
667 &self,
668 account: String,
669 guild_id: String,
670 ) -> impl Stream<Item = ListRolesEvent> + Send + 'static {
671 let storage = self.storage.clone();
672
673 stream! {
674 let account_config = match storage.get_account(&account).await {
675 Ok(Some(acc)) => acc,
676 Ok(None) => {
677 yield ListRolesEvent::Error {
678 message: format!("Account '{}' not found", account),
679 };
680 return;
681 }
682 Err(e) => {
683 yield ListRolesEvent::Error {
684 message: format!("Failed to load account: {}", e),
685 };
686 return;
687 }
688 };
689
690 let client = DiscordClient::new(account_config.bot_token);
691
692 match client.list_roles(&guild_id).await {
693 Ok(roles) => {
694 let total = roles.len();
695 for role in roles {
696 yield ListRolesEvent::Role {
697 id: role.id,
698 name: role.name,
699 color: role.color,
700 permissions: role.permissions,
701 position: role.position,
702 hoist: role.hoist,
703 mentionable: role.mentionable,
704 };
705 }
706 yield ListRolesEvent::Complete { total };
707 }
708 Err(e) => {
709 yield ListRolesEvent::Error { message: e };
710 }
711 }
712 }
713 }
714
715 #[plexus_macros::hub_method(
718 description = "Get channel information",
719 params(
720 account = "Account name to use",
721 channel_id = "Channel ID"
722 )
723 )]
724 async fn get_channel(
725 &self,
726 account: String,
727 channel_id: String,
728 ) -> impl Stream<Item = GetChannelEvent> + Send + 'static {
729 let storage = self.storage.clone();
730
731 stream! {
732 let account_config = match storage.get_account(&account).await {
733 Ok(Some(acc)) => acc,
734 Ok(None) => {
735 yield GetChannelEvent::Error {
736 message: format!("Account '{}' not found", account),
737 };
738 return;
739 }
740 Err(e) => {
741 yield GetChannelEvent::Error {
742 message: format!("Failed to load account: {}", e),
743 };
744 return;
745 }
746 };
747
748 let client = DiscordClient::new(account_config.bot_token);
749
750 match client.get_channel(&channel_id).await {
751 Ok(channel) => {
752 yield GetChannelEvent::ChannelInfo {
753 id: channel.id,
754 name: channel.name,
755 channel_type: channel.channel_type,
756 guild_id: channel.guild_id,
757 position: channel.position,
758 topic: channel.topic,
759 parent_id: channel.parent_id,
760 };
761 }
762 Err(e) => {
763 yield GetChannelEvent::Error { message: e };
764 }
765 }
766 }
767 }
768
769 #[plexus_macros::hub_method(
770 description = "Create a new channel in a guild (type: 0=text, 2=voice, 4=category)",
771 params(
772 account = "Account name to use",
773 guild_id = "Guild ID",
774 name = "Channel name",
775 channel_type = "Channel type (0=text, 2=voice, 4=category)",
776 parent_id = "Parent category ID (optional)"
777 )
778 )]
779 async fn create_channel(
780 &self,
781 account: String,
782 guild_id: String,
783 name: String,
784 channel_type: i32,
785 parent_id: Option<String>,
786 ) -> impl Stream<Item = CreateChannelEvent> + Send + 'static {
787 let storage = self.storage.clone();
788
789 stream! {
790 let account_config = match storage.get_account(&account).await {
791 Ok(Some(acc)) => acc,
792 Ok(None) => {
793 yield CreateChannelEvent::Error {
794 message: format!("Account '{}' not found", account),
795 };
796 return;
797 }
798 Err(e) => {
799 yield CreateChannelEvent::Error {
800 message: format!("Failed to load account: {}", e),
801 };
802 return;
803 }
804 };
805
806 let client = DiscordClient::new(account_config.bot_token);
807
808 match client.create_channel(&guild_id, name, channel_type, parent_id).await {
809 Ok(channel) => {
810 yield CreateChannelEvent::Created {
811 channel_id: channel.id,
812 channel_name: channel.name,
813 };
814 }
815 Err(e) => {
816 yield CreateChannelEvent::Error { message: e };
817 }
818 }
819 }
820 }
821
822 #[plexus_macros::hub_method(
823 description = "Modify a channel's properties",
824 params(
825 account = "Account name to use",
826 channel_id = "Channel ID",
827 name = "New channel name (optional)",
828 topic = "New channel topic (optional)",
829 position = "New channel position (optional)"
830 )
831 )]
832 async fn modify_channel(
833 &self,
834 account: String,
835 channel_id: String,
836 name: Option<String>,
837 topic: Option<String>,
838 position: Option<i32>,
839 ) -> impl Stream<Item = ModifyChannelEvent> + Send + 'static {
840 let storage = self.storage.clone();
841
842 stream! {
843 let account_config = match storage.get_account(&account).await {
844 Ok(Some(acc)) => acc,
845 Ok(None) => {
846 yield ModifyChannelEvent::Error {
847 message: format!("Account '{}' not found", account),
848 };
849 return;
850 }
851 Err(e) => {
852 yield ModifyChannelEvent::Error {
853 message: format!("Failed to load account: {}", e),
854 };
855 return;
856 }
857 };
858
859 let client = DiscordClient::new(account_config.bot_token);
860
861 match client.modify_channel(&channel_id, name, topic, position).await {
862 Ok(channel) => {
863 yield ModifyChannelEvent::Modified {
864 channel_id: channel.id,
865 channel_name: channel.name,
866 };
867 }
868 Err(e) => {
869 yield ModifyChannelEvent::Error { message: e };
870 }
871 }
872 }
873 }
874
875 #[plexus_macros::hub_method(
876 description = "Delete a channel",
877 params(
878 account = "Account name to use",
879 channel_id = "Channel ID"
880 )
881 )]
882 async fn delete_channel(
883 &self,
884 account: String,
885 channel_id: String,
886 ) -> impl Stream<Item = DeleteChannelEvent> + Send + 'static {
887 let storage = self.storage.clone();
888
889 stream! {
890 let account_config = match storage.get_account(&account).await {
891 Ok(Some(acc)) => acc,
892 Ok(None) => {
893 yield DeleteChannelEvent::Error {
894 message: format!("Account '{}' not found", account),
895 };
896 return;
897 }
898 Err(e) => {
899 yield DeleteChannelEvent::Error {
900 message: format!("Failed to load account: {}", e),
901 };
902 return;
903 }
904 };
905
906 let client = DiscordClient::new(account_config.bot_token);
907
908 match client.delete_channel(&channel_id).await {
909 Ok(_) => {
910 yield DeleteChannelEvent::Deleted {
911 channel_id,
912 };
913 }
914 Err(e) => {
915 yield DeleteChannelEvent::Error { message: e };
916 }
917 }
918 }
919 }
920
921 #[plexus_macros::hub_method(
922 streaming,
923 description = "Get message history from a channel",
924 params(
925 account = "Account name to use",
926 channel_id = "Channel ID",
927 limit = "Number of messages to retrieve (1-100)"
928 )
929 )]
930 async fn get_messages(
931 &self,
932 account: String,
933 channel_id: String,
934 limit: i32,
935 ) -> impl Stream<Item = GetMessagesEvent> + Send + 'static {
936 let storage = self.storage.clone();
937
938 stream! {
939 let account_config = match storage.get_account(&account).await {
940 Ok(Some(acc)) => acc,
941 Ok(None) => {
942 yield GetMessagesEvent::Error {
943 message: format!("Account '{}' not found", account),
944 };
945 return;
946 }
947 Err(e) => {
948 yield GetMessagesEvent::Error {
949 message: format!("Failed to load account: {}", e),
950 };
951 return;
952 }
953 };
954
955 let client = DiscordClient::new(account_config.bot_token);
956
957 match client.get_messages(&channel_id, limit).await {
958 Ok(messages) => {
959 let total = messages.len();
960 for message in messages {
961 yield GetMessagesEvent::Message {
962 message_id: message.id,
963 channel_id: message.channel_id.clone(),
964 };
965 }
966 yield GetMessagesEvent::Complete { total };
967 }
968 Err(e) => {
969 yield GetMessagesEvent::Error { message: e };
970 }
971 }
972 }
973 }
974
975 #[plexus_macros::hub_method(
978 description = "Get member information",
979 params(
980 account = "Account name to use",
981 guild_id = "Guild ID",
982 user_id = "User ID"
983 )
984 )]
985 async fn get_member(
986 &self,
987 account: String,
988 guild_id: String,
989 user_id: String,
990 ) -> impl Stream<Item = GetMemberEvent> + Send + 'static {
991 let storage = self.storage.clone();
992
993 stream! {
994 let account_config = match storage.get_account(&account).await {
995 Ok(Some(acc)) => acc,
996 Ok(None) => {
997 yield GetMemberEvent::Error {
998 message: format!("Account '{}' not found", account),
999 };
1000 return;
1001 }
1002 Err(e) => {
1003 yield GetMemberEvent::Error {
1004 message: format!("Failed to load account: {}", e),
1005 };
1006 return;
1007 }
1008 };
1009
1010 let client = DiscordClient::new(account_config.bot_token);
1011
1012 match client.get_member(&guild_id, &user_id).await {
1013 Ok(member) => {
1014 if let Some(user) = member.user {
1015 yield GetMemberEvent::MemberInfo {
1016 user_id: user.id,
1017 username: user.username,
1018 discriminator: user.discriminator,
1019 nick: member.nick,
1020 roles: member.roles,
1021 joined_at: member.joined_at,
1022 };
1023 } else {
1024 yield GetMemberEvent::Error {
1025 message: "Member user information not available".to_string(),
1026 };
1027 }
1028 }
1029 Err(e) => {
1030 yield GetMemberEvent::Error { message: e };
1031 }
1032 }
1033 }
1034 }
1035
1036 #[plexus_macros::hub_method(
1037 description = "Modify a guild member's nickname or roles",
1038 params(
1039 account = "Account name to use",
1040 guild_id = "Guild ID",
1041 user_id = "User ID",
1042 nick = "New nickname (optional)",
1043 roles = "Array of role IDs (optional)"
1044 )
1045 )]
1046 async fn modify_member(
1047 &self,
1048 account: String,
1049 guild_id: String,
1050 user_id: String,
1051 nick: Option<String>,
1052 roles: Option<Vec<String>>,
1053 ) -> impl Stream<Item = ModifyMemberEvent> + Send + 'static {
1054 let storage = self.storage.clone();
1055
1056 stream! {
1057 let account_config = match storage.get_account(&account).await {
1058 Ok(Some(acc)) => acc,
1059 Ok(None) => {
1060 yield ModifyMemberEvent::Error {
1061 message: format!("Account '{}' not found", account),
1062 };
1063 return;
1064 }
1065 Err(e) => {
1066 yield ModifyMemberEvent::Error {
1067 message: format!("Failed to load account: {}", e),
1068 };
1069 return;
1070 }
1071 };
1072
1073 let client = DiscordClient::new(account_config.bot_token);
1074
1075 match client.modify_member(&guild_id, &user_id, nick, roles).await {
1076 Ok(_) => {
1077 yield ModifyMemberEvent::Modified {
1078 user_id,
1079 };
1080 }
1081 Err(e) => {
1082 yield ModifyMemberEvent::Error { message: e };
1083 }
1084 }
1085 }
1086 }
1087
1088 #[plexus_macros::hub_method(
1089 description = "Kick a member from a guild",
1090 params(
1091 account = "Account name to use",
1092 guild_id = "Guild ID",
1093 user_id = "User ID",
1094 reason = "Reason for kick (optional, appears in audit log)"
1095 )
1096 )]
1097 async fn kick_member(
1098 &self,
1099 account: String,
1100 guild_id: String,
1101 user_id: String,
1102 reason: Option<String>,
1103 ) -> impl Stream<Item = KickMemberEvent> + Send + 'static {
1104 let storage = self.storage.clone();
1105
1106 stream! {
1107 let account_config = match storage.get_account(&account).await {
1108 Ok(Some(acc)) => acc,
1109 Ok(None) => {
1110 yield KickMemberEvent::Error {
1111 message: format!("Account '{}' not found", account),
1112 };
1113 return;
1114 }
1115 Err(e) => {
1116 yield KickMemberEvent::Error {
1117 message: format!("Failed to load account: {}", e),
1118 };
1119 return;
1120 }
1121 };
1122
1123 let client = DiscordClient::new(account_config.bot_token);
1124
1125 match client.kick_member(&guild_id, &user_id, reason).await {
1126 Ok(_) => {
1127 yield KickMemberEvent::Kicked {
1128 user_id,
1129 };
1130 }
1131 Err(e) => {
1132 yield KickMemberEvent::Error { message: e };
1133 }
1134 }
1135 }
1136 }
1137
1138 #[plexus_macros::hub_method(
1139 description = "Ban a member from a guild",
1140 params(
1141 account = "Account name to use",
1142 guild_id = "Guild ID",
1143 user_id = "User ID",
1144 reason = "Reason for ban (optional, appears in audit log)",
1145 delete_message_days = "Number of days of message history to delete (0-7, optional)"
1146 )
1147 )]
1148 async fn ban_member(
1149 &self,
1150 account: String,
1151 guild_id: String,
1152 user_id: String,
1153 reason: Option<String>,
1154 delete_message_days: Option<i32>,
1155 ) -> impl Stream<Item = BanMemberEvent> + Send + 'static {
1156 let storage = self.storage.clone();
1157
1158 stream! {
1159 let account_config = match storage.get_account(&account).await {
1160 Ok(Some(acc)) => acc,
1161 Ok(None) => {
1162 yield BanMemberEvent::Error {
1163 message: format!("Account '{}' not found", account),
1164 };
1165 return;
1166 }
1167 Err(e) => {
1168 yield BanMemberEvent::Error {
1169 message: format!("Failed to load account: {}", e),
1170 };
1171 return;
1172 }
1173 };
1174
1175 let client = DiscordClient::new(account_config.bot_token);
1176
1177 match client.ban_member(&guild_id, &user_id, reason, delete_message_days).await {
1178 Ok(_) => {
1179 yield BanMemberEvent::Banned {
1180 user_id,
1181 };
1182 }
1183 Err(e) => {
1184 yield BanMemberEvent::Error { message: e };
1185 }
1186 }
1187 }
1188 }
1189
1190 #[plexus_macros::hub_method(
1191 description = "Unban a member from a guild",
1192 params(
1193 account = "Account name to use",
1194 guild_id = "Guild ID",
1195 user_id = "User ID"
1196 )
1197 )]
1198 async fn unban_member(
1199 &self,
1200 account: String,
1201 guild_id: String,
1202 user_id: String,
1203 ) -> impl Stream<Item = UnbanMemberEvent> + Send + 'static {
1204 let storage = self.storage.clone();
1205
1206 stream! {
1207 let account_config = match storage.get_account(&account).await {
1208 Ok(Some(acc)) => acc,
1209 Ok(None) => {
1210 yield UnbanMemberEvent::Error {
1211 message: format!("Account '{}' not found", account),
1212 };
1213 return;
1214 }
1215 Err(e) => {
1216 yield UnbanMemberEvent::Error {
1217 message: format!("Failed to load account: {}", e),
1218 };
1219 return;
1220 }
1221 };
1222
1223 let client = DiscordClient::new(account_config.bot_token);
1224
1225 match client.unban_member(&guild_id, &user_id).await {
1226 Ok(_) => {
1227 yield UnbanMemberEvent::Unbanned {
1228 user_id,
1229 };
1230 }
1231 Err(e) => {
1232 yield UnbanMemberEvent::Error { message: e };
1233 }
1234 }
1235 }
1236 }
1237
1238 #[plexus_macros::hub_method(
1239 streaming,
1240 description = "Get list of bans for a guild",
1241 params(
1242 account = "Account name to use",
1243 guild_id = "Guild ID"
1244 )
1245 )]
1246 async fn list_bans(
1247 &self,
1248 account: String,
1249 guild_id: String,
1250 ) -> impl Stream<Item = ListBansEvent> + Send + 'static {
1251 let storage = self.storage.clone();
1252
1253 stream! {
1254 let account_config = match storage.get_account(&account).await {
1255 Ok(Some(acc)) => acc,
1256 Ok(None) => {
1257 yield ListBansEvent::Error {
1258 message: format!("Account '{}' not found", account),
1259 };
1260 return;
1261 }
1262 Err(e) => {
1263 yield ListBansEvent::Error {
1264 message: format!("Failed to load account: {}", e),
1265 };
1266 return;
1267 }
1268 };
1269
1270 let client = DiscordClient::new(account_config.bot_token);
1271
1272 match client.list_bans(&guild_id).await {
1273 Ok(bans) => {
1274 let total = bans.len();
1275 for ban in bans {
1276 yield ListBansEvent::Ban {
1277 user_id: ban.user.id,
1278 username: ban.user.username,
1279 discriminator: ban.user.discriminator,
1280 reason: ban.reason,
1281 };
1282 }
1283 yield ListBansEvent::Complete { total };
1284 }
1285 Err(e) => {
1286 yield ListBansEvent::Error { message: e };
1287 }
1288 }
1289 }
1290 }
1291
1292 #[plexus_macros::hub_method(
1295 description = "Create a new role in a guild",
1296 params(
1297 account = "Account name to use",
1298 guild_id = "Guild ID",
1299 name = "Role name",
1300 permissions = "Permission bit string (optional)",
1301 color = "Role color as integer (optional)"
1302 )
1303 )]
1304 async fn create_role(
1305 &self,
1306 account: String,
1307 guild_id: String,
1308 name: String,
1309 permissions: Option<String>,
1310 color: Option<i32>,
1311 ) -> impl Stream<Item = CreateRoleEvent> + Send + 'static {
1312 let storage = self.storage.clone();
1313
1314 stream! {
1315 let account_config = match storage.get_account(&account).await {
1316 Ok(Some(acc)) => acc,
1317 Ok(None) => {
1318 yield CreateRoleEvent::Error {
1319 message: format!("Account '{}' not found", account),
1320 };
1321 return;
1322 }
1323 Err(e) => {
1324 yield CreateRoleEvent::Error {
1325 message: format!("Failed to load account: {}", e),
1326 };
1327 return;
1328 }
1329 };
1330
1331 let client = DiscordClient::new(account_config.bot_token);
1332
1333 match client.create_role(&guild_id, name.clone(), permissions, color).await {
1334 Ok(role) => {
1335 yield CreateRoleEvent::Created {
1336 role_id: role.id,
1337 role_name: role.name,
1338 };
1339 }
1340 Err(e) => {
1341 yield CreateRoleEvent::Error { message: e };
1342 }
1343 }
1344 }
1345 }
1346
1347 #[plexus_macros::hub_method(
1348 description = "Modify a role's properties",
1349 params(
1350 account = "Account name to use",
1351 guild_id = "Guild ID",
1352 role_id = "Role ID",
1353 name = "New role name (optional)",
1354 permissions = "New permission bit string (optional)",
1355 color = "New role color as integer (optional)"
1356 )
1357 )]
1358 async fn modify_role(
1359 &self,
1360 account: String,
1361 guild_id: String,
1362 role_id: String,
1363 name: Option<String>,
1364 permissions: Option<String>,
1365 color: Option<i32>,
1366 ) -> impl Stream<Item = ModifyRoleEvent> + Send + 'static {
1367 let storage = self.storage.clone();
1368
1369 stream! {
1370 let account_config = match storage.get_account(&account).await {
1371 Ok(Some(acc)) => acc,
1372 Ok(None) => {
1373 yield ModifyRoleEvent::Error {
1374 message: format!("Account '{}' not found", account),
1375 };
1376 return;
1377 }
1378 Err(e) => {
1379 yield ModifyRoleEvent::Error {
1380 message: format!("Failed to load account: {}", e),
1381 };
1382 return;
1383 }
1384 };
1385
1386 let client = DiscordClient::new(account_config.bot_token);
1387
1388 match client.modify_role(&guild_id, &role_id, name, permissions, color).await {
1389 Ok(role) => {
1390 yield ModifyRoleEvent::Modified {
1391 role_id: role.id,
1392 role_name: role.name,
1393 };
1394 }
1395 Err(e) => {
1396 yield ModifyRoleEvent::Error { message: e };
1397 }
1398 }
1399 }
1400 }
1401
1402 #[plexus_macros::hub_method(
1403 description = "Delete a role",
1404 params(
1405 account = "Account name to use",
1406 guild_id = "Guild ID",
1407 role_id = "Role ID"
1408 )
1409 )]
1410 async fn delete_role(
1411 &self,
1412 account: String,
1413 guild_id: String,
1414 role_id: String,
1415 ) -> impl Stream<Item = DeleteRoleEvent> + Send + 'static {
1416 let storage = self.storage.clone();
1417
1418 stream! {
1419 let account_config = match storage.get_account(&account).await {
1420 Ok(Some(acc)) => acc,
1421 Ok(None) => {
1422 yield DeleteRoleEvent::Error {
1423 message: format!("Account '{}' not found", account),
1424 };
1425 return;
1426 }
1427 Err(e) => {
1428 yield DeleteRoleEvent::Error {
1429 message: format!("Failed to load account: {}", e),
1430 };
1431 return;
1432 }
1433 };
1434
1435 let client = DiscordClient::new(account_config.bot_token);
1436
1437 match client.delete_role(&guild_id, &role_id).await {
1438 Ok(_) => {
1439 yield DeleteRoleEvent::Deleted {
1440 role_id,
1441 };
1442 }
1443 Err(e) => {
1444 yield DeleteRoleEvent::Error { message: e };
1445 }
1446 }
1447 }
1448 }
1449
1450 #[plexus_macros::hub_method(
1451 description = "Add a role to a member",
1452 params(
1453 account = "Account name to use",
1454 guild_id = "Guild ID",
1455 user_id = "User ID",
1456 role_id = "Role ID"
1457 )
1458 )]
1459 async fn add_role_to_member(
1460 &self,
1461 account: String,
1462 guild_id: String,
1463 user_id: String,
1464 role_id: String,
1465 ) -> impl Stream<Item = AddRoleToMemberEvent> + Send + 'static {
1466 let storage = self.storage.clone();
1467
1468 stream! {
1469 let account_config = match storage.get_account(&account).await {
1470 Ok(Some(acc)) => acc,
1471 Ok(None) => {
1472 yield AddRoleToMemberEvent::Error {
1473 message: format!("Account '{}' not found", account),
1474 };
1475 return;
1476 }
1477 Err(e) => {
1478 yield AddRoleToMemberEvent::Error {
1479 message: format!("Failed to load account: {}", e),
1480 };
1481 return;
1482 }
1483 };
1484
1485 let client = DiscordClient::new(account_config.bot_token);
1486
1487 match client.add_role_to_member(&guild_id, &user_id, &role_id).await {
1488 Ok(_) => {
1489 yield AddRoleToMemberEvent::Added {
1490 user_id,
1491 role_id,
1492 };
1493 }
1494 Err(e) => {
1495 yield AddRoleToMemberEvent::Error { message: e };
1496 }
1497 }
1498 }
1499 }
1500
1501 #[plexus_macros::hub_method(
1502 description = "Remove a role from a member",
1503 params(
1504 account = "Account name to use",
1505 guild_id = "Guild ID",
1506 user_id = "User ID",
1507 role_id = "Role ID"
1508 )
1509 )]
1510 async fn remove_role_from_member(
1511 &self,
1512 account: String,
1513 guild_id: String,
1514 user_id: String,
1515 role_id: String,
1516 ) -> impl Stream<Item = RemoveRoleFromMemberEvent> + Send + 'static {
1517 let storage = self.storage.clone();
1518
1519 stream! {
1520 let account_config = match storage.get_account(&account).await {
1521 Ok(Some(acc)) => acc,
1522 Ok(None) => {
1523 yield RemoveRoleFromMemberEvent::Error {
1524 message: format!("Account '{}' not found", account),
1525 };
1526 return;
1527 }
1528 Err(e) => {
1529 yield RemoveRoleFromMemberEvent::Error {
1530 message: format!("Failed to load account: {}", e),
1531 };
1532 return;
1533 }
1534 };
1535
1536 let client = DiscordClient::new(account_config.bot_token);
1537
1538 match client.remove_role_from_member(&guild_id, &user_id, &role_id).await {
1539 Ok(_) => {
1540 yield RemoveRoleFromMemberEvent::Removed {
1541 user_id,
1542 role_id,
1543 };
1544 }
1545 Err(e) => {
1546 yield RemoveRoleFromMemberEvent::Error { message: e };
1547 }
1548 }
1549 }
1550 }
1551
1552 #[plexus_macros::hub_method(
1555 description = "Edit an existing message",
1556 params(
1557 account = "Account name to use",
1558 channel_id = "Channel ID",
1559 message_id = "Message ID",
1560 content = "New message content",
1561 embed = "Rich embed object (optional)"
1562 )
1563 )]
1564 async fn edit_message(
1565 &self,
1566 account: String,
1567 channel_id: String,
1568 message_id: String,
1569 content: String,
1570 embed: Option<serde_json::Value>,
1571 ) -> impl Stream<Item = EditMessageEvent> + Send + 'static {
1572 let storage = self.storage.clone();
1573
1574 stream! {
1575 let account_config = match storage.get_account(&account).await {
1576 Ok(Some(acc)) => acc,
1577 Ok(None) => {
1578 yield EditMessageEvent::Error {
1579 message: format!("Account '{}' not found", account),
1580 };
1581 return;
1582 }
1583 Err(e) => {
1584 yield EditMessageEvent::Error {
1585 message: format!("Failed to load account: {}", e),
1586 };
1587 return;
1588 }
1589 };
1590
1591 let client = DiscordClient::new(account_config.bot_token);
1592
1593 match client.edit_message(&channel_id, &message_id, content, embed).await {
1594 Ok(msg) => {
1595 yield EditMessageEvent::Edited {
1596 message_id: msg.id,
1597 channel_id: msg.channel_id,
1598 };
1599 }
1600 Err(e) => {
1601 yield EditMessageEvent::Error { message: e };
1602 }
1603 }
1604 }
1605 }
1606
1607 #[plexus_macros::hub_method(
1608 description = "Delete a message",
1609 params(
1610 account = "Account name to use",
1611 channel_id = "Channel ID",
1612 message_id = "Message ID"
1613 )
1614 )]
1615 async fn delete_message(
1616 &self,
1617 account: String,
1618 channel_id: String,
1619 message_id: String,
1620 ) -> impl Stream<Item = DeleteMessageEvent> + Send + 'static {
1621 let storage = self.storage.clone();
1622
1623 stream! {
1624 let account_config = match storage.get_account(&account).await {
1625 Ok(Some(acc)) => acc,
1626 Ok(None) => {
1627 yield DeleteMessageEvent::Error {
1628 message: format!("Account '{}' not found", account),
1629 };
1630 return;
1631 }
1632 Err(e) => {
1633 yield DeleteMessageEvent::Error {
1634 message: format!("Failed to load account: {}", e),
1635 };
1636 return;
1637 }
1638 };
1639
1640 let client = DiscordClient::new(account_config.bot_token);
1641
1642 match client.delete_message(&channel_id, &message_id).await {
1643 Ok(_) => {
1644 yield DeleteMessageEvent::Deleted {
1645 message_id,
1646 channel_id,
1647 };
1648 }
1649 Err(e) => {
1650 yield DeleteMessageEvent::Error { message: e };
1651 }
1652 }
1653 }
1654 }
1655
1656 #[plexus_macros::hub_method(
1657 description = "Add a reaction to a message (emoji can be unicode or custom emoji format)",
1658 params(
1659 account = "Account name to use",
1660 channel_id = "Channel ID",
1661 message_id = "Message ID",
1662 emoji = "Emoji (unicode or custom format: name:id)"
1663 )
1664 )]
1665 async fn add_reaction(
1666 &self,
1667 account: String,
1668 channel_id: String,
1669 message_id: String,
1670 emoji: String,
1671 ) -> impl Stream<Item = AddReactionEvent> + Send + 'static {
1672 let storage = self.storage.clone();
1673
1674 stream! {
1675 let account_config = match storage.get_account(&account).await {
1676 Ok(Some(acc)) => acc,
1677 Ok(None) => {
1678 yield AddReactionEvent::Error {
1679 message: format!("Account '{}' not found", account),
1680 };
1681 return;
1682 }
1683 Err(e) => {
1684 yield AddReactionEvent::Error {
1685 message: format!("Failed to load account: {}", e),
1686 };
1687 return;
1688 }
1689 };
1690
1691 let client = DiscordClient::new(account_config.bot_token);
1692
1693 match client.add_reaction(&channel_id, &message_id, &emoji).await {
1694 Ok(_) => {
1695 yield AddReactionEvent::Added {
1696 message_id,
1697 channel_id,
1698 emoji,
1699 };
1700 }
1701 Err(e) => {
1702 yield AddReactionEvent::Error { message: e };
1703 }
1704 }
1705 }
1706 }
1707
1708 #[plexus_macros::hub_method(
1709 description = "Pin a message in a channel",
1710 params(
1711 account = "Account name to use",
1712 channel_id = "Channel ID",
1713 message_id = "Message ID"
1714 )
1715 )]
1716 async fn pin_message(
1717 &self,
1718 account: String,
1719 channel_id: String,
1720 message_id: String,
1721 ) -> impl Stream<Item = PinMessageEvent> + Send + 'static {
1722 let storage = self.storage.clone();
1723
1724 stream! {
1725 let account_config = match storage.get_account(&account).await {
1726 Ok(Some(acc)) => acc,
1727 Ok(None) => {
1728 yield PinMessageEvent::Error {
1729 message: format!("Account '{}' not found", account),
1730 };
1731 return;
1732 }
1733 Err(e) => {
1734 yield PinMessageEvent::Error {
1735 message: format!("Failed to load account: {}", e),
1736 };
1737 return;
1738 }
1739 };
1740
1741 let client = DiscordClient::new(account_config.bot_token);
1742
1743 match client.pin_message(&channel_id, &message_id).await {
1744 Ok(_) => {
1745 yield PinMessageEvent::Pinned {
1746 message_id,
1747 channel_id,
1748 };
1749 }
1750 Err(e) => {
1751 yield PinMessageEvent::Error { message: e };
1752 }
1753 }
1754 }
1755 }
1756
1757 #[plexus_macros::hub_method(
1758 description = "Unpin a message from a channel",
1759 params(
1760 account = "Account name to use",
1761 channel_id = "Channel ID",
1762 message_id = "Message ID"
1763 )
1764 )]
1765 async fn unpin_message(
1766 &self,
1767 account: String,
1768 channel_id: String,
1769 message_id: String,
1770 ) -> impl Stream<Item = UnpinMessageEvent> + Send + 'static {
1771 let storage = self.storage.clone();
1772
1773 stream! {
1774 let account_config = match storage.get_account(&account).await {
1775 Ok(Some(acc)) => acc,
1776 Ok(None) => {
1777 yield UnpinMessageEvent::Error {
1778 message: format!("Account '{}' not found", account),
1779 };
1780 return;
1781 }
1782 Err(e) => {
1783 yield UnpinMessageEvent::Error {
1784 message: format!("Failed to load account: {}", e),
1785 };
1786 return;
1787 }
1788 };
1789
1790 let client = DiscordClient::new(account_config.bot_token);
1791
1792 match client.unpin_message(&channel_id, &message_id).await {
1793 Ok(_) => {
1794 yield UnpinMessageEvent::Unpinned {
1795 message_id,
1796 channel_id,
1797 };
1798 }
1799 Err(e) => {
1800 yield UnpinMessageEvent::Error { message: e };
1801 }
1802 }
1803 }
1804 }
1805
1806 #[plexus_macros::hub_method(
1809 description = "Create a thread from a message or in a channel",
1810 params(
1811 account = "Account name to use",
1812 channel_id = "Channel ID",
1813 name = "Thread name",
1814 message_id = "Message ID to create thread from (optional, creates standalone thread if omitted)"
1815 )
1816 )]
1817 async fn create_thread(
1818 &self,
1819 account: String,
1820 channel_id: String,
1821 name: String,
1822 message_id: Option<String>,
1823 ) -> impl Stream<Item = CreateThreadEvent> + Send + 'static {
1824 let storage = self.storage.clone();
1825
1826 stream! {
1827 let account_config = match storage.get_account(&account).await {
1828 Ok(Some(acc)) => acc,
1829 Ok(None) => {
1830 yield CreateThreadEvent::Error {
1831 message: format!("Account '{}' not found", account),
1832 };
1833 return;
1834 }
1835 Err(e) => {
1836 yield CreateThreadEvent::Error {
1837 message: format!("Failed to load account: {}", e),
1838 };
1839 return;
1840 }
1841 };
1842
1843 let client = DiscordClient::new(account_config.bot_token);
1844
1845 match client.create_thread(&channel_id, name.clone(), message_id).await {
1846 Ok(thread) => {
1847 yield CreateThreadEvent::Created {
1848 thread_id: thread.id,
1849 thread_name: thread.name,
1850 };
1851 }
1852 Err(e) => {
1853 yield CreateThreadEvent::Error { message: e };
1854 }
1855 }
1856 }
1857 }
1858
1859 #[plexus_macros::hub_method(
1860 description = "Join a thread",
1861 params(
1862 account = "Account name to use",
1863 thread_id = "Thread ID"
1864 )
1865 )]
1866 async fn join_thread(
1867 &self,
1868 account: String,
1869 thread_id: String,
1870 ) -> impl Stream<Item = JoinThreadEvent> + Send + 'static {
1871 let storage = self.storage.clone();
1872
1873 stream! {
1874 let account_config = match storage.get_account(&account).await {
1875 Ok(Some(acc)) => acc,
1876 Ok(None) => {
1877 yield JoinThreadEvent::Error {
1878 message: format!("Account '{}' not found", account),
1879 };
1880 return;
1881 }
1882 Err(e) => {
1883 yield JoinThreadEvent::Error {
1884 message: format!("Failed to load account: {}", e),
1885 };
1886 return;
1887 }
1888 };
1889
1890 let client = DiscordClient::new(account_config.bot_token);
1891
1892 match client.join_thread(&thread_id).await {
1893 Ok(_) => {
1894 yield JoinThreadEvent::Joined {
1895 thread_id,
1896 };
1897 }
1898 Err(e) => {
1899 yield JoinThreadEvent::Error { message: e };
1900 }
1901 }
1902 }
1903 }
1904
1905 #[plexus_macros::hub_method(
1906 description = "Leave a thread",
1907 params(
1908 account = "Account name to use",
1909 thread_id = "Thread ID"
1910 )
1911 )]
1912 async fn leave_thread(
1913 &self,
1914 account: String,
1915 thread_id: String,
1916 ) -> impl Stream<Item = LeaveThreadEvent> + Send + 'static {
1917 let storage = self.storage.clone();
1918
1919 stream! {
1920 let account_config = match storage.get_account(&account).await {
1921 Ok(Some(acc)) => acc,
1922 Ok(None) => {
1923 yield LeaveThreadEvent::Error {
1924 message: format!("Account '{}' not found", account),
1925 };
1926 return;
1927 }
1928 Err(e) => {
1929 yield LeaveThreadEvent::Error {
1930 message: format!("Failed to load account: {}", e),
1931 };
1932 return;
1933 }
1934 };
1935
1936 let client = DiscordClient::new(account_config.bot_token);
1937
1938 match client.leave_thread(&thread_id).await {
1939 Ok(_) => {
1940 yield LeaveThreadEvent::Left {
1941 thread_id,
1942 };
1943 }
1944 Err(e) => {
1945 yield LeaveThreadEvent::Error { message: e };
1946 }
1947 }
1948 }
1949 }
1950}