Skip to main content

communitas_core/
command.rs

1// Copyright (c) 2025 Saorsa Labs Limited
2//
3// Dual-licensed under the AGPL-3.0-or-later and a commercial license.
4// You may use this file under the terms of the GNU Affero General Public License v3.0 or later.
5// For commercial licensing, contact: saorsalabs@gmail.com
6//
7// See the LICENSE-AGPL-3.0 and LICENSE-COMMERCIAL.md files for details.
8
9//! Command/Event/Query Architecture for Headless Core
10//!
11//! This module implements the "Headless Core with Multi-Adapter" pattern that enables:
12//! - GUI adapters (Iced, Swift) to control the application through execute/query/subscribe
13//! - AI agents to control the application via MCP server
14//! - CLI tools to automate workflows
15//! - Test harnesses to verify behavior
16//!
17//! # Architecture
18//!
19//! ```text
20//! ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
21//! │   Iced GUI  │  │  Swift GUI  │  │ MCP Server  │  │     CLI     │
22//! │  (Adapter)  │  │  (Adapter)  │  │  (Adapter)  │  │  (Adapter)  │
23//! └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘
24//!        │                │                │                │
25//!        └────────────────┼────────────────┼────────────────┘
26//!                         │                │
27//!                         ▼                ▼
28//!               ┌─────────────────────────────────────┐
29//!               │        CommunitasApp (Core)         │
30//!               │  execute(cmd) / query(q) / sub(s)   │
31//!               └─────────────────────────────────────┘
32//! ```
33//!
34//! # Key Principles
35//!
36//! 1. **All mutations via Commands** - Every state change MUST go through a command.
37//!    No adapter should directly mutate state.
38//!
39//! 2. **Events record what happened** - After a command executes, it produces events
40//!    that describe what changed. These events can be replayed for debugging/testing.
41//!
42//! 3. **Queries for reads** - All state reads go through the Query enum. This enables
43//!    caching, access control, and consistent responses across adapters.
44//!
45//! 4. **Subscriptions for reactivity** - Adapters subscribe to event streams to
46//!    update their UI when state changes.
47
48use crate::crdt::EntityType;
49use crate::invite::InviteStatus;
50use serde::{Deserialize, Serialize};
51
52// ============================================================================
53// Commands - All mutations to application state
54// ============================================================================
55
56/// Commands represent all possible mutations to the application state.
57///
58/// Every action that modifies state MUST go through a command. This ensures:
59/// - Consistent validation across all adapters
60/// - Event sourcing capability (commands produce events)
61/// - MCP server can expose all application functionality
62/// - Test harnesses can verify behavior
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum Command {
65    // ========================================================================
66    // Profile & Identity Commands
67    // ========================================================================
68    /// Initialize the application with a new or existing identity
69    Initialize {
70        four_words: String,
71        display_name: String,
72        device_name: String,
73        storage_dir: String,
74    },
75
76    /// Update the user's display name
77    UpdateDisplayName { display_name: String },
78
79    // ========================================================================
80    // Networking Commands
81    // ========================================================================
82    /// Start P2P networking with gossip overlay
83    StartNetworking { preferred_port: Option<u16> },
84
85    /// Stop P2P networking gracefully
86    StopNetworking,
87
88    /// Connect to a peer by their four-word identity
89    ConnectToPeer { peer_four_words: String },
90
91    /// Request external address discovery via NAT reflection
92    RequestExternalAddress,
93
94    // ========================================================================
95    // Entity Management Commands
96    // ========================================================================
97    /// Create a new entity (organization, group, channel, project)
98    CreateEntity {
99        name: String,
100        entity_type: EntityType,
101        description: Option<String>,
102        initial_members: Vec<String>,
103    },
104
105    /// Create a local-only entity (not yet linked to network)
106    CreateLocalEntity {
107        name: String,
108        entity_type: EntityType,
109        description: Option<String>,
110    },
111
112    /// Link a local entity to a network identity
113    LinkEntityToNetwork {
114        entity_id: String,
115        four_words: String,
116    },
117
118    /// Mark an entity as synced with network
119    MarkEntitySynced { entity_id: String },
120
121    /// Set parent organization for a child entity
122    SetParentOrganization {
123        entity_id: String,
124        parent_org_id: String,
125    },
126
127    UpdateEntity {
128        entity_type: EntityType,
129        entity_id: String,
130        name: Option<String>,
131        description: Option<Option<String>>,
132    },
133
134    DeleteEntity {
135        entity_type: EntityType,
136        entity_id: String,
137    },
138
139    // ========================================================================
140    // Member Management Commands
141    // ========================================================================
142    /// Add a member to an entity
143    AddMember {
144        entity_type: EntityType,
145        entity_id: String,
146        member_id: String,
147        role: String,
148    },
149
150    /// Remove a member from an entity
151    RemoveMember {
152        entity_type: EntityType,
153        entity_id: String,
154        member_id: String,
155    },
156
157    /// Remove a member from organization and all child entities (cascade)
158    RemoveOrganizationMember { org_id: String, member_id: String },
159
160    /// Set a member's role
161    SetMemberRole {
162        entity_type: EntityType,
163        entity_id: String,
164        member_id: String,
165        new_role: String,
166    },
167
168    // ========================================================================
169    // Permission Commands
170    // ========================================================================
171    /// Set a permission override for a member
172    SetPermissionOverride {
173        entity_type: EntityType,
174        entity_id: String,
175        member_id: String,
176        resource_type: String,
177        access_level: String,
178    },
179
180    /// Remove a permission override for a member
181    RemovePermissionOverride {
182        entity_type: EntityType,
183        entity_id: String,
184        member_id: String,
185        resource_type: String,
186    },
187
188    // ========================================================================
189    // Messaging Commands
190    // ========================================================================
191    /// Send a message to an entity
192    SendMessage {
193        entity_id: String,
194        entity_type: EntityType,
195        text: String,
196        author: String,
197        reply_to_id: Option<String>,
198        attachments: Option<Vec<String>>,
199    },
200
201    /// Send a direct message to one or more recipients
202    SendDirectMessage {
203        recipients: Vec<String>,
204        text: String,
205        author: String,
206    },
207
208    DeleteMessage {
209        entity_id: String,
210        entity_type: EntityType,
211        message_id: String,
212    },
213
214    /// Edit a message's text content
215    EditMessage {
216        entity_id: String,
217        entity_type: EntityType,
218        message_id: String,
219        new_text: String,
220    },
221
222    /// Add a reaction to a message
223    AddReaction {
224        entity_id: String,
225        entity_type: EntityType,
226        message_id: String,
227        emoji: String,
228    },
229
230    /// Remove a reaction from a message
231    RemoveReaction {
232        entity_id: String,
233        entity_type: EntityType,
234        message_id: String,
235        emoji: String,
236    },
237
238    // ========================================================================
239    // Invite Commands
240    // ========================================================================
241    /// Create an invite for someone to join an entity
242    CreateInvite {
243        recipient_id: String,
244        entity_type: EntityType,
245        entity_id: String,
246        role: String,
247        message: Option<String>,
248        expires_in_hours: Option<u32>,
249    },
250
251    /// Accept an invite
252    AcceptInvite { invite_id: String },
253
254    /// Reject an invite
255    RejectInvite { invite_id: String },
256
257    /// Revoke an invite
258    RevokeInvite { invite_id: String },
259
260    // ========================================================================
261    // Virtual Disk Commands
262    // ========================================================================
263    /// Write a file to an entity's virtual disk
264    WriteFile {
265        entity_id: String,
266        disk_type: DiskTypeArg,
267        path: String,
268        data: Vec<u8>,
269    },
270
271    /// Delete a file from an entity's virtual disk
272    DeleteFile {
273        entity_id: String,
274        disk_type: DiskTypeArg,
275        path: String,
276    },
277
278    /// Create a directory in an entity's virtual disk
279    CreateDirectory {
280        entity_id: String,
281        disk_type: DiskTypeArg,
282        path: String,
283    },
284
285    // ========================================================================
286    // Kanban Commands
287    // ========================================================================
288    /// Create a new Kanban board
289    CreateKanbanBoard {
290        entity_id: String,
291        board_name: String,
292        description: Option<String>,
293    },
294
295    /// Create a column in a Kanban board
296    CreateKanbanColumn {
297        board_id: String,
298        column_name: String,
299        position: Option<u32>,
300    },
301
302    /// Create a card in a Kanban column
303    CreateKanbanCard {
304        board_id: String,
305        column_id: String,
306        title: String,
307        description: Option<String>,
308        assignee: Option<String>,
309    },
310
311    /// Move a Kanban card to a different column
312    MoveKanbanCard {
313        board_id: String,
314        card_id: String,
315        target_column_id: String,
316        position: Option<u32>,
317    },
318
319    /// Update a Kanban card
320    UpdateKanbanCard {
321        board_id: String,
322        card_id: String,
323        title: Option<String>,
324        description: Option<String>,
325        assignee: Option<String>,
326    },
327
328    /// Delete a Kanban card
329    DeleteKanbanCard { board_id: String, card_id: String },
330
331    /// Update a Kanban board's name or description
332    UpdateKanbanBoard {
333        board_id: String,
334        name: Option<String>,
335        description: Option<Option<String>>,
336    },
337
338    /// Delete a Kanban board and all its columns and cards
339    DeleteKanbanBoard { board_id: String },
340
341    // ========================================================================
342    // WebRTC Commands (Voice/Video/Screen)
343    // ========================================================================
344    /// Start a voice/video call
345    StartCall {
346        entity_id: String,
347        video_enabled: bool,
348    },
349
350    /// Join an existing call
351    JoinCall { call_id: String },
352
353    /// Leave a call
354    LeaveCall { call_id: String },
355
356    /// Toggle video in a call
357    ToggleVideo { call_id: String, enabled: bool },
358
359    /// Toggle audio in a call
360    ToggleAudio { call_id: String, enabled: bool },
361
362    /// Start screen sharing
363    StartScreenShare { call_id: String },
364
365    /// Stop screen sharing
366    StopScreenShare { call_id: String },
367
368    // ========================================================================
369    // Contact Management Commands
370    // ========================================================================
371    /// Create a new contact (local-only or network-linked)
372    CreateContact {
373        display_name: String,
374        four_words: Option<String>,
375        is_favourite: bool,
376    },
377
378    /// Update a contact's display name
379    UpdateContact {
380        contact_id: String,
381        display_name: Option<String>,
382        is_favourite: Option<bool>,
383    },
384
385    /// Delete a contact
386    DeleteContact { contact_id: String },
387
388    /// Link a local-only contact to a network identity
389    LinkContact {
390        contact_id: String,
391        four_words: String,
392    },
393
394    /// Set a contact as favourite
395    SetFavouriteContact { four_words: String },
396
397    /// Remove a contact from favourites
398    RemoveFavouriteContact { four_words: String },
399
400    // ========================================================================
401    // Website Publishing Commands
402    // ========================================================================
403    /// Create/publish a website for an entity
404    CreateWebsite {
405        entity_id: String,
406        html: String,
407        css: Option<String>,
408        js: Option<String>,
409        metadata: Option<String>,
410    },
411
412    /// Update an existing website
413    UpdateWebsite {
414        entity_id: String,
415        html: Option<String>,
416        css: Option<String>,
417        js: Option<String>,
418        metadata: Option<String>,
419    },
420
421    /// Delete a website
422    DeleteWebsite { entity_id: String },
423}
424
425/// Disk type argument for commands (serializable)
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
427pub enum DiskTypeArg {
428    Private,
429    Public,
430    Shared,
431}
432
433// ============================================================================
434// Events - What happened as a result of commands
435// ============================================================================
436
437/// Events describe what changed as a result of a command.
438///
439/// Events are:
440/// - Immutable records of what happened
441/// - Used to update UI reactively
442/// - Can be replayed for debugging/testing
443/// - Broadcast to subscribers
444#[derive(Debug, Clone, Serialize, Deserialize)]
445pub enum Event {
446    // ========================================================================
447    // Profile & Identity Events
448    // ========================================================================
449    /// Application was initialized
450    Initialized {
451        four_words: String,
452        display_name: String,
453        device_name: String,
454    },
455
456    /// Display name was updated
457    DisplayNameUpdated { old_name: String, new_name: String },
458
459    // ========================================================================
460    // Networking Events
461    // ========================================================================
462    /// Networking started
463    NetworkingStarted {
464        listen_address: String,
465        connection_identity: String,
466    },
467
468    /// Networking stopped
469    NetworkingStopped,
470
471    /// Connected to a peer
472    PeerConnected { peer_four_words: String },
473
474    /// External address discovered
475    ExternalAddressDiscovered { address: String },
476
477    /// Connection failed
478    ConnectionFailed {
479        peer_four_words: String,
480        reason: String,
481    },
482
483    // ========================================================================
484    // Entity Events
485    // ========================================================================
486    /// Entity was created
487    EntityCreated {
488        entity_id: String,
489        name: String,
490        entity_type: EntityType,
491        created_by: String,
492    },
493
494    /// Entity was linked to network
495    EntityLinkedToNetwork {
496        entity_id: String,
497        four_words: String,
498    },
499
500    /// Entity was synced with network
501    EntitySynced { entity_id: String },
502
503    /// Parent organization was set
504    ParentOrganizationSet {
505        entity_id: String,
506        parent_org_id: String,
507    },
508
509    /// Entity was updated
510    EntityUpdated {
511        entity_id: String,
512        entity_type: EntityType,
513        name: Option<String>,
514    },
515
516    /// Entity was deleted
517    EntityDeleted {
518        entity_id: String,
519        entity_type: EntityType,
520    },
521
522    // ========================================================================
523    // Member Events
524    // ========================================================================
525    /// Member was added to an entity
526    MemberAdded {
527        entity_type: EntityType,
528        entity_id: String,
529        member_id: String,
530        role: String,
531    },
532
533    /// Member was removed from an entity
534    MemberRemoved {
535        entity_type: EntityType,
536        entity_id: String,
537        member_id: String,
538    },
539
540    /// Member's role was changed
541    MemberRoleChanged {
542        entity_type: EntityType,
543        entity_id: String,
544        member_id: String,
545        old_role: String,
546        new_role: String,
547    },
548
549    /// Member was removed from organization and child entities
550    OrganizationMemberRemoved {
551        org_id: String,
552        member_id: String,
553        removed_from: Vec<(EntityType, String)>,
554    },
555
556    // ========================================================================
557    // Permission Events
558    // ========================================================================
559    /// Permission override was set
560    PermissionOverrideSet {
561        entity_type: EntityType,
562        entity_id: String,
563        member_id: String,
564        resource_type: String,
565        access_level: String,
566    },
567
568    /// Permission override was removed
569    PermissionOverrideRemoved {
570        entity_type: EntityType,
571        entity_id: String,
572        member_id: String,
573        resource_type: String,
574    },
575
576    // ========================================================================
577    // Message Events
578    // ========================================================================
579    /// Message was sent
580    MessageSent {
581        message_id: String,
582        entity_id: String,
583        entity_type: EntityType,
584        author: String,
585        text: String,
586    },
587
588    /// Message was received
589    MessageReceived {
590        message_id: String,
591        entity_id: String,
592        entity_type: EntityType,
593        author: String,
594        text: String,
595    },
596
597    /// Direct message was sent
598    DirectMessageSent {
599        message_ids: Vec<String>,
600        recipients: Vec<String>,
601    },
602
603    /// Message was deleted
604    MessageDeleted {
605        message_id: String,
606        entity_id: String,
607        entity_type: EntityType,
608    },
609
610    /// Message was edited
611    MessageEdited {
612        message_id: String,
613        entity_id: String,
614        entity_type: EntityType,
615        new_text: String,
616        edited_at: u64,
617    },
618
619    /// Reaction was added to a message
620    ReactionAdded {
621        message_id: String,
622        entity_id: String,
623        entity_type: EntityType,
624        emoji: String,
625        reactor_id: String,
626    },
627
628    /// Reaction was removed from a message
629    ReactionRemoved {
630        message_id: String,
631        entity_id: String,
632        entity_type: EntityType,
633        emoji: String,
634        reactor_id: String,
635    },
636
637    // ========================================================================
638    // Invite Events
639    // ========================================================================
640    /// Invite was created
641    InviteCreated {
642        invite_id: String,
643        recipient_id: String,
644        entity_type: EntityType,
645        entity_id: String,
646        role: String,
647    },
648
649    /// Invite was accepted
650    InviteAccepted {
651        invite_id: String,
652        recipient_id: String,
653        entity_id: String,
654    },
655
656    /// Invite was rejected
657    InviteRejected { invite_id: String },
658
659    /// Invite was revoked
660    InviteRevoked { invite_id: String },
661
662    // ========================================================================
663    // Virtual Disk Events
664    // ========================================================================
665    /// File was written
666    FileWritten {
667        entity_id: String,
668        disk_type: DiskTypeArg,
669        path: String,
670        size_bytes: u64,
671    },
672
673    /// File was deleted
674    FileDeleted {
675        entity_id: String,
676        disk_type: DiskTypeArg,
677        path: String,
678    },
679
680    /// Directory was created
681    DirectoryCreated {
682        entity_id: String,
683        disk_type: DiskTypeArg,
684        path: String,
685    },
686
687    // ========================================================================
688    // Kanban Events
689    // ========================================================================
690    /// Kanban board was created
691    KanbanBoardCreated {
692        board_id: String,
693        entity_id: String,
694        board_name: String,
695    },
696
697    /// Kanban column was created
698    KanbanColumnCreated {
699        column_id: String,
700        board_id: String,
701        column_name: String,
702    },
703
704    /// Kanban card was created
705    KanbanCardCreated {
706        card_id: String,
707        column_id: String,
708        title: String,
709    },
710
711    /// Kanban card was moved
712    KanbanCardMoved {
713        card_id: String,
714        from_column_id: String,
715        to_column_id: String,
716    },
717
718    /// Kanban card was updated
719    KanbanCardUpdated { card_id: String },
720
721    /// Kanban card was deleted
722    KanbanCardDeleted { card_id: String },
723
724    /// Kanban board was updated
725    KanbanBoardUpdated {
726        board_id: String,
727        name: Option<String>,
728    },
729
730    /// Kanban board was deleted
731    KanbanBoardDeleted { board_id: String },
732
733    // ========================================================================
734    // WebRTC Events
735    // ========================================================================
736    /// Call started
737    CallStarted { call_id: String, entity_id: String },
738
739    /// Joined a call
740    CallJoined { call_id: String },
741
742    /// Left a call
743    CallLeft { call_id: String },
744
745    /// Video toggled
746    VideoToggled { call_id: String, enabled: bool },
747
748    /// Audio toggled
749    AudioToggled { call_id: String, enabled: bool },
750
751    /// Screen sharing started
752    ScreenShareStarted { call_id: String },
753
754    /// Screen sharing stopped
755    ScreenShareStopped { call_id: String },
756
757    // ========================================================================
758    // Contact Events
759    // ========================================================================
760    /// Contact was created
761    ContactCreated {
762        contact_id: String,
763        display_name: String,
764        four_words: Option<String>,
765    },
766
767    /// Contact was updated
768    ContactUpdated {
769        contact_id: String,
770        display_name: Option<String>,
771        is_favourite: Option<bool>,
772    },
773
774    /// Contact was deleted
775    ContactDeleted { contact_id: String },
776
777    /// Contact was linked to network identity
778    ContactLinked {
779        contact_id: String,
780        four_words: String,
781    },
782
783    /// Contact was set as favourite
784    ContactFavouriteSet { four_words: String },
785
786    /// Contact was removed from favourites
787    ContactFavouriteRemoved { four_words: String },
788
789    // ========================================================================
790    // Website Events
791    // ========================================================================
792    /// A website was created/published for an entity
793    WebsiteCreated {
794        entity_id: String,
795        website_root_hash: String,
796        published_at: i64,
797        size_bytes: usize,
798    },
799
800    /// A website was updated
801    WebsiteUpdated {
802        entity_id: String,
803        website_root_hash: String,
804        updated_at: i64,
805        size_bytes: usize,
806    },
807
808    /// A website was deleted
809    WebsiteDeleted { entity_id: String },
810
811    // ========================================================================
812    // Error Events
813    // ========================================================================
814    /// An error occurred while processing a command
815    CommandFailed { command_type: String, error: String },
816}
817
818// ============================================================================
819// Queries - All reads from application state
820// ============================================================================
821
822/// Queries represent all possible reads from the application state.
823///
824/// All state reads should go through queries to enable:
825/// - Consistent responses across adapters
826/// - Access control enforcement
827/// - Caching strategies
828/// - MCP server exposure
829#[derive(Debug, Clone, Serialize, Deserialize)]
830pub enum Query {
831    // ========================================================================
832    // Profile & Identity Queries
833    // ========================================================================
834    /// Get the current user profile
835    GetProfile,
836
837    /// Check if networking is active
838    IsNetworkingActive,
839
840    /// Get connection identity
841    GetConnectionIdentity,
842
843    /// Get external address
844    GetExternalAddress,
845
846    // ========================================================================
847    // Entity Queries
848    // ========================================================================
849    /// Get an entity by ID
850    GetEntity { entity_id: String },
851
852    /// List all entities
853    ListEntities,
854
855    /// List entities by type
856    ListEntitiesByType { entity_type: EntityType },
857
858    /// List child entities of an organization
859    ListChildEntities { org_id: String },
860
861    // ========================================================================
862    // Member Queries
863    // ========================================================================
864    /// List members of an entity
865    ListMembers {
866        entity_type: EntityType,
867        entity_id: String,
868    },
869
870    /// Get a member's role
871    GetMemberRole {
872        entity_type: EntityType,
873        entity_id: String,
874        member_id: String,
875    },
876
877    /// Get permission overrides for a member
878    GetPermissionOverrides {
879        entity_type: EntityType,
880        entity_id: String,
881        member_id: String,
882    },
883
884    // ========================================================================
885    // Message Queries
886    // ========================================================================
887    /// Get a single message by ID (includes reactions)
888    GetMessage {
889        entity_id: String,
890        message_id: String,
891    },
892
893    /// Get messages for an entity
894    GetEntityMessages { entity_id: String },
895
896    /// Get thread messages (replies to a parent message)
897    GetThreadMessages {
898        entity_id: String,
899        parent_message_id: String,
900    },
901
902    /// Get direct messages with a peer
903    GetDirectMessages { other_peer_id: String },
904
905    /// Get entity sync state
906    GetEntitySyncState {
907        entity_id: String,
908        entity_type: EntityType,
909    },
910
911    // ========================================================================
912    // Invite Queries
913    // ========================================================================
914    /// Get an invite by ID
915    GetInvite { invite_id: String },
916
917    /// List pending invites for current user
918    ListPendingInvites,
919
920    /// List invites sent by current user
921    ListSentInvites,
922
923    // ========================================================================
924    // Virtual Disk Queries
925    // ========================================================================
926    /// Read a file from an entity's virtual disk
927    ReadFile {
928        entity_id: String,
929        disk_type: DiskTypeArg,
930        path: String,
931    },
932
933    /// List files in a directory
934    ListFiles {
935        entity_id: String,
936        disk_type: DiskTypeArg,
937        path: String,
938    },
939
940    /// Get disk statistics
941    GetDiskStats {
942        entity_id: String,
943        disk_type: DiskTypeArg,
944    },
945
946    // ========================================================================
947    // Kanban Queries
948    // ========================================================================
949    /// Get a Kanban board
950    GetKanbanBoard { board_id: String },
951
952    /// List Kanban boards for an entity
953    ListKanbanBoards { entity_id: String },
954
955    /// Get a Kanban card
956    GetKanbanCard { board_id: String, card_id: String },
957
958    /// List Kanban cards in a board with optional filters
959    ListKanbanCards {
960        board_id: String,
961        column_id: Option<String>,
962        state: Option<String>,
963        assignee_id: Option<String>,
964        tag_id: Option<String>,
965    },
966
967    // ========================================================================
968    // Presence Queries
969    // ========================================================================
970    /// Get presence info for a peer
971    GetPresence { peer_id: String },
972
973    /// List online peers
974    ListOnlinePeers,
975
976    // ========================================================================
977    // WebRTC Queries
978    // ========================================================================
979    /// List active calls
980    ListActiveCalls,
981
982    /// Get call participants
983    GetCallParticipants { call_id: String },
984
985    // ========================================================================
986    // Contact Queries
987    // ========================================================================
988    /// Get a contact by ID or four-words
989    GetContact { contact_id: String },
990
991    /// List all contacts
992    ListContacts,
993
994    /// List favourite contacts only
995    ListFavouriteContacts,
996
997    /// Search contacts by display name
998    SearchContacts { query: String },
999
1000    // ========================================================================
1001    // Website Queries
1002    // ========================================================================
1003    /// Get website information for an entity
1004    GetWebsite { entity_id: String },
1005}
1006
1007// ============================================================================
1008// Query Responses
1009// ============================================================================
1010
1011/// Response types for queries
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub enum QueryResponse {
1014    /// Profile information
1015    Profile {
1016        four_words: String,
1017        display_name: String,
1018        device_name: String,
1019        device_type: String,
1020    },
1021
1022    /// Boolean response
1023    Bool(bool),
1024
1025    /// Optional string response
1026    OptionalString(Option<String>),
1027
1028    /// Entity information
1029    Entity(EntityResponse),
1030
1031    /// List of entities
1032    EntityList(Vec<EntityResponse>),
1033
1034    /// List of members
1035    MemberList(Vec<MemberResponse>),
1036
1037    /// Member role
1038    MemberRole(String),
1039
1040    /// Permission overrides
1041    PermissionOverrides(Vec<(String, String)>),
1042
1043    /// Single message with reactions
1044    Message(MessageResponse),
1045
1046    /// Messages
1047    Messages(Vec<MessageResponse>),
1048
1049    /// Sync state
1050    SyncState(SyncStateResponse),
1051
1052    /// Invite information
1053    Invite(InviteResponse),
1054
1055    /// List of invites
1056    InviteList(Vec<InviteResponse>),
1057
1058    /// File contents
1059    FileContents(Vec<u8>),
1060
1061    /// File list
1062    FileList(Vec<FileInfoResponse>),
1063
1064    /// Disk statistics
1065    DiskStats(DiskStatsResponse),
1066
1067    /// Kanban board
1068    KanbanBoard(KanbanBoardResponse),
1069
1070    /// List of Kanban boards
1071    KanbanBoardList(Vec<KanbanBoardResponse>),
1072
1073    /// Kanban card
1074    KanbanCard(KanbanCardResponse),
1075
1076    /// List of Kanban cards
1077    KanbanCards(Vec<KanbanCardResponse>),
1078
1079    /// Presence information
1080    Presence(PresenceResponse),
1081
1082    /// List of peer IDs
1083    PeerList(Vec<String>),
1084
1085    /// Call information
1086    CallList(Vec<CallResponse>),
1087
1088    /// Call participants
1089    CallParticipants(Vec<String>),
1090
1091    /// Contact information
1092    Contact(ContactResponse),
1093
1094    /// List of contacts
1095    ContactList(Vec<ContactResponse>),
1096
1097    /// Website information
1098    Website(WebsiteResponse),
1099}
1100
1101// ============================================================================
1102// Response Types
1103// ============================================================================
1104
1105/// Entity response data
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1107pub struct EntityResponse {
1108    pub id: String,
1109    pub name: String,
1110    pub entity_type: EntityType,
1111    pub description: Option<String>,
1112    pub created_by: String,
1113    pub created_at: i64,
1114    pub member_count: usize,
1115    pub parent_org_id: Option<String>,
1116    pub network_four_words: Option<String>,
1117    pub is_local_only: bool,
1118}
1119
1120/// Member response data
1121#[derive(Debug, Clone, Serialize, Deserialize)]
1122pub struct MemberResponse {
1123    pub member_id: String,
1124    pub role: String,
1125    pub joined_at: i64,
1126}
1127
1128/// Message response data
1129#[derive(Debug, Clone, Serialize, Deserialize)]
1130pub struct MessageResponse {
1131    pub id: String,
1132    pub entity_id: String,
1133    pub author: String,
1134    pub text: String,
1135    pub timestamp: i64,
1136    pub reply_to_id: Option<String>,
1137    #[serde(default)]
1138    pub reactions: Vec<ReactionResponse>,
1139    pub edited_at: Option<u64>,
1140}
1141
1142#[derive(Debug, Clone, Serialize, Deserialize)]
1143pub struct ReactionResponse {
1144    pub emoji: String,
1145    pub count: u32,
1146    pub user_reacted: bool,
1147    pub peer_ids: Vec<String>,
1148}
1149
1150/// Sync state response data
1151#[derive(Debug, Clone, Serialize, Deserialize)]
1152pub struct SyncStateResponse {
1153    pub entity_id: String,
1154    pub entity_type: EntityType,
1155    pub message_count: usize,
1156    pub last_sync_time: u64,
1157}
1158
1159/// Invite response data
1160#[derive(Debug, Clone, Serialize, Deserialize)]
1161pub struct InviteResponse {
1162    pub id: String,
1163    pub sender_id: String,
1164    pub recipient_id: String,
1165    pub entity_type: EntityType,
1166    pub entity_id: String,
1167    pub role: String,
1168    pub status: InviteStatus,
1169    pub message: Option<String>,
1170    pub created_at: i64,
1171    pub expires_at: Option<i64>,
1172}
1173
1174/// File info response data
1175#[derive(Debug, Clone, Serialize, Deserialize)]
1176pub struct FileInfoResponse {
1177    pub path: String,
1178    pub name: String,
1179    pub is_directory: bool,
1180    pub size_bytes: u64,
1181    pub modified_at: i64,
1182}
1183
1184/// Disk stats response data
1185#[derive(Debug, Clone, Serialize, Deserialize)]
1186pub struct DiskStatsResponse {
1187    pub entity_id: String,
1188    pub disk_type: DiskTypeArg,
1189    pub used_bytes: u64,
1190    pub file_count: u32,
1191    pub dir_count: u32,
1192}
1193
1194/// Kanban board response data
1195#[derive(Debug, Clone, Serialize, Deserialize)]
1196pub struct KanbanBoardResponse {
1197    pub id: String,
1198    pub entity_id: String,
1199    pub name: String,
1200    pub description: Option<String>,
1201    pub column_count: usize,
1202}
1203
1204/// Kanban card response data
1205#[derive(Debug, Clone, Serialize, Deserialize)]
1206pub struct KanbanCardResponse {
1207    pub id: String,
1208    pub column_id: String,
1209    pub title: String,
1210    pub description: Option<String>,
1211    pub assignee: Option<String>,
1212    pub position: u32,
1213}
1214
1215/// Presence response data
1216#[derive(Debug, Clone, Serialize, Deserialize)]
1217pub struct PresenceResponse {
1218    pub peer_id: String,
1219    pub status: String,
1220    pub last_seen: i64,
1221}
1222
1223/// Call response data
1224#[derive(Debug, Clone, Serialize, Deserialize)]
1225pub struct CallResponse {
1226    pub id: String,
1227    pub entity_id: String,
1228    pub participant_count: usize,
1229    pub started_at: i64,
1230}
1231
1232/// Contact response data
1233#[derive(Debug, Clone, Serialize, Deserialize)]
1234pub struct ContactResponse {
1235    pub id: String,
1236    pub display_name: String,
1237    pub four_words: Option<String>,
1238    pub is_favourite: bool,
1239    pub is_online: bool,
1240    pub created_at: i64,
1241    pub last_seen: Option<i64>,
1242}
1243
1244/// Website response data
1245#[derive(Debug, Clone, Serialize, Deserialize)]
1246pub struct WebsiteResponse {
1247    pub entity_id: String,
1248    pub html: String,
1249    pub css: String,
1250    pub js: String,
1251    pub website_root_hash: String,
1252    pub published_at: i64,
1253    pub size_bytes: usize,
1254    pub url: String,
1255}
1256
1257// ============================================================================
1258// Subscriptions - For reactive updates
1259// ============================================================================
1260
1261/// Subscription types for reactive updates
1262///
1263/// Adapters can subscribe to specific event streams to receive
1264/// real-time updates when state changes.
1265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
1266pub enum Subscription {
1267    /// Subscribe to all events
1268    AllEvents,
1269
1270    /// Subscribe to events for a specific entity
1271    EntityEvents { entity_id: String },
1272
1273    /// Subscribe to all message events
1274    MessageEvents,
1275
1276    /// Subscribe to messages for a specific entity
1277    EntityMessages { entity_id: String },
1278
1279    /// Subscribe to presence updates
1280    PresenceUpdates,
1281
1282    /// Subscribe to invite events
1283    InviteEvents,
1284
1285    /// Subscribe to networking events
1286    NetworkingEvents,
1287
1288    /// Subscribe to Kanban events for an entity
1289    KanbanEvents { entity_id: String },
1290
1291    /// Subscribe to call events
1292    CallEvents,
1293}
1294
1295// ============================================================================
1296// Result types
1297// ============================================================================
1298
1299/// Result of executing a command
1300pub type CommandResult = Result<Vec<Event>, CommandError>;
1301
1302/// Result of running a query
1303pub type QueryResult = Result<QueryResponse, QueryError>;
1304
1305/// Error type for command execution
1306#[derive(Debug, Clone, Serialize, Deserialize)]
1307pub struct CommandError {
1308    pub command_type: String,
1309    pub message: String,
1310    pub code: String,
1311}
1312
1313impl std::fmt::Display for CommandError {
1314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1315        write!(f, "{}: {} ({})", self.command_type, self.message, self.code)
1316    }
1317}
1318
1319impl std::error::Error for CommandError {}
1320
1321/// Error type for query execution
1322#[derive(Debug, Clone, Serialize, Deserialize)]
1323pub struct QueryError {
1324    pub query_type: String,
1325    pub message: String,
1326    pub code: String,
1327}
1328
1329impl std::fmt::Display for QueryError {
1330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1331        write!(f, "{}: {} ({})", self.query_type, self.message, self.code)
1332    }
1333}
1334
1335impl std::error::Error for QueryError {}
1336
1337#[cfg(test)]
1338mod tests {
1339    use super::*;
1340
1341    #[test]
1342    fn test_command_serialization() {
1343        let cmd = Command::CreateEntity {
1344            name: "Test Org".to_string(),
1345            entity_type: EntityType::Organisation,
1346            description: Some("A test organization".to_string()),
1347            initial_members: vec!["alice-bob-charlie-dave".to_string()],
1348        };
1349
1350        let json = serde_json::to_string(&cmd).unwrap();
1351        let deserialized: Command = serde_json::from_str(&json).unwrap();
1352
1353        match deserialized {
1354            Command::CreateEntity {
1355                name, entity_type, ..
1356            } => {
1357                assert_eq!(name, "Test Org");
1358                assert_eq!(entity_type, EntityType::Organisation);
1359            }
1360            _ => panic!("Wrong command type"),
1361        }
1362    }
1363
1364    #[test]
1365    fn test_event_serialization() {
1366        let event = Event::EntityCreated {
1367            entity_id: "abc123".to_string(),
1368            name: "Test Org".to_string(),
1369            entity_type: EntityType::Organisation,
1370            created_by: "alice-bob-charlie-dave".to_string(),
1371        };
1372
1373        let json = serde_json::to_string(&event).unwrap();
1374        let deserialized: Event = serde_json::from_str(&json).unwrap();
1375
1376        match deserialized {
1377            Event::EntityCreated {
1378                entity_id, name, ..
1379            } => {
1380                assert_eq!(entity_id, "abc123");
1381                assert_eq!(name, "Test Org");
1382            }
1383            _ => panic!("Wrong event type"),
1384        }
1385    }
1386
1387    #[test]
1388    fn test_query_serialization() {
1389        let query = Query::GetEntity {
1390            entity_id: "abc123".to_string(),
1391        };
1392
1393        let json = serde_json::to_string(&query).unwrap();
1394        let deserialized: Query = serde_json::from_str(&json).unwrap();
1395
1396        match deserialized {
1397            Query::GetEntity { entity_id } => {
1398                assert_eq!(entity_id, "abc123");
1399            }
1400            _ => panic!("Wrong query type"),
1401        }
1402    }
1403
1404    #[test]
1405    fn test_subscription_serialization() {
1406        let sub = Subscription::EntityMessages {
1407            entity_id: "abc123".to_string(),
1408        };
1409
1410        let json = serde_json::to_string(&sub).unwrap();
1411        let deserialized: Subscription = serde_json::from_str(&json).unwrap();
1412
1413        match deserialized {
1414            Subscription::EntityMessages { entity_id } => {
1415                assert_eq!(entity_id, "abc123");
1416            }
1417            _ => panic!("Wrong subscription type"),
1418        }
1419    }
1420}