logo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
use crate::structs::ConnectionState::Disconnected;
use crate::{ConversationID, CwtchLib, ProfileIdentity};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DefaultOnError};
use serde_repr::*;
use std::collections::HashMap;
use crate::event::ContactIdentity;

#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)]
/// Defines the states a Cwtch connection can be in
pub enum ConnectionState {
    /// The Cwtch connection is not conected at all
    Disconnected,
    /// Cwtch is attempting to connect to the address, it may or may not be online
    Connecting,
    /// Cwtch has made a basic connection to the address, but not done authorization. The address is online but unverified as the desired target
    Connected,
    /// Cwtch has authenticated the desired connection
    Authenticated,
    /// In the case of a server connection, Cwtch has finished syncing messages from the server and is caught up
    Synced,
    /// The connection attempt failed
    Failed,
    /// The connection has been killed
    Killed,
}

impl Default for ConnectionState {
    fn default() -> ConnectionState {
        Disconnected
    }
}

impl From<&str> for ConnectionState {
    /// Creates a ConnectionState from a string sent from libcwtch-go
    fn from(name: &str) -> Self {
        match name {
            "Disconnected" => ConnectionState::Disconnected,
            "Connecting" => ConnectionState::Connecting,
            "Connected" => ConnectionState::Connected,
            "Authenticated" => ConnectionState::Authenticated,
            "Synced" => ConnectionState::Synced,
            "Failed" => ConnectionState::Failed,
            "Killed" => ConnectionState::Killed,
            _ => ConnectionState::Disconnected,
        }
    }
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
/// Defines the various authorization modes a contact can be in
pub enum ContactAuthorization {
    /// This is an unknown (new?) contact. The user has not approved them
    Unknown,
    /// The contact is approved by the user (manual action)
    Approved,
    /// The contact is blocked by the user, should be ignored
    Blocked,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(non_snake_case)]
/// Struct to deserialize the results of Get*Attribute requests into
pub struct Attribute {
    /// Was the attribute value found in storage
    pub exists: bool,
    /// The value of the requested attribute
    pub value: String,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(non_snake_case)]
/// Struct to serialize/deserialize events coming off the Cwtch appbus
pub struct CwtchEvent {
    /// the type of event, as defined in https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/event/common.go
    pub event_type: String,
    /// event ID that can be used to respond to some events and keep them differentiated (event_ID because golang naming conventions in libCwtch-go)
    pub event_ID: String,
    /// a map of keys and values of arguments for the event as defined in https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/event/common.go
    pub data: HashMap<String, String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
#[allow(non_snake_case)]
/// AccessControl is a type determining client assigned authorization to a peer
pub struct AccessControl {
    /// Any attempts from this handle to connect are blocked
    pub blocked: bool,
    /// Allows a handle to access the conversation
    pub read:    bool,
    /// Allows a handle to append new messages to the conversation
    pub append:  bool,
}

/// represents an access control list for a conversation. Mapping handles to conversation functions
pub type ACL = HashMap<String, AccessControl>;

#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
/// Struct to serialize/deserialize conversations coming from libcwtch-go
pub struct Conversation {
    /// onion address / id of the conversation
    #[serde(alias = "onion")]
    pub contact_id: ContactIdentity,
    /// unique identifier of the contact/conversation to be used in API access
    pub identifier: ConversationID, // TODO TEST does this work here for serde deserializtion
    /// display name of the conversation, as determined in libcwtch-go from name specified by contact
    pub name: String,
    #[serde_as(deserialize_as = "DefaultOnError")]
    // cwtch loads profile/conversation from storage and leaves status blank, it's filled in "soon" by events...
    /// contact connection status
    pub status: ConnectionState,
    /// has the conversation been manually accpted
    pub accepted: bool,
    ///represents an access control list for a conversation. Mapping handles to conversation functions
    pub access_control_list: ACL,
    /// has the conversation been manually blocked
    pub blocked: bool,
    /// is this conversation a group? if so "onion" will be a group ID
    /// FIXME: deprecate
    pub is_group: bool,
    //attr: HashMap<String, String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
/// Struct to serialize/deserialize servers coming from libcwtch-go
pub struct Server {
    /// onion address of the server
    pub onion: String,
    /// server connection status
    pub status: ConnectionState,
}

#[derive(Debug, Clone)]
/// Struct to serialize/deserialize profiles coming from libcwtch-go
pub struct Profile {
    /// onion address / ID of the profile
    pub profile_id: ProfileIdentity,
    /// nick name of the onion as supplied by libcwtch-go based on "name" attribute
    pub nick: String,
    /// path to a profile image, controled by "picture" attribute
    pub image_path: String,
    /// all profile attributes
    pub attr: HashMap<String, String>,
    /// map of conversation [ onion => conversation ]
    pub conversations: HashMap<ConversationID, Conversation>,
    /// map of servers [ onion => server ]
    pub servers: HashMap<String, Server>,
}

#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Clone)]
#[repr(i32)]
/// Enum matching message types and their overlays as defined in Cwtch
///   - https://git.openprivacy.ca/cwtch.im/cwtch/src/commit/907a7ca638fbcbaac5d652608d455d4217597fa9/model/overlay.go
///   - https://git.openprivacy.ca/cwtch.im/cwtch-ui/src/commit/3a752b73972cbfc53b6da26116fe018ee2ac2343/lib/models/message.dart
pub enum MessageType {
    /// A standard Cwtch message of text
    TextMessage = 1,
    /// This message is a text message but it also contains a reference to the message it is quoting
    QuotedMessage = 10,
    /// A shared contact message
    SuggestContact = 100,
    /// A group invite message
    InviteGroup = 101,
    /// a share file message
    FileShare = 200,
    /// This message is of an unsupported type so malformed
    MalformedMessage,
}

impl From<i32> for MessageType {
    fn from(x: i32) -> Self {
        match x {
            1 => MessageType::TextMessage,
            10 => MessageType::QuotedMessage,
            100 => MessageType::SuggestContact,
            101 => MessageType::InviteGroup,
            200 => MessageType::FileShare,
            _ => MessageType::MalformedMessage,
        }
    }
}


#[derive(Debug, Serialize, Deserialize, Clone)]
/// Struct to serialize/deserialize messages sent over Cwtch between profiles / conversation
pub struct Message {
    /// overlay id that the message is targeting as defined in cwtch/model/overlay.go
    /// [  OverlayChat = 1, OverlayInviteContact = 100, OverlayInviteGroup = 101, OverlayFileSharing = 200 ]
    pub o: MessageType,
    /// data of the message
    pub d: String,
}

impl Message {
    /// parse json into a Message
    pub fn from_json(json: &str) -> Self {
        match serde_json::from_str(json) {
            Ok(m) => m,
            Err(e) => Message{o: MessageType::MalformedMessage, d: e.to_string()}
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
/// Settings as defined in libcwtch-go/utils/settings.go and should be populated by handeling the UpdateGlobalSettings event emited during cwtch.start()
#[allow(non_snake_case)]
pub struct Settings {
    /// locale for the UI to use
    pub Locale: String,
    /// theme of the UI
    pub Theme: String,
    /// Theme mode of the ui, light or dark
    pub ThemeMode: String,
    /// previous pid of the run, managed by libcwtch-go
    pub PreviousPid: i64,
    /// controls if subsequent experiments are enabled at all
    pub ExperimentsEnabled: bool,
    /// map of experiment names and their enabled status
    pub Experiments: HashMap<String, bool>,
    /// Should the app block unknown conversations
    pub BlockUnknownConnections: bool,
    // Notification policy of a UI app
    //pub NotificationPolicy: String, //Todo: NotificationPolicy struct
    // Notification content to show for a UI app
    //pub NotificationContent: String,
    /// Should the UI hide conversation IDs
    pub StreamerMode: bool,
    /// Unused?
    pub StateRootPane: i32,
    /// is this the first run
    pub FirstTime: bool,
    /// UI column mode
    pub UIColumnModePortrait: String,
    /// UI column mode
    pub UIColumnModeLandscape: String,
    /// Path to download files to
    pub DownloadPath: String,
    // Turn on advanced tor config in the UI
    //pub AllowAdvancedTorConfig: bool,
    // Custom torrc value
    //pub CustomTorrc: String,
    // Use the value of CustomTorrc with tor
    //pub UseCustomTorrc: bool,
    // Unused? delete
    //pub UseExternalTor: bool,
    // Tor socks port, if not default
    //pub CustomSocksPort: i32,
    // Tor control port if not default
    //pub CustomControlPort: i32,
    // Use tor cache for faster start
    //pub UseTorCache: bool,
    // Tor config dir
    //pub TorCacheDir: String,
}

/// Enum of experiment types that can be managed in Settings
pub enum Experiments {
    /// experiment enabling in app management and running of Cwtch servers
    ServersExperiment,
    /// experiment enabling use of Cwtch groups
    GroupExperiment,
    /// experiment enabling filesharing
    FileSharingExperiment,
    /// experiment enabling auto downloading of image files to Settings::DownloadPath
    ImagePreviewsExperiment,
}

impl Experiments {
    /// returns the experiment settings key
    pub fn to_key_string(self) -> String {
        match self {
            Experiments::ServersExperiment => "servers-experiment".to_string(),
            Experiments::GroupExperiment => "tapir-groups-experiment".to_string(),
            Experiments::FileSharingExperiment => "filesharing".to_string(),
            Experiments::ImagePreviewsExperiment => "filesharing-images".to_string(),
        }
    }
}

impl Settings {
    /// Given a CwtchLib, handles sending an event to it with updated settings represented by this struct for saving
    pub fn save(&self, cwtch: &dyn CwtchLib) -> Result<(), String> {
        let settings_json = match serde_json::to_string(&self) {
            Ok(s) => s,
            Err(e) => return Err(e.to_string()),
        };
        let save_settings_event: CwtchEvent = CwtchEvent {
            event_type: "UpdateGlobalSettings".to_string(),
            event_ID: "0".to_string(),
            data: HashMap::from([("Data".to_string(), settings_json)]),
        };
        let event_json = match serde_json::to_string(&save_settings_event) {
            Ok(s) => s,
            Err(e) => return Err(e.to_string()),
        };
        cwtch.send_app_event(&event_json);
        return Ok(());
    }
}

impl Profile {
    /// Create a new profile populated from supplied data
    ///   contacts_json as supplied by libcwtch-go, a map of conversations
    ///   server_list as supplied by libcwtch-go, a map of servers
    pub fn new(
        identity: ProfileIdentity,
        name: &str,
        picture: &str,
        conversations_json: &str,
        server_list: &str,
    ) -> Result<Profile, String> {
        let conversations = match Profile::process_conversations(conversations_json) {
            Ok(c) => c,
            Err(e) => return Err(e),
        };
        let servers = match Profile::process_servers(server_list) {
            Ok(s) => s,
            Err(e) => return Err(e),
        };
        Ok(Profile {
            profile_id: identity,
            nick: name.to_string(),
            image_path: picture.to_string(),
            attr: Default::default(),
            conversations,
            servers: servers,
        })
    }

    fn process_conversations(conversations_json: &str) -> Result<HashMap<ConversationID, Conversation>, String> {
        let mut conversations: HashMap<ConversationID, Conversation> = HashMap::new();
        if conversations_json == "null" {
            return Ok(conversations);
        }
        let conversations_map: Vec<Conversation> = match serde_json::from_str(conversations_json) {
            Ok(cm) => cm,
            Err(e) => return Err(format!("invalid json: {:?}", e)),
        };
        for conversation in conversations_map {
            conversations.insert(conversation.identifier, conversation);
        }
        Ok(conversations)
    }

    fn process_servers(servers_json: &str) -> Result<HashMap<String, Server>, String> {
        let mut servers: HashMap<String, Server> = HashMap::new();
        if servers_json == "null" {
            return Ok(servers);
        }
        let servers_map: Vec<Server> = match serde_json::from_str(servers_json) {
            Ok(sm) => sm,
            Err(e) => return Err(format!("invalid json: {:?}", e)),
        };
        for server in servers_map {
            servers.insert(server.onion.clone(), server);
        }
        Ok(servers)
    }

    /// Find a conversation_id associated with a remote handle (used for some events with no conversation id like PeerStateChange)
    pub fn find_conversation_id_by_handle(&self, contact_id: ContactIdentity) -> Option<ConversationID> {
        match self.conversations.values().filter(|c| c.contact_id == contact_id).next() {
            Some(conversation) => Some(conversation.identifier),
            None => None
        }
    }
}