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
//! Define peer relationships and query the social graph.
//!
//! Implements the following methods:
//!
//! - [`Sbot::block`]
//! - [`Sbot::follow`]
//! - [`Sbot::friends_hops`]
//! - [`Sbot::friends_is_blocking`]
//! - [`Sbot::friends_is_following`]
//! - [`Sbot::get_follows`]
//! - [`Sbot::set_relationship`]

use crate::{error::GolgiError, messages::SsbMessageContent, sbot::Sbot, utils};

// re-export friends-related kuska types
pub use kuska_ssb::api::dto::content::{FriendsHops, RelationshipQuery};

impl Sbot {
    /// Follow a peer.
    ///
    /// This is a convenience method to publish a contact message with
    /// following: `true` and blocking: `false`.
    ///
    /// # Example
    ///
    /// ```rust
    /// use golgi::{Sbot, GolgiError};
    ///
    /// async fn follow_peer() -> Result<(), GolgiError> {
    ///     let mut sbot_client = Sbot::init(None, None).await?;
    ///
    ///     let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
    ///
    ///     match sbot_client.follow(ssb_id).await {
    ///         Ok(msg_ref) => {
    ///             println!("follow msg reference is: {}", msg_ref)
    ///         },
    ///         Err(e) => eprintln!("failed to follow {}: {}", ssb_id, e)
    ///     }
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn follow(&mut self, contact: &str) -> Result<String, GolgiError> {
        self.set_relationship(contact, true, false).await
    }

    /// Block a peer.
    ///
    /// This is a convenience method to publish a contact message with
    /// following: `false` and blocking: `true`.
    ///
    /// # Example
    ///
    /// ```rust
    /// use golgi::{Sbot, GolgiError};
    ///
    /// async fn block_peer() -> Result<(), GolgiError> {
    ///     let mut sbot_client = Sbot::init(None, None).await?;
    ///
    ///     let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
    ///
    ///     match sbot_client.block(ssb_id).await {
    ///         Ok(msg_ref) => {
    ///             println!("block msg reference is: {}", msg_ref)
    ///         },
    ///         Err(e) => eprintln!("failed to block {}: {}", ssb_id, e)
    ///     }
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn block(&mut self, contact: &str) -> Result<String, GolgiError> {
        self.set_relationship(contact, false, true).await
    }

    /// Publish a contact message defining the relationship for a peer.
    ///
    /// # Example
    ///
    /// ```rust
    /// use golgi::{Sbot, GolgiError};
    ///
    /// async fn relationship() -> Result<(), GolgiError> {
    ///     let mut sbot_client = Sbot::init(None, None).await?;
    ///
    ///     let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
    ///     let following = true;
    ///     let blocking = false;
    ///
    ///     match sbot_client.set_relationship(ssb_id, following, blocking).await {
    ///         Ok(msg_ref) => {
    ///             println!("contact msg reference is: {}", msg_ref)
    ///         },
    ///         Err(e) => eprintln!("failed to set relationship for {}: {}", ssb_id, e)
    ///     }
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn set_relationship(
        &mut self,
        contact: &str,
        following: bool,
        blocking: bool,
    ) -> Result<String, GolgiError> {
        let msg = SsbMessageContent::Contact {
            contact: Some(contact.to_string()),
            following: Some(following),
            blocking: Some(blocking),
            autofollow: None,
        };
        self.publish(msg).await
    }

    /// Get the follow status of two peers (ie. does one peer follow the other?).
    ///
    /// A `RelationshipQuery` `struct` must be defined and passed into this method.
    ///
    /// # Example
    ///
    /// ```rust
    /// use golgi::{Sbot, GolgiError, api::friends::RelationshipQuery};
    ///
    /// async fn relationship() -> Result<(), GolgiError> {
    ///     let mut sbot_client = Sbot::init(None, None).await?;
    ///
    ///     let peer_a = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
    ///     let peer_b = "@3QoWCcy46X9a4jTnOl8m3+n1gKfbsukWuODDxNGN0W8=.ed25519";
    ///
    ///     let query = RelationshipQuery {
    ///         source: peer_a.to_string(),
    ///         dest: peer_b.to_string(),
    ///     };
    ///
    ///     match sbot_client.friends_is_following(query).await {
    ///         Ok(following) if following == "true" => {
    ///             println!("{} is following {}", peer_a, peer_b)
    ///         },
    ///         Ok(_) => println!("{} is not following {}", peer_a, peer_b),
    ///         Err(e) => eprintln!("failed to query relationship status for {} and {}: {}", peer_a,
    ///         peer_b, e)
    ///     }
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn friends_is_following(
        &mut self,
        args: RelationshipQuery,
    ) -> Result<String, GolgiError> {
        let mut sbot_connection = self.get_sbot_connection().await?;
        let req_id = sbot_connection
            .client
            .friends_is_following_req_send(args)
            .await?;

        utils::get_async(
            &mut sbot_connection.rpc_reader,
            req_id,
            utils::string_res_parse,
        )
        .await
    }

