Skip to content

Commit b0722b3

Browse files
authored
Merge pull request #47 from Asone/42-feature-add-profile-command
feat(cli): key validation with tests
2 parents cf68d87 + fd5f1fa commit b0722b3

13 files changed

Lines changed: 361 additions & 135 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nostrss-cli/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ tonic = "0.9.2"
1313
tabled = "0.12.0"
1414
url = "2.3.1"
1515
cron = "0.12.0"
16-
16+
secp256k1 = "0.27.0"
17+
bech32 = "0.9.1"
1718
[dependencies.nostrss_grpc]
1819
path = "../nostrss-grpc"
1920

nostrss-cli/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Please, do note that any operation performed through CLI will be currently lost
1616
| Command | Description |
1717
|-|-|
1818
| nostrss-cli profile list | Lists the profiles |
19-
| nostrss-cli profile add | Add a new profile (not implemented yet) |
19+
| nostrss-cli profile add | Add a new profile |
2020
| nostrss-cli profile delete | Remove a profile |
2121
| nostrss-cli profile info | Get info of a specific profile |
2222

@@ -25,6 +25,6 @@ Please, do note that any operation performed through CLI will be currently lost
2525
| Command | Description |
2626
|-|-|
2727
| nostrss-cli feed list | Lists the feeds |
28-
| nostrss-cli feed add | Add a new feed (not implemented yet) |
28+
| nostrss-cli feed add | Add a new feed |
2929
| nostrss-cli feed delete | Remove a feed |
3030
| nostrss-cli feed info | Get info of a specific feed |

nostrss-cli/src/commands/feed.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
#![allow(dead_code)]
22

3-
use std::str::FromStr;
4-
53
use clap::{Parser, ValueEnum};
64
use nostrss_grpc::grpc::{
75
nostrss_grpc_client::NostrssGrpcClient, AddFeedRequest, DeleteFeedRequest, FeedInfoRequest,
86
FeedItem, FeedsListRequest,
97
};
10-
use tabled::{Table, Tabled};
8+
use tabled::Tabled;
119
use tonic::{async_trait, transport::Channel};
12-
use url::Url;
1310

1411
use crate::input::{formatter::InputFormatter, input::InputValidators};
1512

