Compare commits

...

190 Commits

Author SHA1 Message Date
glyph 15c5e77da6 Merge pull request 'Add TanglesThread struct import for args' (#61) from tangle_docs_fix into main
Reviewed-on: #61
2022-11-22 13:09:02 +00:00
glyph a42478c169 add TanglesThread struct import for args 2022-11-22 15:07:12 +02:00
glyph 0c083b5ce4 Merge pull request 'Add tangles.thread RPC method and example' (#60) from add_tangles_thread into main
Reviewed-on: #60
2022-11-22 07:56:28 +00:00
glyph ea7fc86ee1 Merge branch 'main' into add_tangles_thread 2022-11-22 07:55:43 +00:00
glyph f6b561ebde clarify tangles.thread doc comment 2022-11-22 09:49:49 +02:00
glyph 8dac8b1a62 remove unneeded imports 2022-11-22 09:44:55 +02:00
glyph 87f75fae94 remove debugging println 2022-11-22 09:44:36 +02:00
glyph a08f3adbef add an example of calling the tangles.thread rpc method 2022-11-18 15:10:56 +02:00
glyph 5c4b92a8bf add tangles.thread rpc method 2022-11-18 15:10:37 +02:00
glyph 195eeb523c make timestamp field an option for SsbMessageKVT and derive eq where partialeq is already derived 2022-11-18 15:04:50 +02:00
glyph 2b37f19a27 Merge pull request 'fix: use new urls' (#58) from decentral1se/golgi:fix-urls into main
Reviewed-on: #58
2022-11-18 07:14:48 +00:00
decentral1se 7024797e0f
fix: use new urls 2022-11-17 19:07:20 +01:00
glyph 9981b64bb2 Merge pull request 'Pass complete args struct to create_history_stream' (#56) from create_history_stream_opts into main
Reviewed-on: #56
2022-08-09 10:15:41 +00:00
glyph ad3a5ed932 Merge branch 'main' into create_history_stream_opts 2022-08-09 10:14:38 +00:00
glyph 457dda6d2d Merge pull request 'Introduce `names` RPC methods' (#53) from names_rpc into main
Reviewed-on: #53
2022-08-09 10:14:02 +00:00
glyph ec5666fe02 Merge branch 'main' into names_rpc 2022-08-09 10:07:23 +00:00
glyph 1335aeb544 bump version 2022-08-09 08:26:39 +01:00
glyph c556373a96 update create_history_stream example 2022-08-09 08:24:53 +01:00
glyph 80e99deb6f pass CreateHistoryStream as arg 2022-08-09 08:24:39 +01:00
glyph e16f567c19 revert friends example code 2022-08-08 08:25:31 +01:00
glyph 821198a400 fix kuska dependency and bump version 2022-08-08 08:13:55 +01:00
glyph 05e2540403 add get_signifier method and call it from get_name 2022-08-08 08:10:13 +01:00
glyph 3fe1a4bbea remove unnecessary imports 2022-08-07 13:17:23 +01:00
notplants 3c37297018 Merge pull request 'Change sbot::init to not panic and instead return an error' (#51) from no-panic into main
Reviewed-on: #51
2022-07-16 05:57:36 +00:00
notplants b24f2e4a06 Combine Sbot and SbotInit errors 2022-07-15 11:16:46 +02:00
glyph 31b432165e introduce names_get_signifier and names_get_image 2022-07-13 09:04:29 +01:00
notplants 48beb4a2e5 Merge branch 'main' of https://git.coopcloud.tech/glyph/golgi into new-rexport 2022-07-12 13:13:31 +02:00
notplants e9667a57be Change sbot::init to not panic and instead return an error 2022-07-12 13:13:22 +02:00
glyph ab86d4fe0b merge main 2022-07-11 08:51:07 +01:00
glyph 22d12f31ac Merge pull request 'Replace SsbMessageValue with SsbMessageKVT in stream examples' (#49) from fix_broken_stream_examples into main
Reviewed-on: #49
2022-07-07 08:16:32 +00:00
notplants fbe7072995 Merge pull request 'Re-export kuska_ssb' (#50) from kuska_ssb into main
Reviewed-on: #50
2022-07-07 07:58:58 +00:00
notplants f41f8b55d2 Reexport kuska_ssb 2022-07-06 12:45:53 +02:00
glyph 15acebbbfa replace SsbMessageValue with SsbMessageKVT in stream examples 2022-07-01 08:58:33 +01:00
glyph 1c44f0e56a Merge pull request 'Remove lockfile from repo' (#48) from remove_lockfile into main
Reviewed-on: #48
2022-07-01 07:41:28 +00:00
glyph 89a98dd6ab add lockfile to git ignore list 2022-06-29 16:01:42 +01:00
glyph e03729907f bump patch version 2022-06-29 16:00:56 +01:00
glyph 175add2c72 remove lockfile 2022-06-29 15:58:39 +01:00
glyph bf7d574064 Merge pull request 'Return KVT from create_history_stream()' (#47) from return_kvt_from_history_stream into main
Reviewed-on: #47
2022-06-29 13:38:55 +00:00
glyph 914ffb70cc fix formatting 2022-06-29 13:56:43 +01:00
glyph 2abe4b5e10 bump version to reflect api change 2022-06-29 13:49:38 +01:00
glyph bb07839691 return kvt instead of value 2022-06-29 13:49:00 +01:00
notplants ca4c1114dd Merge pull request 'Add support for custom keyfile paths' (#41) from keypaths into main
Reviewed-on: #41
2022-06-15 10:27:28 +00:00
notplants 1651b73426 Fix clippy warnings 2022-06-15 12:27:04 +02:00
notplants 1fc56ac8f1 Update kuska-ssb dependency 2022-06-15 12:12:18 +02:00
glyph 2e1e9a79f1 basic names rpc support 2022-06-09 09:15:50 +01:00
notplants 51dc82280a Remove debug statements 2022-05-26 23:11:18 +02:00
notplants 6daddeab9e Debug address 2022-05-26 23:03:43 +02:00
notplants fb115b280f Prefix 127.0.0.1 to address if starts with : 2022-05-26 22:54:19 +02:00
notplants b4987d514a Cargo.toml 2022-05-25 14:01:48 +02:00
notplants 3f3c18b8b7 Add support for custom keyfile paths 2022-05-25 13:49:27 +02:00
glyph 0aa616d92b Merge pull request 'Add a keystore selector (to support Patchwork-style and GoSbot-style paths)' (#39) from keystore_selector into main
Reviewed-on: #39
2022-05-11 14:33:51 +00:00
glyph 9cb8b62843 update docs and doc example code to use new keystore selector 2022-05-11 14:14:25 +02:00
glyph d6546733aa add keystore selector for initialising sbot 2022-05-11 14:14:03 +02:00
glyph 77dd75bcd4 Merge pull request 'Add private message publishing' (#36) from private_msgs into main
Reviewed-on: #36
2022-03-13 09:06:21 +00:00
glyph 1b7b0463a0 Merge branch 'main' into private_msgs 2022-03-13 09:05:23 +00:00
glyph 1d0e31541d update kuska dependency path and fix private msg docs 2022-03-13 10:50:16 +02:00
glyph ab4077b115 temporarily point to kuska-ssb branch 2022-03-10 11:11:15 +02:00
glyph d7ef6a62e0 add private message publishing rpc 2022-03-10 09:47:46 +02:00
glyph 9ad38fb0e8 Merge pull request 'Add unfollow and unblock methods' (#34) from unfollow_and_unblock into main
Reviewed-on: #34
2022-03-03 08:17:06 +00:00
glyph e8294241ec update set_relationship parameter types 2022-03-01 15:42:40 +02:00
glyph 6508aeb3ea add unfollow and unblock methods 2022-03-01 14:19:58 +02:00
glyph 31ec2a2511 Merge pull request 'Add get_blocks method' (#33) from friends_blocks into main
Reviewed-on: #33
2022-03-01 12:17:09 +00:00
glyph 19b1813685 add get_blocks method 2022-03-01 13:54:52 +02:00
glyph 34531400f0 add changelog and bump version 2022-02-26 14:27:39 +02:00
glyph 3148b0a632 Merge pull request 'F ix get_about_message_stream incorrect messages bug' (#32) from fix_about_stream into main
Reviewed-on: #32
2022-02-26 12:21:13 +00:00
glyph 45ad6b53cb ensure about stream returns correct msgs 2022-02-26 14:15:58 +02:00
glyph 935347ffcf fix msg type equality bug 2022-02-26 14:15:27 +02:00
glyph 7b78274a52 Merge pull request 'Blob support' (#30) from get_blob into main
Reviewed-on: #30
2022-02-26 12:13:14 +00:00
glyph d0be016c3f add sigil link check and blob tests 2022-02-25 08:52:26 +02:00
glyph ac0980a7ab add blob hasher function 2022-02-24 19:43:28 +02:00
glyph e6c3a8e993 add method to publish image msg 2022-02-24 19:43:05 +02:00
glyph 6ece486103 add function to generate blob path from blob id 2022-02-24 11:02:21 +02:00
glyph 29c23f424b Merge pull request 'Add license and manifest data' (#29) from prep_for_publish into main
Reviewed-on: #29
2022-02-21 07:11:12 +00:00
glyph ea0eb38260 update lockfile 2022-02-20 11:21:19 +02:00
glyph 5fee37e032 define crate import for kuska 2022-02-20 11:18:37 +02:00
glyph d24a58b81e update dependencies 2022-02-17 14:56:50 +02:00
glyph 8876ce997f add authors section to readme 2022-02-17 11:31:43 +02:00
glyph 57d905cd72 remove version from kuska git dependency 2022-02-16 14:19:47 +02:00
glyph 4b20caa7cf minor improvements in readme examples 2022-02-16 14:19:00 +02:00
glyph 5d82c85944 cleanup imports in example 2022-02-16 14:18:36 +02:00
glyph b0ac8e7a29 clarify friends.hops in the example 2022-02-16 14:17:58 +02:00
glyph 4281f40523 clarify the friends.hops method and set max to 0 2022-02-16 14:17:28 +02:00
glyph 8a37bfcb61 add subset stream example 2022-02-16 14:16:57 +02:00
glyph e3c70554c6 add lockfile 2022-02-15 15:16:30 +02:00
glyph 758ad8f65a remove license file from manifest 2022-02-15 15:16:24 +02:00
glyph ec0118f702 add version for kuska-ssb 2022-02-15 15:15:27 +02:00
glyph b1544feca0 add homepage to manifest 2022-02-15 14:56:38 +02:00
glyph 230ad41644 add manifest fields 2022-02-15 11:19:13 +02:00
glyph f334c03796 add license 2022-02-15 11:18:52 +02:00
glyph 5099b56ecb Merge pull request 'Rename sbot module and improve documentation' (#27) from docs_review into main
Reviewed-on: #27
2022-02-15 08:34:51 +00:00
glyph d4263b967f rename example files 2022-02-15 10:27:34 +02:00
glyph 6a1aa0abda extend documentation for all examples 2022-02-15 10:26:51 +02:00
glyph 506ebec7e4 use full path for pin_mut macro 2022-02-15 10:26:21 +02:00
glyph 74b87b904b polish the utility function docs 2022-02-14 17:00:10 +02:00
glyph 8466510fee polish the message type docs 2022-02-14 17:00:00 +02:00
glyph 32bd924e7e add doc comment examples for whoami api 2022-02-14 16:41:00 +02:00
glyph 1bb9e1f7e1 add doc comment examples for publish api 2022-02-14 16:40:48 +02:00
glyph d0f68086a1 add doc comment examples for invite api 2022-02-14 16:13:29 +02:00
glyph b714bda988 add doc comment examples for history api 2022-02-14 16:13:19 +02:00
glyph 29d1927104 add doc comment examples for get_subset api 2022-02-14 15:52:39 +02:00
glyph 635ebb2a8c add doc comment examples for friends api 2022-02-14 15:52:23 +02:00
glyph 6d2115607b add an example for each about method 2022-02-14 14:31:30 +02:00
glyph e0de8ec0d9 update kuska path and readme 2022-02-14 12:23:34 +02:00
glyph 9c959346f1 clarify api descriptions for about msgs 2022-02-14 12:23:10 +02:00
glyph 1c434193eb remove notes which were accidentally committed 2022-02-09 08:55:11 +02:00
glyph 7aa32e24c7 silly me; revert to 2022-02-08 15:04:44 +02:00
glyph d2a0c38f68 revert connect fn to init 2022-02-08 14:17:46 +02:00
glyph 81e329b40e add handshake error text 2022-02-08 14:17:30 +02:00
glyph d9839f1d06 update use paths 2022-02-08 11:54:21 +02:00
glyph b6fd2e2da5 shorten doc comment line lengths 2022-02-08 11:54:02 +02:00
glyph 5bcdbfa7bd export Sbot as top-level and make api module public 2022-02-08 11:53:27 +02:00
glyph de6689220e add docs for each api module 2022-02-08 11:52:52 +02:00
glyph ebb92aba24 move main sbot type definition to top-level 2022-02-08 10:02:42 +02:00
glyph f40cc793f0 rename sbot dir to api 2022-02-08 10:02:17 +02:00
glyph 5cd2cae3ef refine overview docs and copy to readme 2022-02-08 09:22:59 +02:00
glyph 518f2cbb12 add features and extend example usage section 2022-02-07 16:10:08 +02:00
glyph 623d2d2260 shorten module descriptions (remove 'golgi') 2022-02-07 16:09:51 +02:00
glyph b7b10cda6f Merge pull request 'Split code into modules and individual files' (#26) from refactor_sbot into main
Reviewed-on: #26
2022-02-07 13:13:43 +00:00
glyph b492ff07d0 update use paths in all examples 2022-02-07 14:44:36 +02:00
glyph 3a406cb92e fix use paths 2022-02-07 14:44:17 +02:00
glyph 79705cbb9b address all clippy warnings 2022-02-07 14:07:48 +02:00
glyph 2fff3968db move exported kuska types to crate root 2022-02-07 14:07:15 +02:00
glyph 2b349a9851 split files and rename connect method 2022-02-07 13:54:47 +02:00
glyph 3343e21c11 Merge pull request 'Add invite RPC API support' (#23) from invites into main
Reviewed-on: #23
2022-02-07 07:58:24 +00:00
glyph 5391529466 fix merge conflict 2022-02-04 10:49:11 +02:00
glyph 30587f7ad2 revert to default ip and port for example 2022-02-04 10:43:34 +02:00
glyph ea20ddd2ce Merge branch 'sbot-connection'
Friends API PR was merged in sbot-connection and not main. I'm now
merging the changes from sbot-connection into main.
2022-02-04 10:38:11 +02:00
glyph ec348d57ee Merge pull request 'Add methods for following, blocking and looking up peers' (#18) from friends-requests into sbot-connection
Reviewed-on: #18
2022-02-04 08:31:02 +00:00
glyph 986cd7ecd2 reset default address and port in example 2022-02-04 10:26:14 +02:00
glyph 2689824126 working invites 2022-02-04 10:16:45 +02:00
glyph 08cdc5bede use relationship query struct and latest kuska api 2022-01-31 10:46:58 +02:00
glyph 1e211d894e Merge pull request 'Add get_about_info function' (#15) from get-about-info into main
Reviewed-on: #15
2022-01-18 09:11:44 +00:00
notplants ed923abc2f Merge branch 'friends-requests' into invites 2022-01-17 16:08:31 +00:00
notplants 79630703c2 Remove pub from get_followers 2022-01-17 10:57:13 -05:00
notplants d15474ad61 Remove friends.follow 2022-01-17 10:55:32 -05:00
notplants 18667084ad Add methods for invites 2022-01-16 09:51:54 -05:00
notplants f54e4c47b0 Update example 2022-01-16 08:55:05 -05:00
notplants c406bb4870 Working friends methods 2022-01-15 10:36:52 -05:00
notplants a72fbc9c08 Add git hook 2022-01-15 08:45:02 -05:00
notplants bacd954fa0 Working on friends methods 2022-01-14 16:24:11 -05:00
notplants 6fd27827a0 Fix example 2022-01-14 12:41:15 -05:00
notplants 9027ebfe84 Create new sbot connection for each query 2022-01-14 12:37:34 -05:00
notplants 887d635683 Add get_about_info function 2022-01-13 14:34:27 -05:00
notplants 61fdb42027 Merge pull request 'Add get_profile_info function' (#13) from get_profile_info into main
Reviewed-on: #13
2022-01-13 15:05:34 +00:00
notplants 88c198c0d8 Merge pull request 'Implement get_about_message_stream' (#12) from about-stream into main
Reviewed-on: #12
2022-01-13 15:05:03 +00:00
notplants 8620e17810 Add get_profile_info which gets name, description and image 2022-01-12 15:28:22 -05:00
notplants 0055a1ed65 Merge branch 'about-stream2' into get_profile_info 2022-01-12 15:24:19 -05:00
notplants 1879341499 Chang get_subset_stream to return SsbMessageValue instead of SsbMessageKVT 2022-01-12 15:22:26 -05:00
notplants 0addd8dc47 Convert result to option 2022-01-12 15:01:45 -05:00
notplants ccc4a3371b Working on profile info 2022-01-12 14:58:07 -05:00
notplants 615431496b Use ok_or_else 2022-01-12 12:42:50 -05:00
notplants 6c6413c1b4 Change to use and_then 2022-01-12 12:37:15 -05:00
notplants a3852d89c9 Change get_latest_about_message to use streams 2022-01-12 12:32:09 -05:00
notplants a18e8a7866 Merge pull request 'Create Stream for create_history_stream using async-stream' (#10) from async-stream into main
Reviewed-on: #10
2022-01-06 15:31:57 +00:00
notplants aa60a6e136 Implement get_about_message_stream 2022-01-05 15:08:51 -05:00
notplants 386642d6f1 Combine imports 2022-01-05 14:05:59 -05:00
notplants 0016495525 Change name of _message_type to message_type 2022-01-05 14:02:20 -05:00
notplants 84add5baac Remove rand dependency 2022-01-05 14:00:23 -05:00
notplants 26e2809c9a Get one SbotConnection per stream call 2022-01-05 13:58:48 -05:00
notplants 22417e4d82 Cleanup 2022-01-05 10:58:34 -05:00
notplants 03da29e3f9 Multiple RPC 2022-01-05 10:58:07 -05:00
notplants a076e4741f Multiple RPC reader 2022-01-05 08:50:57 -05:00
notplants db146446d0 Fix imports 2022-01-04 15:05:13 -05:00
notplants 1eb377d065 Fix clippy error 2022-01-04 15:02:20 -05:00
notplants 5711efbece Cleanup 2022-01-04 15:00:50 -05:00
notplants 5927b7dfe6 Cleanup 2022-01-04 15:00:05 -05:00
notplants c4b57ae813 Clean up 2022-01-04 14:59:28 -05:00
notplants 2fd4c12a95 Working example 2022-01-04 14:58:08 -05:00
notplants 8ad65f785d Working create_history_stream 2022-01-04 14:09:49 -05:00
notplants 58d133b5c8 Working recv stream 2022-01-04 12:01:22 -05:00
notplants 0548777948 Working on lifetimes 2022-01-04 11:07:51 -05:00
notplants c06b03ad54 Merge pull request 'Add get_latest_about_message' (#8) from get-latest-about-message2 into main
Reviewed-on: #8
2021-12-31 15:11:38 +00:00
notplants db948f1f67 Response to code review 2021-12-30 13:35:39 -05:00
notplants 0fbd6f0931 Merge pull request 'Add SsbKVT, SsbMessageValue and SsbMessageContent Types' (#6) from kvt into main
Reviewed-on: #6
2021-12-30 18:01:51 +00:00
notplants 65d64bbacf Revert "Remove ContentTypeDecode error variant"
This reverts commit 028e3a47b5.
2021-12-30 07:50:34 -05:00
notplants f764e7dfed Merge branch 'kvt-pr' into profile 2021-12-30 07:50:08 -05:00
notplants 028e3a47b5 Remove ContentTypeDecode error variant 2021-12-30 07:45:40 -05:00
notplants 961f817a8b Replace verbose get_description with more concise version 2021-12-29 21:03:06 -05:00
notplants be19fcbfc4 Add function for get_latest_about_message 2021-12-29 20:51:17 -05:00
notplants a7041e31cc Response to code review 2021-12-29 20:08:55 -05:00
notplants 696701e855 Get description 2021-12-29 15:12:20 -05:00
notplants 0aeed5578e Working on profile 2021-12-29 11:48:53 -05:00
notplants ec32643407 Fix example 2021-12-29 11:38:03 -05:00
notplants 3f2f4d9d59 Fix clippy warnings 2021-12-29 11:31:53 -05:00
notplants ebd2604805 Clean up types 2021-12-29 10:59:05 -05:00
notplants 69f080fe57 Remove unnecessary lookup of type 2021-12-28 20:37:36 -05:00
notplants 1b2b1db95f Deserialize TypedSsbMessageValue 2021-12-28 20:35:46 -05:00
notplants 0952ad9220 Working with KVT types 2021-12-28 14:57:30 -05:00
notplants 37b4a21899 Working on getsubset 2021-12-28 10:24:35 -05:00
notplants 807bb8700c Working on getsubset 2021-12-27 16:43:44 -05:00
29 changed files with 2938 additions and 1248 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
Cargo.lock

5
CHANGELOG.md Normal file
View File

@ -0,0 +1,5 @@
# 0.1.1
_26 February 2022_
- [PR #32](https://git.coopcloud.tech/golgi-ssb/golgi/pulls/32): Fix two filter-related bugs which resulted in `get_about_message_stream(ssb_id)` returning **all** messages authored by `ssb_id`.

1025
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,26 @@
[package]
name = "golgi"
version = "0.1.0"
authors = ["glyph <glyph@mycelial.technology>"]
version = "0.2.4"
edition = "2021"
authors = ["Max Fowler <max@mfowler.info>", "Andrew Reid <glyph@mycelial.technology>"]
readme = "README.md"
description = "An asynchronous, experimental Scuttlebutt client library"
repository = "https://git.coopcloud.tech/golgi-ssb/golgi"
homepage = "http://golgi.mycelial.technology"
license = "LGPL-3.0"
keywords = ["scuttlebutt", "ssb", "decentralized", "peer-for-peer", "p4p"]
exclude = ["git_hooks/", "examples/"]
[dependencies]
async-std = "1.10.0"
async-stream = "0.3.2"
base64 = "0.13.0"
futures = "0.3.18"
futures = "0.3.21"
log = "0.4"
hex = "0.4.3"
kuska-handshake = { version = "0.2.0", features = ["async_std"] }
kuska-sodiumoxide = "0.2.5-0"
# waiting for a pr merge upstream
kuska-ssb = { path = "../ssb" }
# try to replace with miniserde
serde = "1"
kuska-ssb = { git = "https://github.com/Kuska-ssb/ssb" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10.2"

165
LICENSE.txt Normal file
View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -1,18 +1,72 @@
# golgi
_The Golgi complex (aka. Golgi apparatus or Golgi body) packages proteins into membrane-bound vesicles inside the cell before the vesicles are sent to their destination._
_The Golgi complex (aka. Golgi apparatus or Golgi body) packages proteins
into membrane-bound vesicles inside the cell before the vesicles are sent
to their destination._
-----
Golgi is an experimental Scuttlebutt client which uses the [kuska-ssb](https://github.com/Kuska-ssb) libraries and aims to provide a high-level API for interacting with an sbot instance. Development efforts are currently oriented towards [go-sbot](https://github.com/cryptoscope/ssb) interoperability.
## Introduction
Golgi is an asynchronous, experimental Scuttlebutt client that aims to
facilitate Scuttlebutt application development. It provides a high-level
API for interacting with an sbot instance and uses the
[kuska-ssb](https://github.com/Kuska-ssb) libraries to make RPC calls.
## Features
Golgi offers the ability to invoke individual RPC methods while also
providing a number of convenience methods which may involve multiple RPC
calls and / or the processing of data received from those calls. The
`Sbot` `struct` is the primary means of interacting with the library.
Features include the ability to publish messages of various kinds; to
retrieve messages (e.g. `about` and `description` messages) and formulate
queries; to follow, unfollow, block and unblock a peer; to query the social
graph; and to generate pub invite codes.
## Example Usage
```rust
pub async fn run() -> Result<(), GolgiError> {
let mut sbot_client = Sbot::init(None, None).await?;
Basic usage is demonstrated below. Visit the [examples directory](https://git.coopcloud.tech/golgi-ssb/golgi/src/branch/main/examples) in the `golgi` repository for
more comprehensive examples.
```rust
use golgi::{GolgiError, Sbot, sbot::Keystore};
pub async fn run() -> Result<(), GolgiError> {
// Attempt to initialise a connection to an sbot instance using the
// secret file at the Patchwork path and the default IP address, port
// and network key (aka. capabilities key).
let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
// Call the `whoami` RPC method to retrieve the public key for the sbot
// identity.
let id = sbot_client.whoami().await?;
// Print the public key (identity) to `stdout`.
println!("{}", id);
// Compose an SSB post message type.
let post = SsbMessageContent::Post {
text: "Biology, eh?!".to_string(),
mentions: None,
};
// Publish the post.
let post_msg_reference = sbot_client.publish(post).await?;
// Print the reference (sigil-link) for the published post.
println!("{}", post_msg_reference);
Ok(())
}
```
## Authors
- [notplants](https://mfowler.info/)
- [glyph](https://mycelial.technology/)
## License
LGPL-3.0.

99
examples/basic.rs Normal file
View File

@ -0,0 +1,99 @@
use std::process;
use golgi::{messages::SsbMessageContent, sbot::Keystore, GolgiError, Sbot};
// Golgi is an asynchronous library so we must call it from within an
// async function. The `GolgiError` type encapsulates all possible
// error variants for the library.
async fn run() -> Result<(), GolgiError> {
// Attempt to initialise a connection to an sbot instance using the
// secret file at the Patchwork path and the default IP address, port
// and network key (aka. capabilities key).
let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
// Alternatively, we could specify a non-standard IP and port.
// let ip_port = "127.0.0.1:8021".to_string();
// let mut sbot_client = Sbot::init(Some(ip_port), None).await?;
// Call the `whoami` RPC method to retrieve the public key for the sbot
// identity. This is our 'local' public key.
let id = sbot_client.whoami().await?;
// Print the public key (identity) to `stdout`.
println!("whoami: {}", id);
// Compose an SSB `about` message type.
// The `SsbMessageContent` type has many variants and allows for a high
// degree of control when creating messages.
let name = SsbMessageContent::About {
about: id.clone(),
name: Some("golgi".to_string()),
title: None,
branch: None,
image: None,
description: None,
location: None,
start_datetime: None,
};
// Publish the name message. The `publish` method returns a reference to
// the published message.
let name_msg_ref = sbot_client.publish(name).await?;
// Print the message reference to `stdout`.
println!("name_msg_ref: {}", name_msg_ref);
// Compose an SSB `post` message type.
let post = SsbMessageContent::Post {
text: "golgi go womp womp".to_string(),
mentions: None,
};
// Publish the post.
let post_msg_ref = sbot_client.publish(post).await?;
// Print the post reference to `stdout`.
println!("post_msg_ref: {}", post_msg_ref);
// Golgi also exposes convenience methods for some of the most common
// message types. Here we see an example of a convenience method for
// posting a description message. The description is for the local
// identity, ie. we are publishing this about "ourself".
let post_msg_ref = sbot_client
.publish_description("this is a description")
.await?;
// Print the description message reference to `stdout`.
println!("description: {}", post_msg_ref);
let author: String = id.clone();
println!("author: {:?}", author);
// Retrieve the description for the given public key (identity).
let description = sbot_client.get_description(&author).await?;
// Print the description to `stdout`.
println!("found description: {:?}", description);
// Compose and publish another `post` message type.
let post = SsbMessageContent::Post {
text: "golgi go womp womp2".to_string(),
mentions: None,
};
let post_msg_ref = sbot_client.publish(post).await?;
println!("post_msg_ref2: {}", post_msg_ref);
Ok(())
}
// Enable an async main function and execute the `run()` function,
// catching any errors and printing them to `stderr` before exiting the
// process.
#[async_std::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("Application error: {}", e);
process::exit(1);
}
}

129
examples/friends.rs Normal file
View File

@ -0,0 +1,129 @@
use std::process;
use golgi::{
api::friends::{FriendsHops, RelationshipQuery},
sbot::Keystore,
GolgiError, Sbot,
};
// Golgi is an asynchronous library so we must call it from within an
// async function. The `GolgiError` type encapsulates all possible
// error variants for the library.
async fn run() -> Result<(), GolgiError> {
// Attempt to initialise a connection to an sbot instance using the
// secret file at the Patchwork path and the default IP address, port
// and network key (aka. capabilities key).
let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
// Call the `whoami` RPC method to retrieve the public key for the sbot
// identity. This is our 'local' public key.
let id = sbot_client.whoami().await?;
// Print the public key (identity) to `stdout`.
println!("whoami: {}", id);
// Define IDs (public keys) to follow and block.
let to_follow = String::from("@5Pt3dKy2HTJ0mWuS78oIiklIX0gBz6BTfEnXsbvke9c=.ed25519");
let to_block = String::from("@7Y4nwfQmVtAilEzi5knXdS2gilW7cGKSHXdXoT086LM=.ed25519");
// Set the relationship of the local identity to the `to_follow` identity.
// In this case, the `set_relationship` method publishes a `contact`
// message which defines following as `true`.
// A message reference is returned for the published `contact` message.
let response = sbot_client
.set_relationship(&to_follow, Some(true), None)
.await?;
// Print the message reference to `stdout`.
println!("follow_response: {:?}", response);
// Set the relationship of the local identity to the `to_block` identity.
// In this case, the `set_relationship` method publishes a `contact`
// message which defines blocking as `true`.
// A message reference is returned for the published `contact` message.
let response = sbot_client
.set_relationship(&to_block, None, Some(true))
.await?;
// Print the message reference to `stdout`.
println!("follow_response: {:?}", response);
// Golgi also exposes convenience methods for following and blocking.
// Here is an example of a simpler way to follow an identity.
let _follow_response = sbot_client.follow(&to_follow).await?;
// Blocking can be achieved in a similar fashion.
let _block_response = sbot_client.block(&to_block).await?;
// Get a list of peers within 0 hops of the local identity.
// This returns a list of peers whom we follow.
// If `max` is set to 1, the list will include the peers we follow plus
// the peers that they follow.
let follows = sbot_client
.friends_hops(FriendsHops {
max: 0,
start: None,
// The `reverse` parameter is not currently implemented in `go-sbot`.
reverse: Some(false),
})
.await?;
// Print the list of peers to `stdout`.
println!("follows: {:?}", follows);
// Determine if an identity (`source`) is following a second identity (`dest`).
// This method will return `true` or `false`.
let mref = sbot_client
.friends_is_following(RelationshipQuery {
source: id.clone(),
dest: to_follow.clone(),
})
.await?;
// Print the follow status to `stdout`.
println!("isfollowingmref: {}", mref);
// Determine if an identity (`source`) is blocking a second identity (`dest`).
let mref = sbot_client
.friends_is_blocking(RelationshipQuery {
source: id.clone(),
dest: to_block.clone(),
})
.await?;
// Print the block status to `stdout`.
println!("isblockingmref: {}", mref);
let mref = sbot_client
.friends_is_blocking(RelationshipQuery {
source: id.clone(),
dest: to_follow,
})
.await?;
// Output should be `false`.
println!("isblockingmref(should be false): {}", mref);
let mref = sbot_client
.friends_is_following(RelationshipQuery {
source: id,
dest: to_block.clone(),
})
.await?;
// Output should be `false`.
println!("isfollowingmref(should be false): {}", mref);
Ok(())
}
// Enable an async main function and execute the `run()` function,
// catching any errors and printing them to `stderr` before exiting the
// process.
#[async_std::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("Application error: {}", e);
process::exit(1);
}
}

77
examples/invite.rs Normal file
View File

@ -0,0 +1,77 @@
use std::process;
use kuska_ssb::api::dto::content::PubAddress;
use golgi::{messages::SsbMessageContent, sbot::Keystore, GolgiError, Sbot};
// Golgi is an asynchronous library so we must call it from within an
// async function. The `GolgiError` type encapsulates all possible
// error variants for the library.
async fn run() -> Result<(), GolgiError> {
// Attempt to initialise a connection to an sbot instance using the
// secret file at the Patchwork path and the default IP address, port
// and network key (aka. capabilities key).
let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
// Call the `whoami` RPC method to retrieve the public key for the sbot
// identity. This is our 'local' public key.
let id = sbot_client.whoami().await?;
// Print the public key (identity) to `stdout`.
println!("whoami: {}", id);
// Compose a `pub` address type message.
let pub_address_msg = SsbMessageContent::Pub {
address: Some(PubAddress {
// IP address.
host: Some("127.0.0.1".to_string()),
// Port.
port: 8009,
// Public key.
key: id,
}),
};
// Publish the `pub` address message.
// This step is required for successful invite code creation.
let pub_msg_ref = sbot_client.publish(pub_address_msg).await?;
// Print the message reference to `stdout`.
println!("pub_msg_ref: {}", pub_msg_ref);
// Generate an invite code that can be used 1 time.
let invite_code = sbot_client.invite_create(1).await?;
// Print the invite code to `stdout`.
println!("invite (1 use): {:?}", invite_code);
// Generate an invite code that can be used 7 times.
let invite_code_2 = sbot_client.invite_create(7).await?;
// Print the invite code to `stdout`.
println!("invite (7 uses): {:?}", invite_code_2);
// Define an invite code.
let test_invite =
"net:ssbroom2.commoninternet.net:8009~shs:wm8a1zHWjtESv4XSKMWU/rPRhnAoAiSAe4hQSY0UF5A=";
// Redeem an invite code (initiating a mutual follow between the local
// identity and the identity which generated the code (`wm8a1z...`).
let mref = sbot_client.invite_use(test_invite).await?;
// Print the message reference to `stdout`.
println!("mref: {:?}", mref);
Ok(())
}
// Enable an async main function and execute the `run()` function,
// catching any errors and printing them to `stderr` before exiting the
// process.
#[async_std::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("Application error: {}", e);
process::exit(1);
}
}

View File

@ -1,59 +0,0 @@
use std::process;
use kuska_ssb::api::dto::content::{SubsetQuery, TypedMessage};
use golgi::error::GolgiError;
use golgi::sbot::Sbot;
async fn run() -> Result<(), GolgiError> {
let mut sbot_client = Sbot::init(None, None).await?;
let id = sbot_client.whoami().await?;
println!("{}", id);
/*
let name = TypedMessage::About {
about: id,
name: Some("golgi".to_string()),
title: None,
branch: None,
image: None,
description: None,
location: None,
start_datetime: None,
};
let name_msg_ref = sbot_client.publish(name).await?;
println!("{}", name_msg_ref);
let post = TypedMessage::Post {
text: "golgi go womp womp".to_string(),
mentions: None,
};
let post_msg_ref = sbot_client.publish(post).await?;
println!("{}", post_msg_ref);
let post_msg_ref = sbot_client
.publish_description("this is a description")
.await?;
println!("description: {}", post_msg_ref);
*/
let query = SubsetQuery::Type {
op: "type".to_string(),
string: "vote".to_string(),
};
let query_response = sbot_client.getsubset(query).await?;
println!("{}", query_response);
Ok(())
}
#[async_std::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("Application error: {}", e);
process::exit(1);
}
}

166
examples/streams.rs Normal file
View File

@ -0,0 +1,166 @@
use std::process;
use async_std::stream::StreamExt;
use futures::TryStreamExt;
use golgi::{
api::{
get_subset::{SubsetQuery, SubsetQueryOptions},
history_stream::CreateHistoryStream,
},
messages::{SsbMessageContentType, SsbMessageKVT},
sbot::Keystore,
GolgiError, Sbot,
};
// Golgi is an asynchronous library so we must call it from within an
// async function. The `GolgiError` type encapsulates all possible
// error variants for the library.
async fn run() -> Result<(), GolgiError> {
// Attempt to initialise a connection to an sbot instance using the
// secret file at the Patchwork path and the default IP address, port
// and network key (aka. capabilities key).
let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
// Call the `whoami` RPC method to retrieve the public key for the sbot
// identity. This is our 'local' public key.
let id = sbot_client.whoami().await?;
// Print the public key (identity) to `stdout`.
println!("whoami: {}", id);
let author = id.clone();
/* HISTORY STREAM EXAMPLE */
let history_stream_args = CreateHistoryStream::new(author.to_string());
// Create an ordered stream of all messages authored by the `author`
// identity. Messages are returned as KVTs (Key Value Timestamp).
let history_stream = sbot_client
.create_history_stream(history_stream_args)
.await?;
// Pin the stream to the stack to allow polling of the `future`.
futures::pin_mut!(history_stream);
println!("looping through stream");
// Iterate through each element in the stream and match on the `Result`.
// In this case, each element has type `Result<SsbMessageKVT, GolgiError>`.
while let Some(res) = history_stream.next().await {
match res {
Ok(kvt) => {
// Print the `SsbMessageKVT` of this element to `stdout`.
println!("kvt: {:?}", kvt);
}
Err(err) => {
// Print the `GolgiError` of this element to `stderr`.
eprintln!("err: {:?}", err);
}
}
}
println!("reached end of stream");
// Create an ordered stream of all messages authored by the `author`
// identity.
let history_stream = sbot_client
.create_history_stream(CreateHistoryStream::new(author.to_string()))
.await?;
// Collect the stream elements into a `Vec<SsbMessageKVT>` using
// `try_collect`. A `GolgiError` will be returned from the `run`
// function if any element contains an error.
let results: Vec<SsbMessageKVT> = history_stream.try_collect().await?;
// Loop through the `SsbMessageKVT` elements, printing each one
// to `stdout`.
for x in results {
println!("x: {:?}", x);
}
// Create an ordered stream of all messages authored by the `author`
// identity.
let history_stream = sbot_client
.create_history_stream(CreateHistoryStream::new(author.to_string()))
.await?;
// Iterate through the elements in the stream and use `map` to convert
// each `SsbMessageKVT` element into a tuple of
// `(String, SsbMessageContentType)`. This is an example of stream
// conversion.
let type_stream = history_stream.map(|msg| match msg {
Ok(kvt) => {
let message_type = kvt.value.get_message_type()?;
// Return the message key and type.
let tuple: (String, SsbMessageContentType) = (kvt.key, message_type);
Ok(tuple)
}
Err(err) => Err(err),
});
// Pin the stream to the stack to allow polling of the `future`.
futures::pin_mut!(type_stream);
println!("looping through type stream");
// Iterate through each element in the stream and match on the `Result`.
// In this case, each element has type
// `Result<(String, SsbMessageContentType), GolgiError>`.
while let Some(res) = type_stream.next().await {
match res {
Ok(value) => {
println!("value: {:?}", value);
}
Err(err) => {
println!("err: {:?}", err);
}
}
}
println!("reached end of type stream");
/* SUBSET STREAM EXAMPLE */
// Compose a subset query for `post` message types.
let post_query = SubsetQuery::Type {
op: "type".to_string(),
string: "post".to_string(),
};
// Define the options for the query.
let post_query_opts = SubsetQueryOptions {
descending: Some(true),
keys: None,
page_limit: Some(5),
};
// Return 5 `post` type messages from any author in descending order.
let query_stream = sbot_client
.get_subset_stream(post_query, Some(post_query_opts))
.await?;
println!("looping through post query stream");
// Iterate over the stream and pretty-print each returned message
// value while ignoring any errors.
query_stream.for_each(|msg| match msg {
Ok(val) => println!("{:#?}", val),
Err(_) => (),
});
println!("reached end of post query stream");
Ok(())
}
// Enable an async main function and execute the `run()` function,
// catching any errors and printing them to `stderr` before exiting the
// process.
#[async_std::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("Application error: {}", e);
process::exit(1);
}
}

84
examples/tangles.rs Normal file
View File

@ -0,0 +1,84 @@
use std::process;
use async_std::stream::StreamExt;
use golgi::{api::tangles::TanglesThread, sbot::Keystore, GolgiError, Sbot};
// Golgi is an asynchronous library so we must call it from within an
// async function. The `GolgiError` type encapsulates all possible
// error variants for the library.
async fn run() -> Result<(), GolgiError> {
// Attempt to initialise a connection to an sbot instance using the
// secret file at the Patchwork path and the default IP address, port
// and network key (aka. capabilities key).
//let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
let mut sbot_client =
Sbot::init(Keystore::GoSbot, Some("127.0.0.1:8021".to_string()), None).await?;
// Call the `whoami` RPC method to retrieve the public key for the sbot
// identity. This is our 'local' public key.
let id = sbot_client.whoami().await?;
// Print the public key (identity) to `stdout`.
println!("whoami: {}", id);
/* TANGLES THREAD EXAMPLE */
// Define the message key that will be used to generate the tangle stream.
// This must be the key of the root message in the thread (ie. the first
// message of the thread).
let msg_key = "%vY53ethgEG1tNsKqmLm6isNSbsu+jwMf/UNGMfUiLm0=.sha256".to_string();
// Instantiate the arguments for the `tangles.thread` RPC by instantiating
// a new instant of the `TanglesThread` struct.
let args = TanglesThread::new(msg_key).keys_values(true, true);
// It's also possible to define the maximum number of messages
// returned from the stream by setting the `limit` value:
// Note: a limit of 3 will return a maximum of 4 messages! The root
// message + 3 other messages in the thread.
// let args = TanglesThread::new(msg_key).keys_values(true, true).limit(3);
// Create an ordered stream of all messages comprising the thread
// in which the given message resides. Messages are returned as KVTs
// (Key Value Timestamp).
let thread_stream = sbot_client.tangles_thread(args).await?;
// Pin the stream to the stack to allow polling of the `future`.
futures::pin_mut!(thread_stream);
println!("looping through stream");
// Iterate through each element in the stream and match on the `Result`.
// In this case, each element has type `Result<SsbMessageKVT, GolgiError>`.
while let Some(res) = thread_stream.next().await {
match res {
Ok(kvt) => {
// Print the `SsbMessageKVT` of this element to `stdout`.
println!("kvt: {:?}", kvt);
}
Err(err) => {
// Print the `GolgiError` of this element to `stderr`.
eprintln!("err: {:?}", err);
}
}
}
println!("reached end of stream");
// Take a look at the `examples/streams.rs` file in this repo for more
// ways of handling streams.
Ok(())
}
// Enable an async main function and execute the `run()` function,
// catching any errors and printing them to `stderr` before exiting the
// process.
#[async_std::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("Application error: {}", e);
process::exit(1);
}
}

1
git_hooks/pre-commit Executable file
View File

@ -0,0 +1 @@
cargo fmt

473
src/api/about.rs Normal file
View File

@ -0,0 +1,473 @@
//! Retrieve data about a peer.
//!
//! Implements the following methods:
//!
//! - [`Sbot::get_about_info`]
//! - [`Sbot::get_about_message_stream`]
//! - [`Sbot::get_description`]
//! - [`Sbot::get_image`]
//! - [`Sbot::get_latest_about_message`]
//! - [`Sbot::get_name`]
//! - [`Sbot::get_name_and_image`]
//! - [`Sbot::get_profile_info`]
//! - [`Sbot::get_signifier`]
use std::collections::HashMap;
use async_std::stream::{Stream, StreamExt};
use crate::{
api::get_subset::{SubsetQuery, SubsetQueryOptions},
error::GolgiError,
messages::{SsbMessageContentType, SsbMessageValue},
sbot::Sbot,
utils,
};
impl Sbot {
/// Get all the `about` type messages for a peer in order of recency
/// (ie. most recent messages first).
///
/// # Example
///
/// ```rust
/// use async_std::stream::{Stream, StreamExt};
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn about_message_stream() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let about_message_stream = sbot_client.get_about_message_stream(ssb_id).await?;
///
/// // Make the stream into an iterator.
/// futures::pin_mut!(about_message_stream);
///
/// about_message_stream.for_each(|msg| {
/// match msg {
/// Ok(val) => println!("msg value: {:?}", val),
/// Err(e) => eprintln!("error: {}", e),
/// }
/// }).await;
///
/// Ok(())
/// }
/// ```
pub async fn get_about_message_stream(
&mut self,
ssb_id: &str,
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, GolgiError> {
let query = SubsetQuery::Author {
op: "author".to_string(),
feed: ssb_id.to_string(),
};
// specify that most recent messages should be returned first
let query_options = SubsetQueryOptions {
descending: Some(true),
keys: None,
page_limit: None,
};
let get_subset_stream = self.get_subset_stream(query, Some(query_options)).await?;
// TODO: after fixing sbot regression,
// change this subset query to filter by type about in addition to author
// and remove this filter section
// filter down to about messages
let about_message_stream = get_subset_stream.filter(|msg| match msg {
Ok(val) => val.is_message_type(SsbMessageContentType::About),
Err(_err) => false,
});
// return about message stream
Ok(about_message_stream)
}
/// Get the latest `image` value for a peer. The value is a blob reference.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn image_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let image = sbot_client.get_image(ssb_id).await?;
///
/// println!("peer {} is identified by {}", ssb_id, image);
///
/// Ok(())
/// }
/// ```
pub async fn get_image(&mut self, ssb_id: &str) -> Result<String, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection
.client
.names_get_image_for_req_send(ssb_id)
.await?;
utils::get_async(
&mut sbot_connection.rpc_reader,
req_id,
utils::string_res_parse,
)
.await
}
/// Get the value of the latest `about` type message, containing the given
/// `key`, for a peer.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn name_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
/// let key = "name";
///
/// let name_info = sbot_client.get_latest_about_message(ssb_id, key).await?;
///
/// match name_info {
/// Some(name) => println!("peer {} is named {}", ssb_id, name),
/// None => println!("no name found for peer {}", ssb_id)
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_latest_about_message(
&mut self,
ssb_id: &str,
key: &str,
) -> Result<Option<String>, GolgiError> {
// get about_message_stream
let about_message_stream = self.get_about_message_stream(ssb_id).await?;
// now we have a stream of about messages with most recent at the front
// of the vector
futures::pin_mut!(about_message_stream);
// iterate through the vector looking for most recent about message with
// the given key
let latest_about_message_res: Option<Result<SsbMessageValue, GolgiError>> =
about_message_stream
// find the first msg that contains the field `key`
.find(|res| match res {
Ok(msg) => msg.content.get(key).is_some(),
Err(_) => false,
})
.await;
// Option<Result<SsbMessageValue, GolgiError>> -> Option<SsbMessageValue>
let latest_about_message = latest_about_message_res.and_then(|msg| msg.ok());
// Option<SsbMessageValue> -> Option<String>
let latest_about_value = latest_about_message.and_then(|msg| {
msg
// SsbMessageValue -> Option<&Value>
.content
.get(key)
// Option<&Value> -> <Option<&str>
.and_then(|value| value.as_str())
// Option<&str> -> Option<String>
.map(|value| value.to_string())
});
// return value is either `Ok(Some(String))` or `Ok(None)`
Ok(latest_about_value)
}
/// Get the latest `name`, `description` and `image` values for a peer,
/// as defined in their `about` type messages.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn profile_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let profile_info = sbot_client.get_profile_info(ssb_id).await?;
///
/// let name = profile_info.get("name");
/// let description = profile_info.get("description");
/// let image = profile_info.get("image");
///
/// match (name, description, image) {
/// (Some(name), Some(desc), Some(image)) => {
/// println!(
/// "peer {} is named {}. their profile image blob reference is {} and they describe themself as follows: {}",
/// ssb_id, name, image, desc,
/// )
/// },
/// (_, _, _) => {
/// eprintln!("failed to retrieve all profile info values")
/// }
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_profile_info(
&mut self,
ssb_id: &str,
) -> Result<HashMap<String, String>, GolgiError> {
let key_to_search_for = vec!["description"];
let description = self.get_about_info(ssb_id, key_to_search_for).await?;
let mut profile_info = self.get_name_and_image(ssb_id).await?;
// extend the `profile_info` HashMap by adding the key-value from the
// `description` HashMap; `profile_info` now contains all three
// key-value pairs
profile_info.extend(description.into_iter());
Ok(profile_info)
}
/// Get the latest `name` and `image` values for a peer. This method can
/// be used to display profile images of a list of users.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn name_and_image_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let profile_info = sbot_client.get_name_and_image(ssb_id).await?;
///
/// let name = profile_info.get("name");
/// let image = profile_info.get("image");
///
/// match (name, image) {
/// (Some(name), Some(image)) => {
/// println!(
/// "peer {} is named {}. their profile image blob reference is {}.",
/// ssb_id, name, image,
/// )
/// },
/// (Some(name), None) => {
/// println!(
/// "peer {} is named {}. no image blob reference was found for them.",
/// ssb_id, name,
/// )
/// },
/// (_, _) => {
/// eprintln!("failed to retrieve all profile info values")
/// }
/// }
///
/// Ok(())
/// }
pub async fn get_name_and_image(
&mut self,
ssb_id: &str,
) -> Result<HashMap<String, String>, GolgiError> {
let mut name_and_image: HashMap<String, String> = HashMap::new();
let name = self.get_name(ssb_id).await?;
let image = self.get_image(ssb_id).await?;
name_and_image.insert("name".to_string(), name);
name_and_image.insert("image".to_string(), image);
Ok(name_and_image)
}
/// Get the latest values for the provided keys from the `about` type
/// messages of a peer. The method will return once a value has been
/// found for each key, or once all messages have been checked.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn about_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
/// let keys_to_search_for = vec!["name", "description"];
///
/// let about_info = sbot_client.get_about_info(ssb_id, keys_to_search_for).await?;
///
/// let name = about_info.get("name");
/// let description = about_info.get("description");
///
/// match (name, description) {
/// (Some(name), Some(desc)) => {
/// println!(
/// "peer {} is named {}. they describe themself as: {}",
/// ssb_id, name, desc,
/// )
/// },
/// (Some(name), None) => {
/// println!(
/// "peer {} is named {}. no description was found for them.",
/// ssb_id, name,
/// )
/// },
/// (_, _) => {
/// eprintln!("failed to retrieve all profile info values")
/// }
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_about_info(
&mut self,
ssb_id: &str,
mut keys_to_search_for: Vec<&str>,
) -> Result<HashMap<String, String>, GolgiError> {
// get about_message_stream
let about_message_stream = self.get_about_message_stream(ssb_id).await?;
// now we have a stream of about messages with most recent at the front
// of the vector
// `pin_mut!` is needed for iteration
futures::pin_mut!(about_message_stream);
let mut profile_info: HashMap<String, String> = HashMap::new();
// iterate through the stream while it still has more values and
// we still have keys we are looking for
while let Some(res) = about_message_stream.next().await {
// if there are no more keys we are looking for, then we are done
if keys_to_search_for.is_empty() {
break;
}
// if there are still keys we are looking for, then continue searching
match res {
Ok(msg) => {
// for each key we are searching for, check if this about
// message contains a value for that key
for key in &keys_to_search_for.clone() {
let about_val = msg.content.get("about").and_then(|val| val.as_str());
let option_val = msg
.content
.get(key)
.and_then(|val| val.as_str())
.map(|val| val.to_string());
match option_val {
// only return val if this msg is about the given ssb_id
Some(val) if about_val == Some(ssb_id) => {
// if a value is found, then insert it
profile_info.insert(key.to_string(), val);
// remove this key from keys_to_search_for,
// since we are no longer searching for it
keys_to_search_for.retain(|val| val != key)
}
_ => continue,
}
}
}
Err(_err) => {
// skip errors
continue;
}
}
}
Ok(profile_info)
}
/// Get the latest `name` value for a peer. The public key of the peer
/// will be returned if no `name` value is found.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn name_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let name = sbot_client.get_name(ssb_id).await?;
///
/// println!("peer {} is named {}", ssb_id, name);
///
/// Ok(())
/// }
/// ```
pub async fn get_name(&mut self, ssb_id: &str) -> Result<String, GolgiError> {
self.get_signifier(ssb_id).await
}
/// Get the latest `description` value for a peer.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn description_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// if let Some(desc) = sbot_client.get_description(ssb_id).await? {
/// println!("peer {} describes themself as follows: {}", ssb_id, desc)
/// } else {
/// eprintln!("no description found for peer {}", ssb_id)
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_description(&mut self, ssb_id: &str) -> Result<Option<String>, GolgiError> {
self.get_latest_about_message(ssb_id, "description").await
}
/// Get the latest `name` value (signifier) for a peer. The public key of
/// the peer will be returned if no `name` value is found.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn name_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let name = sbot_client.get_signifier(ssb_id).await?;
///
/// println!("peer {} is named {}", ssb_id, name);
///
/// Ok(())
/// }
/// ```
pub async fn get_signifier(&mut self, ssb_id: &str) -> Result<String, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection
.client
.names_get_signifier_req_send(ssb_id)
.await?;
utils::get_async(
&mut sbot_connection.rpc_reader,
req_id,
utils::string_res_parse,
)
.await
}
}

432
src/api/friends.rs Normal file
View File

@ -0,0 +1,432 @@
//! Define peer relationships and query the social graph.
//!
//! Implements the following methods:
//!
//! - [`Sbot::block`]
//! - [`Sbot::unblock`]
//! - [`Sbot::follow`]
//! - [`Sbot::unfollow`]
//! - [`Sbot::friends_hops`]
//! - [`Sbot::friends_is_blocking`]
//! - [`Sbot::friends_is_following`]
//! - [`Sbot::get_blocks`]
//! - [`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`.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn follow_peer() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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, Some(true), None).await
}
/// Unfollow a peer.
///
/// This is a convenience method to publish a contact message with
/// following: `false`.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn unfollow_peer() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// match sbot_client.unfollow(ssb_id).await {
/// Ok(msg_ref) => {
/// println!("unfollow msg reference is: {}", msg_ref)
/// },
/// Err(e) => eprintln!("failed to unfollow {}: {}", ssb_id, e)
/// }
///
/// Ok(())
/// }
/// ```
pub async fn unfollow(&mut self, contact: &str) -> Result<String, GolgiError> {
self.set_relationship(contact, Some(false), None).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, sbot::Keystore};
///
/// async fn block_peer() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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> {
// we want to unfollow and block
self.set_relationship(contact, Some(false), Some(true))
.await
}
/// Unblock a peer.
///
/// This is a convenience method to publish a contact message with
/// blocking: `false`.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn unblock_peer() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// match sbot_client.unblock(ssb_id).await {
/// Ok(msg_ref) => {
/// println!("unblock msg reference is: {}", msg_ref)
/// },
/// Err(e) => eprintln!("failed to unblock {}: {}", ssb_id, e)
/// }
///
/// Ok(())
/// }
/// ```
pub async fn unblock(&mut self, contact: &str) -> Result<String, GolgiError> {
self.set_relationship(contact, None, Some(false)).await
}
/// Publish a contact message defining the relationship for a peer.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn relationship() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
/// let following = Some(true);
/// // Could also be `None` to only publish the following relationship.
/// let blocking = Some(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: Option<bool>,
blocking: Option<bool>,
) -> Result<String, GolgiError> {
let msg = SsbMessageContent::Contact {
contact: Some(contact.to_string()),
following,
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, sbot::Keystore};
///
/// async fn relationship() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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, sbot::Keystore};
///
/// async fn relationship() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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 blocked by the local peer.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn follows() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let follows = sbot_client.get_blocks().await?;
///
/// if follows.is_empty() {
/// println!("we do not block any peers")
/// } else {
/// follows.iter().for_each(|peer| println!("we block {}", peer))
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_blocks(&mut self) -> Result<Vec<String>, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection.client.friends_blocks_req_send().await?;
utils::get_source_until_eof(
&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, sbot::Keystore};
///
/// async fn follows() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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, sbot::Keystore};
///
/// async fn followers() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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, sbot::Keystore};
///
/// async fn peers_within_range() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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
}
}

87
src/api/get_subset.rs Normal file
View File

@ -0,0 +1,87 @@
//! Perform subset queries.
//!
//! Implements the following methods:
//!
//! - [`Sbot::get_subset_stream`]
use async_std::stream::Stream;
use crate::{error::GolgiError, messages::SsbMessageValue, sbot::Sbot, utils};
// re-export subset-related kuska types
pub use kuska_ssb::api::dto::content::{SubsetQuery, SubsetQueryOptions};
impl Sbot {
/// Make a subset query, as defined by the [Subset replication for SSB specification](https://github.com/ssbc/ssb-subset-replication-spec).
///
/// Calls the `partialReplication. getSubset` RPC method.
///
/// # Arguments
///
/// * `query` - A `SubsetQuery` which specifies the desired query filters.
/// * `options` - An Option<`SubsetQueryOptions`> which, if provided, adds
/// additional specifications to the query, such as page limit and/or
/// descending results.
///
/// # Example
///
/// ```rust
/// use async_std::stream::StreamExt;
/// use golgi::{
/// Sbot,
/// GolgiError,
/// api::get_subset::{
/// SubsetQuery,
/// SubsetQueryOptions
/// },
/// sbot::Keystore
/// };
///
/// async fn query() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let post_query = SubsetQuery::Type {
/// op: "type".to_string(),
/// string: "post".to_string()
/// };
///
/// let post_query_opts = SubsetQueryOptions {
/// descending: Some(true),
/// keys: None,
/// page_limit: Some(5),
/// };
///
/// // Return 5 `post` type messages from any author in descending order.
/// let query_stream = sbot_client
/// .get_subset_stream(post_query, Some(post_query_opts))
/// .await?;
///
/// // Iterate over the stream and pretty-print each returned message
/// // value while ignoring any errors.
/// query_stream.for_each(|msg| match msg {
/// Ok(val) => println!("{:#?}", val),
/// Err(_) => (),
/// });
///
/// Ok(())
/// }
/// ```
pub async fn get_subset_stream(
&mut self,
query: SubsetQuery,
options: Option<SubsetQueryOptions>,
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection
.client
.getsubset_req_send(query, options)
.await?;
let get_subset_stream = utils::get_source_stream(
sbot_connection.rpc_reader,
req_id,
utils::ssb_message_res_parse,
)
.await;
Ok(get_subset_stream)
}
}

59
src/api/history_stream.rs Normal file
View File

@ -0,0 +1,59 @@
//! Return a history stream.
//!
//! Implements the following methods:
//!
//! - [`Sbot::create_history_stream`]
use async_std::stream::Stream;
pub use kuska_ssb::api::dto::CreateHistoryStreamIn as CreateHistoryStream;
use crate::{error::GolgiError, messages::SsbMessageKVT, sbot::Sbot, utils};
impl Sbot {
/// Call the `createHistoryStream` RPC method. Returns messages in the form
/// of KVTs (Key Value Timestamp).
///
/// # Example
///
/// ```rust
/// use async_std::stream::StreamExt;
/// use golgi::{
/// Sbot,
/// GolgiError,
/// sbot::Keystore,
/// api::history_stream::CreateHistoryStream
/// };
///
/// async fn history() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519".to_string();
///
/// let args = CreateHistoryStream::new(ssb_id);
/// let history_stream = sbot_client.create_history_stream(args).await?;
///
/// history_stream.for_each(|msg| {
/// match msg {
/// Ok(val) => println!("msg value: {:?}", val),
/// Err(e) => eprintln!("error: {}", e),
/// }
/// }).await;
///
/// Ok(())
/// }
/// ```
pub async fn create_history_stream(
&mut self,
args: CreateHistoryStream,
) -> Result<impl Stream<Item = Result<SsbMessageKVT, GolgiError>>, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection
.client
.create_history_stream_req_send(&args)
.await?;
let history_stream =
utils::get_source_stream(sbot_connection.rpc_reader, req_id, utils::kvt_res_parse)
.await;
Ok(history_stream)
}
}

79
src/api/invite.rs Normal file
View File

@ -0,0 +1,79 @@
//! Create and use invite codes.
//!
//! Implements the following methods:
//!
//! - [`Sbot::invite_create`]
//! - [`Sbot::invite_use`]
use crate::{error::GolgiError, sbot::Sbot, utils};
impl Sbot {
/// Generate an invite code.
///
/// Calls the `invite.create` RPC method and returns the code.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn invite_code_generator() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let invite_code = sbot_client.invite_create(5).await?;
///
/// println!("this invite code can be used 5 times: {}", invite_code);
///
/// Ok(())
/// }
/// ```
pub async fn invite_create(&mut self, uses: u16) -> Result<String, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection.client.invite_create_req_send(uses).await?;
utils::get_async(
&mut sbot_connection.rpc_reader,
req_id,
utils::string_res_parse,
)
.await
}
/// Use an invite code.
///
/// Calls the `invite.use` RPC method and returns a reference to the follow
/// message.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn invite_code_consumer() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let invite_code = "127.0.0.1:8008:@0iMa+vP7B2aMrV3dzRxlch/iqZn/UM3S3Oo2oVeILY8=.ed25519~ZHNjeajPB/84NjjsrglZInlh46W55RcNDPcffTPgX/Q=";
///
/// match sbot_client.invite_use(invite_code).await {
/// Ok(msg_ref) => println!("consumed invite code. msg reference: {}", msg_ref),
/// Err(e) => eprintln!("failed to consume the invite code: {}", e),
/// }
///
/// Ok(())
/// }
/// ```
pub async fn invite_use(&mut self, invite_code: &str) -> Result<String, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection
.client
.invite_use_req_send(invite_code)
.await?;
utils::get_async(
&mut sbot_connection.rpc_reader,
req_id,
utils::string_res_parse,
)
.await
}
}

12
src/api/mod.rs Normal file
View File

@ -0,0 +1,12 @@
//! API for interacting with a running go-sbot instance.
pub mod about;
pub mod friends;
pub mod get_subset;
pub mod history_stream;
pub mod invite;
pub mod private;
pub mod publish;
pub mod tangles;
pub mod whoami;
pub use crate::sbot::*;

66
src/api/private.rs Normal file
View File

@ -0,0 +1,66 @@
//! Publish Scuttlebutt private messages.
//!
//! Implements the following method:
//!
//! - [`Sbot::publish_private`]
use crate::{error::GolgiError, messages::SsbMessageContent, sbot::Sbot, utils};
impl Sbot {
/// Publish a private message.
///
/// # Arguments
///
/// * `msg` - A `PrivateMessage` `struct` whose fields include `text` and
/// `recipients`.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, messages::SsbMessageContent, sbot::Keystore};
///
/// async fn publish_a_private_msg() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let text = String::from("Hi friends, I have a super secrect message to share with you about the wonders of intra-cellular transport mechanics.");
///
/// // We must also include the local identity (public key) here if we wish
/// // to be able to read the message. Ie. the sender must be included in
/// // the list of recipients.
/// let recipients = vec![
/// String::from("@OKRij/n7Uu42A0Z75ty0JI0cZxcieD2NyjXrRdYKNOQ=.ed25519"),
/// String::from("@Sih4JGgs5oQPXehRyHS5qrYbx/0hQVUqChojX0LNtcQ=.ed25519"),
/// String::from("@BVA85B7a/a17v2ZVcLkMgPE+v7X5rQVAHEgQBbCaKMs=.ed25519"),
/// ];
///
/// let msg_ref = sbot_client.publish_private(text, recipients).await?;
///
/// println!("msg reference for the private msg: {}", msg_ref);
///
/// Ok(())
/// }
/// ```
pub async fn publish_private(
&mut self,
text: String,
recipients: Vec<String>,
) -> Result<String, GolgiError> {
let msg = SsbMessageContent::Post {
text: text.to_string(),
mentions: None,
};
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection
.client
.private_publish_req_send(msg, recipients)
.await?;
utils::get_async(
&mut sbot_connection.rpc_reader,
req_id,
utils::string_res_parse,
)
.await
}
}

196
src/api/publish.rs Normal file
View File

@ -0,0 +1,196 @@
//! Publish Scuttlebutt messages.
//!
//! Implements the following methods:
//!
//! - [`Sbot::publish`]
//! - [`Sbot::publish_description`]
//! - [`Sbot::publish_image`]
//! - [`Sbot::publish_name`]
//! - [`Sbot::publish_post`]
use kuska_ssb::api::dto::content::Image;
use crate::{error::GolgiError, messages::SsbMessageContent, sbot::Sbot, utils};
impl Sbot {
/// Publish a message.
///
/// # Arguments
///
/// * `msg` - A `SsbMessageContent` `enum` whose variants include `Pub`,
/// `Post`, `Contact`, `About`, `Channel` and `Vote`.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, messages::SsbMessageContent, sbot::Keystore};
///
/// async fn publish_a_msg() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// // Construct an SSB message of type `post`.
/// let post = SsbMessageContent::Post {
/// text: "And then those vesicles, filled with the Golgi products, move to the rest of the cell".to_string(),
/// mentions: None,
/// };
///
/// let msg_ref = sbot_client.publish(post).await?;
///
/// println!("msg reference for the golgi post: {}", msg_ref);
///
/// Ok(())
/// }
/// ```
pub async fn publish(&mut self, msg: SsbMessageContent) -> Result<String, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection.client.publish_req_send(msg).await?;
utils::get_async(
&mut sbot_connection.rpc_reader,
req_id,
utils::string_res_parse,
)
.await
}
/// Publish a post.
///
/// Convenient wrapper around the `publish` method which constructs and
/// publishes a `post` type message appropriately from a string.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn publish_a_post() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let text = "The Golgi is located right near the nucleus.";
///
/// let msg_ref = sbot_client.publish_post(text).await?;
///
/// println!("msg reference for the golgi post: {}", msg_ref);
///
/// Ok(())
/// }
/// ```
pub async fn publish_post(&mut self, text: &str) -> Result<String, GolgiError> {
let msg = SsbMessageContent::Post {
text: text.to_string(),
mentions: None,
};
self.publish(msg).await
}
/// Publish a description for the local identity.
///
/// Convenient wrapper around the `publish` method which constructs and
/// publishes an `about` type description message appropriately from a string.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn publish_a_description() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let description = "The Golgi apparatus was identified by the Italian scientist Camillo Golgi in 1897.";
///
/// let msg_ref = sbot_client.publish_description(description).await?;
///
/// println!("msg reference for the golgi description: {}", msg_ref);
///
/// Ok(())
/// }
/// ```
pub async fn publish_description(&mut self, description: &str) -> Result<String, GolgiError> {
let msg = SsbMessageContent::About {
about: self.id.to_string(),
name: None,
title: None,
branch: None,
image: None,
description: Some(description.to_string()),
location: None,
start_datetime: None,
};
self.publish(msg).await
}
/// Publish an image for the local identity.
///
/// Requires the image to have been added to the local blobstore. The
/// ID of the blob (sigil-link in the form `&...=.sha256`) must be provided.
///
/// Convenient wrapper around the `publish` method which constructs and
/// publishes an `about` type name message appropriately from a string.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn publish_an_image() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let blob_id = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256";
///
/// let msg_ref = sbot_client.publish_image(blob_id).await?;
///
/// println!("msg reference: {}", msg_ref);
///
/// Ok(())
/// }
/// ```
pub async fn publish_image(&mut self, blob_id: &str) -> Result<String, GolgiError> {
let msg = SsbMessageContent::About {
about: self.id.to_string(),
name: None,
title: None,
branch: None,
image: Some(Image::OnlyLink(blob_id.to_string())),
description: None,
location: None,
start_datetime: None,
};
self.publish(msg).await
}
/// Publish a name for the local identity.
///
/// Convenient wrapper around the `publish` method which constructs and
/// publishes an `about` type name message appropriately from a string.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn publish_a_name() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let name = "glyphski_golgionikus";
///
/// let msg_ref = sbot_client.publish_name(name).await?;
///
/// println!("msg reference: {}", msg_ref);
///
/// Ok(())
/// }
/// ```
pub async fn publish_name(&mut self, name: &str) -> Result<String, GolgiError> {
let msg = SsbMessageContent::About {
about: self.id.to_string(),
name: Some(name.to_string()),
title: None,
branch: None,
image: None,
description: None,
location: None,
start_datetime: None,
};
self.publish(msg).await
}
}

61
src/api/tangles.rs Normal file
View File

@ -0,0 +1,61 @@
//! Take a reference to the root message of a thread and return a stream of all
//! messages in the thread. This includes the root message and all replies.
//!
//! Implements the following methods:
//!
//! - [`Sbot::tangles_thread`]
use async_std::stream::Stream;
pub use kuska_ssb::api::dto::TanglesThread;
use crate::{error::GolgiError, messages::SsbMessageKVT, sbot::Sbot, utils};
impl Sbot {
/// Call the `tanglesThread` RPC method. Returns messages in the form
/// of KVTs (Key Value Timestamp).
///
/// # Example
///
/// ```rust
/// use async_std::stream::StreamExt;
/// use golgi::{
/// Sbot,
/// GolgiError,
/// api::tangles::TanglesThread,
/// sbot::Keystore
/// };
///
/// async fn tangle() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let msg_key = "%kmXb3MXtBJaNugcEL/Q7G40DgcAkMNTj3yhmxKHjfCM=.sha256";
///
/// let args = TanglesThread::new(msg_key).keys_values(true, true);
/// let tangles_thread = sbot_client.tangles_thread(msg_id).await?;
///
/// tangles_thread.for_each(|msg| {
/// match msg {
/// Ok(kvt) => println!("msg kvt: {:?}", kvt),
/// Err(e) => eprintln!("error: {}", e),
/// }
/// }).await;
///
/// Ok(())
/// }
/// ```
pub async fn tangles_thread(
&mut self,
args: TanglesThread,
) -> Result<impl Stream<Item = Result<SsbMessageKVT, GolgiError>>, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection
.client
.tangles_thread_req_send(&args)
.await?;
let thread_stream =
utils::get_source_stream(sbot_connection.rpc_reader, req_id, utils::kvt_res_parse)
.await;
Ok(thread_stream)
}
}

45
src/api/whoami.rs Normal file
View File

@ -0,0 +1,45 @@
//! Return the SSB ID of the local sbot instance.
//!
//! Implements the following methods:
//!
//! - [`Sbot::whoami`]
use crate::{error::GolgiError, sbot::Sbot, utils};
impl Sbot {
/// Get the public key of the local identity.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn fetch_id() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let pub_key = sbot_client.whoami().await?;
///
/// println!("local ssb id: {}", pub_key);
///
/// Ok(())
/// }
/// ```
pub async fn whoami(&mut self) -> Result<String, GolgiError> {
let mut sbot_connection = self.get_sbot_connection().await?;
let req_id = sbot_connection.client.whoami_req_send().await?;
let result = utils::get_async(
&mut sbot_connection.rpc_reader,
req_id,
utils::json_res_parse,
)
.await?;
let id = result
.get("id")
.ok_or_else(|| GolgiError::Sbot("id key not found on whoami call".to_string()))?
.as_str()
.ok_or_else(|| GolgiError::Sbot("whoami returned non-string value".to_string()))?;
Ok(id.to_string())
}
}

145
src/blobs.rs Normal file
View File

@ -0,0 +1,145 @@
//! Blob utilities which do not require RPC calls.
//!
//! Offers the following functions:
//!
//! - [`get_blob_path()`]
//! - [`hash_blob()`]
use base64;
use sha2::{Digest, Sha256};
use crate::error::GolgiError;
/// Lookup the filepath for a blob.
///
/// # Example
///
/// ```rust
/// use golgi::{blobs, GolgiError};
///
/// fn lookup_blob_path() -> Result<(), GolgiError> {
/// let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256";
///
/// let blob_path = blobs::get_blob_path(blob_ref)?;
///
/// println!("{}", blob_path);
///
/// // 26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f
///
/// Ok(())
/// }
/// ```
pub fn get_blob_path(blob_id: &str) -> Result<String, GolgiError> {
// ensure the id starts with the correct sigil link
if !blob_id.starts_with('&') {
return Err(GolgiError::SigilLink(format!(
"incorrect first character, expected '&' sigil: {}",
blob_id
)));
}
// find the dot index denoting the start of the algorithm definition tag
let dot_index = blob_id
.rfind('.')
.ok_or_else(|| GolgiError::SigilLink(format!("no dot index was found: {}", blob_id)))?;
// obtain the base64 portion (substring) of the blob id
let base64_str = &blob_id[1..dot_index];
// decode blob substring from base64 (to bytes)
let blob_bytes = base64::decode_config(base64_str, base64::STANDARD)?;
// represent the blob bytes as hex, removing all unnecessary characters
let blob_hex = format!("{:02x?}", blob_bytes)
.replace('[', "")
.replace(']', "")
.replace(',', "")
.replace(' ', "");
// split the hex representation of the decoded base64
// this is how paths are formatted for the blobstore
// e.g. 26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f
// full path would be: `/home/user/.ssb-go/blobs/sha256/26/524773dc...`
let blob_path = format!("{}/{}", &blob_hex[..2], &blob_hex[2..]);
Ok(blob_path)
}
/// Hash the given blob byte slice and return the hex representation and
/// blob ID (sigil-link).
///
/// This function can be used when adding a blob to the local blobstore.
pub fn hash_blob(buffer: &[u8]) -> Result<(String, String), GolgiError> {
// hash the bytes
let hash = Sha256::digest(buffer);
// generate a hex representation of the hash
let hex_hash = format!("{:02x?}", hash)
.replace('[', "")
.replace(']', "")
.replace(',', "")
.replace(' ', "");
// encode the hash
let b64_hash = base64::encode(&hash[..]);
// format the base64 encoding as a blob sigil-link (blob id)
let blob_id = format!("&{}.sha256", b64_hash);
Ok((hex_hash, blob_id))
}
#[cfg(test)]
mod tests {
use crate::blobs;
/* HAPPY PATH TESTS */
#[test]
fn blob_path() {
let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256";
let blob_path = blobs::get_blob_path(blob_ref);
assert!(blob_path.is_ok());
assert_eq!(
blob_path.unwrap(),
"26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f"
);
}
#[test]
fn hashed_blob() {
// pretend this represents a file which has been written to a buffer
let buffer = vec![7; 128];
let hash_result = blobs::hash_blob(&buffer);
assert!(hash_result.is_ok());
let (hex_hash, blob_id) = hash_result.unwrap();
assert_eq!(
hex_hash,
"4c1398e54d53e925cff04da532f4bbaf15f75b5981fc76c2072dfdc6491a9fb1"
);
assert_eq!(
blob_id,
"&TBOY5U1T6SXP8E2lMvS7rxX3W1mB/HbCBy39xkkan7E=.sha256"
);
}
/* SAD PATH TESTS */
#[test]
fn blob_id_without_sigil() {
let blob_ref = "JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256";
match blobs::get_blob_path(blob_ref) {
Err(e) => assert_eq!(e.to_string(), "SSB blob ID error: incorrect first character, expected '&' sigil: JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256"),
_ => panic!(),
}
}
#[test]
fn blob_id_without_algo() {
let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=";
match blobs::get_blob_path(blob_ref) {
Err(e) => assert_eq!(e.to_string(), "SSB blob ID error: no dot index was found: &JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8="),
_ => panic!(),
}
}
}

View File

@ -1,6 +1,7 @@
//! Custom error type for `golgi`.
//! Custom error type.
use std::io::Error as IoError;
use std::str::Utf8Error;
use base64::DecodeError;
use kuska_handshake::async_std::Error as HandshakeError;
@ -33,8 +34,17 @@ pub enum GolgiError {
Rpc(RpcError),
/// Go-sbot error.
Sbot(String),
/// SSB sigil-link error.
SigilLink(String),
/// JSON serialization or deserialization error.
SerdeJson(JsonError),
/// Error decoding typed SSB message from content.
ContentType(String),
/// Error decoding UTF8 string from bytes
Utf8Parse {
/// The underlying parse error.
source: Utf8Error,
},
}
impl std::error::Error for GolgiError {
@ -47,7 +57,10 @@ impl std::error::Error for GolgiError {
GolgiError::Feed(ref err) => Some(err),
GolgiError::Rpc(ref err) => Some(err),
GolgiError::Sbot(_) => None,
GolgiError::SigilLink(_) => None,
GolgiError::SerdeJson(ref err) => Some(err),
GolgiError::ContentType(_) => None,
GolgiError::Utf8Parse { ref source } => Some(source),
}
}
}
@ -58,15 +71,24 @@ impl std::fmt::Display for GolgiError {
// TODO: add context (what were we trying to decode?)
GolgiError::DecodeBase64(_) => write!(f, "Failed to decode base64"),
GolgiError::Io { ref context, .. } => write!(f, "IO error: {}", context),
GolgiError::Handshake(ref err) => write!(f, "{}", err),
GolgiError::Handshake(ref err) => write!(f, "Handshake failure: {}", err),
GolgiError::Api(ref err) => write!(f, "SSB API failure: {}", err),
GolgiError::Feed(ref err) => write!(f, "SSB feed error: {}", err),
// TODO: improve this variant with a context message
// then have the core display msg be: "SSB RPC error: {}", context
GolgiError::Rpc(ref err) => write!(f, "SSB RPC failure: {}", err),
GolgiError::Sbot(ref err) => write!(f, "Sbot returned an error response: {}", err),
GolgiError::Sbot(ref err) => write!(f, "Sbot encountered an error: {}", err),
GolgiError::SigilLink(ref context) => write!(f, "SSB blob ID error: {}", context),
GolgiError::SerdeJson(_) => write!(f, "Failed to serialize JSON slice"),
//GolgiError::WhoAmI(ref err) => write!(f, "{}", err),
GolgiError::ContentType(ref err) => write!(
f,
"Failed to decode typed message from SSB message content: {}",
err
),
GolgiError::Utf8Parse { source } => {
write!(f, "Failed to deserialize UTF8 from bytes: {}", source)
}
}
}
}
@ -106,3 +128,9 @@ impl From<JsonError> for GolgiError {
GolgiError::SerdeJson(err)
}
}
impl From<Utf8Error> for GolgiError {
fn from(err: Utf8Error) -> Self {
GolgiError::Utf8Parse { source: err }
}
}

View File

@ -2,30 +2,78 @@
//! # golgi
//!
//! _The Golgi complex (aka. Golgi apparatus or Golgi body) packages proteins into membrane-bound vesicles inside the cell before the vesicles are sent to their destination._
//! _The Golgi complex (aka. Golgi apparatus or Golgi body) packages proteins
//! into membrane-bound vesicles inside the cell before the vesicles are sent
//! to their destination._
//!
//! -----
//!
//! Golgi is an experimental Scuttlebutt client which uses the [kuska-ssb](https://github.com/Kuska-ssb) libraries and aims to provide a high-level API for interacting with an sbot instance. Development efforts are currently oriented towards [go-sbot](https://github.com/cryptoscope/ssb) interoperability.
//! ## Introduction
//!
//! Golgi is an asynchronous, experimental Scuttlebutt client that aims to
//! facilitate Scuttlebutt application development. It provides a high-level
//! API for interacting with an sbot instance and uses the
//! [kuska-ssb](https://github.com/Kuska-ssb) libraries to make RPC calls.
//! Development efforts are currently oriented towards
//! [go-sbot](https://github.com/ssbc/go-ssb) interoperability.
//!
//! ## Features
//!
//! Golgi offers the ability to invoke individual RPC methods while also
//! providing a number of convenience methods which may involve multiple RPC
//! calls and / or the processing of data received from those calls. The
//! [`Sbot`](crate::sbot::Sbot) `struct` is the primary means of interacting
//! with the library.
//!
//! Features include the ability to publish messages of various kinds; to
//! retrieve messages (e.g. `about` and `description` messages) and formulate
//! queries; to follow, unfollow, block and unblock a peer; to query the social
//! graph; and to generate pub invite codes.
//!
//! Visit the [API modules](crate::api) to view the available methods.
//!
//! ## Example Usage
//!
//! Basic usage is demonstrated below. Visit the [examples directory](https://git.coopcloud.tech/golgi-ssb/golgi/src/branch/main/examples) in the `golgi` repository for
//! more comprehensive examples.
//!
//! ```rust
//! use golgi::GolgiError;
//! use golgi::sbot::Sbot;
//! use golgi::{messages::SsbMessageContent, GolgiError, Sbot, sbot::Keystore};
//!
//! pub async fn run() -> Result<(), GolgiError> {
//! let mut sbot_client = Sbot::init(None, None).await?;
//! // Attempt to connect to an sbot instance using the default IP address,
//! // port and network key (aka. capabilities key).
//! let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
//!
//! // Call the `whoami` RPC method to retrieve the public key for the sbot
//! // identity.
//! let id = sbot_client.whoami().await?;
//!
//! // Print the public key (identity) to `stdout`.
//! println!("{}", id);
//!
//! // Compose an SSB post message type.
//! let post = SsbMessageContent::Post {
//! text: "Biology, eh?!".to_string(),
//! mentions: None,
//! };
//!
//! // Publish the post.
//! let post_msg_reference = sbot_client.publish(post).await?;
//!
//! // Print the reference (sigil-link) for the published post.
//! println!("{}", post_msg_reference);
//!
//! Ok(())
//! }
//! ```
pub mod api;
pub mod blobs;
pub mod error;
pub mod messages;
pub mod sbot;
mod utils;
pub mod utils;
pub use crate::error::GolgiError;
pub use crate::{error::GolgiError, sbot::Sbot};
pub use kuska_ssb;

104
src/messages.rs Normal file
View File

@ -0,0 +1,104 @@
//! Message types and conversion methods.
use kuska_ssb::api::dto::content::TypedMessage;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::Debug;
use crate::error::GolgiError;
/// `SsbMessageContent` is a type alias for `TypedMessage` from the `kuska_ssb` library.
/// It is aliased in golgi to fit the naming convention of the other message
/// types: `SsbMessageKVT` and `SsbMessageValue`.
///
/// See the [kuska source code](https://github.com/Kuska-ssb/ssb/blob/master/src/api/dto/content.rs#L103) for the type definition of `TypedMessage`.
pub type SsbMessageContent = TypedMessage;
/// The `value` of an SSB message (the `V` in `KVT`).
///
/// More information concerning the data model can be found in the
/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[allow(missing_docs)]
pub struct SsbMessageValue {
pub previous: Option<String>,
pub author: String,
pub sequence: u64,
pub timestamp: f64,
pub hash: String,
pub content: Value,
pub signature: String,
}
/// Message content types.
#[derive(Debug, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum SsbMessageContentType {
About,
Vote,
Post,
Contact,
Unrecognized,
}
impl SsbMessageValue {
/// Get the type field of the message content as an enum, if found.
///
/// If no `type` field is found or the `type` field is not a string,
/// it returns an `Err(GolgiError::ContentType)`.
///
/// If a `type` field is found but with an unknown string,
/// it returns an `Ok(SsbMessageContentType::Unrecognized)`.
pub fn get_message_type(&self) -> Result<SsbMessageContentType, GolgiError> {
let msg_type = self
.content
.get("type")
.ok_or_else(|| GolgiError::ContentType("type field not found".to_string()))?;
let mtype_str: &str = msg_type.as_str().ok_or_else(|| {
GolgiError::ContentType("type field value is not a string as expected".to_string())
})?;
let enum_type = match mtype_str {
"about" => SsbMessageContentType::About,
"post" => SsbMessageContentType::Post,
"vote" => SsbMessageContentType::Vote,
"contact" => SsbMessageContentType::Contact,
_ => SsbMessageContentType::Unrecognized,
};
Ok(enum_type)
}
/// Helper function which returns `true` if this message is of the given type,
/// and `false` if the type does not match or is not found.
pub fn is_message_type(&self, message_type: SsbMessageContentType) -> bool {
let self_message_type = self.get_message_type();
match self_message_type {
Ok(mtype) => mtype == message_type,
Err(_err) => false,
}
}
/// Convert the content JSON value into an `SsbMessageContent` `enum`,
/// using the `type` field as a tag to select which variant of the `enum`
/// to deserialize into.
///
/// See the [Serde docs on internally-tagged enum representations](https://serde.rs/enum-representations.html#internally-tagged) for further details.
pub fn into_ssb_message_content(self) -> Result<SsbMessageContent, GolgiError> {
let m: SsbMessageContent = serde_json::from_value(self.content)?;
Ok(m)
}
}
/// An SSB message represented as a key-value-timestamp (`KVT`).
///
/// More information concerning the data model can be found in the
/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[allow(missing_docs)]
pub struct SsbMessageKVT {
pub key: String,
pub value: SsbMessageValue,
pub timestamp: Option<f64>,
pub rts: Option<f64>,
}

View File

@ -1,73 +1,158 @@
//! Sbot type and associated methods.
//! Sbot type and connection-related methods.
use async_std::net::TcpStream;
use kuska_handshake::async_std::BoxStream;
use kuska_sodiumoxide::crypto::{auth, sign::ed25519};
use kuska_ssb::{
api::{
dto::{
//content::{About, Post},
content::{SubsetQuery, TypedMessage},
CreateHistoryStreamIn,
},
ApiCaller,
},
api::ApiCaller,
discovery, keystore,
keystore::OwnedIdentity,
rpc::{RpcReader, RpcWriter},
};
use crate::error::GolgiError;
use crate::utils;
/// The Scuttlebutt identity, keys and configuration parameters for connecting to a local sbot
/// instance, as well as handles for calling RPC methods and receiving responses.
/// Keystore selector to specify the location of the secret file.
///
/// This enum is used when initiating a connection with an sbot instance.
pub enum Keystore {
/// Patchwork default keystore path: `.ssb/secret` in the user's home directory.
Patchwork,
/// GoSbot default keystore path: `.ssb-go/secret` in the user's home directory.
GoSbot,
/// GoSbot keystore in a custom location
CustomGoSbot(String),
/// Patchwork keystore in a custom location
CustomPatchwork(String),
}
/// A struct representing a connection with a running sbot.
/// A client and an rpc_reader can together be used to make requests to the sbot
/// and read the responses.
/// Note there can be multiple SbotConnection at the same time.
pub struct SbotConnection {
/// Client for writing requests to go-bot
pub client: ApiCaller<TcpStream>,
/// RpcReader object for reading responses from go-sbot
pub rpc_reader: RpcReader<TcpStream>,
}
/// Holds the Scuttlebutt identity, keys and configuration parameters for
/// connecting to a local sbot and implements all Golgi API methods.
pub struct Sbot {
id: String,
/// The ID (public key value) of the account associated with the local sbot instance.
pub id: String,
public_key: ed25519::PublicKey,
private_key: ed25519::SecretKey,
address: String,
// aka caps key (scuttleverse identifier)
network_id: auth::Key,
client: ApiCaller<TcpStream>,
rpc_reader: RpcReader<TcpStream>,
}
impl Sbot {
/// Initiate a connection with an sbot instance. Define the IP address, port and network key
/// for the sbot, then retrieve the public key, private key (secret) and identity from the
/// `.ssb-go/secret` file. Open a TCP stream to the sbot and perform the secret handshake. If successful, create a box stream and split it into a writer and reader. Return RPC handles to the sbot as part of the `struct` output.
pub async fn init(ip_port: Option<String>, net_id: Option<String>) -> Result<Sbot, GolgiError> {
let address = if ip_port.is_none() {
/// Initiate a connection with an sbot instance. Define the IP address,
/// port and network key for the sbot, then retrieve the public key,
/// private key (secret) and identity from the `.ssb-go/secret` file.
pub async fn init(
keystore: Keystore,
ip_port: Option<String>,
net_id: Option<String>,
) -> Result<Sbot, GolgiError> {
let mut address = if ip_port.is_none() {
"127.0.0.1:8008".to_string()
} else {
ip_port.unwrap()
};
if address.starts_with(':') {
address = format!("127.0.0.1{}", address);
}
let network_id = if net_id.is_none() {
discovery::ssb_net_id()
} else {
auth::Key::from_slice(&hex::decode(net_id.unwrap()).unwrap()).unwrap()
};
let OwnedIdentity { pk, sk, id } = keystore::from_gosbot_local()
.await
.expect("couldn't read local secret");
let OwnedIdentity { pk, sk, id } = match keystore {
Keystore::Patchwork => keystore::from_patchwork_local().await.map_err(|_err| {
GolgiError::Sbot(
"sbot initialization error: couldn't read local patchwork secret from default location".to_string(),
)
})?,
Keystore::GoSbot => keystore::from_gosbot_local().await.map_err(|_err| {
GolgiError::Sbot(
"sbot initialization error: couldn't read local go-sbot secret from default location".to_string(),
)
})?,
Keystore::CustomGoSbot(key_path) => {
keystore::from_custom_gosbot_keypath(key_path.to_string())
.await
.map_err(|_err| {
GolgiError::Sbot(format!(
"sbot initialization error: couldn't read local go-sbot secret from: {}",
key_path
))
})?
}
Keystore::CustomPatchwork(key_path) => {
keystore::from_custom_patchwork_keypath(key_path.to_string())
.await
.map_err(|_err| {
GolgiError::Sbot(format!(
"sbot initialization error: couldn't read local patchwork secret from: {}",
key_path
))
})?
}
};
Ok(Self {
id,
public_key: pk,
private_key: sk,
address,
network_id,
})
}
/// Creates a new connection with the sbot, using the address, network_id,
/// public_key and private_key supplied when Sbot was initialized.
///
/// Note that a single Sbot can have multiple SbotConnection at the same time.
pub async fn get_sbot_connection(&self) -> Result<SbotConnection, GolgiError> {
let address = self.address.clone();
let network_id = self.network_id.clone();
let public_key = self.public_key;
let private_key = self.private_key.clone();
Sbot::_get_sbot_connection_helper(address, network_id, public_key, private_key).await
}
/// Private helper function which creates a new connection with sbot,
/// but with all variables passed as arguments.
///
/// Open a TCP stream to the sbot and perform the secret handshake. If
/// successful, create a box stream and split it into a writer and reader.
/// Return RPC handles to the sbot as part of the `struct` output.
async fn _get_sbot_connection_helper(
address: String,
network_id: auth::Key,
public_key: ed25519::PublicKey,
private_key: ed25519::SecretKey,
) -> Result<SbotConnection, GolgiError> {
let socket = TcpStream::connect(&address)
.await
.map_err(|source| GolgiError::Io {
source,
context: "socket error; failed to initiate tcp stream connection".to_string(),
context: "failed to initiate tcp stream connection".to_string(),
})?;
let handshake = kuska_handshake::async_std::handshake_client(
&mut &socket,
network_id.clone(),
pk,
sk.clone(),
pk,
public_key,
private_key.clone(),
public_key,
)
.await
.map_err(GolgiError::Handshake)?;
@ -77,94 +162,7 @@ impl Sbot {
let rpc_reader = RpcReader::new(box_stream_read);
let client = ApiCaller::new(RpcWriter::new(box_stream_write));
Ok(Self {
id,
public_key: pk,
private_key: sk,
address,
network_id,
client,
rpc_reader,
})
}
/// Call the `partialReplication getSubset` RPC method and return a vector
/// of messages as KVTs (key, value, timestamp).
// TODO: add args for `descending` and `page` (max number of msgs in response)
pub async fn getsubset(&mut self, query: SubsetQuery) -> Result<String, GolgiError> {
let req_id = self.client.getsubset_req_send(query).await?;
utils::get_async(&mut self.rpc_reader, req_id, utils::getsubset_res_parse).await
}
/// Call the `whoami` RPC method and return an `id`.
pub async fn whoami(&mut self) -> Result<String, GolgiError> {
let req_id = self.client.whoami_req_send().await?;
utils::get_async(&mut self.rpc_reader, req_id, utils::whoami_res_parse)
.await
.map(|whoami| whoami.id)
}
/// Call the `publish` RPC method and return a message reference.
///
/// # Arguments
///
/// * `msg` - A `TypedMessage` `enum` whose variants include `Pub`, `Post`, `Contact`, `About`,
/// `Channel` and `Vote`. See the `kuska_ssb` documentation for further details such as field
/// names and accepted values for each variant.
pub async fn publish(&mut self, msg: TypedMessage) -> Result<String, GolgiError> {
let req_id = self.client.publish_req_send(msg).await?;
utils::get_async(&mut self.rpc_reader, req_id, utils::publish_res_parse).await
}
/// Wrapper for publish which constructs and publishes a post message appropriately from a string.
///
/// # Arguments
///
/// * `text` - A reference to a string slice which represents the text to be published in the post
pub async fn publish_post(&mut self, text: &str) -> Result<String, GolgiError> {
let msg = TypedMessage::Post {
text: text.to_string(),
mentions: None,
};
self.publish(msg).await
}
/// Wrapper for publish which constructs and publishes an about description message appropriately from a string.
///
/// # Arguments
///
/// * `description` - A reference to a string slice which represents the text to be published as an about description.
pub async fn publish_description(&mut self, description: &str) -> Result<String, GolgiError> {
let msg = TypedMessage::About {
about: self.id.to_string(),
name: None,
title: None,
branch: None,
image: None,
description: Some(description.to_string()),
location: None,
start_datetime: None,
};
self.publish(msg).await
}
/*
pub async fn publish_post(&mut self, post: Post) -> Result<String, GolgiError> {
let req_id = self.client.publish_req_send(post).await?;
utils::get_async(&mut self.rpc_reader, req_id, utils::publish_res_parse).await
}
*/
/// Call the `createHistoryStream` RPC method and print the output.
async fn create_history_stream(&mut self, id: String) -> Result<(), GolgiError> {
let args = CreateHistoryStreamIn::new(id);
let req_id = self.client.create_history_stream_req_send(&args).await?;
// TODO: we should return a vector of messages instead of printing them
utils::print_source_until_eof(&mut self.rpc_reader, req_id, utils::feed_res_parse).await
let sbot_connection = SbotConnection { rpc_reader, client };
Ok(sbot_connection)
}
}

View File

@ -1,41 +1,66 @@
/*
use kuska_handshake::async_std::BoxStream;
use kuska_sodiumoxide::crypto::sign::ed25519;
use kuska_ssb::discovery;
use kuska_ssb::keystore;
use kuska_ssb::keystore::OwnedIdentity;
*/
//! Utility methods.
use std::fmt::Debug;
use async_std::io::Read;
use kuska_ssb::api::dto::WhoAmIOut;
use kuska_ssb::feed::Feed;
use async_std::{io::Read, net::TcpStream, stream::Stream};
use async_stream::stream;
use kuska_ssb::rpc::{RecvMsg, RequestNo, RpcReader};
use serde_json::Value;
use crate::error::GolgiError;
use crate::messages::{SsbMessageKVT, SsbMessageValue};
pub fn getsubset_res_parse(body: &[u8]) -> Result<String, GolgiError> {
// TODO: cleanup with proper error handling etc.
Ok(std::str::from_utf8(body).unwrap().to_string())
/// Parse an array of bytes (returned by an rpc call) into a `KVT`.
///
/// # Arguments
///
/// * `body` - An array of u8 to be parsed.
pub fn kvt_res_parse(body: &[u8]) -> Result<SsbMessageKVT, GolgiError> {
let value: Value = serde_json::from_slice(body)?;
let kvt: SsbMessageKVT = serde_json::from_value(value)?;
Ok(kvt)
}
pub fn feed_res_parse(body: &[u8]) -> Result<Feed, GolgiError> {
Ok(Feed::from_slice(body)?)
/// Parse an array of bytes (returned by an rpc call) into a `String`.
///
/// # Arguments
///
/// * `body` - An array of u8 to be parsed.
pub fn string_res_parse(body: &[u8]) -> Result<String, GolgiError> {
Ok(std::str::from_utf8(body)?.to_string())
}
//pub fn publish_res_parse(body: &[u8]) -> Result<PublishOut, GolgiError> {
pub fn publish_res_parse(body: &[u8]) -> Result<String, GolgiError> {
//Ok(serde_json::from_slice(body)?)
// TODO: cleanup with proper error handling etc.
Ok(std::str::from_utf8(body).unwrap().to_string())
/// Parse an array of bytes (returned by an rpc call) into a `serde_json::Value`.
///
/// # Arguments
///
/// * `body` - An array of u8 to be parsed.
pub fn json_res_parse(body: &[u8]) -> Result<Value, GolgiError> {
let message: Value = serde_json::from_slice(body)?;
Ok(message)
}
pub fn whoami_res_parse(body: &[u8]) -> Result<WhoAmIOut, GolgiError> {
Ok(serde_json::from_slice(body)?)
/// Parse an array of bytes (returned by an rpc call) into an `SsbMessageValue`.
///
/// # Arguments
///
/// * `body` - An array of u8 to be parsed.
pub fn ssb_message_res_parse(body: &[u8]) -> Result<SsbMessageValue, GolgiError> {
let message: SsbMessageValue = serde_json::from_slice(body)?;
Ok(message)
}
/// Take in an RPC request number along with a handling function and wait for
/// an RPC response which matches the request number. Then, call the handling
/// function on the response.
///
/// # Arguments
///
/// * `rpc_reader` - A `RpcReader` which can return Messages in a loop
/// * `req_no` - A `RequestNo` of the response to listen for
/// * `f` - A function which takes in an array of `u8` and returns a
/// `Result<T, GolgiError>`. This is a function which parses the response from
/// the `RpcReader`. `T` is a generic type, so this parse function can return
/// multiple possible types (`String`, JSON, custom struct etc.)
pub async fn get_async<'a, R, T, F>(
rpc_reader: &mut RpcReader<R>,
req_no: RequestNo,
@ -51,17 +76,86 @@ where
if id == req_no {
match msg {
RecvMsg::RpcResponse(_type, body) => {
return f(&body).map_err(|err| err);
return f(&body);
}
RecvMsg::ErrorResponse(message) => {
return Err(GolgiError::Sbot(message));
}
RecvMsg::CancelStreamRespose() => {
return Err(GolgiError::Sbot(
"sbot returned CancelStreamResponse before any content".to_string(),
));
}
_ => {}
}
}
}
}
/// Take in an RPC request number along with a handling function and call
/// the handling function on all RPC responses which match the request number,
/// appending the result of each parsed message to a vector until a
/// `CancelStreamResponse` is found (marking the end of the stream).
///
/// # Arguments
///
/// * `rpc_reader` - A `RpcReader` which can return Messages in a loop
/// * `req_no` - A `RequestNo` of the response to listen for
/// * `f` - A function which takes in an array of `u8` and returns a
/// `Result<T, GolgiError>`. This is a function which parses the response from
/// the `RpcReader`. `T` is a generic type, so this parse function can return
/// multiple possible types (`String`, JSON, custom struct etc.)
pub async fn get_source_until_eof<'a, R, T, F>(
rpc_reader: &mut RpcReader<R>,
req_no: RequestNo,
f: F,
) -> Result<Vec<T>, GolgiError>
where
R: Read + Unpin,
F: Fn(&[u8]) -> Result<T, GolgiError>,
T: Debug,
{
let mut messages: Vec<T> = Vec::new();
loop {
let (id, msg) = rpc_reader.recv().await?;
if id == req_no {
match msg {
RecvMsg::RpcResponse(_type, body) => {
let parsed_response: Result<T, GolgiError> = f(&body);
match parsed_response {
Ok(parsed_message) => {
messages.push(parsed_message);
}
Err(err) => {
return Err(err);
}
}
}
RecvMsg::ErrorResponse(message) => {
return Err(GolgiError::Sbot(message));
}
RecvMsg::CancelStreamRespose() => break,
_ => {}
}
}
}
Ok(messages)
}
/// Take in an RPC request number along with a handling function and call the
/// handling function on all responses which match the request number. Then,
/// prints out the result of the handling function.
///
/// This function useful for debugging and only prints the output.
///
/// # Arguments
///
/// * `rpc_reader` - A `RpcReader` which can return Messages in a loop
/// * `req_no` - A `RequestNo` of the response to listen for
/// * `f` - A function which takes in an array of `u8` and returns a
/// `Result<T, GolgiError>`. This is a function which parses the response from
/// the `RpcReader`. `T` is a generic type, so this parse function can return
/// multiple possible types (`String`, JSON, custom struct etc.)
pub async fn print_source_until_eof<'a, R, T, F>(
rpc_reader: &mut RpcReader<R>,
req_no: RequestNo,
@ -90,3 +184,61 @@ where
}
Ok(())
}
/// Take in an RPC request number along with a handling function (parsing
/// results of type `T`) and produce an `async_std::stream::Stream` of results
/// of type `T`, where the handling function is called on all `RpcReader`
/// responses which match the request number.
///
/// # Arguments
///
/// * `req_no` - A `RequestNo` of the response to listen for
/// * `f` - A function which takes in an array of `u8` and returns a
/// `Result<T, GolgiError>`. This is a function which parses the response from
/// the `RpcReader`. `T` is a generic type, so this parse function can return
/// multiple possible types (`String`, JSON, custom struct etc.)
pub async fn get_source_stream<'a, F, T>(
mut rpc_reader: RpcReader<TcpStream>,
req_no: RequestNo,
f: F,
) -> impl Stream<Item = Result<T, GolgiError>>
where
F: Fn(&[u8]) -> Result<T, GolgiError>,
T: Debug + serde::Deserialize<'a>,
{
// we use the async_stream::stream macro to allow for creating a stream which calls async functions
// see https://users.rust-lang.org/t/how-to-create-async-std-stream-which-calls-async-function-in-poll-next/69760
let source_stream = stream! {
loop {
// get the next message from the rpc_reader
let (id, msg) = rpc_reader.recv().await?;
let x : i32 = id.clone();
// check if the next message from rpc_reader matches the req_no we are looking for
// if it matches, then this rpc response is for the given request
// and if it doesn't match, then we ignore it
if x == req_no {
match msg {
RecvMsg::RpcResponse(_type, body) => {
// parse an item of type T from the message body using the provided
// function for parsing
let item = f(&body)?;
// return Ok(item) as the next value in the stream
yield Ok(item)
}
RecvMsg::ErrorResponse(message) => {
// if an error is received
// return an Err(err) as the next value in the stream
yield Err(GolgiError::Sbot(message.to_string()));
}
// if we find a CancelStreamResponse
// this is the end of the stream
RecvMsg::CancelStreamRespose() => break,
// if we find an unknown response, we just continue the loop
_ => {}
}
}
}
};
// finally return the stream object
source_stream
}