Skip to main content

plexus_comms/activations/discord/
activation.rs

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    // ==================== Account Management ====================
44
45    #[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    // ==================== Discord API Operations ====================
124
125    #[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            // Get account
145            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            // Create Discord client
164            let client = DiscordClient::new(account_config.bot_token);
165
166            // Send message
167            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            // Get account
202            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            // Create Discord client
219            let client = DiscordClient::new(account_config.bot_token);
220
221            // Create webhook
222            match client.create_webhook(&channel_id, name).await {
223                Ok(webhook_url) => {
224                    // Extract webhook ID from URL
225                    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    // ==================== Gateway Listener ====================
246
247    #[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            // Check if already listening
262            {
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            // Get account configuration
273            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            // Create Gateway connection
294            let gateway = DiscordGateway::new(account_config.bot_token);
295
296            // Create channel for Gateway events
297            let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
298
299            // Spawn Gateway task
300            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            // Store the task handle
310            {
311                let mut gateways = active_gateways.lock().await;
312                gateways.insert(account.clone(), gateway_task);
313            }
314
315            // Stream events from Gateway
316            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            // Clean up on disconnect
380            {
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    // ==================== Guild/Server Management ====================
438
439    #[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    // ==================== Channel Management ====================
716
717    #[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    // ==================== Member Management ====================
976
977    #[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    // ==================== Role Management ====================
1293
1294    #[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    // ==================== Message Management ====================
1553
1554    #[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    // ==================== Thread Management ====================
1807
1808    #[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}