453 lines
17 KiB
Rust
453 lines
17 KiB
Rust
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, FileKey};
|
|
|
|
#[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
|
|
/// Overlay => data
|
|
/// 1 TextMessage: d = plain text message
|
|
/// 10 QuotedMessage: d = json of QuotedMessageStructure, body = plain text of message
|
|
pub struct MessageWrapper {
|
|
/// 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, for OverlayChat, a
|
|
pub d: String,
|
|
}
|
|
|
|
impl MessageWrapper {
|
|
/// parse json into a Message
|
|
pub fn from_json(json: &str) -> Self {
|
|
match serde_json::from_str(json) {
|
|
Ok(m) => m,
|
|
Err(e) => MessageWrapper {o: MessageType::MalformedMessage, d: e.to_string()}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
/// defined: cwtch-ui/lib/model/messages/quotedmessage.dart ~ln 15
|
|
#[allow(non_snake_case)]
|
|
pub struct QuotedMessageStructure {
|
|
/// hash message id of message being quoted
|
|
pub quotedHash: String,
|
|
/// plain text of the reply message
|
|
pub body: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
/// LocallyIndexedMessage is a type wrapper around a Message and a TimeLine Index that is local to this
|
|
/// instance of the timeline.
|
|
/// defined in: cwtch/model/message.go ~ln 31
|
|
#[allow(non_snake_case)]
|
|
pub struct LocallyIndexedMessage {
|
|
/// The message
|
|
pub Message: LocalMessage,
|
|
/// the local index in this timeline instance
|
|
pub LocalIndex: i32,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
/// Message is a local representation of a given message sent over a group chat channel.
|
|
/// defined in: cwtch/model/message.go ~ln 38
|
|
#[allow(non_snake_case)]
|
|
pub struct LocalMessage {
|
|
/// Timestamp of the message from sender
|
|
pub Timestamp: String, //time.Time
|
|
/// Timestamp message was received at
|
|
pub Received: String, //time.Time
|
|
/// ContactId of the sender
|
|
pub PeerID: String,
|
|
/// Message contents
|
|
pub Message: MessageWrapper,
|
|
/// author's signature verifying the message
|
|
pub Signature: String,
|
|
/// the signature of hte previous message in the sender's timeline
|
|
pub PreviousMessageSig: String,
|
|
/// was message confirmed by server
|
|
pub ReceivedByServer: bool,
|
|
/// was the message ack'ed by the recipient peer
|
|
pub Acknowledged: bool, // peer to peer
|
|
/// message if there was an error, empty if fine
|
|
pub Error: String,
|
|
/// Application specific flags, useful for storing small amounts of metadata
|
|
pub Flags: u64
|
|
}
|
|
|
|
// Only for internal use?
|
|
/*
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
/// FilesharingOverlayMessage presents the canonical format of the File Sharing functionality Overlay Message
|
|
/// This is the format that the UI will parse to display the message
|
|
/// defined: cwtch/functionality/filesharing/filesharing_functionality.go ~ln 156
|
|
pub struct FilesharingOverlayMessage {
|
|
#[serde(alias = "f")]
|
|
pub name: String,
|
|
#[serde(alias = "h")]
|
|
pub hash: String,
|
|
#[serde(alias = "n")]
|
|
pub nonce: String,
|
|
#[serde(alias = "s")]
|
|
pub size: u64,
|
|
}
|
|
*/
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
/// SharedFile struct from cwtch defined in cwtch/functionality/filesharing/filesharing_functionality.go ~ln 464
|
|
#[allow(non_snake_case)]
|
|
pub struct SharedFile {
|
|
/// The roothash.nonce identifier derived for this file share
|
|
pub FileKey: FileKey,
|
|
|
|
/// Path is the OS specific location of the file
|
|
pub Path: String,
|
|
|
|
/// DateShared is the original datetime the file was shared
|
|
/// go time.Time, => DateTime<FixedOffset>
|
|
/// DateTime::parse_from_rfc3339(cwtch_event.data["TimestampSent"].as_str()).unwrap_or( DateTime::from(Utc::now())),
|
|
pub DateShared: String,
|
|
|
|
/// Active is true if the file is currently being shared, false otherwise
|
|
pub Active: bool,
|
|
|
|
/// Expired is true if the file is not eligible to be shared (because e.g. it has been too long since the file was originally shared,
|
|
/// or the file no longer exists).
|
|
pub Expired: bool
|
|
}
|
|
|
|
#[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> {
|
|
cwtch.update_settings(&self);
|
|
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
|
|
}
|
|
}
|
|
}
|