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}