nostrss-cli/src/commands/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ pub trait CommandsHandler {
1414
_ = stdin().read_line(&mut data);
1515

1616
match validator {
17-
Some(validator) => match validator(data.clone()) {
18-
true => data,
17+
Some(validator) => match validator(data.clone().trim().to_string()) {
18+
true => data.trim().to_string(),
1919
false => {
2020
println!("Invalid value provided.");
2121
self.get_input(label, Some(validator))
2222
}
2323
},
24-
None => data,
24+
None => data.trim().to_string(),
2525
}
2626
}
2727

nostrss-cli/src/commands/profile.rs

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
use clap::{Parser, ValueEnum};
44
use nostrss_grpc::grpc::{
5-
nostrss_grpc_client::NostrssGrpcClient, DeleteProfileRequest, ProfileInfoRequest, ProfileItem,
6-
ProfilesListRequest,
5+
nostrss_grpc_client::NostrssGrpcClient, AddProfileRequest, DeleteProfileRequest,
6+
NewProfileItem, ProfileInfoRequest, ProfileItem, ProfilesListRequest,
77
};
88
use tabled::{Table, Tabled};
99
use tonic::{async_trait, transport::Channel};
1010

11+
use crate::input::{formatter::InputFormatter, input::InputValidators};
12+
1113
use super::CommandsHandler;
1214

1315
#[derive(Clone, PartialEq, Parser, Debug, ValueEnum)]
@@ -148,7 +150,7 @@ impl CommandsHandler for ProfileCommandsHandler {}
148150
impl ProfileCommandsHandler {
149151
pub async fn handle(&mut self, action: ProfileActions) {
150152
match action {
151-
ProfileActions::Add => self.add(),
153+
ProfileActions::Add => self.add().await,
152154
ProfileActions::Delete => self.delete().await,
153155
ProfileActions::List => self.list().await,
154156
ProfileActions::Info => self.info().await,
@@ -178,7 +180,71 @@ impl ProfileCommandsHandler {
178180
}
179181
}
180182

181-
fn add(&self) {}
183+
async fn add(&mut self) {
184+
println!("=== Add a profile ===");
185+
let id = self.get_input("Id: ", Some(InputValidators::required_input_validator));
186+
let private_key: String = self
187+
.get_input(
188+
"Private key (hex or bech32): ",
189+
Some(InputValidators::key_validator),
190+
)
191+
.trim()
192+
.to_string();
193+
let name: Option<String> =
194+
InputFormatter::string_nullifier(self.get_input("(optional) Name: ", None));
195+
let relays = InputFormatter::input_to_vec(
196+
self.get_input("(optional) Relays ids (separated with coma):", None),
197+
);
198+
let display_name: Option<String> =
199+
InputFormatter::string_nullifier(self.get_input("(optional) Display name: ", None));
200+
let description: Option<String> =
201+
InputFormatter::string_nullifier(self.get_input("(optional) Description: ", None));
202+
let picture: Option<String> = InputFormatter::string_nullifier(
203+
self.get_input("(optional) Profile picture URL: ", None),
204+
);
205+
let banner: Option<String> = InputFormatter::string_nullifier(
206+
self.get_input("(optional) Banner picture URL: ", None),
207+
);
208+
let nip05: Option<String> =
209+
InputFormatter::string_nullifier(self.get_input("(optional) NIP-05: ", None));
210+
let lud16: Option<String> =
211+
InputFormatter::string_nullifier(self.get_input("(optional) Lud16: ", None));
212+
let pow_level: String = self.get_input("(optional) Publishing PoW level: ", None);
213+
let pow_level = pow_level.parse().unwrap_or(0);
214+
215+
let recommended_relays: Vec<String> = InputFormatter::input_to_vec(self.get_input(
216+
"(optional) Recommended relays ids (seperated with coma): ",
217+
None,
218+
));
219+
220+
let request = tonic::Request::new(AddProfileRequest {
221+
profile: NewProfileItem {
222+
id,
223+
private_key,
224+
name,
225+
relays,
226+
display_name,
227+
description,
228+
picture,
229+
banner,
230+
nip05,
231+
lud16,
232+
pow_level: Some(pow_level),
233+
recommended_relays,
234+
},
235+
});
236+
237+
let response = self.client.add_profile(request).await;
238+
239+
match response {
240+
Ok(_) => {
241+
println!("Profile successfuly added");
242+
}
243+
Err(e) => {
244+
println!("Error: {}: {}", e.code(), e.message());
245+
}
246+
}
247+
}
182248

183249
async fn delete(&mut self) {
184250
let id = self.get_input("Id: ", None);

nostrss-cli/src/input/formatter.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ impl InputFormatter {
44
pub fn input_to_vec(value: String) -> Vec<String> {
55
value.split(',').map(|e| e.trim().to_string()).collect()
66
}
7+
8+
pub fn string_nullifier(value: String) -> Option<String> {
9+
match value.len() > 0 {
10+
true => Some(value.trim().to_string()),
11+
false => None,
12+
}
13+
}
714
}
815

916
#[cfg(test)]

nostrss-cli/src/input/input.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::str::FromStr;
22

3+
use bech32::FromBase32;
34
use url::Url;
45

56
pub struct InputValidators {}
@@ -30,6 +31,38 @@ impl InputValidators {
3031
Err(_) => false,
3132
}
3233
}
34+
35+
pub fn key_validator(value: String) -> bool {
36+
let decoded = bech32::decode(value.trim());
37+
38+
match decoded {
39+
Ok(result) => {
40+
if let Ok(bytes) = Vec::<u8>::from_base32(&result.1) {
41+
// Check if the decoded bytes have the expected length
42+
if bytes.len() != 32 {
43+
return false;
44+
}
45+
}
46+
}
47+
Err(_) => {
48+
let key_bytes = value.trim().as_bytes();
49+
50+
// Validate key length
51+
if key_bytes.len() != 64 {
52+
return false;
53+
}
54+
55+
// Validate key contains only hexadecimal characters
56+
for &byte in key_bytes.iter() {
57+
if !byte.is_ascii_hexdigit() {
58+
return false;
59+
}
60+
}
61+
}
62+
};
63+
64+
true
65+
}
3366
}
3467

3568
#[cfg(test)]
@@ -79,4 +112,37 @@ mod tests {
79112

80113
assert_eq!(result, false);
81114
}
115+
116+
#[test]
117+
fn key_validator_test() {
118+
let value = "6789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345".to_string();
119+
120+
let result = InputValidators::key_validator(value);
121+
122+
assert_eq!(result, true);
123+
124+
let value = "6789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string();
125+
126+
let result = InputValidators::key_validator(value);
127+
128+
assert_eq!(result, false);
129+
130+
let value = "6789abcdef0123456789abcdef0123456789abcdef0123456789abkdef012345".to_string();
131+
132+
let result = InputValidators::key_validator(value);
133+
134+
assert_eq!(result, false);
135+
136+
let value = "nsec14uuscmj9ac0f3lqfq33cuq6mu8q7sscvpyyhsjn5r8q9w5pdafgq0qrj8a".to_string();
137+
138+
let result = InputValidators::key_validator(value);
139+
140+
assert_eq!(result, true);
141+
142+
let value = "nsec14uuscmj9ac0f3lqfq33cuq6mu8q7sscvpyyhsjn5r8q9w5pdafgq0qrj8d".to_string();
143+
144+
let result = InputValidators::key_validator(value);
145+
146+
assert_eq!(result, false);
147+
}
82148
}

nostrss-core/src/grpc/feed_request.rs

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use nostrss_grpc::grpc::{
22
self, AddFeedRequest, AddFeedResponse, DeleteFeedRequest, DeleteFeedResponse, FeedInfoRequest,
33
FeedInfoResponse, FeedItem, FeedsListRequest, FeedsListResponse,
44
};
5-
use std::{ops::Index, sync::Arc};
5+
use std::sync::Arc;
66
use tokio::sync::{Mutex, MutexGuard};
77
use tonic::{Code, Request, Response, Status};
88

@@ -25,6 +25,40 @@ impl FeedRequestHandler {
2525
Ok(Response::new(grpc::FeedsListResponse { feeds }))
2626
}
2727

28+
pub async fn feed_info(
29+
app: MutexGuard<'_, App>,
30+
request: Request<FeedInfoRequest>,
31+
) -> Result<Response<FeedInfoResponse>, Status> {
32+
let id = &request.into_inner().id;
33+
match app.rss.feeds.clone().into_iter().find(|f| &f.id == id) {
34+
Some(feed) => Ok(Response::new(FeedInfoResponse {
35+
feed: FeedItem::from(feed),
36+
})),
37+
None => {
38+
return Err(Status::new(Code::NotFound, "Feed not found"));
39+
}
40+
}
41+
}
42+
43+
pub async fn add_feed(
44+
mut app: MutexGuard<'_, App>,
45+
request: Request<AddFeedRequest>,
46+
) -> Result<Response<AddFeedResponse>, Status> {
47+
let data = request.into_inner();
48+
let feed = Feed::from(data);
49+
let map = Arc::new(Mutex::new(app.feeds_map.clone()));
50+
let clients = Arc::new(Mutex::new(app.clients.clone()));
51+
52+
app.rss.feeds.push(feed.clone());
53+
54+
let job = schedule(feed.schedule.clone().as_str(), feed.clone(), map, clients).await;
55+
56+
_ = app.rss.feeds_jobs.insert(feed.id.clone(), job.guid());
57+
_ = app.rss.scheduler.add(job).await;
58+
59+
Ok(Response::new(AddFeedResponse {}))
60+
}
61+
2862
// Interface to delete a feed on instance
2963
pub async fn delete_feed(
3064
mut app: MutexGuard<'_, App>,
@@ -56,40 +90,6 @@ impl FeedRequestHandler {
5690
_ = app.scheduler.remove(job_uuid.unwrap()).await;
5791
Ok(Response::new(grpc::DeleteFeedResponse {}))
5892
}
59-
60-
pub async fn add_feed(
61-
mut app: MutexGuard<'_, App>,
62-
request: Request<AddFeedRequest>,
63-
) -> Result<Response<AddFeedResponse>, Status> {
64-
let data = request.into_inner();
65-
let feed = Feed::from(data);
66-
let map = Arc::new(Mutex::new(app.feeds_map.clone()));
67-
let clients = Arc::new(Mutex::new(app.clients.clone()));
68-
69-
app.rss.feeds.push(feed.clone());
70-
71-
let job = schedule(feed.schedule.clone().as_str(), feed.clone(), map, clients).await;
72-
73-
_ = app.rss.feeds_jobs.insert(feed.id.clone(), job.guid());
74-
_ = app.rss.scheduler.add(job).await;
75-
76-
Ok(Response::new(AddFeedResponse {}))
77-
}
78-
79-
pub async fn feed_info(
80-
app: MutexGuard<'_, App>,
81-
request: Request<FeedInfoRequest>,
82-
) -> Result<Response<FeedInfoResponse>, Status> {
83-
let id = &request.into_inner().id;
84-
match app.rss.feeds.clone().into_iter().find(|f| &f.id == id) {
85-
Some(feed) => Ok(Response::new(FeedInfoResponse {
86-
feed: FeedItem::from(feed),
87-
})),
88-
None => {
89-
return Err(Status::new(Code::NotFound, "Feed not found"));
90-
}
91-
}
92-
}
9393
}
9494

9595
#[cfg(test)]

0 commit comments

Comments
 (0)