diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 000000000..52cea5899 --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,127 @@ +name: Release CLI +on: + push: + branches: + - main +jobs: + build: + strategy: + fail-fast: true + matrix: + build: + - linux-x86_64 + # Windows is erroring right now + # - windows-x86_64-gnu + - macos-aarch64 + include: + - build: linux-x86_64 + os: warp-ubuntu-latest-x64-16x + target: x86_64-unknown-linux-musl + - build: macos-aarch64 + os: warp-macos-13-arm64-6x + target: aarch64-apple-darwin + # - build: windows-x86_64-gnu + # os: windows-latest + # target: x86_64-pc-windows-gnu + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: "Cache" + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ./examples/cli/target/${{ matrix.target }} + key: ${{ matrix.target }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} + + - run: sudo apt install musl-tools + if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }} + - name: Install rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + target: ${{ matrix.target }} + + - name: Install openssl windows + if: ${{ matrix.build == 'windows-x86_64-gnu' }} + uses: crazy-max/ghaction-chocolatey@v3 + with: + args: install openssl + + - name: Build target + uses: actions-rs/cargo@v1 + if: + with: + use-cross: false + command: build + args: --release --target ${{ matrix.target }} --manifest-path examples/cli/Cargo.toml --target-dir examples/cli/target + + - name: Upload binary + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.build }} + path: examples/cli/target/${{ matrix.target }}/release/xmtp_cli* + retention-days: 1 + + release: + needs: ["build"] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + path: ./artifacts + + - name: Get short SHA + id: slug + run: echo "::set-output name=sha7::$(echo ${GITHUB_SHA} | cut -c1-7)" + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: cli-${{ steps.slug.outputs.sha7 }} + release_name: cli-${{ steps.slug.outputs.sha7 }} + body: "Release of cli for commit ${{ github.sha}}" + draft: false + prerelease: true + + # - name: Upload Release Asset + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # upload_url: ${{ steps.create_release.outputs.upload_url }} + # asset_path: ./examples/cli/target/x86_64-pc-windows-gnu.exe + # asset_name: cli-x86_64-pc-windows-gnu.exe + # asset_content_type: application/octet-stream + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./artifacts/macos-aarch64/xmtp_cli + asset_name: cli-macos-aarch64 + asset_content_type: application/octet-stream + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./artifacts/linux-x86_64/xmtp_cli + asset_name: cli-linux-x86_64 + asset_content_type: application/octet-stream diff --git a/Cargo.lock b/Cargo.lock index 66050540f..e9521545e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1506,6 +1506,22 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "femme" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc04871e5ae3aa2952d552dae6b291b3099723bf779a8054281c1366a54613ef" +dependencies = [ + "cfg-if", + "js-sys", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "ff" version = "0.12.1" @@ -5530,6 +5546,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", + "serde", + "serde_json", "wasm-bindgen-macro", ] @@ -5885,9 +5903,9 @@ name = "xmtp_cli" version = "0.1.0" dependencies = [ "clap", - "env_logger", "ethers", "ethers-core", + "femme", "futures", "hex", "kv-log-macro", diff --git a/examples/cli/Cargo.toml b/examples/cli/Cargo.toml index ef6ea4154..37aef147f 100644 --- a/examples/cli/Cargo.toml +++ b/examples/cli/Cargo.toml @@ -14,9 +14,9 @@ path = "cli-client.rs" [dependencies] clap = { version = "4.4.6", features = ["derive"] } -env_logger = "0.10.0" ethers = "2.0.4" ethers-core = "2.0.4" +femme = "2.2.1" futures = "0.3.28" hex = "0.4.3" kv-log-macro = "1.0.7" diff --git a/examples/cli/README.md b/examples/cli/README.md index c8f83ab69..a9c53e002 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -40,7 +40,7 @@ Use the CLI to send a [double ratchet message](https://github.com/xmtp/libxmtp/b 6. Add user 2 to the group ```bash - ./xli.sh --db user1.db3 add-group-member $GROUP_ID $USER_2_ACCOUNT_ADDRESS + ./xli.sh --db user1.db3 add-group-members $GROUP_ID --account-addresses $USER_2_ACCOUNT_ADDRESS ``` 7. Send a message diff --git a/examples/cli/cli-client.rs b/examples/cli/cli-client.rs index 399d58699..ef5a53a67 100644 --- a/examples/cli/cli-client.rs +++ b/examples/cli/cli-client.rs @@ -3,6 +3,7 @@ XLI is a Commandline client using XMTPv3. */ mod json_logger; +mod serializable; extern crate ethers; extern crate log; @@ -11,15 +12,20 @@ extern crate xmtp_mls; use std::{fs, path::PathBuf, time::Duration}; use clap::{Parser, Subcommand, ValueEnum}; +use ethers::signers::{coins_bip39::English, MnemonicBuilder}; use kv_log_macro::{error, info}; use prost::Message; -use serde::Serialize; +use crate::{ + json_logger::make_value, + serializable::{SerializableGroup, SerializableMessage}, +}; +use serializable::maybe_get_text; use thiserror::Error; use xmtp_api_grpc::grpc_api_helper::Client as ApiClient; use xmtp_cryptography::{ signature::{RecoverableSignature, SignatureError}, - utils::{rng, seeded_rng, LocalWallet}, + utils::{rng, LocalWallet}, }; use xmtp_mls::{ builder::{ClientBuilderError, IdentityStrategy, LegacyIdentity}, @@ -33,9 +39,6 @@ use xmtp_mls::{ utils::time::now_ns, InboxOwner, Network, }; -use xmtp_proto::xmtp::mls::message_contents::EncodedContent; - -use crate::json_logger::make_value; type Client = xmtp_mls::client::Client; type ClientBuilder = xmtp_mls::builder::ClientBuilder; @@ -65,8 +68,8 @@ enum Permissions { enum Commands { /// Register Account on XMTP Network Register { - #[clap(long = "seed", default_value_t = 0)] - wallet_seed: u64, + #[clap(long)] + seed_phrase: Option, }, CreateGroup { #[clap(value_enum, default_value_t = Permissions::EveryoneIsAdmin)] @@ -81,21 +84,25 @@ enum Commands { #[arg(value_name = "Message")] msg: String, }, + GroupInfo { + #[arg(value_name = "Group ID")] + group_id: String, + }, ListGroupMessages { #[arg(value_name = "Group ID")] group_id: String, }, - AddGroupMember { + AddGroupMembers { #[arg(value_name = "Group ID")] group_id: String, - #[arg(value_name = "Wallet Address")] - account_address: String, + #[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ')] + account_addresses: Vec, }, - RemoveGroupMember { + RemoveGroupMembers { #[arg(value_name = "Group ID")] group_id: String, - #[arg(value_name = "Wallet Address")] - account_address: String, + #[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ')] + account_addresses: Vec, }, /// Information about the account that owns the DB Info {}, @@ -152,13 +159,13 @@ async fn main() { if cli.json { crate::json_logger::start(log::LevelFilter::Info); } else { - env_logger::init(); + femme::with_level(femme::LevelFilter::Info); } info!("Starting CLI Client...."); - if let Commands::Register { wallet_seed } = &cli.command { + if let Commands::Register { seed_phrase } = &cli.command { info!("Register"); - if let Err(e) = register(&cli, wallet_seed).await { + if let Err(e) = register(&cli, seed_phrase.clone()).await { error!("Registration failed: {:?}", e) } return; @@ -166,7 +173,7 @@ async fn main() { match &cli.command { #[allow(unused_variables)] - Commands::Register { wallet_seed } => { + Commands::Register { seed_phrase } => { unreachable!() } Commands::Info {} => { @@ -192,25 +199,21 @@ async fn main() { let group_list = client .find_groups(None, None, None, None) .expect("failed to list groups"); - for group in group_list.iter() { group.sync().await.expect("error syncing group"); - let group_id = hex::encode(group.group_id.clone()); - let members = group - .members() - .unwrap() - .into_iter() - .map(|m| m.account_address) - .collect::>(); - info!( - "group members", - { - command_output: true, - members: make_value(&members), - group_id: group_id, - } - ); } + let serializable_group_list = group_list + .iter() + .map(|g| g.into()) + .collect::>(); + + info!( + "group members", + { + command_output: true, + groups: make_value(&serializable_group_list), + } + ); } Commands::Send { group_id, msg } => { info!("Sending message to group", { group_id: group_id, message: msg }); @@ -251,9 +254,9 @@ async fn main() { ) } } - Commands::AddGroupMember { + Commands::AddGroupMembers { group_id, - account_address, + account_addresses, } => { let client = create_client(&cli, IdentityStrategy::CachedOnly) .await @@ -264,18 +267,18 @@ async fn main() { .expect("failed to get group"); group - .add_members(vec![account_address.clone()]) + .add_members(account_addresses.clone()) .await .expect("failed to add member"); info!( "Successfully added {} to group {}", - account_address, group_id, { command_output: true, group_id: group_id} + account_addresses.join(", "), group_id, { command_output: true, group_id: group_id} ); } - Commands::RemoveGroupMember { + Commands::RemoveGroupMembers { group_id, - account_address, + account_addresses, } => { let client = create_client(&cli, IdentityStrategy::CachedOnly) .await @@ -286,13 +289,13 @@ async fn main() { .expect("failed to get group"); group - .remove_members(vec![account_address.clone()]) + .remove_members(account_addresses.clone()) .await .expect("failed to add member"); info!( "Successfully removed {} from group {}", - account_address, group_id, { command_output: true } + account_addresses.join(", "), group_id, { command_output: true } ); } Commands::CreateGroup { permissions } => { @@ -314,7 +317,17 @@ async fn main() { let group_id = hex::encode(group.group_id); info!("Created group {}", group_id, { command_output: true, group_id: group_id}) } - + Commands::GroupInfo { group_id } => { + let client = create_client(&cli, IdentityStrategy::CachedOnly) + .await + .unwrap(); + let group = &client + .group(hex::decode(group_id).expect("bad group id")) + .expect("group not found"); + group.sync().await.unwrap(); + let serializable: SerializableGroup = group.into(); + info!("Group {}", group_id, { command_output: true, group_id: group_id, group_info: make_value(&serializable) }) + } Commands::Clear {} => { fs::remove_file(cli.db.unwrap()).unwrap(); } @@ -346,11 +359,16 @@ async fn create_client(cli: &Cli, account: IdentityStrategy) -> Result Result<(), CliError> { - let w = if wallet_seed == &0 { - Wallet::LocalWallet(LocalWallet::new(&mut rng())) +async fn register(cli: &Cli, maybe_seed_phrase: Option) -> Result<(), CliError> { + let w: Wallet = if let Some(seed_phrase) = maybe_seed_phrase { + Wallet::LocalWallet( + MnemonicBuilder::::default() + .phrase(seed_phrase.as_str()) + .build() + .unwrap(), + ) } else { - Wallet::LocalWallet(LocalWallet::new(&mut seeded_rng(*wallet_seed))) + Wallet::LocalWallet(LocalWallet::new(&mut rng())) }; let client = create_client( @@ -390,37 +408,6 @@ async fn send(group: MlsGroup<'_, ApiClient>, msg: String) -> Result<(), CliErro Ok(()) } -#[derive(Serialize, Debug, Clone)] -struct SerializableMessage { - sender_account_address: String, - sent_at_ns: u64, - message_text: Option, - // content_type: String -} - -impl SerializableMessage { - fn from_stored_message(msg: &StoredGroupMessage) -> Self { - let maybe_text = maybe_get_text(msg); - Self { - sender_account_address: msg.sender_account_address.clone(), - sent_at_ns: msg.sent_at_ns as u64, - message_text: maybe_text, - } - } -} - -fn maybe_get_text(msg: &StoredGroupMessage) -> Option { - let contents = msg.decrypted_message_bytes.clone(); - let Ok(encoded_content) = EncodedContent::decode(contents.as_slice()) else { - return None; - }; - let Ok(decoded) = TextCodec::decode(encoded_content) else { - log::warn!("Skipping over unrecognized codec"); - return None; - }; - Some(decoded) -} - fn format_messages( messages: Vec, my_account_address: String, diff --git a/examples/cli/serializable.rs b/examples/cli/serializable.rs new file mode 100644 index 000000000..cd1c4eb02 --- /dev/null +++ b/examples/cli/serializable.rs @@ -0,0 +1,78 @@ +use prost::Message; +use serde::Serialize; +use xmtp_mls::{ + codecs::{text::TextCodec, ContentCodec}, + groups::MlsGroup, + storage::group_message::StoredGroupMessage, +}; +use xmtp_proto::{api_client::XmtpMlsClient, xmtp::mls::message_contents::EncodedContent}; + +#[derive(Serialize, Debug)] +pub struct SerializableGroupMetadata { + creator_account_address: String, + policy: String, +} + +#[derive(Serialize, Debug)] +pub struct SerializableGroup { + pub group_id: String, + pub members: Vec, + pub metadata: SerializableGroupMetadata, +} + +impl From<&MlsGroup<'_, A>> for SerializableGroup { + fn from(group: &MlsGroup<'_, A>) -> Self { + let group_id = hex::encode(group.group_id.clone()); + let members = group + .members() + .expect("could not load members") + .into_iter() + .map(|m| m.account_address) + .collect::>(); + + let metadata = group.metadata().expect("could not load metadata"); + + Self { + group_id, + members, + metadata: SerializableGroupMetadata { + creator_account_address: metadata.creator_account_address.clone(), + policy: metadata + .preconfigured_policy() + .expect("could not get policy") + .to_string(), + }, + } + } +} + +#[derive(Serialize, Debug, Clone)] +pub struct SerializableMessage { + sender_account_address: String, + sent_at_ns: u64, + message_text: Option, + // content_type: String +} + +impl SerializableMessage { + pub fn from_stored_message(msg: &StoredGroupMessage) -> Self { + let maybe_text = maybe_get_text(msg); + Self { + sender_account_address: msg.sender_account_address.clone(), + sent_at_ns: msg.sent_at_ns as u64, + message_text: maybe_text, + } + } +} + +pub fn maybe_get_text(msg: &StoredGroupMessage) -> Option { + let contents = msg.decrypted_message_bytes.clone(); + let Ok(encoded_content) = EncodedContent::decode(contents.as_slice()) else { + return None; + }; + let Ok(decoded) = TextCodec::decode(encoded_content) else { + log::warn!("Skipping over unrecognized codec"); + return None; + }; + Some(decoded) +} diff --git a/xmtp_api_grpc/src/lib.rs b/xmtp_api_grpc/src/lib.rs index 2b31c764c..c7f4c966d 100644 --- a/xmtp_api_grpc/src/lib.rs +++ b/xmtp_api_grpc/src/lib.rs @@ -53,9 +53,14 @@ mod tests { let client = Client::create(LOCALHOST_ADDRESS.to_string(), false) .await .unwrap(); - let req = BatchQueryRequest { requests: vec![] }; + let req = BatchQueryRequest { + requests: vec![QueryRequest { + content_topics: vec!["some-random-topic-with-no-messages".to_string()], + ..QueryRequest::default() + }], + }; let result = client.batch_query(req).await.unwrap(); - assert_eq!(result.responses.len(), 0); + assert_eq!(result.responses.len(), 1); } #[tokio::test] diff --git a/xmtp_mls/src/groups/group_permissions.rs b/xmtp_mls/src/groups/group_permissions.rs index 192315b73..bad0e66d5 100644 --- a/xmtp_mls/src/groups/group_permissions.rs +++ b/xmtp_mls/src/groups/group_permissions.rs @@ -345,8 +345,9 @@ pub(crate) fn policy_group_creator_is_admin() -> PolicySet { ) } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub enum PreconfiguredPolicies { + #[default] EveryoneIsAdmin, GroupCreatorIsAdmin, } @@ -370,9 +371,9 @@ impl PreconfiguredPolicies { } } -impl Default for PreconfiguredPolicies { - fn default() -> Self { - PreconfiguredPolicies::EveryoneIsAdmin +impl std::fmt::Display for PreconfiguredPolicies { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) } }