1
1
use clap:: Parser ;
2
- use futures:: { pin_mut , FutureExt } ;
2
+ use futures:: FutureExt ;
3
3
use ipld_core:: ipld;
4
4
use libp2p:: futures:: StreamExt ;
5
5
use libp2p:: Multiaddr ;
6
6
use rust_ipfs:: p2p:: MultiaddrExt ;
7
- use rust_ipfs:: { Ipfs , Keypair , PubsubEvent , UninitializedIpfs } ;
7
+ use rust_ipfs:: { ConnectionEvents , Ipfs , Keypair , PubsubEvent , UninitializedIpfs } ;
8
8
9
+ use parking_lot:: Mutex ;
10
+ use pollable_map:: stream:: StreamMap ;
9
11
use rustyline_async:: Readline ;
10
12
use std:: time:: Duration ;
11
13
use std:: { io:: Write , sync:: Arc } ;
@@ -41,6 +43,8 @@ async fn main() -> anyhow::Result<()> {
41
43
42
44
let topic = opt. topic . unwrap_or_else ( || String :: from ( "ipfs-chat" ) ) ;
43
45
46
+ let main_topic = Arc :: new ( Mutex :: new ( topic. clone ( ) ) ) ;
47
+
44
48
let keypair = Keypair :: generate_ed25519 ( ) ;
45
49
46
50
let peer_id = keypair. public ( ) . to_peer_id ( ) ;
@@ -95,6 +99,16 @@ async fn main() -> anyhow::Result<()> {
95
99
96
100
let mut st = ipfs. connection_events ( ) . await ?;
97
101
102
+ let mut main_events = StreamMap :: new ( ) ;
103
+
104
+ let mut listener_st = StreamMap :: new ( ) ;
105
+
106
+ let mut main_event_st = ipfs. pubsub_events ( None ) . await ?;
107
+
108
+ let stream = ipfs. pubsub_subscribe ( topic. clone ( ) ) . await ?;
109
+
110
+ listener_st. insert ( topic. clone ( ) , stream) ;
111
+
98
112
for addr in opt. connect {
99
113
let Some ( peer_id) = addr. peer_id ( ) else {
100
114
writeln ! ( stdout, ">{addr} does not contain a p2p protocol. skipping" ) ?;
@@ -109,41 +123,138 @@ async fn main() -> anyhow::Result<()> {
109
123
writeln ! ( stdout, "Connected to {}" , peer_id) ?;
110
124
}
111
125
112
- let mut event_stream = ipfs. pubsub_events ( & topic) . await ?;
113
-
114
- let stream = ipfs. pubsub_subscribe ( & topic) . await ?;
115
-
116
- pin_mut ! ( stream) ;
117
-
118
- tokio:: spawn ( topic_discovery ( ipfs. clone ( ) , topic. clone ( ) ) ) ;
126
+ let owned_topic = topic. to_string ( ) ;
127
+ tokio:: spawn ( topic_discovery ( ipfs. clone ( ) , owned_topic) ) ;
119
128
120
129
tokio:: task:: yield_now ( ) . await ;
121
130
122
131
loop {
123
132
tokio:: select! {
124
- data = stream. next( ) => {
125
- if let Some ( msg) = data {
126
- writeln!( stdout, "{}: {}" , msg. source. expect( "Message should contain a source peer_id" ) , String :: from_utf8_lossy( & msg. data) ) ?;
133
+ Some ( ( topic, msg) ) = listener_st. next( ) => {
134
+ writeln!( stdout, "> {topic}: {}: {}" , msg. source. expect( "Message should contain a source peer_id" ) , String :: from_utf8_lossy( & msg. data) ) ?;
135
+ }
136
+ Some ( conn_ev) = st. next( ) => {
137
+ match conn_ev {
138
+ ConnectionEvents :: IncomingConnection { peer_id, .. } => {
139
+ writeln!( stdout, "> {peer_id} connected" ) ?;
140
+ }
141
+ ConnectionEvents :: OutgoingConnection { peer_id, .. } => {
142
+ writeln!( stdout, "> {peer_id} connected" ) ?;
143
+ }
144
+ ConnectionEvents :: ClosedConnection { peer_id, .. } => {
145
+ writeln!( stdout, "> {peer_id} disconnected" ) ?;
146
+ }
127
147
}
128
148
}
129
- conn_ev = st. next( ) => {
130
- if let Some ( ev) = conn_ev {
131
- writeln!( stdout, "connection event: {ev:?}" ) ?;
149
+ Some ( event) = main_event_st. next( ) => {
150
+ match event {
151
+ PubsubEvent :: Subscribe { peer_id, topic: Some ( topic) } => writeln!( stdout, "{} subscribed to {}" , peer_id, topic) ?,
152
+ PubsubEvent :: Unsubscribe { peer_id, topic: Some ( topic) } => writeln!( stdout, "{} unsubscribed from {}" , peer_id, topic) ?,
153
+ _ => unreachable!( ) ,
132
154
}
133
155
}
134
- Some ( event) = event_stream . next( ) => {
156
+ Some ( ( topic , event) ) = main_events . next( ) => {
135
157
match event {
136
- PubsubEvent :: Subscribe { peer_id } => writeln!( stdout, "{} subscribed" , peer_id) ?,
137
- PubsubEvent :: Unsubscribe { peer_id } => writeln!( stdout, "{} unsubscribed" , peer_id) ?,
158
+ PubsubEvent :: Subscribe { peer_id, topic: None } => writeln!( stdout, "{} subscribed to {}" , peer_id, topic) ?,
159
+ PubsubEvent :: Unsubscribe { peer_id, topic: None } => writeln!( stdout, "{} unsubscribed from {}" , peer_id, topic) ?,
160
+ _ => unreachable!( )
138
161
}
139
162
}
140
163
line = rl. readline( ) . fuse( ) => match line {
141
164
Ok ( rustyline_async:: ReadlineEvent :: Line ( line) ) => {
142
- if let Err ( e) = ipfs. pubsub_publish( topic. clone( ) , line. as_bytes( ) . to_vec( ) ) . await {
143
- writeln!( stdout, "Error publishing message: {e}" ) ?;
165
+ let line = line. trim( ) ;
166
+ if !line. starts_with( '/' ) {
167
+ if !line. is_empty( ) {
168
+ let topic_to_publish = & * main_topic. lock( ) ;
169
+ if let Err ( e) = ipfs. pubsub_publish( topic_to_publish. clone( ) , line. as_bytes( ) . to_vec( ) ) . await {
170
+ writeln!( stdout, "> error publishing message: {e}" ) ?;
171
+ continue ;
172
+ }
173
+ writeln!( stdout, "{peer_id}: {line}" ) ?;
174
+ }
144
175
continue ;
145
176
}
146
- writeln!( stdout, "{peer_id}: {line}" ) ?;
177
+
178
+ let mut command = line. split( ' ' ) ;
179
+
180
+ match command. next( ) {
181
+ Some ( "/subscribe" ) => {
182
+ let topic = match command. next( ) {
183
+ Some ( topic) => topic. to_string( ) ,
184
+ None => {
185
+ writeln!( stdout, "> topic must be provided" ) ?;
186
+ continue ;
187
+ }
188
+ } ;
189
+ let event_st = ipfs. pubsub_events( topic. clone( ) ) . await ?;
190
+ let Ok ( st) = ipfs. pubsub_subscribe( topic. clone( ) ) . await else {
191
+ writeln!( stdout, "> already subscribed to topic" ) ?;
192
+ continue ;
193
+ } ;
194
+
195
+ listener_st. insert( topic. clone( ) , st) ;
196
+ main_events. insert( topic. clone( ) , event_st) ;
197
+ writeln!( stdout, "> subscribed to {}" , topic) ?;
198
+ * main_topic. lock( ) = topic;
199
+ continue ;
200
+ }
201
+ Some ( "/unsubscribe" ) => {
202
+ let topic = match command. next( ) {
203
+ Some ( topic) => topic. to_string( ) ,
204
+ None => main_topic. lock( ) . clone( )
205
+ } ;
206
+
207
+ listener_st. remove( & topic) ;
208
+ main_events. remove( & topic) ;
209
+
210
+ if !ipfs. pubsub_unsubscribe( & topic) . await . unwrap_or_default( ) {
211
+ writeln!( stdout, "> unable to unsubscribe from {}" , topic) ?;
212
+ continue ;
213
+ }
214
+
215
+ writeln!( stdout, "> unsubscribe from {}" , topic) ?;
216
+ if let Some ( some_topic) = main_events. keys( ) . next( ) {
217
+ * main_topic. lock( ) = some_topic. clone( ) ;
218
+ writeln!( stdout, "> setting current topic to {}" , some_topic) ?;
219
+ }
220
+ continue ;
221
+ }
222
+ Some ( "/list-topics" ) => {
223
+ let topics = ipfs. pubsub_subscribed( ) . await . unwrap_or_default( ) ;
224
+ if topics. is_empty( ) {
225
+ writeln!( stdout, "> not subscribed to any topics" ) ?;
226
+ continue ;
227
+ }
228
+
229
+ let current_topic = main_topic. lock( ) . clone( ) ;
230
+
231
+ writeln!( stdout, "> list of topics" ) ?;
232
+ for topic in topics {
233
+ writeln!( stdout, "\t {topic} {}" , if current_topic == topic { "- current" } else { "" } ) ?;
234
+ }
235
+ }
236
+ Some ( "/set-current-topic" ) => {
237
+ let topic = match command. next( ) {
238
+ Some ( topic) if !topic. is_empty( ) => topic. to_string( ) ,
239
+ None | _ => {
240
+ writeln!( stdout, "> topic must be provided" ) ?;
241
+ continue ;
242
+ }
243
+ } ;
244
+
245
+ let topics = ipfs. pubsub_subscribed( ) . await . unwrap_or_default( ) ;
246
+ if topics. is_empty( ) || !topics. contains( & topic) {
247
+ writeln!( stdout, "> not subscribed to topic \" {topic}\" " ) ?;
248
+ continue ;
249
+ }
250
+
251
+ * main_topic. lock( ) = topic. clone( ) ;
252
+
253
+ writeln!( stdout, "> topic set to {topic}" ) ?;
254
+ }
255
+ _ => continue
256
+ }
257
+
147
258
}
148
259
Ok ( rustyline_async:: ReadlineEvent :: Eof ) => {
149
260
cancel. notify_one( ) ;
0 commit comments