kcode_k1_chat_thread_ktools/
lib.rs1#![forbid(unsafe_code)]
2#![doc = include_str!("../Documentation.md")]
3
4use std::sync::Arc;
5
6use kcode_k1_access_kmap::K1AccessKmap;
7use kcode_k1_chat_thread_actions::ChatThreadActions;
8pub use kcode_k1_chat_thread_actions::{AccessContext, AccessPolicy, ProfileId};
9use kcode_k1_ktool_social::SocialKtools;
10
11const DIFFERENT_AUTHORIZATION: &str =
12 "different Kmap access context, profile ID, or policy is already active";
13const NO_SOCIAL_AUTHORIZATION: &str = "social authorization is not active";
14const NO_SOCIAL_KTOOLS: &str = "social Ktools are unavailable";
15
16#[derive(Clone, Eq, PartialEq)]
17struct Authorization {
18 context: AccessContext,
19 profile_id: ProfileId,
20 policy: AccessPolicy,
21}
22
23pub struct ChatThreadKtools {
24 actions: ChatThreadActions,
25 social: Option<SocialKtools>,
26 authorization: Option<Authorization>,
27}
28
29impl ChatThreadKtools {
30 pub fn new(kmap: Arc<K1AccessKmap>) -> Self {
31 Self {
32 actions: ChatThreadActions::new(kmap),
33 social: None,
34 authorization: None,
35 }
36 }
37
38 pub fn new_with_social(kmap: Arc<K1AccessKmap>, social: SocialKtools) -> Self {
39 Self {
40 actions: ChatThreadActions::new(kmap),
41 social: Some(social),
42 authorization: None,
43 }
44 }
45
46 pub fn bind_authorization(
47 &mut self,
48 context: AccessContext,
49 profile_id: ProfileId,
50 policy: AccessPolicy,
51 ) -> Result<(), String> {
52 let candidate = Authorization {
53 context,
54 profile_id,
55 policy,
56 };
57 if self
58 .authorization
59 .as_ref()
60 .is_some_and(|active| active != &candidate)
61 {
62 return Err(DIFFERENT_AUTHORIZATION.to_owned());
63 }
64 self.actions.bind_authorization(
65 candidate.context.clone(),
66 candidate.profile_id,
67 candidate.policy.clone(),
68 )?;
69 self.authorization = Some(candidate);
70 Ok(())
71 }
72
73 pub fn clear_authorization(&mut self) {
74 self.actions.clear_authorization();
75 self.authorization = None;
76 }
77
78 pub fn launch(&mut self, name: &str, arguments: &str) -> Result<String, String> {
79 if matches!(name, "ListContacts" | "ListGroups" | "GetGroup") {
80 let social = self
81 .social
82 .as_ref()
83 .ok_or_else(|| NO_SOCIAL_KTOOLS.to_owned())?;
84 let context = self
85 .authorization
86 .as_ref()
87 .map(|authorization| &authorization.context)
88 .ok_or_else(|| NO_SOCIAL_AUTHORIZATION.to_owned())?;
89 return social.launch(context, name, arguments);
90 }
91 self.actions.launch(name, arguments)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 #[test]
98 fn social_names_are_the_only_intercepted_names() {
99 for name in ["ListContacts", "ListGroups", "GetGroup"] {
100 assert!(matches!(name, "ListContacts" | "ListGroups" | "GetGroup"));
101 }
102 for name in ["CurrentTime", "KmapOpenNode", "WebSearch", "SetLaunchNode"] {
103 assert!(!matches!(name, "ListContacts" | "ListGroups" | "GetGroup"));
104 }
105 }
106
107 #[test]
108 fn complete_package_stays_below_the_managed_limit() {
109 let files = [
110 include_str!("../Cargo.toml"),
111 include_str!("../Documentation.md"),
112 include_str!("lib.rs"),
113 ];
114 let count = files
115 .iter()
116 .flat_map(|file| file.lines())
117 .filter(|line| !line.trim().is_empty())
118 .count();
119 assert!(count < 500, "complete package has {count} nonblank lines");
120 }
121}