    /// Get the block status of two peers (ie. does one peer block the other?).
    ///
    /// A `RelationshipQuery` `struct` must be defined and passed into this method.
    ///
    /// # Example
    ///
    /// ```rust
    /// use golgi::{Sbot, GolgiError, api::friends::RelationshipQuery};
    ///
    /// async fn relationship() -> Result<(), GolgiError> {
    ///     let mut sbot_client = Sbot::init(None, None).await?;
    ///
    ///     let peer_a = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
    ///     let peer_b = "@3QoWCcy46X9a4jTnOl8m3+n1gKfbsukWuODDxNGN0W8=.ed25519";
    ///
    ///     let query = RelationshipQuery {
    ///         source: peer_a.to_string(),
    ///         dest: peer_b.to_string(),
    ///     };
    ///
    ///     match sbot_client.friends_is_blocking(query).await {
    ///         Ok(blocking) if blocking == "true" => {
    ///             println!("{} is blocking {}", peer_a, peer_b)
    ///         },
    ///         Ok(_) => println!("{} is not blocking {}", peer_a, peer_b),
    ///         Err(e) => eprintln!("failed to query relationship status for {} and {}: {}", peer_a,
    ///         peer_b, e)
    ///     }
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn friends_is_blocking(
        &mut self,
        args: RelationshipQuery,
    ) -> Result<String, GolgiError> {
        let mut sbot_connection = self.get_sbot_connection().await?;
        let req_id = sbot_connection
            .client
            .friends_is_blocking_req_send(args)
            .await?;

        utils::get_async(
            &mut sbot_connection.rpc_reader,
            req_id,
            utils::string_res_parse,
        )
        .await
    }

    /// Get a list of peers followed by the local peer.
    ///
    /// # Example
    ///
    /// ```rust
    /// use golgi::{Sbot, GolgiError};
    ///
    /// async fn follows() -> Result<(), GolgiError> {
    ///     let mut sbot_client = Sbot::init(None, None).await?;
    ///
    ///     let follows = sbot_client.get_follows().await?;
    ///
    ///     if follows.is_empty() {
    ///         println!("we do not follow any peers")
    ///     } else {
    ///         follows.iter().for_each(|peer| println!("we follow {}", peer))
    ///     }
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn get_follows(&mut self) -> Result<Vec<String>, GolgiError> {
        self.friends_hops(FriendsHops {
            max: 0,
            start: None,
            reverse: Some(false),
        })
        .await
    }

    /// Get a list of peers following the local peer.
    ///
    /// NOTE: this method is not currently working as expected.
    ///
    /// go-sbot does not currently implement the `reverse=True` parameter.
    /// As a result, the parameter is ignored and this method returns follows
    /// instead of followers.
    ///
    /// # Example
    ///
    /// ```rust
    /// use golgi::{Sbot, GolgiError};
    ///
    /// async fn followers() -> Result<(), GolgiError> {
    ///     let mut sbot_client = Sbot::init(None, None).await?;
    ///
    ///     // let followers = sbot_client.get_followers().await?;
    ///
    ///     // if followers.is_empty() {
    ///     //     println!("no peers follow us")
    ///     // } else {
    ///     //     followers.iter().for_each(|peer| println!("{} is following us", peer))
    ///     // }
    ///
    ///     Ok(())
    /// }
    /// ```
    async fn _get_followers(&mut self) -> Result<Vec<String>, GolgiError> {
        self.friends_hops(FriendsHops {
            max: 0,
            start: None,
            reverse: Some(true),
        })
        .await
    }

    /// Get a list of peers within the specified hops range.
    ///
    /// A `RelationshipQuery` `struct` must be defined and passed into this method.
    ///
    /// Hops = 0 returns a list of peers followed by the local identity.
    /// Those peers may or may not be mutual follows (ie. friends).
    ///
    /// Hops = 1 includes the peers followed by the peers we follow.
    /// For example, if the local identity follows Aiko and Aiko follows
    /// Bridgette and Chris, hops = 1 will return a list including the public
    /// keys for Aiko, Bridgette and Chris (even though Bridgette and Chris
    /// are not followed by the local identity).
    ///
    /// When reverse = True, hops = 0 should return a list of peers who
    /// follow the local identity, ie. followers (but this is not currently
    /// implemented in go-sbot).
    ///
    /// # Example
    ///
    /// ```rust
    /// use golgi::{Sbot, GolgiError, api::friends::FriendsHops};
    ///
    /// async fn peers_within_range() -> Result<(), GolgiError> {
    ///     let mut sbot_client = Sbot::init(None, None).await?;
    ///
    ///     let hops = 2;
    ///
    ///     let query = FriendsHops {
    ///         max: hops,
    ///         reverse: Some(false),
    ///         start: None,
    ///     };
    ///
    ///     let peers = sbot_client.friends_hops(query).await?;
    ///
    ///     if peers.is_empty() {
    ///         println!("no peers found within {} hops", hops)
    ///     } else {
    ///         peers.iter().for_each(|peer| println!("{} is within {} hops", peer, hops))
    ///     }
    ///
    ///     Ok(())
    /// }
    /// ```
    pub async fn friends_hops(&mut self, args: FriendsHops) -> Result<Vec<String>, GolgiError> {
        let mut sbot_connection = self.get_sbot_connection().await?;
        let req_id = sbot_connection.client.friends_hops_req_send(args).await?;
        utils::get_source_until_eof(
            &mut sbot_connection.rpc_reader,
            req_id,
            utils::string_res_parse,
        )
        .await
    }
}